Embedding Flutter Modules into Native Android and iOS Apps

We used Flutter Add-to-App to ship a side feature inside an existing Android and iOS app, saving weeks of duplicated work while keeping performance and user experience intact.

Embedding Flutter Modules into Native Android and iOS Apps

We have an app built in native iOS and Android. It is a large codebase with years of history, complex navigation, and a fairly involved CI/CD setup.

The product team wanted to add a small mini-game inside the Rewards tab. The goal was to make the rewards section stickier. Static coupons were not cutting it, and they wanted a "Falling Coins" style interaction to drive daily logins.

I was already working with Flutter at the time, and I had some bandwidth. My lead asked if I could take this on without disturbing the main native teams. It was clearly a side feature, valuable for engagement but not worth weeks of duplicated native work.

Given those constraints, I had to figure out how to build this without slowing down the native release train. The options were not great:

  • WebViews: We tried this first. The touch latency was awful, and it felt like a cheap webpage stuck inside a premium app.
  • Unity: Overkill. Adding the Unity runtime would have bloated our APK and IPA size by 20MB or more for a game that users might play for 30 seconds.
  • Native: Writing physics logic and collision detection twice, once in Swift and once in Kotlin, and trying to keep the gravity constants in sync was not appealing.

That is when we decided to try Flutter Add-to-App.

The Experiment: A Game Engine in Disguise

We treated Flutter not as a UI framework, but as a lightweight rendering engine. We used the official Add-to-App documentation to integrate a Flutter module into our existing Gradle and CocoaPods setups.

The game itself was a simple 2D game, built using Flame, a game engine on top of Flutter. We were dealing with sprites, basic physics, collision detection, and a predictable game loop, exactly the kind of workload where Flutter performs well. We bundled our game assets, images and sounds, directly inside the module, which kept the native project clean.

Here is why this approach clicked. In the game, we had to calculate the trajectory of falling coins and detect when the user’s basket caught them.

  • In native: I would have had to write the same updatePosition logic in Kotlin and Swift, hoping the math matched perfectly on both platforms.
  • In Flutter: I wrote the physics logic once in Dart.

The result was consistency. The game felt identical on an iPhone 14 and a Samsung S21. The animation stayed locked at 60fps because Flutter draws directly to its own Skia and Impeller rendering pipeline, bypassing native UI hierarchy limitations.

Sometimes, watching hot reload work instantly on both the iOS simulator and Android emulator, it honestly feels like Flutter is taking over the parts of mobile development that used to be the most painful. I have written more about this shift earlier in Flutter Is Taking Over.

The Gotchas

While the game logic was smooth, the integration required some architectural work.

1. Engine warm-up

The first time we launched the game activity, there was a visible black screen for about 400 milliseconds. This happens because the Flutter engine takes time to spin up.

The fix: We implemented pre-warming. We initialized the FlutterEngine in Application.onCreate on Android and AppDelegate on iOS, and cached it for reuse.

Here is the code we added to Application.kt to warm the engine before the user ever sees it:

// Pre-warm the engine in the background
val flutterEngine = FlutterEngine(this)

// Start executing Dart code to warm up the engine
flutterEngine.dartExecutor.executeDartEntrypoint(
    DartExecutor.DartEntrypoint.createDefault()
)

// Cache it globally to be picked up by the Activity later
FlutterEngineCache.getInstance().put("my_game_engine_id", flutterEngine)

By the time the user tapped the game tab, the engine was already hot and waiting, with no visible lag. On iOS, we did the exact same thing in AppDelegate using a FlutterEngineGroup to pre-warm and reuse engines efficiently across view controllers.

2. Talking to the host

The game needed to talk to the native app when something meaningful happened, for example when a user won a coupon or finished a round. This is where MethodChannel comes in.

We treated the Flutter module as a self-contained game engine, but delegated anything related to user state, persistence, or rewards back to the native app. Flutter emitted events, and the host app decided what to do with them.

  • Dart: Calls platform.invokeMethod("couponWon", {"value": 10}) when the game logic determines a reward.
  • Kotlin / Swift: Listens for the couponWon event, validates it, and persists it using existing native infrastructure like Room, Core Data, or backend APIs.

This separation kept the Flutter side focused purely on gameplay, while the native app remained the source of truth for user data and rewards. MethodChannels are simple, but they require discipline. Method names become an implicit contract between Flutter and native code, and changing them needs coordination.

3. The navigation struggle

This was the trickiest part. When a user is inside the Flutter game, they expect the Android hardware Back button to pause the game, not kill the Activity immediately.

We intercepted the back gesture on the native side and asked Flutter whether it could handle it. If the game was running, Flutter paused it. If the user was already at a menu, Flutter told native to go ahead and close the screen.

Pros and Cons

If you are considering embedding Flutter for a specific feature like a game, a dashboard, or a complex form, here is an honest breakdown.

Pros

  • Pixel consistency: The game looks identical across platforms.
  • Performance: For 2D animations and casual games, Flutter outperforms WebViews and is significantly lighter than Unity.
  • Iteration speed: Tweaking gameplay variables took seconds with hot reload. In native code, each change would have required a full rebuild.

Cons

  • App size: Even with optimizations, linking the Flutter engine added roughly 5 to 7MB to the app. Android App Bundles helped, but the base increase is real.
  • Memory usage: Running native navigation and a Flutter engine simultaneously increases RAM usage. We explicitly cleaned up the engine on exit to avoid issues on low-end devices.
  • Context switching: Moving between Android Studio and VS Code can be mentally taxing during active development.

Final Thoughts

Looking back, this was less about Flutter and more about being pragmatic.

We had a side feature to ship, limited time, and no appetite to pull the native teams into a long build cycle. Flutter Add-to-App gave us a way to move fast, keep the feature isolated, and avoid writing the same logic twice.

I would not use this approach for core app flows or anything deeply tied to navigation and app state. But for side features like games, promos, or experimental surfaces, it works surprisingly well if you are clear about boundaries.

If you are thinking about trying this, start small. Treat Flutter as a tool, not a strategy, and use it where it genuinely saves time. In our case, it did exactly that.

💡
This piece was written by Harshit Sachan as part of the Guest Posts series on Sahil's Playbook.

Subscribe to Sahil's Playbook

Clear thinking on product, engineering, and building at scale. No noise. One email when there's something worth sharing.
[email protected]
Subscribe
Mastodon