画面を離脱するときに、何かしらの処理を行いたいことがあります。例えば画面を離脱するときに DB になんか記録するとか、API を叩いておく、とかです。アプリが動いている間にやってくれれば良い程度で、バックグラウンドに回った時のことは考えません。
画面離脱時をハンドリング
画面を離脱するときは DisposableEffect(Unit)
の onDispose
でフックすると良さそうです。DisposableEffect
はクリーンアップ処理が可能な副作用で、key に Unit
を指定すれば「画面から離脱する時の処理ブロック」を実現できます。
@Composable fun SampleScreen() { ... DisposableEffect(Unit) { onDispose { // ここで処理を書きたい } } ...
自分は基本的に ViewModel を利用しているので、ViewModel を用意して `recordData()` という関数を書いてみる事にします。
@HiltViewModel class SampleScreenViewModel @Inject constructor() : ViewModel() { ... fun recordData() { // データを保存する処理を書く予定 } ... } fun SampleScreen() { ... DisposableEffect(Unit) { onDispose { viewModel.recordData() } } ...
ViewModel に処理を書く
ViewModel の recordData()
関数を実装しましょう。メインスレッドを止めたくはないので、recordData()
のなかで非同期に処理を実行させたいです。
@HiltViewModel class SampleScreenViewModel @Inject constructor() : ViewModel() { ... fun recordData() { viewModelScope.launch { // データを保存する } } ... }
で、↑は間違いです。viewModelScope
を使っているので、画面離脱時にこの ViewModel も破棄されるため、viewModelScope
ではコルーチンの処理がキャンセルされてしまいます。
DB に書き込みするだけであれば、ひょっとするとうまく動くかもしれません。SQLite への書き込み処理が早いためです。もう少し時間のかかる処理の場合は ViewModel の破棄が走り、コルーチンはキャンセルされる事でしょう。
よって、このコンポーザブルが破棄されても動くことのできるより広いスコープを使う必要があります。手っ取り早くGlobalScope
(デリケート扱いです)を使ってみましょう。
@HiltViewModel class SampleViewModel @Inject constructor() : ViewModel() { ... fun recordData() { GlobalScope.launch { // データを保存する } } ... }
↑の例は動きますが、ハードコードしていて良くありません。ここの場合だと、テスト時に問題になる可能性があります。よって外から注入できるようにしておくと良いでしょう。
@HiltViewModel class SampleViewModel @Inject constructor(private val externalScope: CoroutineScope = GlobalScope) : ViewModel() { ... fun recordData() { externalScope.launch { // データを保存する } } ... }
GlobalScope
は先ほども書いた通りデリケート扱いですが、そこまで時間を占有しない処理で、処理をキャンセルする必要がなく、処理の生存期間がアプリケーション起動中とイコールな場合は利用しても良いと思います。
GlobalScope
または、withContext(NonCancellable)
を使うという選択肢もありそうです。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/