Flutter feels native — when you do the last 5%
Why so many Flutter apps feel almost-but-not-quite right, and the specific platform-affordance work that turns 'cross-platform' from a compromise into an advantage.
Most criticism of Flutter comes from a fair place: people open a real Flutter app and something feels off. Scroll physics that don't quite match. A share sheet that looks generic. A keyboard avoidance that overshoots. None of these are bugs in Flutter. They are bugs in how the app was built.
Flutter handles 95% of cross-platform parity for free. The remaining 5% — the platform affordances users feel without consciously naming — needs deliberate, per-platform work. Here is what that work looks like in practice, drawn from apps we have shipped.
Scroll physics
The default ScrollPhysics in Flutter adapts to platform conventions, but only if you let it. Wrap scrollable widgets in a builder that selects ClampingScrollPhysics on Android and BouncingScrollPhysics on iOS, and respect the platform's overscroll behaviour. It takes ten lines and is the single biggest contributor to "this feels native".
Share sheets and document pickers
The default share_plus implementation is fine for basic use, but the iOS share sheet has affordances — Markup, Save to Files, AirDrop targeting — that users rely on. Spend the time wiring in the platform-specific UIActivityViewController surface for iOS and the proper Intent.ACTION_SEND with EXTRA_STREAM URIs for Android. Your power users will notice.
Haptics
iOS users have been trained over a decade to feel specific haptic patterns at specific moments: a soft tap when a toggle flips, a heavier impact when a confirmation succeeds, a notification pattern when something completes. Android haptics are similar but distinct. HapticFeedback.lightImpact() everywhere is not a strategy; it is laziness. Audit your interaction points and choose deliberately.
Keyboard avoidance
The default Scaffold.resizeToAvoidBottomInset works for simple forms and falls apart for anything with absolutely-positioned widgets, custom focus management, or multi-step flows. Build a small KeyboardAvoider widget that uses MediaQuery.viewInsets.bottom directly, and respect the iOS keyboard's interactive dismiss gesture. This is a one-day investment that prevents months of Slack messages about "the keyboard is covering the button".
Pull-to-refresh
On iOS, pull-to-refresh has specific timing curves and a specific spinner. On Android, the Material spinner has its own behaviour. RefreshIndicator with a platform check inside its child gets you 90% there; the remaining work is matching the gesture's resistance curve to platform expectation.
Native sheets and modal styles
iOS modal presentation styles — pageSheet, formSheet, full-screen — are signals to the user about reversibility and scope. A Cupertino-style dragable sheet for ephemeral choices, a full-screen Material modal for committed flows. Mixing these breaks user expectation and makes your app feel "made in Flutter" in the bad way.
The point
None of this is hard. None of it is hidden. It is just deliberate work that requires someone on the team to care about the platform-specific feel. When that person exists, Flutter apps are indistinguishable from native at the level users perceive. When they don't, you ship the kind of app that gives Flutter its bad reputation — and the framework takes the blame for a decision the team made.