Open13

Flutter: flutter-samples の add-to-app のコードを読んだり試したりする

Kouta ImanakaKouta Imanaka

そもそも一般的なフル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 テンプレートを指定してたりするので、それはそうか
https://docs.flutter.dev/development/add-to-app/android/project-setup#create-a-flutter-module

flutter create -t module --org com.example my_flutter

Kouta ImanakaKouta Imanaka

このような構成のFlutterプロジェクトだと、プロジェクトルートに .android フォルダおよび .ios フォルダが出来上がり、この中にあるファイルを既存ネイティブアプリプロジェクトに結びつけることで、NativeとFlutterの融合が出来るようになる。

たぶんセットアップ方法は公式通りなので言及することなし。
https://docs.flutter.dev/development/add-to-app/android/project-setup#manual-integration
これの Option B 側。

ちなみに Option AOption 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の導入が必要になる
Kouta ImanakaKouta Imanaka

Fullscreenアプリは伝統的なカウンターアプリではあるのだが、
データモデルとしては、Android/iOS Nativeレイヤーで count を持つようにしているため(冗長的ではあるがデータの取り回しの好例にはなる)、
まずFlutterレイヤーでのクリックイベントがNativeレイヤーにイベント伝搬し、Nativeレイヤーでインクリメントをした後に変更値をFlutterレイヤーに返すという処理になっている。
(Androidにおいて値の受け口がApplicationクラスに依存しているのはどうなんだ?と思わなくもないが、じゃあベストな所はどこかと問われると難しい。)

なお、FlutterActivityからNative側のMainActivityに遷移(バックキーで戻る)した場合、カウント値が表示されているが、ただ単にMyApplicationにストアされている値を表示しているにすぎず、onActivityResultのような渡し方ではないことに注意する。

Kouta ImanakaKouta Imanaka

FullscreenアプリではFlutterEngineをApplicationクラスで初期化することで、キャッシュ済みEngineを使用している。
これは高速化に寄与する面(FlutterEngineはnon-trivialなwarm-up timeがかかるので先回りしてやっておく)があるが、このタイミングでDartロジックが動き始めるので実装に注意する必要がある。

また、FlutterEngineをFlutterActivityなどの管理外で保持する場合、ライフサイクルから逸脱するので自前でdestroy()する必要がある。

そして、FlutterEngineのプリウォームは、UI表示上の恩恵以外でも使用することが出来る(例えばService内で動かしてデータフェッチするとか)が、その際はAndroid/iOSのプラットフォーム要件を遵守するように実装する。

Kouta ImanakaKouta Imanaka

最後に、AndroidのFullscreenアプリでは透過画面を実装できる。
つまり、Nativeの上に透明なFlutter画面があるという状態が実現できる。

Kouta ImanakaKouta Imanaka

Add-to-appのiOS側の構成は、

  1. CocoaPodsによる都度ビルド(要FlutterSDK)(オススメ)
    • 毎回ビルドします(多少のキャッシュはすると思うけれど)
  2. FlutterEngine、コンパイル済みDartコード、Flutterプラグインを含むFrameworkの作成
    • チームメンバーにFlutter SDKやCocoaPodsを導入させたくない場合
  3. コンパイル済みDartコードとFlutterプラグインをFrameworkで提供するがFlutterEngineはCocoaPodsで導入する
    • デカすぎる Flutter.xcframework が気になるあなたへ

https://docs.flutter.dev/development/add-to-app/ios/project-setup

Kouta ImanakaKouta Imanaka

ところでadd-to-appなmoduleプロジェクトで flutter run すると、実際のNativeプロジェクトではなく .android か .ios の中にある空のNativeプロジェクトで起動する。
Flutterレイヤーだけ動かして試したいとき有用。

・・・だけど Flutter 3.7.6 における Android テンプレートは multidex エラーが出てうまくいきません :sob:


もしPR出して修正するならこのあたりがクサい。

https://github.com/flutter/flutter/blob/master/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl

Kouta ImanakaKouta Imanaka

iOSにおけるFlutter画面の表示方法もAndroidと似通っていて、

  1. FlutterEngineを初期化・保持する(高速化のために推奨; 省略することも出来るがその場合画面生成時などでEngine初期化が走るのでパフォーマンスが悪い)
  2. SwiftUIであれUIKitであれ、FlutterEngineをグローバルで参照できるようにする
  3. FlutterViewControllerを用意し、他のViewControllerでpresentする
Kouta ImanakaKouta Imanaka

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側の抽象レイヤーだと推察

https://docs.flutter.dev/development/add-to-app/performance

Kouta ImanakaKouta Imanaka

複数のFlutterインスタンスをNativeアプリでホストすることができる(ここではマルチテナントと呼ぶ)。

https://docs.flutter.dev/development/add-to-app/multiple-flutters

実は単一のFlutterインスタンスでもVMエントリーポイントを複数持ち起動させることが出来る。
これとの違いは、各インスタンスが独立していて、ナビゲーションスタックやUI・アプリ状態を独立させることが出来る。アプリ全体の状態保持の責任を明確化したり、モジュール性を向上させることが出来る。

将来、他者がFlutter側を開発するなどで品質が一定にならないケースがあるとき、いっそ分けてしまって我関せず、とすることもできよう。

複数のFlutterインスタンスを抱えるマルチテナントFlutterアプリを構築するには、 FlutterEngineGroup クラスを使い、FlutterEngineを生成する。
これにより、GPUコンテキストやフォントメトリクスなどの再利用できるリソースの多くを、生成されたFlutterEngineで共用できる。

FlutterEngine間のコミュニケーションはPlatformChannelあるいはPigeonを使う必要があると書かれています。
つまり、FlutterEngine A -> Native -> FlutterEngine B みたいにトランポリンをする必要がある。