- 2021/11/03 追記 -
記事執筆時はaddRepeatingJob
で説明を行っていましたが、削除されたためrepeatOnLifecycle
に変更を行いました。
このブログでは度々お伝えしていますが、Kotlin Coroutinesは非同期処理を強力に支援してくれます。
特に、値を複数回送受信することができるFlow
の強化により、Kotlin Coroutinesの表現力がより一層向上し、一部LiveDataやRxJavaから移行する動きもあります。
一方で、AndroidのLifecycle
を考慮し、非同期処理を安全に利用するためには、いくつか注意する必要があります。
今回は、以前からあるlaunchWhenStartedやlaunchWhenResumed等のlaunchWhenXX系と、lifecycle-runtime-ktx:2.4.0-alpha01で追加されたrepeatOnLifecycleの違いと使い分けについて紹介します。
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秒に一度インクリメントした数字をログ出力します。
Activity
等のLifecycleOwner
はlifecycleScope
を持っており、lifecycleScope
でlaunch
すれば、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
~ onStop
、onResume
~ onPause
の間だけ動作させたいことがあります。
その一つの実現方法はlaunchWhenStarted
やlaunchWhenResumed
を使うことです。
例えば、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
は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終了
launchWhenXX
とrepeatOnLifecycle
はバックグラウンドに行った際と、戻った際の挙動が違いました。
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
をsubscribe
する際は、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に送信します。
launchWhenStarted
を使ってFlow
をcollect
してみましょう。
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
今回作成しているMutableStateFlow
はbuffer
を指定していないため、データが受け取られない間はemit
が待機になります。
そのため、バックグラウンドの間はデータがインクリメントされないことになります。
では、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 も同時に追加されているので、合わせて確認してみてください。
また、今回紹介しませんでしたがcallbackFlowやshareInで作成したFlowの挙動も、ぜひ手元で動かしながら試してみてください。
Kotlin Coroutinesの解説本をZennにて販売しています。より詳しく学びたい方は、こちらも合わせて確認してみて下さい。