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

# この記事について
今回は、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の中で複数の処理を行う場合のキャンセルとなるとさらなる考慮が必要となるみたいです。

[公式ドキュメント](https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja#coroutine-cancellable)を見てみると
>コルーチンにおけるキャンセルは協調的です。つまり、コルーチンの 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](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/)を見たところ、このようになっているみたいですね。
今回参照したいのは、「キャンセルされたかどうか」なので、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`メソッドを追加しました。
`isCompleted`と`isCancelled`がどちらも`true`となっていることが確認できた時点で、`Cancelled`であることは確認できそうだったので、`isActive`の取得は除外しています。

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

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

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

```

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

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


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

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

# 参考記事
https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja#coroutine-cancellable

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/

https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629