Pedro Sousa
← Articles

Article · Blog

Flutter solves the wrong problem when you already have a working Kotlin app

The decision to go cross-platform is almost always made before anyone understands what problem is actually being solved. I've worked on both sides of that decision, and the mistake I kept seeing wasn't technical. It was a misdiagnosis.

Flutter solves the wrong problem when you already have a working Kotlin app

At a fintech I worked at, the argument for adopting Flutter was "reducing maintenance costs with two teams." The Android app was already two years old, Kotlin, modular architecture, reasonable test coverage. The iOS app was smaller, newer.

Six months later, we had three codebases. The new Flutter app, the legacy Kotlin app that still had to keep running while the migration wasn't finished, and the iOS app that nobody knew what to do with anymore.

Maintenance costs didn't go down. They tripled temporarily. And Flutter ended up holding the bill for technical debt that wasn't even its fault.


The problem cross-platform promises to solve

The promise is straightforward: one team, one language, two apps.

In practice, what you're buying is an abstraction layer between your product and the platform. That layer has a cost. And that cost doesn't show up in the first sprint. It shows up when you need something the platform delivers natively but the framework hasn't exposed yet, when a rendering bug only affects one Android vendor, when a platform SDK update lands and the framework takes three months to catch up.

I saw this with Flutter in payment apps. Integration with certain anti-fraud SDKs required native code regardless. The platform channel became the place where the "write once" promise quietly broke down.

The app ended up 80% Flutter, 20% Kotlin and Swift scattered across platform channels that nobody wanted to touch.


What most people get wrong without realizing it

The decision to go cross-platform gets made based on a comparison of languages. It should be made based on a comparison of products.

There's a huge difference between:

  1. Starting a brand new product from scratch, no existing install base, no technical debt, with a small team that needs to cover two platforms.
  2. Migrating a mature Android app with an established user base, a defined architecture, and a team that knows the code.

In the first scenario, Flutter or React Native make sense as an initial choice. In the second, the migration almost never pays off within the expected timeframe.

What I've seen repeatedly: the decision gets made with the roadmap from scenario 1 applied to the context of scenario 2.


What well-structured Kotlin delivers that Flutter can't replicate

I've worked on Android apps with Kotlin, Clean Architecture and Coroutines. And I've worked on Flutter apps with the same architectural intent.

The difference that came up most in production wasn't about language. It was about integration.

Kotlin with Jetpack, ViewModel, StateFlow and native integration with the Android lifecycle delivers a coherent mental model. You're always working with the abstractions the operating system expects. The framework and the platform speak the same language.

Flutter delivers visual consistency across platforms, but at the cost of staying one step behind the operating system. Widgets that behave differently on older Android versions. Animations that iOS renders one way and Android another. Back gesture handling that changed in Android 13 and broke the navigator in half the Flutter apps I saw in production.

These are solvable problems. But they're problems a native Kotlin app would never have had in the first place.


Where cross-platform actually wins

I'm not saying Flutter or React Native are bad choices. I'm saying they solve a specific problem that needs to actually exist for the choice to make sense.

The problem they solve well: speed of platform coverage with a small team and a product that doesn't depend on deep integration with the operating system.

Content apps, internal tools, products with relatively simple UI and predictable flows. In those contexts, Flutter delivers what it promises.

Financial apps with biometrics, camera for OCR, third-party SDK integrations, rich notifications, native homescreen widgets, complex deep links, and accessibility that actually needs to work in high contrast mode on Android 14. In those contexts, the platform channel becomes a debt that grows with every feature.


The thesis I stand by

Cross-platform is not an architecture decision. It's a product decision. And most teams treat it as a technology decision.

When you decide to go cross-platform because of the maintenance cost of two teams, you're assuming the problem is having two teams. But the real problem might be something else entirely: lack of shared documentation, inconsistent architecture across platforms, design decisions that ignore native conventions, or simply an Android team and an iOS team that never talked to each other about how to solve the same problem.

Those problems don't disappear with Flutter. They hide inside the Dart code and resurface when the product needs to grow.


What I would do differently today

In a situation where the Android app already exists, already has users, already has architecture, I'd ask one question before any cross-platform decision:

Is iOS still missing or behind because there aren't enough engineers, or because there isn't enough product definition?

If it's engineers, Flutter might make sense to cover iOS while the team grows. If it's product, the platform decision is premature.

The other thing I'd avoid: treating the legacy app as a problem to be migrated. Mature Kotlin apps with solid architecture are assets. Migrating them to Flutter isn't modernization. It's trading a system you know for a system you're still getting to know.

Sometimes the best move is to keep Android in Kotlin, build iOS natively in Swift, and invest in shared conventions for architecture, naming and API contracts between the two teams.

Two well-architected native apps deliver more than one poorly architected cross-platform app.


The real trap

Cross-platform reduces the number of languages. It doesn't reduce product complexity.

You'll still have to decide how to handle different behavior between platforms. You'll still have to deal with UI guidelines that diverge. You'll still have to test on devices with different characteristics. You'll still have to maintain two sets of UI tests because the screens don't behave the same way.

The complexity doesn't disappear. It just moves.

And the risk is that in the new location, it becomes less visible. Hidden in platform channels, in if (Platform.isAndroid) conditionals scattered throughout the code, in rendering workarounds that nobody documents because "it worked."

Cross-platform done right isn't less complexity. It's complexity redistributed in a different way.

The question worth asking before any decision: are you redistributing complexity to a place the team can see and control, or to a place that will only surface when users start complaining?