An opinionated Flutter add-to-app framework providing type-safe navigation and cross-platform state sharing for apps that embed Flutter inside a native iOS or Android host.
| Package | Role |
|---|---|
inlay |
Core framework plugin (Dart + Swift + Kotlin) - navigation, storage, appearance |
inlay_gen |
Code generator - typed route and store classes for Dart, Kotlin, and Swift from annotated schemas |
inlay_compose |
Optional Jetpack Compose integration - embed inlay screens and dialogs in Compose apps |
Integrating Flutter into an existing native app is painful:
- Navigation - There are no official guidelines for seamless navigation between native and Flutter screens. The typical approach is raw
MethodChannelcalls with arbitrary strings, which is error-prone and not type-safe. - State - There is no standard way to share data between native code and Flutter, especially when multiple Flutter screens (engines) are involved.
This framework solves both problems with a code-generation-driven, type-safe approach. Engine management (based on FlutterEngineGroup) is handled for you behind the scenes.
- Navigation - Type-safe routing between Native and Flutter screens (in both directions), with code-generated route classes for Dart, Swift, and Kotlin. Screens can return a typed result to their caller (declare
result:on the route). - Shared State - A key-value storage layer that stays in sync across all Flutter engines and native code. Changes made anywhere are automatically broadcast to all other consumers.
- Engine Management - The framework creates and destroys Flutter engines automatically. You never interact with
FlutterEngineGroupdirectly (unless you want to). - Theme & Locale Propagation -
InlayAppearancelets the host drive dark mode and an in-app language override across every Flutter engine, live (see State Management).
// A Flutter screen - navigable from native or from other Flutter screens
@InlayFlutterRoute('/sounds-notifications/:contactId')
class SoundsNotificationsPage {
const SoundsNotificationsPage({required this.contactId});
final String contactId;
}
// A Flutter dialog - rendered by Flutter over a native screen
@InlayFlutterDialog('/confirm-delete/:itemId')
class ConfirmDeleteDialog {
const ConfirmDeleteDialog({required this.itemId});
final String itemId;
}
// A native screen - navigable from Flutter
@InlayNativeRoute()
class NativeEditProfilePage {
const NativeEditProfilePage({required this.contactId});
final String contactId;
}Run code generation to produce type-safe route classes for Dart, Swift, and Kotlin.
From Dart (Flutter):
// Open a Flutter screen (creates a new native container with a Flutter engine)
await InlayNavigator.instance.push(
SoundsNotificationsPage(contactId: 'abc-123'),
);
// Open a native screen from Flutter
await InlayNavigator.instance.push(
NativeEditProfilePage(contactId: 'abc-123').toNativeRoute(),
);From Swift (iOS):
InlayNavigator.shared.push(
from: viewController,
route: SoundsNotificationsPage(contactId: "abc-123"),
animated: true
)From Kotlin (Android):
InlayNavigator.push(context, SoundsNotificationsPage(contactId = "abc-123"))For Jetpack Compose support add the optional inlay_compose plugin alongside inlay in your module's pubspec.yaml. Projects that don't use Compose depend only on inlay - no Compose transitive dependencies. See the Navigation guide for details.
Define a store (Dart):
@InlayStore(key: 'sounds_notifications')
class SoundsNotificationsStore {
const SoundsNotificationsStore({
@InlayStoreKey() required this.contactId,
this.mute = false,
this.sound = 'Default',
});
final String contactId;
final bool mute;
final String sound;
}Read and write from native - Swift:
let scope = KeyValueStorageImpl.shared.createScope()
let store = SoundsNotificationsStore(storage: scope, contactId: "abc-123")
store.mute = true
scope.startObserving { entries in
if store.containsChanges(in: entries) {
// reload from store typed properties
}
}Read and write from native - Kotlin:
val scope = KeyValueStorageImpl.createScope()
val store = SoundsNotificationsStore(scope, contactId = "abc-123")
store.mute = true
scope.startObserving { entries ->
if (store.containsChanges(entries)) {
// reload from store typed properties
}
}On the Flutter side, the generated store can be used directly or combined with the optional InlayCubit helper (see the State Management guide).
- Navigation - Route definitions, dialogs & bottom sheets, cross-boundary navigation, go_router & auto_route integration
- State Management - Stores, native access, optional Cubit integration, cross-engine sync
- Testing - Widget-testing screens that navigate and use stores, via
package:inlay/testing.dartfakes
Each package also has its own README with focused usage instructions, and the example folder contains a complete Flutter module plus native iOS and Android host apps exercising every feature.
This package is built with 💙 by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.
-
Creators of Patrol – the next-gen testing framework for Flutter.
-
Production-Ready – We use this package in apps with millions of users.
-
Full-Cycle Product Development – We take your product from scratch to long-term maintenance.