Blog

launchWhenXXとrepeatOnLifecycleの違い【Android / Kotlin Coroutines】

- 2021/11/03 追記 -

記事執筆時はaddRepeatingJobで説明を行っていましたが、削除されたためrepeatOnLifecycleに変更を行いました。


このブログでは度々お伝えしていますが、Kotlin Coroutinesは非同期処理を強力に支援してくれます。

特に、値を複数回送受信することができるFlowの強化により、Kotlin Coroutinesの表現力がより一層向上し、一部LiveDataRxJavaから移行する動きもあります。

一方で、AndroidのLifecycleを考慮し、非同期処理を安全に利用するためには、いくつか注意する必要があります。

今回は、以前からあるlaunchWhenStartedlaunchWhenResumed等のlaunchWhenXX系と、lifecycle-runtime-ktx:2.4.0-alpha01で追加されたrepeatOnLifecycleの違いと使い分けについて紹介します。

suspend functionに対する挙動

suspend functionを、いくつかの方法でLifecycleから実行します。

今回対象とするメソッドは以下のとおりです。

suspend fun sample() {
    try {
        var i = 0
        while (true) {
            Log.d("SampleActivity", i.toString())
            i++
            delay(1_000)
        }
    } catch (e: CancellationException) {
        Log.d("SampleActivity", "canceled")
    }
}

キャンセルされるまで1秒に一度インクリメントした数字をログ出力します。

lifecycleScope

Activity等のLifecycleOwnerlifecycleScopeを持っており、lifecycleScopelaunchすれば、onDestory時に自動でCoroutinesをキャンセルしてくれます。

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

    lifecycleScope.launch {
        sample()
    }
}
D/SampleActivity: 0
D/SampleActivity: ON_CREATE
D/SampleActivity: ON_START
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleActivity: 2
D/SampleActivity: 3
D/SampleActivity: ON_PAUSE
D/SampleActivity: ON_STOP
D/SampleActivity: canceled
D/SampleActivity: ON_DESTROY // ← activity終了

これは非常に便利ですが、onStart ~ onStoponResume ~ onPauseの間だけ動作させたいことがあります。

launchWhenXX

その一つの実現方法はlaunchWhenStartedlaunchWhenResumed を使うことです。

例えば、launchWhenStarted等を使った場合、onStart ~ onStopの間のみ動作し、それ以外は一時停止状態になります。

また、onDestory時にキャンセルされます。

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

    lifecycleScope.launchWhenStarted {
        sample()
    }
}
D/SampleActivity: ON_CREATE
D/SampleActivity: ON_START
D/SampleActivity: 0
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleActivity: 2
D/SampleActivity: ON_PAUSE
D/SampleActivity: 3
D/SampleActivity: ON_STOP // ← バックグラウンド
D/SampleActivity: ON_START // ← フォアグラウンド
D/SampleActivity: 4
D/SampleActivity: ON_RESUME
D/SampleActivity: 5
D/SampleActivity: 6
D/SampleActivity: ON_PAUSE
D/SampleActivity: 7
D/SampleActivity: ON_STOP
D/SampleActivity: canceled
D/SampleActivity: ON_DESTROY // ← activity終了

フォアグラウンドに戻ると、中断したところから再開します。

repeatOnLifecycle

では、新しく追加された repeatOnLifecycle だとどうでしょうか?

repeatOnLifecycleはsuspend functionのため、一度lifecycleScopeを使ってlaunchした中で呼ぶ必要があります。

指定したLifecycle.Stateに満たなければキャンセルされ、再開時は最初から開始されます。

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

    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            sample()
        }
    }
}
D/SampleActivity: ON_CREATE
D/SampleActivity: ON_START
D/SampleActivity: 0
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleActivity: 2
D/SampleActivity: ON_PAUSE
D/SampleActivity: 3
D/SampleActivity: canceled
D/SampleActivity: ON_STOP  // ← バックグラウンド
D/SampleActivity: ON_START // ← フォアグラウンド
D/SampleActivity: 0
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleActivity: 2
D/SampleActivity: ON_PAUSE
D/SampleActivity: 3
D/SampleActivity: canceled
D/SampleActivity: ON_STOP
D/SampleActivity: ON_DESTROY  // ← activity終了

使い分け

launchWhenXXrepeatOnLifecycleはバックグラウンドに行った際と、戻った際の挙動が違いました。

repeatOnLifecycleを使ったほうが、バックグラウンドに行った際にCoroutinesを完全にキャンセルしてくれるため、よりリソースを削減してくれます。

一方で、フォアグラウンド時に前の状態から再開したい場合、launchWhenXXを使う必要があるでしょう。

- 2021/11/03 追記 -

公式ドキュメントlaunchWhenXXは将来的に削除される計画が書かれたため、今後はrepeatOnLifecycleを使うのが良いでしょう。

Caution: This API is not recommended to use as it can lead to wasted resources in some cases. Please, use the Lifecycle.repeatOnLifecycle API instead. This API will be removed in a future release.

注意:このAPIは、場合によってはリソースの浪費につながる可能性があるため、使用をお勧めしません。代わりにLifecycle.repeatOnLifecycle APIを使用してください。このAPIは、将来のリリースで削除される予定です。

Flowに対する挙動の違い

flowsubscribeする際は、bufferに関して重大な異なる動作をするので、より慎重に選択する必要があります。

以下のようなViewModelを用意します。

class SampleViewModel : ViewModel() {
    private val _flow = MutableSharedFlow<Int>()
    val flow = _flow.asSharedFlow()

    init {
        viewModelScope.launch {
            var i = 0
            while (true) {
                _flow.emit(i)
                Log.d("SampleViewModel", i.toString())
                i++
                delay(1_000)
            }
        }
    }
}

1秒に一度インクリメントした数字をViewに送信します。

launchWhenXX

launchWhenStartedを使ってFlowcollectしてみましょう。

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

    lifecycleScope.launchWhenStarted {
        viewModel.flow.collect {
            Log.d("SampleActivity", it.toString())
        }
    }
}
D/SampleActivity: ON_CREATE
D/SampleActivity: ON_START
D/SampleViewModel: 0
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleViewModel: 1
D/SampleActivity: 2
D/SampleViewModel: 2
D/SampleActivity: ON_PAUSE
D/SampleActivity: 3
D/SampleViewModel: 3
D/SampleActivity: ON_STOP  // ← バックグラウンド
D/SampleActivity: ON_START // ← フォアグラウンド
D/SampleViewModel: 4
D/SampleActivity: 5
D/SampleActivity: ON_RESUME
D/SampleActivity: 5
D/SampleViewModel: 5

今回作成しているMutableStateFlowbufferを指定していないため、データが受け取られない間はemitが待機になります。

そのため、バックグラウンドの間はデータがインクリメントされないことになります。

repeatOnLifecycle

では、repeatOnLifecycle で試してみましょう。

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

    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.flow.collect {
                Log.d("SampleActivity", it.toString())
            }
        }
    }
}
D/SampleActivity: ON_CREATE
D/SampleActivity: ON_START
D/SampleActivity: 0
D/SampleViewModel: 0
D/SampleActivity: ON_RESUME
D/SampleActivity: 1
D/SampleViewModel: 1
D/SampleActivity: 2
D/SampleViewModel: 2
D/SampleActivity: ON_PAUSE
D/SampleActivity: 3
D/SampleViewModel: 3
D/SampleActivity: ON_STOP  // ← バックグラウンド
D/SampleViewModel: 4
D/SampleViewModel: 5
D/SampleViewModel: 6
D/SampleActivity: ON_START  // ← フォアグラウンド
D/SampleActivity: ON_RESUME
D/SampleActivity: 7
D/SampleViewModel: 7
D/SampleActivity: 8
D/SampleViewModel: 8

launchWhenStartedと異なり、バックグラウンドの間もインクリメントが進んでいることがわかります。

また、フォアグラウンドの再開時は、そのタイミングの値から取得しています。

これは、バックグラウンドに行っている間、collectをキャンセルしているためです。

使い分けについて

bufferを消費していくことはわかりにくいため、repeatOnLifecycle を使ったほうが良いと考えています。

バックグラウンドに行っている間に動作を止めたい場合、別途ViewModelにlifecycleの状態を伝えることで実現することが出来ます。

まとめ

今回はlifecycle-runtime-ktx:2.4.0-alpha01で追加されたrepeatOnLifecycleを、launchWhenXXと比較しつつ紹介しました。

launchWhenXXはわかりにくい挙動がいくつかあったので、個人的にはrepeatOnLifecycleを積極的に使っていきたいと感じました。

repeatOnLifecycleと同じような挙動をする Flow.flowWithLifecycle も同時に追加されているので、合わせて確認してみてください。

また、今回紹介しませんでしたがcallbackFlowshareInで作成したFlowの挙動も、ぜひ手元で動かしながら試してみてください。


Kotlin Coroutinesの解説本をZennにて販売しています。より詳しく学びたい方は、こちらも合わせて確認してみて下さい。

詳解 Kotlin Coroutines [2021] | Zenn

人気の記事

Jetpack Compose, React, Flutter, SwiftUIを比較する

Jetpack ComposeとViewModelについて考える

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

ViewModelでString resourcesを扱いたい

Jetpack Composeのコンポーネントはなぜ返り値がないのか

MVVMでモデルに処理を寄せる【Android】