Most Android features a product team needs are handled well inside Flutter and Dart: navigation, networking, state, local persistence, push notifications.
A CTO who sends the team to write Kotlin "for performance" without measuring first is adding complexity with no justification behind it.
This post covers where the abstraction genuinely breaks down, how to validate that with data, and how to integrate a native module without losing the benefits of a shared codebase.
Where Flutter actually falls short
There is a narrow band of requirements where the cross-platform abstraction creates real friction: OEM APIs Flutter does not expose yet, callbacks needing latency under 16ms per frame, FFI or NDK rendering to interop with C/C++ libraries processing image or audio in real time, and deep integration with device-specific hardware like advanced biometric sensors (common in healthcare software) or proprietary BLE protocols generic plugins do not cover.
- Your team starts patching third-party plugins to cover OEM functionality gaps.
- Benchmarks on the critical feature show consistent jank, with frame build time creeping past the 16ms budget for 60fps.
- The use case involves audio or video buffers that cannot pass through the message channel without a copy penalty.
If none of those apply, Flutter solves the problem with less surface to maintain.
How to validate it during discovery
Before committing a sprint to native modules, you need evidence, tested with concrete tools and documented in the repo.
Metric · Tool · Reference threshold
Cold start time · adb shell am start -W · +200ms over the Flutter baseline warrants investigation
Sustained FPS · flutter run --profile + DevTools · Measured during the critical interaction
Jank frames (% of total) · DevTools / Perfetto · Typical acceptance threshold of 1%
APK size delta · ./gradlew assembleRelease · Compared before and after adding the module
The practical test starts with a prototype branch: implement the feature twice, once in pure Dart and once with the candidate native module, run both on a flagship, mid-range, and low-end device, capture traces with Perfetto, and document the difference in an ADR. If the Dart variant meets the thresholds, skip the native module.
At Somnio, we structure that analysis inside our product discovery process, so the benchmark and the ADR are deliverables before development starts.
How to integrate without breaking the base
Flutter offers two main mechanisms for talking to Kotlin. MethodChannel is the most direct: define a channel, register handlers in Kotlin under android/app/src/main/kotlin/, and call it from Dart with invokeMethod.
Arguments travel as Map<String, Any?>, so type errors surface at runtime.
Pigeon solves that by generating typed interfaces in both Dart and Kotlin from an annotated .dart file, with the contract versioned in the repo. For any integration passing more than two arguments, Pigeon measurably cuts integration bugs.
The module should live in its own package, android/app/src/main/kotlin/com/yourapp/native_modules/, with unit tests in androidTest and its own build.gradle.
In CI, ./gradlew testDebugUnitTest runs as a separate step before flutter build apk, catching native regressions early.
- Each native module adds a surface that needs updating with every major Android, Kotlin, and Gradle release, similar to what a major Flutter version upgrade demands from the rest of the app.
- If your team has two Kotlin-fluent developers and fourteen Dart engineers, the module's bus factor is two. Closing that gap with specialized staff augmentation keeps a single module from depending on one person.
- Native code can reach system APIs Flutter's sandbox does not expose, so each module needs its own permissions review.
Keeping this surface governable is, at its core, the same problem as structuring a full cross-platform squad: native modules need a clear owner, not a gray zone between Flutter and Android.
How this translates into a business decision
A native Kotlin module adds 15% to 30% more development hours than the same feature in Dart, once you count integration, separate testing, and ongoing maintenance.
That cost is justified when the alternative is a degraded experience that hurts retention, or a technical limit that blocks a real product differentiator, like wearables measuring heart rate through the camera or fintech platforms rendering forex charts in real time.
- Does the feature touch system APIs Flutter does not expose?
- Do benchmarks show a measurable gap between Dart and Kotlin on target devices?
- Can the team maintain native code long term?
- Does the end-user impact justify the investment delta?
If all four answers are yes, the native module earns its place. The decision should never rest on technology preference, only on evidence captured with tools and documented in artifacts that outlive the sprint where it was made.
Frequently asked questions
Does Pigeon fully replace MethodChannel?
Pigeon generates code on top of MethodChannel, adding type safety and a versionable contract without replacing the underlying transport. For simple, single-argument integrations, MethodChannel remains a valid option that skips the code generation step.
How much does a native module affect APK size?
It depends on the module's dependencies, but the delta is measurable by comparing ./gradlew assembleRelease output before and after adding it. That number should be part of the acceptance metrics defined before development starts.
What if the team has no Kotlin experience?
The module's bus factor drops to whoever can maintain it, which is exactly why team capacity is one of the four discovery checklist questions. If that capacity does not exist, the real cost includes the learning curve or bringing in a specialized profile.
Do native modules affect Flutter upgrades?
Every major version of Flutter, Android, Kotlin, and Gradle can require adjustments to the native module. That extra maintenance surface is one of the trade-offs a team should weigh before committing to the integration.
Is the ADR mandatory or just a recommendation?
It is a documentation practice that turns the decision into a persistent artifact in the repo, accessible to future team members and technical reviews. Without it, the reasoning behind a native integration depends on the memory of whoever was in the discovery sprint.
Adding native Kotlin only where the metrics justify it is how we protect a Flutter core without inflating its maintenance surface, it is the same discipline behind every module we have shipped for hardware-heavy products.
At Somnio Software, we work closely with companies to design and build high-quality digital products using modern technologies and development best practices.
If you're looking for a trusted partner to bring structure, expertise, and innovation to your next software project, we'd love to connect. Contact us to learn how we can help turn your product vision into reality.

.png)

