Flutter: flutter-samples の add-to-app のコードを読んだり試したりする
まずは定番の Fullscreen アプリから
そもそも一般的なフルFlutterなアプリと、add-to-appで参照されるFlutterモジュールの、構成上の大きな違いは、 pubspec.yaml
内に以下の定義がない(アプリ)かある(モジュール)か。
// ...
flutter:
# This section identifies your Flutter project as a module meant for
# embedding in a native host app. These identifiers should _not_ ordinarily
# be changed after generation - they are used to ensure that the tooling can
# maintain consistency when adding or modifying assets and plugins.
# They also do not have any bearing on your native host application's
# identifiers, which may be completely independent or the same as these.
module:
androidX: true
androidPackage: dev.flutter.example.flutter_module
iosBundleIdentifier: dev.flutter.example.flutterModule
いやまぁ公式ドキュメントでも module
テンプレートを指定してたりするので、それはそうか
flutter create -t module --org com.example my_flutter
このような構成のFlutterプロジェクトだと、プロジェクトルートに .android
フォルダおよび .ios
フォルダが出来上がり、この中にあるファイルを既存ネイティブアプリプロジェクトに結びつけることで、NativeとFlutterの融合が出来るようになる。
たぶんセットアップ方法は公式通りなので言及することなし。Option B
側。
ちなみに Option A
と Option B
の違いは、
Option A - Depend on the Android Archive (AAR)
This option packages your Flutter library as a generic local Maven repository composed of AARs and POMs artifacts. This option allows your team to build the host app without installing the Flutter SDK. You can then distribute the artifacts from a local or remote repository.
- 👍 AARとして配布する形式なので、Nativeアプリ開発するだけなのにFlutter SDKが必要ということがない
- 👍 あまりFlutter側を触らないケースにおいてNativeアプリ開発の阻害をしない
- 👎 Flutter側の検証にひと手間が掛かる(AAR設定しないとNative側に組み込めないので)
Option B - Depend on the module’s source code
This option enables a one-step build for both your Android project and Flutter project. This option is convenient when you work on both parts simultaneously and rapidly iterate, but your team must install the Flutter SDK to build the host app.
- 👍 すぐにビルドして試すことが出来る
- 👍 Native側とFlutter側を頻繁に行き来する場合において検証しやすい
- 👎 チーム全体でFlutter SDKの導入が必要になる
Fullscreenアプリは伝統的なカウンターアプリではあるのだが、
データモデルとしては、Android/iOS Nativeレイヤーで count
を持つようにしているため(冗長的ではあるがデータの取り回しの好例にはなる)、
まずFlutterレイヤーでのクリックイベントがNativeレイヤーにイベント伝搬し、Nativeレイヤーでインクリメントをした後に変更値をFlutterレイヤーに返すという処理になっている。
(Androidにおいて値の受け口がApplicationクラスに依存しているのはどうなんだ?と思わなくもないが、じゃあベストな所はどこかと問われると難しい。)
なお、FlutterActivityからNative側のMainActivityに遷移(バックキーで戻る)した場合、カウント値が表示されているが、ただ単にMyApplicationにストアされている値を表示しているにすぎず、onActivityResultのような渡し方ではないことに注意する。
FullscreenアプリではFlutterEngineをApplicationクラスで初期化することで、キャッシュ済みEngineを使用している。
これは高速化に寄与する面(FlutterEngineはnon-trivialなwarm-up timeがかかるので先回りしてやっておく)があるが、このタイミングでDartロジックが動き始めるので実装に注意する必要がある。
また、FlutterEngineをFlutterActivityなどの管理外で保持する場合、ライフサイクルから逸脱するので自前でdestroy()
する必要がある。
そして、FlutterEngineのプリウォームは、UI表示上の恩恵以外でも使用することが出来る(例えばService内で動かしてデータフェッチするとか)が、その際はAndroid/iOSのプラットフォーム要件を遵守するように実装する。
ところで Add-to-app な環境で Hot Reload などをどうやって動かすのか分からなかったが、
当該Flutterプロジェクトで flutter attach
を単に実行するだけで良いっぽい
ただ、俺の環境ではちょっと動作怪しかったんだよな・・・
最後に、AndroidのFullscreenアプリでは透過画面を実装できる。
つまり、Nativeの上に透明なFlutter画面があるという状態が実現できる。
Add-to-appのiOS側の構成は、
- CocoaPodsによる都度ビルド(要FlutterSDK)(オススメ)
- 毎回ビルドします(多少のキャッシュはすると思うけれど)
- FlutterEngine、コンパイル済みDartコード、Flutterプラグインを含むFrameworkの作成
- チームメンバーにFlutter SDKやCocoaPodsを導入させたくない場合
- コンパイル済みDartコードとFlutterプラグインをFrameworkで提供するがFlutterEngineはCocoaPodsで導入する
- デカすぎる
Flutter.xcframework
が気になるあなたへ
- デカすぎる
ところでadd-to-appなmoduleプロジェクトで flutter run
すると、実際のNativeプロジェクトではなく .android か .ios の中にある空のNativeプロジェクトで起動する。
Flutterレイヤーだけ動かして試したいとき有用。
・・・だけど Flutter 3.7.6 における Android テンプレートは multidex エラーが出てうまくいきません :sob:
もしPR出して修正するならこのあたりがクサい。
iOSにおけるFlutter画面の表示方法もAndroidと似通っていて、
- FlutterEngineを初期化・保持する(高速化のために推奨; 省略することも出来るがその場合画面生成時などでEngine初期化が走るのでパフォーマンスが悪い)
- SwiftUIであれUIKitであれ、FlutterEngineをグローバルで参照できるようにする
- FlutterViewControllerを用意し、他のViewControllerでpresentする
Dart VMとFlutterEngineとIsolateの関係性はあまりよく分かってない。
今時点での認識は:
- Dart VM (Dart ランタイム)
- アプリケーションセッション毎にひとつ存在
- Android: 初めてFlutterEngineが構築されるとき
- iOS: Dartのエントリーポイントが実行されたとき
- シャットダウンはされないらしい
- Dart Isolate
- Dartのメモリとスレッドのコンテナ
- FlutterEngineインスタンス毎にひとつのIsolateが存在
- 複数のIsolateを同じDart VM上でホストできる
- Android: FlutterEngineインスタンスでDartExecutor.executeDartEntrypoint()を実行すると発生
- iOS: FlutterEngineで runWithEntrypoint: を実行すると発生
- FlutterEngine
- 明確にドキュメントに書かれていないが、Dart VMとIsolate(Dartロジックの実行コンテキストとみて良いか?)を準備するためのNative側の抽象レイヤーだと推察
複数のFlutterインスタンスをNativeアプリでホストすることができる(ここではマルチテナントと呼ぶ)。
実は単一のFlutterインスタンスでもVMエントリーポイントを複数持ち起動させることが出来る。
これとの違いは、各インスタンスが独立していて、ナビゲーションスタックやUI・アプリ状態を独立させることが出来る。アプリ全体の状態保持の責任を明確化したり、モジュール性を向上させることが出来る。
将来、他者がFlutter側を開発するなどで品質が一定にならないケースがあるとき、いっそ分けてしまって我関せず、とすることもできよう。
複数のFlutterインスタンスを抱えるマルチテナントFlutterアプリを構築するには、 FlutterEngineGroup
クラスを使い、FlutterEngineを生成する。
これにより、GPUコンテキストやフォントメトリクスなどの再利用できるリソースの多くを、生成されたFlutterEngineで共用できる。
FlutterEngine間のコミュニケーションはPlatformChannelあるいはPigeonを使う必要があると書かれています。
つまり、FlutterEngine A -> Native -> FlutterEngine B みたいにトランポリンをする必要がある。