ViewModelとの連携

コードは参考サイトからの流用になります。
ViewModelの定義の中で変更を検知したい値(count)をMutableLiveDataとして定義する。初期値はここでは0

class SampleViewModel: ViewModel() {
    val count = MutableLiveData(0)

    fun countUp() {
        val c = count.value ?: 0
        count.value = c + 1
    }
}

ViewModelのパラメータの変更を監視するには、observeAsState()を使います。このAPIによって、LiveDataをStateとして扱うことができます。つまり、LiveDataの値が変更されたことをコンポーザブルが検出し、UIの再構築が行われます。その結果、LiveDataの値の変更が画面表示に反映されるのです。

@Composable
fun AppScreen(viewModel: SampleViewModel = viewModel()) {
    val count: Int by viewModel.count.observeAsState(0)
    Column {
        Text(text = "Count: $count")
        Button(onClick = {viewModel.countUp()}) {
            Text(text = "Count up")
        }
    }
}

ここでデフォルト引数としてviewModel()としておくことで上位から渡さなくてもすでに対象のViewModelがインスタンス化されている場合は、そのインスタンスを返してくれる。
上記のComposableの表示

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppScreen()
        }
    }
}

なので複数のComposableからViewModelを利用する場合も以下のようにできる

@Composable
fun AppScreen() {
    Column {
        CountText()
        CountButton()
    }
}

@Composable
fun CountText(viewModel: SampleViewModel = viewModel()) {
    val count: Int by viewModel.count.observeAsState(0)
    Text(text = "Count: $count")
}

@Composable
fun CountButton(viewModel: SampleViewModel = viewModel()) {
    Button(onClick = {viewModel.countUp()}) {
        Text(text = "Count up")
    }
}

参考
Jetpack Compose入門(16) ViewModelとの連携 - 縁側プログラミング

JetpackComposeのUI更新の仕組み

Column {
    var displayCount by remember { mutableStateOf(0) }
    Text(
        text = "$count",
        modifier = Modifier.padding(20.dp)
    )
    Button(
        onClick = { count++ }
    ) {
        Text(text = "Count up!")
    }
}

displayCountの宣言時にmutableStateOf(0)で初期値を0に設定。
また、型がMutableStateなのでJetpackComposeが値の変更を検知する。
ButtonクリックでonClickイベント発動
→countの値更新
→JetpackComposeが値変更を検知
→Columnから再構築される(このときcountはremember宣言しているので再構築前の値を覚えている)
→表示が1に更新される

Jetpack Composeは、「宣言型」UIフレームワークです。宣言型UIでは、宣言時(UI作成時)にUIの状態をすべて決定し、後から変更しません。UIコンポーネントの状態(表示する文字列や色や大きさなど)を後から変更するということはできない仕組みになっているのです。この仕組みのおかげでソースコードがシンプルになります。

では状態を変更せずにどうやって動的なUIを実現するのかというと、UIの更新が必要になるたびにUI全体を一から再構築します。もちろん再構築はフレームワークが自動で行います。Jetpack Composeフレームワークが状態の更新を検出したら、UI全体を構築しなおます。この状態更新の検出に使われるのが、上のサンプルでも使っているMutableStateというわけです。

参考:
Jetpack Compose入門(11) ボタンクリックでUIを更新する - 縁側プログラミング

MVVMの基礎から見ていく①

@Composable
fun CountUp() {
    var count: Int by remember {
        mutableStateOf(0)
    }

    Column {
        Text(text = "$count")
        Button(
            onClick = {
                count += 1
            }
        ) {
            Text(text = "Count Up!")
        }
    }
}

変数の宣言でby rememberとすることで状態の保存をし、状態の変化をトリガーとしてUIが更新される。
mutableStateOf(0)とすることで初期値が0になる
ただ、これだとView自身が状態を持つ変数countを持ってしまっている。
ViewModelを作ってそちら側に実際の状態を保持するように変更する。
ViewModel側

/**
 * カウントの状態を持つViewModel
 */
class CountViewModel : ViewModel() {
    /**
     * カウントの回数を保持する
     */
    val count: MutableState<Int> = mutableStateOf<Int>(0)

    /**
     * カウントアップボタンのクリックイベントを処理する
     * [count]の値を1増やし、表示を更新する
     */
    fun onCountUpTapped() {
        val currentValue = requireNotNull(count.value)
        count.value = currentValue + 1
    }
}

元々処理をしていたComposable側。

@Composable
fun CountUp(viewModel: CountViewModel) {
    val count: Int by viewModel.count

    Column {
        Text(text = "$count")
        Button(
            onClick = {
                viewModel.onCountUpTapped()
            }
        ) {
            Text(text = "Count Up!")
        }
    }
}

引数のviewModelはMainActivityのプロパティとして定義する

class MainActivity : ComponentActivity() {
    private val viewModel: CountViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CountUp(viewModel = viewModel)
        }
    }
}

CheckBoxを含むリストアイテムでアイテム全体をチェックON/OFF切り替え対象にする

Checkboxの中ではなく親要素に指定することで対象を変更できる
修正前

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = { onToggle() },
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

修正後

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

参考:JetpackComposeCodeLab

Navigation

RallyDestination でicon、route(destinationを指定するためのString)、表示するComposableをまとめてinterfaceとして定義(本来はComposableは分離するべきとのこと)
Overview, Accounts, Bills で各画面に対する値を指定

/**
 * Contract for information needed on every Rally navigation destination
 */
interface RallyDestination {
    val icon: ImageVector
    val route: String
    val screen: @Composable () -> Unit
}

/**
 * Rally app navigation destinations
 */
object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
    override val screen: @Composable () -> Unit = { OverviewScreen() }
}

object Accounts : RallyDestination {
    override val icon = Icons.Filled.AttachMoney
    override val route = "accounts"
    override val screen: @Composable () -> Unit = { AccountsScreen() }
}

object Bills : RallyDestination {
    override val icon = Icons.Filled.MoneyOff
    override val route = "bills"
    override val screen: @Composable () -> Unit = { BillsScreen() }
}

実際に使う部分は以下

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                currentScreen.screen()
            }
        }
    }
}

val rallyTabRowScreens = listOf(Overview, Accounts, Bills)

currentScreenに初期状態としてOverviewを設定

NavController を取得するには、rememberNavController() 関数を呼び出します。これにより、(rememberSaveable を使用して)構成変更後も存続する NavController が作成されて記憶されます。
NavController は常に、コンポーザブル階層の最上位(通常は App コンポーザブル内)に作成して配置します。これにより、NavController を参照する必要があるすべてのコンポーザブルにアクセス権が付与されます。
特定のデスティネーションのコピーがバックスタックの一番上に最大で 1 つだけ配置されるように、Compose Navigation API には launchSingleTop フラグが用意されています。このフラグを次のように navController.navigate() アクションに渡すことができます。

とのことなのでComposableの一番上で
val navController = rememberNavController()
を呼んで使うようにする。
launchSingleTop に関しては全部のnavigate(route)に指定するのは非効率なので拡張関数を定義して使うのが良い

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

これを定義して以下のように使う。

navController.navigateSingleTopTo(newScreen.route)

また、navControllerはナビゲーション階層の最上位に配置し、下位に渡さないようにする。(独立しなくなる)
以下のように実行するメソッドを渡すようにする。

                composable(route = Overview.route) {
                    OverviewScreen(
                        onClickSeeAllAccounts = {
                            navController.navigateSingleTopTo(Accounts.route)
                        },
                        onClickSeeAllBills = {
                            navController.navigateSingleTopTo(Bills.route)
                        }
                    )
                }


参考:Jetpack Compose Navigation  |  Android Developers

Jetpack Compose CodeLab / Jetpack Compose の状態 - ViewModelの状態

警告: ViewModel はコンポジションの一部ではありません。そのため、コンポーザブルで作成された状態(保存された値など)は保持しないでください。メモリリークが発生する可能性があります。

とのこと。

ViewModel の外部から変更できないように_tasksを定義する。
外部から参照するときはtasks側を見る。
このViewModelのデータに変更を加えることが出来るのはこのクラス内のメソッドだけに制限する。

class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask>
        get() = _tasks

   fun remove(item: WellnessTask) {
       _tasks.remove(item)
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

ViewModelは他のComposableには渡さない。
渡してしまうと依存が生まれ、部品として使いづらくなる。
必要なデータやメソッドだけを渡すようにする。
基本的にビジネスロジックはComposableな関数には作らないことでテストしやすくする。

Jetpack Composeでのリスト操作

Jetpack Compose の状態のCodeLab続き

データクラスを作って仮データを入れる

data class WellnessTask(val id: Int, val label: String)
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

このデータクラスのListをLazyColumnに表示するには以下のようにListを引数で貰うときにremember

@Composable
fun WellnessTaskList(
    modifier: Modifier = Modifier,
    list: List<WellnessTask> = remember { getWellnessTasks() }
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(list) { task ->
            WellnessTaskItem(taskName = task.label)
        }
    }
}
Composeで監視できるリスト

可変オブジェクト(ArrayList や mutableListOf, など)を使用しても機能しない。
Compose で監視できる MutableList のインスタンスを作成する必要がある。
List等既に設定したいCOLLECTIONがある場合はtoMutableStateList()を使用してMutableListを作成できる。
これからListを作るような場合は mutableStateListOfを使って初期値含めてMutableListを作る。

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

次回はViewModelの状態