Discover how a scale-up merged its native iOS and Android codebases into a single Flutter app, achieving 40% devcost savings without losing performance.

Imagine managing a logistics app where a delivery driver in Berlin reports a critical bug onAndroid, while an iOS user in London requests a new route-planning feature. Your Android team of four developers starts digginginto Kotlin files, while your three iOS developers work in Swift. By the time both teams write, test, and ship their respectiveupdates, three weeks have passed, and you have billed eighty hours of development time for a single business update. This wasthe weekly reality for FleetFlow, a mid-sized logistics scale-up with 250,000 monthly active users.
Maintaining two separate native codebases was draining our budget and slowing our product release cycles. Our productroadmap was constantly split, with Android features lagging weeks behind iOS because of differing team velocities. We knew something had to change, but we could not afford to compromise on app speed, fluid animations, or deep integration with device GPS sensors.This case study details how we executed a native to flutter migration, merging our separate Swift and Kotlin codebases into a singlecross platform app development workflow. We will show you the exact strategy, the architectural patterns, the real code we used tobridge native APIs, and the financial audit that proved we achieved a 40 percent app development cost savings. If youare a product manager, engineering lead, or founder caught in the double-codebase trap, this guide will show you howto escape it without sacrificing your user experience.
Before the migration, FleetFlow operated with two distinct product teams. Team iOS consisted of four developers working withSwift and UIKit, the traditional user interface framework for Apple devices. Team Android had four developers using Kotlin and Jetpack Compose, the moderntoolkit for building Android interfaces. While both teams were highly skilled, our engineering efficiency was cut in half by design.Every new feature required two separate design handoffs, two independent implementation cycles, and two distinct quality assurance (QA) testingpasses. If we wanted to add a simple barcode-scanning feature to our driver app, the workflow looked like this:1. Product managers wrote a single functional specification document. 2. Designers created separate UI mockups for iOS andAndroid to match platform-specific guidelines. 3. The iOS team implemented the scanner using Apple's AVFoundation framework. 4. The Android team implemented the scanner using Google's CameraX library. 5. QA engineers wrotetwo sets of automated test scripts and manually tested both apps on various physical devices.
This parallel track meant we were payingfor eight developers to produce a single functional outcome. When we audited our sprint metrics over a six-month period, wediscovered that 45 percent of our development hours were spent resolving inconsistencies between the two platforms. A button would be blueon iOS but gray on Android, or a validation rule would allow special characters on one platform but crash on the other.We were not building new value, we were constantly fighting to keep our two apps from drifting apart. The overhead ofmanaging two native codebases was no longer sustainable for our growing company.
We evaluated several paths forward, including staying native but shrinking our feature list, migrating to React Native (theJavaScript-based framework created by Meta), or adopting Flutter (Google's UI toolkit that uses the Dart programming language).React Native was a strong contender because our web team already knew React. However, React Native relies on a JavaScript bridge, which is a translation layer that passes messages back and forth between the JavaScript code and the native platform elements. For our logistics app, which requires real-time GPS tracking and rapid rendering of complex maps, this bridge was a potential performance bottleneck.Flutter took a different approach. Instead of using native platform UI components, Flutter compiles its Dart code directly to ARM machine code, the low-level instructions that mobile processors understand. It uses its own rendering engine to draw every single pixel of theuser interface on a canvas, much like a video game engine does. This means a button in Flutter looks and behaves exactlythe same on an iPhone 15 as it does on a Samsung Galaxy S23, without needing to pass througha heavy translation layer.
To test this, we built a small proof-of-concept app that rendered a listof 10,000 delivery addresses with infinite scrolling and map integration. The results convinced our engineering leadership.Flutter consistently maintained a solid 60 frames per second (FPS, the speed at which images are updated on a screen), evenduring rapid scrolling. The decision was clear. Flutter offered the write-once-run-anywhere efficiency of cross platform appdevelopment without the performance compromises that usually plague hybrid frameworks.
One of the biggest mistakes a scale-up can make is attempting a "big bang" migration, which meanshalting all new feature development for six months to rewrite the entire app from scratch. This approach is highly risky because market conditionschange, bugs accumulate in the dark, and your competitors will continue to ship new updates while your team is stalled.We opted for a phased native to flutter migration. We decided to build our new features in Flutter and integrate them into ourexisting native apps using Flutter modules. This hybrid approach allowed us to keep our current apps live and stable while slowly swapping out nativescreens for Flutter screens over time.
To execute this phased migration safely, we followed a strict five-step checklist:* Step 1: Audit and Inventory. We mapped every single screen, API endpoint, and third-party dependencyin our native apps.
This phasedapproach kept our business running smoothly. We continued to release monthly updates to our users, and our engineering team learned Flutter onthe job without the high-pressure environment of a single, massive launch day.
To ensure our new single codebase did not turn into a disorganized mess of files, we adoptedClean Architecture principles. Clean Architecture is a software design pattern that divides the app into distinct layers, ensuring that business logic (how the app processes data) is completely separated from the UI logic (how the app displays data).
We chose BLoC (Business Logic Component), a state management library for Flutter that helps manage how data changes and updates what is shownon the screen. In BLoC, the UI sends events (like "user clicked login"), the BLoCprocesses these events using business rules, and it emits new states (like "login loading" or "login success") backto the UI.
Our directory structure was organized by feature rather than by file type. This made it incredibly easy forour developers to find everything related to a specific part of the app in one place. Here is a simplified look at ourfolder structure:
lib/
├── features/
│ ├── delivery_tracking/
││ ├── data/
│ │ │ ├── models/ # Data objects representing API responses
││ │ └── datasources/ # Code that fetches data from APIs or databases
│ │├── domain/
│ │ │ ├── entities/ # Pure business logic objects
│ ││ └── repositories/ # Interfaces defining data operations
│ │ └── presentation/
│ │├── blocs/ # State management files
│ │ ├── pages/ # Full screen layouts
││ └── widgets/ # Reusable UI components
│ └── user_profile/
└── core/# Shared utilities, themes, and network clients
By isolating our data models and business rules from the Flutter UIframework, we ensured that if we ever need to swap out our state management system or even change our database technology in thefuture, we can do so without rewriting our entire user interface.
One of our biggest concerns during our cross platform app development journey was whether Flutter could handle our heavy reliance onnative device features, specifically high-accuracy GPS tracking and local background storage. Flutter handles this through Platform Channels, a messagingsystem that allows your Dart code to call native iOS (Swift) or Android (Kotlin) code.
When the Flutterapp needs to access a device-specific API, it sends a message over a MethodChannel. The native platform receives thismessage, executes the native Swift or Kotlin code, and returns the result back to Flutter.
Here is a practical exampleof how we implemented a native channel to fetch the device's battery level, which our app uses to alert drivers iftheir phone is about to die during a route:
// Dart side (Flutter)
import 'package:flutter/services.dart';
class BatteryInfo {
// Define the channel name
static const platform= MethodChannel('com.fleetflow.app/battery');
Future<int> getBatteryLevel() async{
try {
// Call the native method
final int result = await platform.invokeMethod('getBatteryLevel');
return result;
} on PlatformException catch (e) {
print("Failed to get battery level: '${e.message}'.");
return -1;
}
}}
On the Android side, we registered a handler in our main Kotlin file to catch this method calland respond with the native battery system status:
// Kotlin side (Android)
import android.content.Context
import android.os.BatteryManager
import io.flutter.embedding.android.FlutterActivity
importio.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
classMainActivity: FlutterActivity() {
private val CHANNEL = "com.fleetflow.app/battery"override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
}else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManagerreturn batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}}
This approach allowed us to write native code only when absolutely necessary, while keeping 95 percent ofour application logic inside our shared Dart codebase.
Usershave an intuitive sense of how their phone's operating system should feel. iPhone users expect smooth, rubber-band stylescrolling and swipe-to-go-back gestures. Android users expect ripple effects when they tap buttons and a different styleof page transitions. If a cross platform app ignores these subtle patterns, it feels cheap and alien to the user.To achieve a true native feel, we utilized Flutter's built-in platform-aware widgets. Flutter provides two distinctUI libraries: Material Design widgets for Android and Cupertino widgets for iOS. Instead of manually writing conditional statements for every single buttonand menu, we created wrapper widgets that automatically adapt based on the platform the app is running on.
For example,our loading indicator adapts dynamically:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class AdaptiveLoader extends StatelessWidget {
constAdaptiveLoader({Key? key}) : super(key: key);
@override
Widget build(BuildContextcontext) {
if (Platform.isIOS) {
return const CupertinoActivityIndicator();
}else {
return const CircularProgressIndicator();
}
}
}
Furthermore, webenefited greatly from Flutter's transition to Impeller, its new graphics rendering engine. Impeller pre-compiles shaders, the small programs that tell the graphics processor how to draw lighting and shadows. In older cross platform tools, compiling theseshaders on the fly caused noticeable frame drops (often called jank) the first time a user opened an animation. Impeller completely eliminates this issue, delivering smooth 120Hz scrolling on modern devices.
Before our migration, our deployment process was a logistical nightmare. We hadto run separate build pipelines on separate services, manage different provisioning profiles for iOS, and handle Android keystores manually. Ittook our team an average of four hours to package, test, and upload a new release to both Google Play and theApple App Store.
By migrating to a unified Flutter codebase, we were able to consolidate our Continuous Integration and Continuous Deployment(CI/CD, the automated system that tests and builds our app whenever we change the code) into a single pipeline. We chose Codemagic, a CI/CD tool specifically optimized for Flutter apps.
We configured our workflow torun every time a developer merged code into our main branch:
This automated pipeline reduced ourdeployment time from four hours of manual coordination to just fifteen minutes of automated processing. Our developers simply push their code to GitHub, and the machines handle the rest, ensuring that both platforms receive the exact same version of the app at the exact samemoment.
The decision to migrate wasultimately driven by business metrics. To measure the financial impact of our native to flutter migration, we tracked our engineering costs,QA hours, and subscription tools over a twelve-month period. Six months before the migration were compared directly with six monthsafter the migration was completed.
Here is the exact breakdown of our resource allocation and costs:
| Resource or Metric | Native (Swift + Kotlin) | Flutter (Single Codebase) | Cost Reduction |
|---|---|---|---|
| Active Mobile Developers | 8 (4 iOS, 4 Android) | 5(Cross-functional) | 37.5% |
| Average Sprint Velocity (Points) | 42 points per sprint | 68 points per sprint | +61.9% (Efficiency Gain) |
| Third-Party Tool Subscriptions | $1,200 per month | $450per month | 62.5% |
| Total Engineering Spend (6 Months) | $480,000 | $288,000 | 40% |
The most significant saving camefrom our team structure. Because we no longer needed separate teams to build the same feature twice, we were able to transitionthree of our mobile developers to our backend and platform engineering teams, where we had a severe talent shortage.
Our QAcosts plummeted because our testing team only had to write a single set of integration test scripts. Instead of writing separate tests forthe iOS login flow and the Android login flow, they wrote one script in Integration Test (Flutter's built-intesting framework) that ran on both platforms. This dramatically reduced human error and cut our time-to-market for newreleases by more than half.
Our migrationwas highly successful, but it was not without its challenges. Moving from native development to a cross platform framework requires a shiftin mindset, and we fell into a few traps along the way. Here are the three main pitfalls we encountered and howyou can avoid them:
When we first started, we were eager to move fast,so we imported third-party open source packages for everything, including custom buttons, date pickers, and chart displays.We quickly realized that many of these packages were maintained by single developers as hobbies. When a new version of Flutter was released, several of these packages broke, blocking our entire build pipeline.
To avoid this, limit your dependencies. If a UIelement can be built with fifty lines of custom Dart code, build it yourself rather than importing an external package.
###Ignoring Platform-Specific Asset Rules We assumed Flutter would handle all our app icons and splash screens automatically. However, iOSand Android have very different rules for how they render adaptive icons and launch screens.
To avoid this, use toolslike flutter_launcher_icons and flutter_native_splash early in your project setup to generate thecorrect native assets for each platform automatically.
iOS devices rely on swipe gestures togo back, while Android devices have a dedicated back button (either physical or virtual). In our early Flutter builds, clicking the Androidback button would occasionally close the entire app instead of going back to the previous screen.
To avoid this, alwayswrap your nested navigation views in a PopScope widget to intercept and handle the back button press correctly on Android devices.
Today, our Flutter app isfully live, boasting a 4.8-star rating on the App Store and a 4.7-starrating on Google Play. Our crash-free rate is at an all-time high of 99.92percent, proving that cross platform app development can match, and sometimes even exceed, the stability of native applications.
Thebiggest long-term benefit of our migration has been the cross-training of our engineering team. Our former iOS and Androidspecialists have evolved into unified mobile product engineers. An engineer who spent years writing Swift is now fully comfortable writing Dart, andthey understand the Android operating system far better than they did before.
We have also future-proofed our product roadmap. Because Flutter supports desktop (macOS, Windows, Linux) and web environments from the same codebase, we are alreadyexperimenting with running our driver app on desktop terminals in our sorting warehouses. We can reuse 85 percent of our existingmobile code, allowing us to launch a desktop warehousing tool in a fraction of the time it would take to build a newone from scratch.
Key takeaways
- Consolidated Codebase: Migrating from nativeSwift and Kotlin to Flutter allowed us to merge two teams into one, reducing our engineering spend by 40 percent.
- No Performance Loss: Flutter's direct-to-ARM compilation and Impeller rendering engine maintained a fluid60 to 120 FPS experience on both platforms.
- Unified CI/CD: Weconsolidated our deployment pipelines, reducing release preparation time from four hours of manual work to fifteen minutes of automated building. *Adaptive UI: By utilizing platform-aware widgets, we maintained the distinct design patterns expected by iOS and Android users alike.
If you are currently struggling with the high costs and slow release cycles of maintaining separate iOS and Android apps, anative to flutter migration might be the right path for your business. It is a proven way to achieve massive app development cost savingswithout losing the speed, performance, or native feel that your users expect. If you are planning a project like this,we are happy to talk it through.
01 · RelatedDiscover why developers who combine clean code with product thinking and UI/UX empathy rise fasterto technical leadership positions.
Read post
02 · RelatedA step-by-step engineering case study of an API credential exposure and how modern product teams automate secret detection and rotation.
Read post
03 · RelatedDiscover how physics-based feedback and UI micro-animations reduce cart abandonment,build transaction trust, and drive mobile app retention.
Read postWe will reply in plain English within one business day, NDA on request. Discovery call is free.