8
7

More than 1 year has passed since last update.

【Kotlin Coroutine】コルーチンのキャンセルと再開処理

Last updated at Posted at 2021-09-22

この記事について

今回は、Android開発におけるメジャーな非同期処理方法である「Kotlin Coroutine」を用いて、ライフサイクルに応じたキャンセル処理と、非同期処理の再開方法を記事にしていきたいと思います。

Coroutineのキャンセル

まずは、Coroutineのキャンセル方法について見ていきましょう。
API通信におけるCoroutineの使用を例にとって解説します。
ちなみに、本記事ではMVVMを採用し、ViewModelから非同期処理を開始するケースで話を進めていきます。

MyViewModel.kt
   /**
    * API通信結果を取得
    */
   fun getApiResult() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                repository.accessApi()
            }
        }
    }

viewModelScope.launchで非同期処理のJobを作成し、withContext(Dispatchers.IO)で囲まれたrepository.accessApi()の処理をIOスレッドで実行するという、ごくごく一般的なCoroutineの使い方ですね。

ここでの非同期処理キャンセル方法として、Jobをキャンセルしていきたいと思います。

MyViewModel.kt
   /** コルーチンジョブ */
   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に紐づけるとすると・・・

MyFragment.kt
    override fun onPause() {
        super.onPause()
        // 非同期処理をキャンセル
        viewModel.cancelCoroutineJob()
    }

onPauseで呼び出してあげるのが最適ですかね。

Job内で複数処理を行う場合のCoroutineキャンセル

ここで、もう一歩踏み込んでみます。
上記のJobでは、一度のAPI通信のみを実行していましたが、一つのJobの中で複数の処理を行う場合のキャンセルとなるとさらなる考慮が必要となるみたいです。

公式ドキュメントを見てみると

コルーチンにおけるキャンセルは協調的です。つまり、コルーチンの Job がキャンセルされても、キャンセルを確認または停止するまでコルーチンはキャンセルされません。コルーチンでブロック操作を実行する場合、コルーチンがキャンセル可能であることを確認してください。

なるほど。。。
つまり、上のコードでいうところのcoroutineJob?.cancel()を実行したタイミングで即座にキャンセルされるわけではなく、キャンセルされたか確認してくれるまでは動き続けてるってことみたいですね。

では、API通信で取得した結果をDBに保存するテイで、キャンセル処理を行なっていきましょう。

MyViewModel.kt
   /** コルーチンジョブ */
   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になっている状態をキャッチできるようにしていく必要がありそうです。

ということで、前回までのキャンセル処理に付け加える形で実装していきましょう!

MyViewModel.kt
/** コルーチンジョブ */
   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メソッドを追加しました。
isCompletedisCancelledがどちらもtrueとなっていることが確認できた時点で、Cancelledであることは確認できそうだったので、isActiveの取得は除外しています。

これでcoroutineJobがキャンセルされたかどうかが取得できるようになりましたね。
ということで、適切な場所でこのメソッドを呼び出してあげましょう!

MyFragment.kt
    override fun onResume() {
        super.onResume()

        // 前回の画面離脱時に通信がキャンセルされている場合は再度通信を行う
        if (viewModel.isJobCancelled()) {
            viewModel.getApiResult()
        }
    }

Fragmentが破棄されていなければ、画面復帰時にはonResumeに入ってくるのでここで呼び出してあげるのがいいでしょう。

というわけで、これで無事Coroutineのキャンセルと再開をすることができました!

まとめ

本記事では、Kotlin Coroutineを用いた非同期処理を実行する上で考慮すべき、キャンセルと再開処理について述べさせていただきました。

Coroutineは一見簡単に非同期処理が書けるように見えますが、掘り下げていけば色々と考慮点がありそうなので、自分ももっと勉強していかなければと思いました。

参考記事

8
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7