How to add Flutter to an existing Android app
Adding a Flutter screen to an existing Android app looks easy in the official documentation, and for a fresh demo app it is: you create a Flutter module, declare a FlutterActivity in the manifest, and the Flutter screen opens from Kotlin. Yes, it really is that short. The reason this page exists is that in a real Android app - one with years of history, a few hundred screens and a build setup nobody wants to touch - that first screen is the easy part.
What add-to-app gives your team is clear enough: screens written once for both platforms, and a Flutter module your developers can iterate on with hot reload while the native app keeps shipping. What it asks of the host is less obvious, and the quick start doesn't say much about it. This page goes through those decisions from the point of view of the Android tech lead who has to make them, together with the problems we have seen along the way.
Most of the plumbing involved - engine management, prewarming, typed navigation, shared state - is generic add-to-app work that every team ends up building in some form. We built Inlay to cover it and point to it where it's relevant, but the decisions are the same whether you use a framework or write it yourself. For the step-by-step setup, see the Android section of Inlay's Getting started; for which screens to migrate first, see the planning page and, from your own code, Converge. If you have not yet read how Flutter engines work, start there.
What changes in your Android project
Flutter arrives as a module: a separate Flutter project (flutter create --template module) with a hidden . wrapper that your host's build hooks into. Nothing in the host moves; the module is added next to it. Three things change for your team from day one:
- The build gets a second compiler. Compiling the app now includes compiling Dart, so native builds get slower and every machine that builds the host needs the Flutter SDK - unless you consume the module as a prebuilt artifact (below).
- Flutter code lives in exactly one module. Flutter doesn't support packing several Flutter libraries into one app, so every team's Flutter features end up in one module and one Dart build. Plan for a shared module split into packages from the start - see Scaling & code sharing.
- The daily loop splits in two. Flutter developers can work in the module as a standalone app with plain
flutter run; the host is only needed to verify the integration. Keep the module runnable on its own from the first week - hybrid debugging with two debuggers attached is slow and flaky, and teams that skip this spend their days waiting for Gradle.
Hosting Flutter: Activity, Fragment or Compose
Flutter renders into a native container, and the container has to match your app's architecture:
- An Activity per Flutter screen is the simplest and works when Flutter takes over the whole screen. Most multi-activity hosts start here.
- A Fragment is what single-activity apps built around fragments or Navigation Component need, and the only option when Flutter is part of a screen. The catch: the host activity has to forward seven callbacks to the fragment (back, new intents, permissions, activity results, memory trim and so on - the full list), and a missed one fails silently - a permission dialog or a file picker simply never returns its result. Inlay provides
InlayFlutterHostActivity, which wires the forwarding, and a delegate for hosts with their own base activity. - A composable for Jetpack Compose hosts. Inlay's optional
inlay_composepackage wraps the fragment asInlayFlutterScreen.
Navigation and state across the native-Flutter boundary
Every path between a native screen and a Flutter screen is a contract: which screen, with which parameters, returning what. Written as string routes and raw method channels, these contracts drift apart silently - a renamed parameter on one side is a runtime failure on the other. Make the contracts typed and generated so a change is a compile error on both sides: Pigeon does this for method calls, and Inlay does it for routes and shared state (Native-Flutter communication compares the options). Whatever you pick, the same mechanism has to work in the other direction, with Flutter requesting native screens through a handler the host registers once.
Shared state is the other half. Two Flutter screens on different engines don't share Dart memory, so session, user preferences and theme have to live on the native side and be synchronized to every engine. Our rule for what crosses the boundary: app-level concerns stay native - push notifications, background work, analytics, session handling - and Flutter gets their results over the typed contract. Plugins that assume a long-lived engine don't survive engines created per screen; see Plugins in a multi-engine setup.
What breaks in large Android hosts
These are the problems that show up only when the host has years of history. None of them is hard, but each one has cost us a sprint when found late:
- Repository declarations. The setup guide expects repositories in
dependencyResolutionManagementwithPREFER_SETTINGS. Hosts with a stricter mode, or repositories scattered across module build files, need a Gradle cleanup first. - The Flutter Maven repository. Engine artifacts are downloaded from
storage.. In one of our projects, we quickly found out it was blocked in the corporate network. If dependencies go through Artifactory or Nexus, ask for a mirror before the first sprint (the build readsgoogleapis. com FLUTTER_STORAGE_BASE_URLfor exactly this). - Toolchain. Flutter requires Java 17 compatibility, and the Kotlin DSL integration requires Flutter 3.27 or newer. A later Flutter upgrade can force a host toolchain upgrade - plan them together.
- ABIs. Flutter ships AOT code only for
arm64-v8a,armeabi-v7aandx86_64. If a third-party SDK brings other ABIs, restrict the host withabiFiltersas the guide recommends, or expect a missinglibflutter.crash on those devices.so - AndroidX and minSdk. The module only supports AndroidX hosts, and the
minSdkhas to satisfy Flutter, your plugins and Inlay (API 24).
Before the first Flutter screen goes to production
- The transition. A matching theme background on the Flutter container, or users see a white flash before the first frame.
- Back navigation. The system back and the predictive back gesture must pop Flutter's inner routes before closing the container. Inlay's
InlayBackButtonDispatcherhandles the hand-off seamlessly. - Theme, locale and font scale. If the host overrides them in-app rather than following the system, Flutter doesn't know - pass them in (e.g. with an Inlay shared store).
- Minified release build. Missing keep rules for plugin classes only show up with R8 on.
- Process death. Test with "Don't keep activities" enabled.
- App size. Measure the release AAB's download size in the Play Console, not a debug APK.
- Crash reporting. Dart errors don't reach your crash dashboard unless reported explicitly.