Muhammed Shibli

Flutter · Android

When a Flutter App Needs Native Kotlin (A Real Example)

· by Muhammed Shibli

Flutter handles the overwhelming majority of what I build, but I've shipped at least one app where Flutter genuinely wasn't the right tool: a caller ID app whose entire purpose lives inside Android's call-screening system, a place Flutter can't reach.

The caller ID app: the project that taught me this the hard way

The brief was straightforward on paper: show caller identity information on the incoming call screen, the way dedicated caller-ID apps do. I started, as I do with most mobile projects, assuming Flutter would cover it — three years of Flutter work gives you a strong default toward reaching for it first. I looked at the available community plugins for call-screening integration, tested a couple, and ran into the same wall each time: they either wrapped a small slice of the telecom API surface (enough for a demo, not enough for the real feature) or required so much native platform code underneath that the "Flutter plugin" was really just a native Android component with a thin Dart-facing API bolted on.

That was the moment the actual decision got made. Not "can I hack this into working," but "is the thing wrapping the real functionality here Flutter or Android, and am I lying to myself about which one is doing the work."

What CallScreeningService actually requires

Android's CallScreeningService is a system service your app implements to intercept and act on incoming calls before the standard call UI takes over — allow, silently reject, or flag as spam, plus surfacing custom UI tied directly into the call experience. It's not an API you call from application code; it's a component the Android telecom framework instantiates and drives directly, with strict lifecycle and threading expectations that assume you're operating at the OS integration layer, not inside an app's normal UI thread.

Alongside it, ConnectionService and the broader telecom manager APIs are how an app registers as a call-handling participant at all. None of this is exposed through Flutter's standard widget and rendering pipeline, because none of it is really a "screen" in Flutter's sense — it's a system-level hook Android calls into your code, sometimes before your app's normal UI has even started.

Could you bridge this through Flutter's platform channels — implementing the actual CallScreeningService natively in Kotlin, then using a MethodChannel to pass data back to a Flutter UI layer for everything else? Technically, yes, and I considered exactly that. But once the native side is implementing the entire call-screening lifecycle, handling telecom framework callbacks, and managing the state that actually matters for the app's core feature, the Flutter layer left over is settings screens and history lists — the least differentiated, least risky part of the app. I made the call to keep the whole thing native rather than maintain a bridge whose only job was carrying scraps back to a framework that couldn't touch the important part anyway.

Where I drew the line between Flutter and native

The rule that came out of this project, which I've applied to every decision since: ask whether the app's core, differentiating feature requires deep OS integration, not whether any feature does. A location-tracking feature, a background sync job, even most camera and sensor work — Flutter handles these well through mature plugins, because they're common enough that the ecosystem has solved them properly. Call-screen replacement, system keyboard implementation, and certain background service categories sit in a different tier: the OS expects a native component with specific system contracts, and no cross-platform abstraction fully closes that gap without becoming a thin shell over native code anyway.

If the OS-level requirement is one feature among many in an otherwise normal app, platform channels bridging Flutter and a native module are the right answer — you keep Flutter's productivity for 90% of the app and drop into Kotlin (or Swift) for the specific piece that needs it. If the OS-level requirement is the app, like the caller ID app, going fully native from the start is more honest than pretending Flutter is doing meaningful work.

The general rule I use now

Before scoping any new app, I ask one question early: does this app's core value depend on replacing or deeply hooking into a system-level component — the call screen, a system keyboard, specific always-on background behaviors, or a platform API so new that Flutter plugin support hasn't caught up yet? If yes, that's native territory, and the conversation with the client shifts to whether it's a fully native build or a Flutter app with a native module bridged in. If no — and for the large majority of apps, including booking platforms, delivery apps, live streaming, and social products I've built — Flutter is not just adequate, it's the better choice for shipping both platforms without doubling the team and the maintenance burden.

When people ask me to "just do it in Flutter anyway"

This comes up more than you'd expect, usually from a founder who's heard "Flutter does everything now" somewhere and doesn't want to hear otherwise. My answer is always the same: I'll tell you honestly when a plugin genuinely covers what you need versus when it's a thin wrapper hiding native code underneath, because building on a fragile plugin for your app's core feature is a worse outcome than an honest native build from the start. The caller-ID project is the concrete example I point to — not because native is ever the default, but because it's proof I'll say "not Flutter" when that's the actual right answer, not just the popular one.

For the fuller picture of how I decide between Flutter and native generally, see Flutter vs native in 2026, or mobile app development for how this fits into how I scope projects.

Frequently asked questions

Can Flutter apps access the Android call screen at all?

Not directly, and not reliably through community plugins either — I tried before deciding to go fully native. Android's CallScreeningService and the broader telecom stack are deeply OS-integrated APIs that expect a native Android component implementing specific system interfaces, not a cross-platform framework's rendering engine sitting on top.

Could I use Flutter for most of the app and native Kotlin just for the call screen?

In theory yes, via platform channels bridging Flutter and native code for that one feature. In practice, once the core value of an app IS the call-screen behavior, you end up building most of the real logic natively anyway, and Flutter becomes a thin wrapper around settings screens — at which point the cross-platform benefit shrinks a lot. I made the call to go native for the whole app rather than maintain that bridge for the app's entire reason for existing.

How do I know if my app idea needs native code?

Ask whether the core feature requires replacing or deeply integrating with a system-level Android or iOS component — the call screen, a system keyboard, certain background service types, or day-one platform APIs that haven't reached mature Flutter plugin support yet. If the answer's no, Flutter almost certainly handles it. If yes, that specific piece needs native code, and you decide from there whether it's worth a full native app or a hybrid.

Does needing native code mean the whole app has to be native?

No — plenty of apps are 95% Flutter with a native module for one deep feature, bridged through platform channels. My caller ID app tipped fully native because the call-screen feature WAS the app's core purpose, not a peripheral feature. A different app where the native-requiring feature is one screen among many is usually better served by staying Flutter with a native bridge for that piece.

Related reading

Keep going