Tatsuro のテックブログ

アプリ開発の技術情報を発信します。

【Android × Kotlin】StateFlow で値を監視する

今回は、StateFlow で値を監視する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Electric Eel | 2022.1.1
  • JDK 11.0.15
  • Android Gradle Plugin 7.4.0
  • Kotlin 1.8.0
  • Gradle 7.6.0
  • org.jetbrains.kotlinx:kotlinx-coroutines-android 1.6.4
  • androidx.lifecycle 2.5.1

StateFlow とは?

StateFlow は状態保持に特化した Flow です。最新の値のみ保持する特徴を持っているため、UI の状態保持に最適で、この Flow で UI の状態を保持することで、画面の表示を常に最新に維持することができます。

また、Android Jetpack には Activity や Fragment などのライフサイクルを考慮して suspend 関数を呼ぶ関数が用意されています。よって、Activity や Fragment がフォアグラウンドに表示されているときだけ StateFlow から値を収集することが可能であり、これにより、メモリリークやクラッシュを回避できます。

StateFlow を使う

実際に StateFlow を試します。

ViewModel の解説のときと同様に、Button をクリックした回数を ViewModel に保持し、その回数を Activity の TextView に表示してみます。

このとき、Button のクリック回数を素の Int 型で保持するのではなくて、StateFlow で保持します。

ライブラリのインポート

Android で StateFlow を使う場合、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {
    // Kotlin Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
    // AndroidX Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
}

まず、Android で StateFlow を使う場合には org.jetbrains.kotlinx:kotlinx-coroutines-android をインポートします。また、以降のサンプルで StateFlow を ViewModel に保持するためにlifecycle-viewmodel-ktx をインポートし、Activity で安全に StateFlow の値を収集するために lifecycle-runtime-ktx をインポートします。

ViewModel で値を保持する

まず、ViewModel を以下のように実装します。

class MainViewModel : ViewModel() {

    /** Button のクリック回数 */
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

    /** クリック回数をカウントアップする。 */
    fun countUp() {
        _count.value = count.value + 1
    }
}

最初に、StateFlow で値を保持する箇所を見ていきます。

    /** Button のクリック回数 */
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

StateFlow には大きく分けて MutableStateFlow と StateFlow の 2 つのクラスが存在します。この 2 つは以下のような違いがあります。

クラス 特徴
MutableStateFlow 値を変更できる。
StateFlow 値を変更できない。

StateFlow を扱う場合、値の変更は ViewModel 内部に留めて、値の変更通知を受け取る Activity や Fragment などではその値を変更できないようにします。

よって、値そのものは private が付与された MutableStateFlow で保持して、それを StateFlow に変換したものを ViewModel の外部に公開します。

次に、クリック回数をカウントアップするメソッドを見ていきます。

    /** クリック回数をカウントアップする。 */
    fun countUp() {
        _count.value = count.value + 1
    }

MutableStateFlow と StateFlow には value プロパティというものがあり、このプロパティを使って値の取得や設定ができます。

Activity で値の変更通知を受け取る

そして、Activity を以下のように実装します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel =
            ViewModelProvider(this)[MainViewModel::class.java]

        // ボタンがクリックされるたびに、クリック回数をカウントアップする。
        findViewById<Button>(R.id.button)
            .setOnClickListener {
                viewModel.countUp()
            }

        // クリック回数が変更されたら、TextView を更新する。
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.count.collect {
                    findViewById<TextView>(R.id.text).text = it.toString()
                }
            }
        }
    }
}

まず、Button がクリックされたら ViewModel のカウントアップメソッドを呼びます。

        // ボタンがクリックされるたびに、クリック回数をカウントアップする。
        findViewById<Button>(R.id.button)
            .setOnClickListener {
                viewModel.countUp()
            }

そして、ViewModel のクリック回数の変更通知を監視して、クリック回数を TextView に設定します。

        // クリック回数が変更されたら、TextView を更新する。
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.count.collect {
                    findViewById<TextView>(R.id.text).text = it.toString()
                }
            }
        }

クリック回数を保持する StateFlow は Flow なので collect メソッドで収集します。

しかし、直接収集すると、Activity が破棄された後やバックグラウンドにいるときも収集してしまいます。

そこで、Activity の lifecycleScope と repeatOnLifecycle メソッドを使います。

Activity の lifecycleScope は Activity の CoroutineScope であり、Activity が破棄されると実行中の Coroutine がキャンセルされます。

そして、repeatOnLifecycle メソッドはライフサイクルが指定された状態になったときに渡されたブロックを呼びます。

今回のサンプルの場合、ライフサイクルが Start から Stop の間にブロックを呼びます。

この repeatOnLifecycle のブロックの中でクリック回数の StateFlow を収集し、 収集したクリック回数を TextView に設定します。

StateFlow VS LiveData

StateFlow と似た機能を持つものとして、LiveData があります。

StateFlow は LiveData と比較すると、値を null 安全で保持することができるため、StateFlow の方を積極的に使うとよいでしょう。

参考

StateFlow と SharedFlow

関連記事

ViewModel の使用方法については、以下で解説しています。

tatsurotech.hatenablog.com

LiveData で値を監視する方法については、以下で解説しています。

tatsurotech.hatenablog.com