この記事について
今回は、Android開発におけるメジャーな非同期処理方法である「Kotlin Coroutine」を用いて、ライフサイクルに応じたキャンセル処理と、非同期処理の再開方法を記事にしていきたいと思います。
Coroutineのキャンセル
まずは、Coroutineのキャンセル方法について見ていきましょう。
API通信におけるCoroutineの使用を例にとって解説します。
ちなみに、本記事ではMVVMを採用し、ViewModelから非同期処理を開始するケースで話を進めていきます。
/**
* API通信結果を取得
*/
fun getApiResult() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
repository.accessApi()
}
}
}
viewModelScope.launch
で非同期処理のJob
を作成し、withContext(Dispatchers.IO)
で囲まれたrepository.accessApi()
の処理をIOスレッドで実行するという、ごくごく一般的なCoroutineの使い方ですね。
ここでの非同期処理キャンセル方法として、Job
をキャンセルしていきたいと思います。
/** コルーチンジョブ */
private var coroutineJob: Job? = null
/**
* API通信結果を取得
*/
fun getApiResult() {
coroutineJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
repository.accessApi()
}
}
}
/**
* 非同期処理をキャンセルする
*/
fun cancelCoroutineJob() {
coroutineJob?.cancel()
}
ViewModelのフィールド変数としてcoroutineJob
を定義しました。
定義したcoroutineJob
に、viewModel.launch
で生成されるJobを代入し、cancelCoroutineJob()
メソッドが呼び出されたタイミングで、キャンセルが実行されることになります。
例えば、このViewModelに紐づくActivityやFragmentから、ユーザーが離脱したタイミングで非同期処理がキャンセルされるのが望ましいですね。
ということで、MyFragment
というFragmentに紐づけるとすると・・・
override fun onPause() {
super.onPause()
// 非同期処理をキャンセル
viewModel.cancelCoroutineJob()
}
onPause
で呼び出してあげるのが最適ですかね。
Job内で複数処理を行う場合のCoroutineキャンセル
ここで、もう一歩踏み込んでみます。
上記のJobでは、一度のAPI通信のみを実行していましたが、一つのJobの中で複数の処理を行う場合のキャンセルとなるとさらなる考慮が必要となるみたいです。
公式ドキュメントを見てみると
コルーチンにおけるキャンセルは協調的です。つまり、コルーチンの Job がキャンセルされても、キャンセルを確認または停止するまでコルーチンはキャンセルされません。コルーチンでブロック操作を実行する場合、コルーチンがキャンセル可能であることを確認してください。
なるほど。。。
つまり、上のコードでいうところのcoroutineJob?.cancel()
を実行したタイミングで即座にキャンセルされるわけではなく、キャンセルされたか確認してくれるまでは動き続けてるってことみたいですね。
では、API通信で取得した結果をDBに保存するテイで、キャンセル処理を行なっていきましょう。
/** コルーチンジョブ */
private var coroutineJob: Job? = null
/**
* API通信結果を取得し、結果をDBに保存
*/
fun getApiResult() {
coroutineJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
// API通信結果を取得
val result = repository.accessApi()
// 非同期処理がキャンセルされている場合はここで処理から抜ける
// if (coroutineJob?.isActive == false) return@withContext
yield()
// DBにAPIから取得した結果を保存
repository.insertResult(result)
}
}
}
/**
* 非同期処理をキャンセルする
*/
fun cancelCoroutineJob() {
coroutineJob?.cancel()
}
こんな感じでしょうか。
APIから結果の取得が完了し、DBに結果を保存するタイミングでキャンセル確認を行っています。
isActive
ではなく、ensureActive
でもいいみたいですね。
ご指摘いただき、yield()
もしくはensureActive()
の方が適切なようでした。
(2021/10/09追記)
このタイミングでcancelCoroutineJob()
が呼ばれていれば、非同期処理がキャンセルされるというわけですね。なるほどなるほど。。。
以上、ここまでがキャンセル処理になります!
Coroutineの再開処理
ここからは、キャンセルされた非同期処理の再開方法について見ていきます。
そもそも、Coroutine自体に再開処理が備わっているわけではないので、前回の非同期処理結果を取得し、処理がキャンセルしていた場合には再度非同期処理を開始するという方法をとっていきたいと思います。
ではまず、非同期処理のフェーズごとにJob
がどのような状態になっているのかを見ていきましょう!
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
Jobのdocを見たところ、このようになっているみたいですね。
今回参照したいのは、「キャンセルされたかどうか」なので、StateがCancelled
になっている状態をキャッチできるようにしていく必要がありそうです。
ということで、前回までのキャンセル処理に付け加える形で実装していきましょう!
/** コルーチンジョブ */
private var coroutineJob: Job? = null
/**
* API通信結果を取得し、結果をDBに保存
*/
fun getApiResult() {
coroutineJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
// API通信結果を取得
val result = repository.accessApi()
// 非同期処理がキャンセルされている場合はここで処理から抜ける
yield()
// DBにAPIから取得した結果を保存
repository.insertResult(result)
}
}
}
/**
* 非同期処理をキャンセルする
*/
fun cancelCoroutineJob() {
coroutineJob?.cancel()
}
/**
* 前回の通信がキャンセルされて終わったかどうかをチェックする
*
* @return true:通信がキャンセルされた, false:通信成功 or 通信中 or null
*/
fun isJobCanceled(): Boolean {
return coroutineJob?.let {
it.isCompleted && it.isCancelled
} ?: false
}
isJobCancelled
メソッドを追加しました。
isCompleted
とisCancelled
がどちらもtrue
となっていることが確認できた時点で、Cancelled
であることは確認できそうだったので、isActive
の取得は除外しています。
これでcoroutineJob
がキャンセルされたかどうかが取得できるようになりましたね。
ということで、適切な場所でこのメソッドを呼び出してあげましょう!
override fun onResume() {
super.onResume()
// 前回の画面離脱時に通信がキャンセルされている場合は再度通信を行う
if (viewModel.isJobCancelled()) {
viewModel.getApiResult()
}
}
Fragmentが破棄されていなければ、画面復帰時にはonResume
に入ってくるのでここで呼び出してあげるのがいいでしょう。
というわけで、これで無事Coroutineのキャンセルと再開をすることができました!
まとめ
本記事では、Kotlin Coroutineを用いた非同期処理を実行する上で考慮すべき、キャンセルと再開処理について述べさせていただきました。
Coroutineは一見簡単に非同期処理が書けるように見えますが、掘り下げていけば色々と考慮点がありそうなので、自分ももっと勉強していかなければと思いました。
参考記事