既存のAndroidアプリにFlutterを追加する方法
既存のAndroidアプリにFlutter画面を追加するのは、公式ドキュメントを読む限り簡単そうに見えます。新規のデモアプリであれば実際に簡単です。Flutterモジュールを作成し、マニフェストにFlutterActivityを宣言すれば、KotlinからFlutter画面が開きます。本当にそれだけです。それでもこのページがあるのは、何年もの歴史があり、数百の画面を抱え、誰も触りたがらないビルド設定を持つ実際のAndroidアプリでは、最初の画面こそが簡単な部分だからです。
add-to-app(既存ネイティブアプリへのFlutter組み込み)がチームにもたらすものは明快です。両プラットフォーム向けに一度だけ書けばよい画面と、ネイティブアプリのリリースを続けながら開発者がホットリロードで反復できるFlutterモジュールです。一方で、ホスト側に何が求められるかはそれほど明らかではなく、クイックスタートもほとんど触れていません。このページでは、それらの判断を下すAndroidのテックリードの視点から、私たちが実際に遭遇した問題とあわせて順に見ていきます。
関係する仕組みの大半、つまりエンジン管理、プリウォーム、型付きナビゲーション、共有状態は、どのチームも何らかの形で作ることになる汎用的なadd-to-appの作業です。私たちはそれをカバーするためにInlayを作り、関連する箇所で紹介していますが、フレームワークを使っても自前で書いても、下すべき判断は同じです。ステップごとのセットアップはInlayのGetting startedのAndroidセクションを、どの画面から移行すべきかは計画のページと、自分のコードから判断するならConvergeを参照してください。Flutterエンジンの仕組みをまだ読んでいない場合は、そちらから始めてください。
Androidプロジェクトで何が変わるか
Flutterはモジュールとして導入されます。これは独立したFlutterプロジェクト(flutter create --template module)で、ホストのビルドが接続する隠し.ラッパーを持っています。ホスト側のものは何も移動せず、モジュールはその隣に追加されます。初日からチームにとって3つのことが変わります。
- ビルドに2つ目のコンパイラが加わります。 アプリのコンパイルにDartのコンパイルが含まれるようになるため、ネイティブビルドは遅くなり、ホストをビルドするすべてのマシンにFlutter SDKが必要になります。ただし、モジュールをビルド済みアーティファクトとして取り込む場合は別です(後述)。
- Flutterのコードはただ1つのモジュールに置かれます。 Flutterは複数のFlutterライブラリを1つのアプリにまとめることをサポートしていないため、すべてのチームのFlutter機能が1つのモジュール、1つのDartビルドに集まります。最初からパッケージに分割した共有モジュールを前提に計画してください。詳しくはスケーリングとコード共有を参照してください。
- 日々の開発ループが2つに分かれます。 Flutter開発者は、モジュールを単体のアプリとして通常の
flutter runで動かして作業できます。ホストが必要になるのは統合を確認するときだけです。最初の週から、モジュールを単体で動かせる状態を保ってください。2つのデバッガをアタッチするハイブリッドデバッグは遅く不安定で、これを省いたチームは一日中Gradleを待つことになります。
Flutterのホスト方法:Activity、Fragment、Compose
Flutterはネイティブのコンテナに描画され、そのコンテナはアプリのアーキテクチャに合わせる必要があります。
- Flutter画面ごとにActivityを使う方法が最もシンプルで、Flutterが画面全体を担う場合に適しています。マルチActivityのホストの多くはここから始めます。
- Fragmentは、FragmentやNavigation Componentを中心に構築されたシングルActivityのアプリで必要になり、Flutterが画面の一部になる場合の唯一の選択肢です。注意点として、ホストのActivityは7つのコールバックをFragmentに転送する必要があります(戻る操作、新しいIntent、権限、Activityの結果、メモリのトリムなど。完全なリスト)。1つでも漏れると何のエラーもなく失敗し、権限ダイアログやファイルピッカーが結果を返さなくなるだけです。Inlayは転送を配線済みの
InlayFlutterHostActivityと、独自のベースActivityを持つホスト向けのデリゲートを提供しています。 - Jetpack ComposeのホストにはComposableを使います。Inlayのオプションパッケージ
inlay_composeが、FragmentをInlayFlutterScreenとしてラップします。
ネイティブとFlutterの境界をまたぐナビゲーションと状態
ネイティブ画面とFlutter画面の間のすべての経路はコントラクトです。どの画面へ、どのパラメータで、何を返すのか。文字列のルートと生のメソッドチャネルで書くと、これらのコントラクトは気づかないうちにずれていきます。片側でパラメータ名を変えると、もう片側でランタイムエラーになります。コントラクトは型付きにし、コード生成してください。 そうすれば変更は両側でコンパイルエラーになります。メソッド呼び出しについてはPigeonが、ルートと共有状態についてはInlayがこれを実現します(選択肢の比較はネイティブとFlutterの通信を参照)。何を選ぶにしても、同じ仕組みが逆方向にも機能する必要があります。つまり、ホストが一度だけ登録するハンドラを通じて、Flutterがネイティブ画面を要求できなければなりません。
もう半分は共有状態です。異なるエンジン上の2つのFlutter画面はDartのメモリを共有しないため、セッション、ユーザー設定、テーマはネイティブ側に置き、すべてのエンジンに同期する必要があります。境界を越えるものについての私たちのルールは、アプリレベルの関心事はネイティブに残すことです。プッシュ通知、バックグラウンド処理、アナリティクス、セッション管理などがそれにあたり、Flutterは型付きのコントラクトを通じてその結果を受け取ります。長寿命のエンジンを前提とするプラグインは、画面ごとに作成されるエンジンでは生き残れません。マルチエンジン構成でのプラグインを参照してください。
大規模なAndroidホストで壊れるもの
以下は、ホストに何年もの歴史がある場合にのみ表面化する問題です。どれも難しくはありませんが、発見が遅れたせいで、それぞれ1スプリント分のコストがかかったことがあります。
- リポジトリの宣言。 セットアップガイドは、リポジトリが
PREFER_SETTINGS付きのdependencyResolutionManagementに宣言されていることを前提としています。より厳格なモードを使っているホストや、リポジトリがモジュールのビルドファイルに散らばっているホストでは、まずGradleの整理が必要です。 - FlutterのMavenリポジトリ。 エンジンのアーティファクトは
storage.からダウンロードされます。私たちのあるプロジェクトでは、これが社内ネットワークでブロックされていることがすぐに判明しました。依存関係がArtifactoryやNexusを経由する場合は、最初のスプリントの前にミラーを依頼してください(ビルドはまさにこの目的でgoogleapis. com FLUTTER_STORAGE_BASE_URLを読み取ります)。 - ツールチェーン。 FlutterはJava 17互換を必要とし、Kotlin DSLでの統合にはFlutter 3.27以降が必要です。後のFlutterのアップグレードがホストのツールチェーンのアップグレードを強いることもあるので、両者はまとめて計画してください。
- ABI。 FlutterがAOTコードを提供するのは
arm64-v8a、armeabi-v7a、x86_64のみです。サードパーティのSDKが他のABIを持ち込む場合は、ガイドの推奨どおりabiFiltersでホストを制限してください。さもないと、それらの端末でlibflutter.が見つからずにクラッシュします。so - AndroidXとminSdk。 モジュールはAndroidXのホストのみをサポートし、
minSdkはFlutter、使用するプラグイン、Inlay(API 24)のすべての要件を満たす必要があります。
最初のFlutter画面を本番に出す前に
- 画面遷移。 Flutterコンテナにテーマに合った背景色を設定してください。そうしないと、最初のフレームの前にユーザーに白いフラッシュが見えます。
- 戻るナビゲーション。 システムの戻る操作と予測型「戻る」ジェスチャーは、コンテナを閉じる前にFlutter内部のルートをポップする必要があります。Inlayの
InlayBackButtonDispatcherがこの受け渡しをシームレスに処理します。 - テーマ、ロケール、フォントスケール。 ホストがシステム設定に従わずアプリ内でこれらを上書きしている場合、Flutterはそれを知りません。値を渡してください(たとえばInlayの共有ストアを使います)。
- 難読化されたリリースビルド。 プラグインのクラスに対するkeepルールの漏れは、R8を有効にしたときにしか表面化しません。
- プロセスの終了。 「アクティビティを保持しない」を有効にしてテストしてください。
- アプリサイズ。 デバッグAPKではなく、Play ConsoleでリリースAABのダウンロードサイズを計測してください。
- クラッシュレポート。 Dartのエラーは、明示的に報告しない限りクラッシュダッシュボードに届きません。