Kotlin Coroutineを利用したFlutter pluginの作成

## 意図

Flutterアプリを実装していく中で、Android pluginを自作するケースはまれによくあります。
その中でも、「plugin内で非同期処理をしたい」というケースがありました。
今回は、coroutineを利用して、それを実現してみます。

## TL;DR

- そもそも非同期に実行されるので、Coroutineが必須ではないケースが多そう
- それでもCoroutineを使う場合は、例外処理に気をつける（クラッシュするならまだしも、制御が戻ってこないケースがある）

## サンプル

以下の3つのパターンで、フィボナッチ数（重めの処理）を計算するサンプルを作りました。

コード全体: https://github.com/noboru-i/flutter_coroutine_sample

1. Coroutineを利用して、Android plugin側で非同期に計算したもの（3秒sleepと待ち合わせもしている）
2. Android plugin側では同期的に計算しているもの
3. Dartコード側で同期的に計算しているもの

3.だけがUIアニメーションをブロックしていますが、それ以外はブロックしていないことがわかります。

（↓クリックしないと、アニメーションgifが動かないかも？）
![flutter_coroutine.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8708/5ce0a067-4ae8-8380-790c-b7f5342227f9.gif)


## 準備

`flutter_coroutine_sample` という本体側のFlutterプロジェクトと、
`long_process` というpluginプロジェクトを作成します。

```bash
flutter create --project-name flutter_coroutine_sample --org dev.noboru --template=app --platforms=ios,android .
mkdir -p packages/long_process
cd packages
flutter create --org dev.noboru --template=plugin --platforms=android,ios long_process
```

Android側の実装をしていくので、Android Studioを開いておきます。
"Open" より、 `packages/long_process/example/android/build.gradle` を開きます。
以下のように、Project viewで"Android"を選択すると、"app"の他にplugin名の表示があり、その中にplugin本体コードがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8708/cf53514e-70d4-88ac-49be-07232b282601.png)

## Android側の実装

`packages/long_process/android/build.gradle` を開いて、coroutine関連の依存を追加します。

```gradle
android {
...
    dependencies {
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
    }
}
```

`packages/long_process/android/src/main/kotlin/dev/noboru/long_process/LongProcessPlugin.kt` に、 `CoroutineScope`　を作成します。

```kotlin
class LongProcessPlugin : FlutterPlugin, MethodCallHandler {
    private lateinit var channel: MethodChannel

    private val mainScope = CoroutineScope(Dispatchers.Main)
// ...
    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
        mainScope.cancel()
    }
}
```

これを利用して、 `onMethodCall` 内に実装していきます。
今回は、"CPU heavyな処理"としてフィボナッチ数の取得を用意しました。

2つの機能を作ります。
1つは、シンプルにフィボナッチ数の取得を呼び出すもの。 
もう1つは、3秒待ちの処理とフィボナッチ数の取得を待ち合わせるもの。
それぞれ、 `getWithSync` と `getWithAsync` として実装します。

```kotlin
    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            // This is a simple case.
            "getWithSync" -> {
                try {
                    val count = call.argument<Long>("count")!!
                    val fibonacciResult = fibonacci(count)
                    result.success(fibonacciResult)
                } catch (e: Throwable) {
                    result.error("ERROR", "unknown error on getWithAsync", e)
                }
            }
            // This is the case of a joining task.
            "getWithAsync" -> {
                mainScope.launch(Dispatchers.IO) {
                    supervisorScope {
                        try {
                            val count = call.argument<Long>("count")!!
                            var fibonacciResult: Long? = null
                            awaitAll(
                                async { fibonacciResult = fibonacci(count) },
                                async { threeSecondsProcess() })
                            result.success(fibonacciResult)
                        } catch (e: Throwable) {
                            result.error("ERROR", "unknown error on getWithAsync", e)
                        }
                    }
                }
            }
        }
    }
```

特筆すべきは、 `catch (e: Throwable)` だと思います。
これが無い場合、処理内で想定外の `Exception/Error` が発生した場合に、どこにも伝搬されず、 `result.success` も `result.error` も呼び出されない状況となりました。
（例えば、 `getWithSync` を `count = -1` で実行した場合に `StackOverflowError` となってクラッシュしました。
 `getWithAsync` で `fibonacci` の中で例外をthrowしてみたら、ログにも何も出ないまま、Dart側に制御が戻りませんでした）
通常、Dartコードでは `await` を伴って実行しているため、「それ以降の処理がいつまで経っても実行されず、デバッグ実行してもそこで迷子になってしまう。」という自体になります。

## そもそも、Coroutineにする必要があるのか？

最初に書いたように、 `getWithSync` のケースでも、UIをブロックしない動作になっています。
単純に「Androidコード側に重たい処理を書きたい」というケースであれば、わざわざCoroutineにしないほうが良いと思います。（例外処理が理解しづらくなる）
今回のサンプルのように、「複数の非同期処理を、いい感じに実行したい」とかのケースではCoroutineを利用するのが良いと思いますが、例外処理には気をつけて、「必ず、何かしらの結果をDart側に返す」という意識が必要だと思います。

## 参考

Flutter pluginの作成
https://docs.flutter.dev/development/packages-and-plugins/developing-packages

Kotlin Coroutineの導入
https://developer.android.com/kotlin/coroutines?hl=ja

Coroutineを利用した場合の例外処理
https://star-zero.medium.com/coroutines-async%E3%81%A8exception-9c0f079edb0e