LoginSignup
2
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-11

意図

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

準備

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本体コードがあります。
image.png

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秒待ちの処理とフィボナッチ数の取得を待ち合わせるもの。
それぞれ、 getWithSyncgetWithAsync として実装します。

    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.successresult.error も呼び出されない状況となりました。
(例えば、 getWithSynccount = -1 で実行した場合に StackOverflowError となってクラッシュしました。
getWithAsyncfibonacci の中で例外を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

2
0
0

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
2
0