Inlay setup
Wire an existing native app (iOS, Android, or both) to a Flutter module through inlay, ending with a Flutter screen opening from native code. This skill is the ordered process and the gotchas; the canonical docs and the reference example are the source of truth for code — follow them at each step instead of guessing:
- Navigation guide (concepts, setup, router integration) — https://github.com/leancodepl/inlay/blob/main/docs/navigation.md
- Codegen config and annotations — https://pub.dev/packages/inlay_gen
- Reference example (module + native hosts, exercises every feature) — https://github.com/leancodepl/inlay/tree/main/example
Set up every platform the project has. If both an iOS and an Android host exist, wiring only one is an incomplete setup — tell the user if you skip a platform and why.
1. Survey the project
- Is there already a Flutter module (
flutter: module:in itspubspec.yaml)? If not, create one withflutter create --template module <name>. A plain Flutter app cannot be embedded — it must be a module. - Which hosts exist — iOS (UIKit/SwiftUI), Android (Views/Compose)?
- Which Flutter router does the module use (go_router, auto_route, none)? This decides the entrypoint style in step 3.
2. Module: dependencies and codegen config
In the module's pubspec.yaml:
dependencies:inlay(andinlay_composeonly if the Android host uses Jetpack Compose — it pulls Compose onto the classpath).dev_dependencies:inlay_gen,build_runner.
Create inlay.yaml next to pubspec.yaml. It names the schema files and the per-language
output directories:
routes: lib/inlay/routes.dart
stores: lib/inlay/stores.dart
dart:
output: lib/src/generated/
kotlin:
output: <module>_native/android/src/main/kotlin/<package path>/generated/
package: <host package>.generated
swift:
output: <module>_native/ios/<module>_native/Sources/<module>_native/Generated/
Create the schema files: plain Dart classes annotated with @InlayFlutterRoute('/path/:param'),
@InlayFlutterDialog(...), @InlayNativeRoute(), @InlayStore(...). They are specs only —
app code never imports them. See
https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#defining-routes
Companion native plugin. The generated Kotlin/Swift must live in a small Flutter plugin
inside the module (the <module>_native/ paths above) so flutter build aar bundles them into
binary artifacts. Mirror the reference:
https://github.com/leancodepl/inlay/tree/main/example/example_module/example_module_native —
a plugin pubspec.yaml (android package/pluginClass, ios pluginClass, dependency on
inlay), stub plugin classes, an Android library build.gradle.kts, and on iOS both
integration manifests: ios/<module>_native/Package.swift (Swift Package Manager - depends on
../FlutterFramework and ../inlay, product name with - instead of _) and
ios/<module>_native.podspec (CocoaPods - s.source_files pointing at
<module>_native/Sources/<module>_native/**/*.swift, s.dependency 'inlay'). Without the
Package.swift the plugin is built as a CocoaPods xcframework instead of a Swift package. Add
the plugin to the module's dependencies by path.
Then run codegen from the module: dart run build_runner build (or
dart run inlay_gen:inlay_gen --config inlay.yaml), followed by dart format on the Dart
output directory — the generator's raw output is not formatter-clean.
3. Module: the Dart entrypoint
Every engine runs one top-level entrypoint, inlayMain by default, annotated with
@pragma('vm:entry-point'). Its shape depends on the router — copy the matching example:
- go_router — https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#go_router-example
- auto_route — https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#auto_route-example
- no router (sealed-class pattern matching) — https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#imperative-sealed-class--pattern-matching
Common to all: WidgetsFlutterBinding.ensureInitialized(), then
await KeyValueStorage.instance.init(), then resolve the initial route. Declarative setups also
need backButtonDispatcher: InlayBackButtonDispatcher() and an InlayNativePopGestureObserver
wrapped around the router's child.
4. Android host
Follow https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#setup plus the reference host: https://github.com/leancodepl/inlay/tree/main/example/example_android
settings.gradle(.kts): add thedownload.flutter.iomaven repo, then include the module sources:include(":app") include(":<module>") apply(from = File(settingsDir, "<path to module>/.android/include_flutter.groovy")) project(":<module>").projectDir = File(settingsDir, "<path to module>")- App
dependencies:implementation(project(":flutter"))andimplementation(project(":inlay")). The generated Kotlin arrives transitively through the companion plugin. Application.onCreate:InlayNavigator.init(applicationContext)and, if the schema has native routes,InlayNavigator.setNativeRouteHandler(...).- Any Activity hosting an
InlayFlutterFragment(including via Compose'sInlayFlutterScreenor dialogs) must forward seven callbacks — extendInlayFlutterHostActivity, or useInlayFragmentHostDelegatefrom a custom base class: https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#host-activity-forwarding
5. iOS host
Follow https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#setup plus the reference host: https://github.com/leancodepl/inlay/tree/main/example/example_ios
- Integrate the module. Prefer Swift Package Manager (Flutter 3.44+, Xcode 15+); use
CocoaPods only if the host already depends on it (Flutter keeps CocoaPods in maintenance
mode and its registry goes read-only on 2 December 2026). Follow
https://docs.flutter.dev/add-to-app/ios/project-setup exactly:
- SwiftPM: in the module run
flutter build swift-package --platform ios. In Xcode add the generated<module>/build/ios/SwiftPackages/FlutterNativeIntegrationpackage (reference in place) and linkFlutterNativeIntegration; set theFLUTTER_SWIFT_PACKAGE_OUTPUTbuild setting to$(SRCROOT)/<path>/build/ios/SwiftPackages; add a scheme Build pre-action/bin/sh $FLUTTER_SWIFT_PACKAGE_OUTPUT/Scripts/flutter_integration.sh prebuild(build settings from the app target) and a Run Script build phase/bin/sh $FLUTTER_SWIFT_PACKAGE_OUTPUT/Scripts/flutter_integration.sh assemblewith input file list$(FLUTTER_SWIFT_PACKAGE_OUTPUT)/Scripts/FlutterAssembleInputs.xcfilelistand "Based on dependency analysis" off. Optionally setFLUTTER_APPLICATION_PATHandENABLE_USER_SCRIPT_SANDBOXING=NOso Xcode rebuilds Dart changes. The example's XcodeGenproject.ymlencodes all of this. - CocoaPods:
Podfileloads the module'spodhelper.rb, callsinstall_all_flutter_pods(flutter_application_path)andflutter_post_install(installer)inpost_install; thenpod install.
- SwiftPM: in the module run
- Add the generated Swift directory
(
<module>_native/ios/<module>_native/Sources/<module>_native/Generated) to the app target's sources so host code can use the typed routes/stores - the generated types are internal to the plugin module (the example does this in its XcodeGenproject.yml). AppDelegate, in this order:InlayNavigator.shared.setOnEngineCreated { engine in GeneratedPluginRegistrant.register(with: engine) // import FlutterPluginRegistrant } InlayNavigator.shared.start() InlayNavigator.shared.setNativeRouteHandler(MyNativeRouteHandler()) // if native routes exist
6. Verify
- From the module:
flutter pub get, then codegen, thendart analyze. - Build the Android host (
./gradlew :app:assembleDebug) and the iOS host (xcodebuildor Xcode). Both must compile. - Add one real navigation call in each host (e.g.
InlayNavigator.push(context, SomePage(...))/InlayNavigator.shared.push(from:route:)) and confirm the Flutter screen opens on a device or simulator.
Gotchas
.android/.iosare generated. They appear only afterflutter pub getruns in the module. Errors like a missinginclude_flutter.groovyorpodhelper.rbmean pub get hasn't run — they are not checked in.build/ios/SwiftPackagesis generated too. A missingFlutterNativeIntegrationpackage meansflutter build swift-package --platform ioshasn't run. Re-run it after adding or removing module dependencies (Dart-only changes rebuild from Xcode). As of Flutter 3.44 the command still runspod installfor a module and builds every plugin as a pod first (flutter/flutter#184590), so CocoaPods must be installed even for a SwiftPM host. If a plugin's podspec paths change or it gains aPackage.swift, delete the module's.ios/Pods,.ios/Podfile.lockandbuild/ios/SwiftPackagesfirst — the stale Pods project fails with "Build input files cannot be found", and the cached CocoaPods framework otherwise conflicts with the new Swift package ("multiple packages declare targets with a conflicting name").- iOS plugin registration is manual. The iOS embedding does not register plugins on
engines inlay creates — without the
setOnEngineCreatedcallback (set beforestart()), every plugin with native iOS code throwsMissingPluginException. On Android registration is automatic; registering again insetOnEngineCreatedthere double-registers and is a bug. See https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#plugin-registration-setonenginecreated - Regenerate all languages together. Generated code embeds a schema fingerprint; a host
built from stale Kotlin/Swift fails loudly at navigation time
(
InlaySchemaMismatchException). After any schema change, rerun codegen and rebuild the hosts. See https://github.com/leancodepl/inlay/blob/main/docs/navigation.md#schema-fingerprint-automatic-drift-detection - Compose dialogs: never wrap
InlayFlutterDialogin a ComposeDialogor a Navigationdialog()destination — aFragmentManagercannot attach fragments inside a Compose dialog window. Use it directly, driven by state.