Jetpack Compose入門(16) ViewModelとの連携

この記事をシェア

今回はJetpack ComposeとViewModelの連携方法を説明していきます。ViewModelはMVVMアーキテクチャでViewとModelを結びつける役割を担います。Jetpack ComposeアプリにおけるViewは、もちろんコンポーザブルで作成します。そこで今回は、コンポーザブルからViewModelにアクセスする方法を確認していきます。

ViewModelの役割

AndroidアプリにおけるViewModelの役割は、従来のViewシステムでもJetpack Composeでも同じです。ささっと確認しておきましょう。

  • ViewとModelを接続する
    • 従来のViewをコンポーザブルに置き換えても、ModelとViewModelは基本的に従来と同じものを使えます。ViewModelは、View階層のコンポーザブルとModelを結び付けるために存在します。コンポーザブルの操作を受けてModelを更新する、View→Modelの方向と、Modelが更新されたときにViewの表示を更新する、Model→Viewの方向とがあります。
  • Viewのライフサイクルを超えてデータを保持する
    • Jetpack Composeのライフサイクルは従来のViewやFragmentに比べてシンプルですが、それでもActivityのライフサイクルからは逃れられません。画面の向きを変えたときなど、Activityが再生成されるとコンポーザブルが保持している変数も初期化されてしまいます。rememberSaveableを使ってコンポーザブル内でデータを保持することもできますが、ViewModelでデータを保持した方が簡単です。

準備

最初に必要な依存関係をbuild.gradle(:app)に追加しておきます。

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0"
    implementation "androidx.compose.runtime:runtime-livedata:1.0.5

lifecycle-viewmodel-composeはコンポーザブル内でViewModelのインスタンスを取得するために使います。最新バージョンはこちらで確認できます。少し前まではアルファバージョンでしたが、2021年10月に正式版になり、androidx.lifecycleパッケージと同じバージョンの2.4.0になりました。

runtime-livedataは、コンポーザブル内でViewModelのパラメータの変化を監視するために使います。最新バージョンはこちらで確認できます。この記事の執筆時点では1.0.5が最新です。androidx.composeパッケージは複数のパッケージから構成されているので、他のパッケージとバージョンをそろえておいたほうが無難だと思います。

ViewModelの作成

ViewModel自体の実装は、Jetpack Composeを使わない場合と変わりません。今回は下記のように数値を一つだけ保持する簡単なViewModelを用意します。

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

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

唯一のパラメータのcountMutableLiveDataで実装しています。countUp()を呼び出すと、countがインクリメントされます。本来はViewModelはModelを参照するのですが、今回は簡単のためModelは省略します。

ViewModelを取得

コンポーザブル関数でViewModelオブジェクトを取得するには、androidx.lifecycle.viewmodel.composeパッケージのviewModel()が使えます。viewModel()は、クラスが一致するViewModelのオブジェクトを返します。初めて呼び出したときは新しくインスタンスを作成し、すでにインスタンス作成済みの場合は同じインスタンスを返します。これについてはこの記事の最後で補足します。

コンポーザブル関数内で使うViewModel変数は、関数の引数として定義し、そのデフォルト値としてviewModel()で取得したインスタンスを渡すのが定石です。こうすることによって、呼び出し側の記述をシンプルにしつつ、必要な時は別のインスタンスを指定することが可能になります。

@Composable
fun AppScreen(viewModel: SampleViewModel = viewModel()) {
    ...
}

この例ではviewModel変数に先ほど作成したSampleViewModelのインスタンスが渡されます。

ViewModelのパラメータの監視

ViewModelのパラメータの変更を監視するには、observeAsState()を使います。このAPIによって、LiveDataStateとして扱うことができます。つまり、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")
        }
    }
}

この例では、先ほど作成したSampleViewModelcountを状態変数として取得しています。これによって、SampleViewModel#countの値が変更されると画面が再構築され、Textの文字列表示が更新されます。なお、observeAsState()の引数は、LiveDataが初期化されていないときに使われる初期値ですが、今回はSampleViewModel#count側にも初期値を設定しているので、observeAsState()で設定した初期値が表示されることはありません。

count更新のトリガになるのはButtononClickです。onClickのコールバック内でSampleViewModel#countUp()を呼び出しているので、ボタンをクリックするとcountが更新され、画面が更新されるという流れになっています。

コンポーザブルの表示

先ほど作成したAppScreen()コンポーザブル関数を呼び出す側では、何も特別な処理は必要ありません。ViewModelはデフォルト値が設定されているので、呼び出し側では単にAppScreen()を引数なしで呼び出すだけです。

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

結果

サンプルの動作は以下のようになります。なお、コンポーネントの配置はModifierなどで調整していますので、上に記載のソースコードと全く同じではありません。ModifierColumnの配置について詳しく知りたい方は、基本編の「コンポーネントを装飾する」や「コンポーネントを配置する」をご覧ください。

ViewModelのサンプル

補足:複数のコンポーザブルからViewModelを利用する

先に説明した通り、 androidx.lifecycle.viewmodel.composeパッケージのviewModel()関数を使うと、すでに対象のViewModelがインスタンス化されている場合は、そのインスタンスを返してくれます。したがって、複数のコンポーザブルから一つのViewModelを利用するような場合もシンプルにソースコードを記述できます。

例えば上のAppScreen()では一つのコンポーザブル関数内にTextButtonを配置していましたが、これを二つのコンポーザブルに分割し、それぞれのコンポーザブル内で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")
    }
}

まとめ

今回は、コンポーザブル関数からViewModelを利用する方法を学びました。ViewModelを取得するのに便利なviewModel()と、LiveDataStateとして扱うことでコンポーザブルを表示を更新するときに使うobserveAsState()はぜひ使いこなしてください。


Jetpack Compose入門


この記事をシェア