Inlayをはじめる
InlayをFlutterモジュールとネイティブのホストに組み込む方法。パッケージ、コード生成、ネイティブ側の初期化、エンジンのプリウォーム。
Inlayは、アノテーションを付けたDartのスキーマクラスから、Dart、Swift、Kotlin向けの 型安全なナビゲーションAPIと状態共有APIを生成します。画面間のナビゲーションや状態共有を 行う前に、3つのものを揃える必要があります。適切なパッケージ、生成されたコード、そして 一度だけ行うネイティブ側の初期化です。このページでは、そのセットアップを順に説明します。 機能そのものについては、ナビゲーションと 状態管理のページで扱います。
Inlayの動作要件
| 最小要件 | |
|---|---|
| Flutter | 3.41(Swift Package Manager経由でiOSホストに組み込む場合は3.44以上) |
| Dart | 3.11 |
| iOS | inlayプラグインは13.0、SwiftUIヘルパー(InlayFlutterView、.)は16.0 |
| Android | API 24(Android 7.0) |
これらはフレームワーク自体の下限です。ビルドに使うFlutter SDKがより高い要件を課す場合があります (たとえばFlutter 3.47ではiOSの最小バージョンが15に引き上げられました)。実際の最小要件は、いずれか高い方になります。
モジュールとホストのビルドに使うFlutterのバージョンは固定してください。その理由と方法は バージョニングを参照してください。
パッケージ
Inlayはいくつかのパッケージに分かれています。
| パッケージ | 役割 |
|---|---|
inlay | フレームワークのコアとなるプラグイン(Dart + Swift + Kotlin)。ナビゲーション、ストレージ、プラットフォームチャネル。 |
inlay_gen | コード生成ツール。アノテーションを付けたスキーマから、Dart、Kotlin、Swift向けの型付きルート/ストアクラスを生成します。dev dependencyとして追加します。 |
inlay_compose | Jetpack Compose向けのヘルパー(InlayFlutterScreen、InlayFlutterDialog)を追加するオプションのプラグイン。 |
Flutterモジュールのpubspec.にinlayを追加します。コード生成はビルド時にしか実行されないため、
inlay_gen(とbuild_runner)はdev_dependenciesに入れます。inlay_composeは、
AndroidホストでJetpack Composeを使っている場合にのみ追加します。
# pubspec.yaml of your Flutter module
dependencies:
inlay: ^0.1.1
inlay_compose: ^0.1.1 # optional - Jetpack Compose only
dev_dependencies:
inlay_gen: ^0.1.1
build_runner: ^2.11.0Composeを使わないプロジェクトはinlayだけに依存します。Androidのクラスパスに
Composeの依存関係が入り込むことはありません。その場合はInlayNavigator.や
InlayNavigator.を直接使ってください。
コード生成
Inlayの型安全性はコード生成によって実現されています。ルートとストアを、アノテーションを付けた
普通のDartクラスとして一度記述するだけで、inlay_genがそれに対応する型安全なクラスを
Dart、Kotlin、Swift向けに出力します。同期がずれかねない手書きのルートパスや文字列キーは
ありません。スキーマが変われば、その不一致は、スキーマを使うすべてのプラットフォームで
コンパイルエラーとして現れます。
スキーマファイルはPigeonの定義と同じように機能し、仕様の記述専用です。アプリのコードが スキーマファイルをimportすることはなく、importするのは生成された出力です。アノテーションを 付けたクラスは、ルートとストアの形をジェネレーターに伝えるためだけのものです。
スキーマファイルを書く
ルートとストアの定義は専用のファイルに置きましょう。慣例では
lib/とlib/です。ルートはアノテーションを付けたクラスで、
そのフィールドが型付きでシリアライズされるパラメータになります。ストアはアノテーションを付けた
クラスで、そのフィールドが同期されるキーバリューのレコードになります。
import 'package:inlay/inlay.dart';
// A Flutter screen. `:userId` in the path maps to the `userId` field. Keep path
// fields required - the generator doesn't enforce it, and a null path parameter
// would encode as an empty path segment.
// Simple fields (primitives, enums) become optional query parameters; complex
// fields like `badges` travel only in the typed params envelope.
@InlayFlutterRoute('/profile/:userId')
class ProfilePage {
const ProfilePage({required this.userId, this.badges});
final String userId;
final List<UserBadge>? badges; // nested types and enums are supported
}
// A native screen reachable from Flutter, type-safely.
@InlayNativeRoute()
class NativeSettingsPage {
const NativeSettingsPage({this.source});
final String? source;
}
class UserBadge {
const UserBadge({required this.label, required this.level});
final String label;
final BadgeLevel level;
}
enum BadgeLevel { bronze, silver, gold }import 'package:inlay/inlay.dart';
@InlayStore(key: 'user_preferences')
class UserPreferencesStore {
const UserPreferencesStore({
@InlayStoreKey() required this.userId, // marks the identifying key
this.displayName = 'User',
this.darkMode = false,
this.tags = const [],
});
final String userId;
final String displayName;
final bool darkMode;
final List<String> tags;
}ダイアログ、パスパラメータとクエリパラメータの違い、ネイティブルートのハンドラー、 ストアのキーなど、アノテーションの完全なリファレンスは、ルートの定義と ストアの定義を参照してください。
inlay. yamlを設定する
各モジュールのルートにはinlay.があります。ジェネレーターにスキーマファイルの場所を伝え、
言語ごとに生成コードの出力先を宣言します。
# Schema inputs (specs only - not imported by app code)
routes: lib/inlay/routes.dart
stores: lib/inlay/stores.dart
# Output target per language. Omit kotlin or swift to skip that language.
# Dart is always generated; under build_runner it defaults to lib/src/generated/.
dart:
output: lib/src/generated/
kotlin:
output: android/src/main/kotlin/com/example/app/generated/
package: com.example.app.generated # required for Kotlin
swift:
output: ios/Classes/Generated/KotlinとSwiftの出力先は、ネイティブのホストがコンパイルするディレクトリに向けてください。 そうすれば、生成されたdata classやstructがネイティブのビルドにそのまま取り込まれます。
ジェネレーターを実行する
推奨される方法はbuild_runnerのビルダーを使うことです。inlay_genはビルダーを
auto_apply: dependentsで宣言しているため、依存関係にinlay_genを持つモジュールでは自動的に
実行されます。build.を追加する必要はありません。モジュールのディレクトリで生成を行い、
スキーマを変更するたびに再生成してください。
dart run build_runner build
# or, while iterating on schemas:
dart run build_runner watchbuild_runnerを使いたくない場合(たとえばCIのステップやネイティブのビルドフックなど)は、
スタンドアロンのCLIが同じinlay.を読み込みます。
dart run inlay_gen:inlay_gen --config inlay.yaml生成されるもの
上記のスキーマから、ジェネレーターは言語の組ごとに1つのファイルを出力します。境界の同期を 保つため、どのファイルにも同じ型が含まれます。
| ファイル | 内容 |
|---|---|
routes. | sealedのFlutterRoute / FlutterDialogRoute階層とネイティブルートのクラス。シリアライズが組み込まれており、網羅的なパターンマッチングが可能です。 |
stores. | キーバリュー層との間のシリアライズを備えた型付きのストアクラス。 |
Routes. / Routes. | 同じルートに対応するKotlinのdata class/Swiftのstructと、型付きのNativeRouteHandlerベース。 |
Stores. / Stores. | 同じストアに対応するKotlinのdata class/Swiftのstruct。 |
アプリのコードはDartの出力をimportし(import 'package:my_module/)、
ネイティブのホストは生成されたKotlin/Swiftの型を直接使います。*.ファイルはすべて
生成コードとして扱ってください。手で編集せず、代わりにジェネレーターを再実行します。
ネイティブ側の初期化
開発中はFlutterモジュールをスタンドアロンのアプリとして実行できる状態に保ち、
ホストとの統合は最後のステップとして扱うことをおすすめします。ハイブリッドデバッグ
(ネイティブのデバッガーとFlutterのデバッガーを同時にアタッチすること)は遅く、不安定に
なりがちです。大規模にadd-to-appを進めているチームは、ほぼすべてをまずスタンドアロンで
開発しています。日々の開発ループはモジュールでの普通のflutter runのままです。以下の
ホスト側の配線は一度きりのセットアップです。
アプリの起動時に、フレームワークの初期化を一度だけ呼び出します。これにより共有の
FlutterEngineGroupが生成され、Flutter → ネイティブのルートを処理するハンドラーが登録されます。
どちらのエントリポイントも、オプションのprewarmパラメータ(デフォルトはtrue)を受け取ります。
// AppDelegate
import FlutterPluginRegistrant
// Register plugins on every engine Inlay creates - set before start()
// so the prewarmed engine is covered too.
InlayNavigator.shared.setOnEngineCreated { engine in
GeneratedPluginRegistrant.register(with: engine)
}
InlayNavigator.shared.start() // prewarm: true by default
InlayNavigator.shared.setNativeRouteHandler(MyNativeRouteHandler())iOSではsetOnEngineCreatedの行を省略しないでください。これがモジュールのプラグインを
動作させる要です。Flutterのプラグインはエンジンごとに登録されますが、iOSのembeddingは
それを代わりに行ってはくれません。この呼び出しがないと、iOSのネイティブコードを持つ
すべてのプラグインが、どの画面でもMissingPluginExceptionで失敗します。Androidは
プラグインを自動的に登録するため、初期化に同等の行は必要ありません。仕組みについては
マルチエンジン構成でのプラグインで解説しています。
NativeRouteHandlerの実装例と、Flutter → ネイティブのナビゲーションがどのように
ディスパッチされるかについては、ネイティブルートの処理
を参照してください。
Dartのエントリポイント
フレームワークが立ち上げるすべてのFlutterエンジンは、同じDartのエントリポイント(デフォルトでは
inlayMain)を実行します。最低限、runAppの前にFlutterのバインディングと共有ストレージ層を
初期化する必要があります。
@pragma('vm:entry-point')
void inlayMain() async {
WidgetsFlutterBinding.ensureInitialized();
await KeyValueStorage.instance.init();
// ... resolve the initial route and runApp(...)
// (see the Navigation page for router wiring)
}スキーマのずれは自動的に検出されます。ジェネレーターは、ルート/ストアのスキーマの
安定したフィンガープリントをDart、Kotlin、Swiftの出力に埋め込みます。すべてのルートは
それをPageSettings内に保持し、生成されたデコーダーが受信時に検証します。異なるリビジョンの
スキーマからビルドされたホストとモジュールは、最初のナビゲーションで、両方のフィンガー
プリントを示すわかりやすいエラーとともに失敗します。アプリのコードで配線すべきものは何もありません。
別のトップレベル関数を使いたい場合(たとえば、1つのモジュールに複数のadd-to-appエントリポイントを 持たせたい場合)は、起動時にフレームワークにその関数を指定します。非表示のプリウォーム済みエンジンが すでに存在する場合は新しいエントリポイントで再生成され、すでに動いているエンジンは元のエントリポイントのままです。
InlayNavigator.shared.setDartEntrypoint("myOtherMain")初期ルートをウィジェットに変換する方法は、宣言的なルーター(go_router、auto_route)を使うか、 命令的なパターンマッチングを使うかによって異なります。どちらについても Flutterルーターとの連携で解説しています。
エンジンのプリウォーム
デフォルトでは、フレームワークは非表示のプリウォーム済みエンジンを1つ保持しています。そのため、 最初のFlutterへのナビゲーションでエンジンの起動コストを払わずに済みます。これを無効にしたり、 明示的に破棄したりすることもできます。
InlayNavigator.shared.setPrewarmEnabled(false)
InlayNavigator.shared.destroyPrewarmedEngine()同じFlutterEngineGroupから生成されたエンジンはリソースを共有するため、エンジンを
追加するコストはわずかです。生成と破棄はInlayが代わりに行います。その背後にある
エンジンモデルについてはコアコンセプトを参照してください。