Skip to main content
EngineeringProduct Building

Flutter in Production: What We Learned After Shipping 3 Cross-Platform Apps

Mortgy
4 min read

Flutter is our go-to for cross-platform apps. We have shipped three production Flutter apps over the past year — a fintech app, an edtech platform, and a field service tool. Each one taught us something new about what Flutter does well and where you need to be careful. Here are the real lessons, not the marketing pitch.

State management: we settled on Riverpod

After trying Provider, BLoC, and Riverpod across different projects, we have standardized on Riverpod for all new Flutter work. The reason is simple: it handles dependency injection and state management in a single pattern, it is compile-time safe (no runtime errors from missing providers), and it works identically in tests.

BLoC is powerful but overly ceremonial for most apps. The event-state pattern adds boilerplate that slows development without proportional benefits for typical CRUD and API-driven apps. We reserve BLoC for complex stateful features like real-time collaborative editing where the explicit event stream is genuinely useful.

Our Riverpod architecture follows a three-layer pattern: UI widgets depend on controllers (StateNotifier or AsyncNotifier), controllers depend on repositories, and repositories depend on data sources (API clients, local storage). Each layer is independently testable and the dependency graph is explicit.

Performance: the 90% rule

Flutter gives you 60fps out of the box for 90% of screens. The other 10% — long scrolling lists, complex animations layered over live data, and screens with many simultaneous network images — require deliberate optimization. The most impactful optimizations we have found:

Use const constructors everywhere possible. This single practice prevents unnecessary widget rebuilds and is the easiest performance win in Flutter. We enforce it via lint rules. Use ListView.builder instead of ListView for any list longer than 20 items — it lazily builds only visible items. Cache network images with cached_network_image and set appropriate memory limits. For complex animations, use RepaintBoundary to isolate expensive painting operations.

The Flutter DevTools performance overlay is your best friend. We run it on every screen during development and flag any frame that takes more than 16ms. Most jank comes from layout thrashing (deeply nested widgets that trigger multiple layout passes) or unnecessary rebuilds (providers that notify listeners too broadly).

Platform channels: bridging to native code

Every Flutter app eventually needs platform-specific code. For our fintech app, it was biometric authentication and secure enclave storage. For our edtech app, it was push notification handling with custom payloads. For our field service app, it was background location tracking.

We use Pigeon for type-safe platform channels instead of raw MethodChannels. Pigeon generates the boilerplate for both Dart and native sides from a single interface definition. This eliminates the string-based method name matching that causes runtime errors and makes platform channel code genuinely maintainable.

Our rule of thumb: if a plugin exists with 90%+ pub.dev score and active maintenance, use it. If not, write a thin platform channel wrapper that delegates to the native API. Never fight the platform — embrace it through clean bridges.

Testing strategy that actually works

Flutter testing tools are excellent but underused. Our testing pyramid: 60% unit tests (repositories, controllers, business logic), 30% widget tests (component rendering and interaction), 10% integration tests (critical user flows end-to-end). This ratio gives us confidence in refactoring without the brittleness of too many integration tests.

The golden file testing feature is underrated. For screens with complex layouts, we snapshot the rendered widget and compare against a reference image. This catches visual regressions that unit tests miss. We run golden tests on CI for every pull request.

Would we choose Flutter again?

Yes, with caveats. Flutter is the best cross-platform framework available today for apps that need both iOS and Android with a shared codebase. The Dart language is productive and the widget system is genuinely well designed. Hot reload makes iteration fast.

But it is not the right choice for everything. Apps that are 50%+ platform-specific code (heavy AR, complex camera pipelines, deep OS integration) should be native. Apps that are primarily content display with minimal interactivity could use React Native or even a PWA. Flutter shines for interactive, data-driven apps with moderate platform integration needs — which describes most startup products.

Mortgy

Founder & CEO