every Tech Blog

株式会社エブリーのTech Blogです。

LiveData を Kotlin Coroutines Flow に移行した話

この記事は every Tech Blog Advent Calendar 2024(夏) 19 日目の記事です。

はじめに

こんにちは、DELISH KITCHEN でクライアントエンジニアを担当している kikuchi です。

Kotlin Fest 2024 の開催が近づいてきましたので、今回は折角の機会ですので Kotlin に関わる話として
DELISH KITCHEN で一部の処理を LiveData から Kotlin Coroutines Flow に移行した話をまとめてみたいと思います。

移行を考えた背景

現状 DELISH KITCHEN ではアーキテクチャに MVVM (Model View ViewModel) を採用しており、ViewModel で更新された LiveData を
View が observe するという一般的な実装となっていますが、今回 Flow を調査する過程で

  • オペレータ (map など) が使用できる
  • Null 安全性が保証される
  • 既に通信周りを Coroutines で実装していたため、Flow に移行しやすい
  • ワンショット通知を無理やり LiveData で実現していた箇所を適切な方法に修正ができる (SharedFlow の利用)
  • 新しい技術の習得

というメリットを感じ、今回新規で実装する箇所から徐々に Flow を採用することを決断しました。
特に新しい技術の習得というのはエンジニアにとって成長に繋がる良い機会ですので、積極的に取り入れたいと考えました。

LiveData と Flow の違いについて

早速ですが、本項目では LiveData と Flow の違いを細かくまとめていきます。

LiveData について

LiveData とは Android Jetpack の一部であり、Android のライフサイクルに対応した監視ができる仕組みです。

監視のタイミングですが、オブザーバーのライフサイクルの状態が STARTED か RESUMED の場合のみ LiveData の変更通知を受け取ることができます。
オブザーバーを Activity や Fragment のライフサイクルと紐づけている場合、onStarted や onResume の場合 (画面がアクティブな状態) でのみ通知を受け取ることができ、
onPause や onStop の場合 (画面が非アクティブな状態) では通知を受け取ることができません。
また observe の処理さえ定義しておけば自動的に変更通知を受け取れるようになります。
つまりは実装上で明示的にオブザーバーを開始・終了する必要は無く、また画面がアクティブであるかの判定も不要ということになります。

上記をふまえて、オブザーバーを Activity のライフサイクルに紐づけてデータを監視する簡単な実装例を載せたいと思います。

// ViewModel 側の実装
class TestViewModel {

    private val _testData = MutableLiveData<Int>()
    val testData: LiveData<Int> = _testData

    fun updateTestData() {
        _testData.postValue(1)  ...①
    }
}


// View 側の実装
class TestActivity : AppCompatActivity {

    private val viewModel: TestViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel.testData.observe(this) { data ->  ...②
            data ?: return@observe  ...③

            // 監視しているデータの更新通知を受け取ったら実行する処理
            updateView(data)
        }
    }
}

ViewModel 内部では更新可能な MutableLiveData を更新し、連動して外部に公開している LiveData も更新されるように実装することで、
① のように ViewModel 内で値を更新すると、② の observe の処理が自動的に発火する挙動となります。
実装例を見て分かる通り、明示的にライフサイクルに応じて監視を開始・終了する処理はしていません。

ここで 1 つ注意点があり、LiveData は Java で実装されているため Null 安全性は保証されておらず、Null が設定されてしまう可能性があるため、
念の為 ③ のような Null チェックが必要となります。

Flow について

Flow とは Kotlin Coroutines 上で非同期にデータを取り扱うための仕組みです。

Flow には常に最新の値を保持する StateFlow と、replay パラメータで設定した数分の過去の値を保持できる SharedFlow が存在しますが、
今回は StateFlow を利用してオブザーバーを Activity のライフサイクルに紐づけてデータを監視する実装例を載せたいと思います。

// ViewModel 側の実装
class TestViewModel {

    // MutableStateFlow では初期値が必要
    private val _testData = MutableStateFlow<Int>(0)
    val testData: StateFlow<Int> = _testData

    fun updateTestData() {
        _testData.value = 1  ...①
    }
}


// View 側の実装
class TestActivity : AppCompatActivity {

    private val viewModel: TestViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        lifecycleScope.launch {  ...②
            repeatOnLifecycle(Lifecycle.State.STARTED) {  ...③
                viewModel.testData.collect { data ->  ...④
                    // 監視しているデータの更新通知を受け取ったら実行する処理
                    updateView(data)
                }
            }
        }
    }
}

LiveData と同様、ViewModel 内部では更新可能な MutableStateFlow を更新し、連動して外部に公開している StateFlow も更新されるように実装することで、
① のように ViewModel 内で値を更新すると、オブザーバー側で通知を受け取ることができます。
ただし LiveData と異なる点は、② のように必ず lifecycleScope.launch のスコープ内であることを明示する必要があり、③ のようにどのライフサイクルで通知を受け取るか
明示する必要があります。実装例だと画面がアクティブな場合に受け取れるよう repeatOnLifecycle(Lifecycle.State.STARTED) を指定しています。
(上記 ② と ③ の指定をすることで、LiveData のケースと同じ契機で更新通知を受け取ることができます)
データを受け取る処理は ④ のように collect で受け取ります。

Flow は LiveData と違い Kotlin で実装されているため Null 安全性が保証されており、Null チェックは不要となります。
また、具体的な例は割愛しますが Flow はオペレータを使用できるため、直接 Flow 型のデータを操作する場合は filter や map などを使用することができます。
こちらは LiveData に無い Flow の強みと言えます。

LiveData と Flow の比較

今回は簡単な実装例のみ載せましたが、ほぼ同じような挙動にすることが可能だと分かりました。
ただ細かい部分で違いがありましたので、違いを一覧でまとめてみたいと思います。

LiveData Flow
実装の複雑さ やや複雑
Null 安全性の保証 されていない (が実装でカバーできる) されている
オペレーター使用可否 使用不可 使用可能
監視タイミングの管理コスト 低 (Android のライフサイクルと連動) 中 (実装で明示する必要あり)
KMP での使用可否 不可 可能

移行にあたって苦労した点

LiveData と同じ用途で実装しようとするとどうしてもコードの記述量が増えてしまいましたが、今回は紐づけるライフサイクルを変更する必要が無いため、
以下のように拡張関数を定義することでコードの煩雑さを解消しました。

// 拡張関数
fun <T> Flow<T>.observe(viewLifecycleOwner: LifecycleOwner, action: (T) -> Unit) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            collect { action(it) }
        }
    }
}


// オブザーバー側
class TestActivity : AppCompatActivity {
    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel.testData.observe(this) { data ->
            updateView(data)
        }
    }
}

また、移行当初は Android Jetpack Lifecycle ライブラリで使用されており、かつ現在は Deprecated となっている lifecycleScope.launchWhenStarted で定義していたことで、
オブザーバー側が STOPPED となってもコルーチンがキャンセルされず停止された状態のままとなっており、リソースが浪費されている問題が発生していました。
こちらは前述した viewLifecycleOwner.repeatOnLifecycle を使用することで、意図通りオブザーバー側が STARTED 状態になる度に実行し、STOPPED になる度にキャンセルされる
挙動となったため回避できたのですが、Deprecated は常に意識して日々改修していく重要さを改めて痛感しました。

まとめ

LiveData は Android のライフサイクルが考慮されているなど Android アプリに対して最適化された作りとなっており、対して Flow はオペレータの使用が可能である、
Null 安全性が保証されているなど、より Kotlin の恩恵を受けることができるため、どちらを採用する場合でもメリットがあります。
そのため開発するアプリの規模や実現したい機能、学習コストなどからどちらを採用するか検討すると良いかと考えています。

近年サーバーサイド Kotlin の導入事例も増えてきていますので、アプリエンジニアでもサーバー開発をすることを見越して Kotlin の標準 API である Flow を採用する、
という考え方もあるかもしれません。
エブリーではこれからも職種にとらわれずいろいろな事に挑戦できる環境を作って行きたいと考えています。

今回紹介した内容が少しでも皆さまのお役に立てれば幸いです。

おわりに

Kotlin Fest 2024 まで、あと 3 日!
https://www.kotlinfest.dev/

株式会社エブリー は、ひよこスポンサー として Kotlin Fest 2024 に参加します。
ぜひ、ブースでお会いしましょう!