JetpackCompose にて、画面離脱時に非同期で何かする

画面を離脱するときに、何かしらの処理を行いたいことがあります。例えば画面を離脱するときに DB になんか記録するとか、API を叩いておく、とかです。アプリが動いている間にやってくれれば良い程度で、バックグラウンドに回った時のことは考えません。

画面離脱時をハンドリング

画面を離脱するときは DisposableEffect(Unit)onDispose でフックすると良さそうです。DisposableEffect はクリーンアップ処理が可能な副作用で、key に Unit を指定すれば「画面から離脱する時の処理ブロック」を実現できます。

@Composable
fun SampleScreen() {
...
    DisposableEffect(Unit) {
        onDispose {
   // ここで処理を書きたい 
        }
    }
...

developer.android.com

自分は基本的に 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/