意図
Flutterアプリを実装していく中で、Android pluginを自作するケースはまれによくあります。
その中でも、「plugin内で非同期処理をしたい」というケースがありました。
今回は、coroutineを利用して、それを実現してみます。
TL;DR
- そもそも非同期に実行されるので、Coroutineが必須ではないケースが多そう
- それでもCoroutineを使う場合は、例外処理に気をつける(クラッシュするならまだしも、制御が戻ってこないケースがある)
サンプル
以下の3つのパターンで、フィボナッチ数(重めの処理)を計算するサンプルを作りました。
コード全体: https://github.com/noboru-i/flutter_coroutine_sample
- Coroutineを利用して、Android plugin側で非同期に計算したもの(3秒sleepと待ち合わせもしている)
- Android plugin側では同期的に計算しているもの
- Dartコード側で同期的に計算しているもの
3.だけがUIアニメーションをブロックしていますが、それ以外はブロックしていないことがわかります。
(↓クリックしないと、アニメーションgifが動かないかも?)
準備
flutter_coroutine_sample
という本体側のFlutterプロジェクトと、
long_process
というpluginプロジェクトを作成します。
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本体コードがあります。
Android側の実装
packages/long_process/android/build.gradle
を開いて、coroutine関連の依存を追加します。
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
を作成します。
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
として実装します。
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