I’ve been building Flutter apps since the early days, shipped multiple apps to both stores, and made every mistake possible so you don’t have to. Here’s the unfiltered truth about Flutter in production.
State Management: The Great Debate
Every Flutter developer has opinions about state management. After trying them all, here’s my take:
For most apps: Riverpod. It’s type-safe, testable, and handles complex dependency injection elegantly. The learning curve is worth it.
For simple apps: Just use StatefulWidget and ChangeNotifier. Seriously. Not every app needs a state management framework.
Avoid: Rolling your own solution unless you have a very specific reason.
// Riverpod makes async state trivial
final userProvider = FutureProvider<User>((ref) async {
final api = ref.watch(apiProvider);
return api.fetchCurrentUser();
});
Performance Pitfalls
The Build Method Trap
Your build() method runs more often than you think. Every setState triggers a rebuild. Every parent rebuild triggers child rebuilds.
// Bad: Creates new objects on every build
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => doSomething(), // New closure every time
child: Text('Click'),
);
}
// Better: Extract callbacks
final _onPressed = () => doSomething();
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _onPressed,
child: Text('Click'),
);
}
ListView Performance
For long lists, always use ListView.builder. But also:
- Use
constconstructors wherever possible - Keep list items simple; extract complex items to separate widgets
- Consider
cacheExtentfor smoother scrolling
Image Loading
Use cached_network_image. Configure memory and disk cache sizes based on your app’s needs. Implement proper placeholder and error widgets.
Platform-Specific Gotchas
iOS
- Keyboard handling: iOS keyboard behavior differs from Android. Test thoroughly.
- Safe areas: Don’t forget
SafeAreawidget, especially with notched devices. - Permissions: iOS permission dialogs are unforgiving. Get the UX right the first time.
Android
- Back button: Handle
WillPopScopeproperly for navigation. - Notifications: Firebase Cloud Messaging setup is more complex on Android 13+.
- App links: Deep linking configuration requires careful manifest setup.
Testing Strategy
// Widget tests are your best friend
testWidgets('Login button shows loading state', (tester) async {
await tester.pumpWidget(MyApp());
await tester.tap(find.text('Login'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
Aim for:
- Unit tests for business logic
- Widget tests for UI components
- Integration tests for critical user flows
Skip the 100% coverage goal. Focus on testing what matters.
The Build & Release Pipeline
Invest in CI/CD early. My setup:
- GitHub Actions for CI (tests, analysis)
- Fastlane for builds and store uploads
- Firebase App Distribution for beta testing
Automate everything. Manual releases introduce human error.
What Flutter Does Better Than Native
- Iteration speed: Hot reload is genuinely transformative
- UI consistency: One codebase, one UI, two platforms
- Custom UI: Complex animations and custom designs are easier in Flutter
Where Native Still Wins
- Platform-specific features: Some things require platform channels
- App size: Flutter apps are larger than optimized native apps
- Talent availability: Native developers are easier to find (for now)
Final Advice
Ship early. Ship often. Your first Flutter app won’t be perfect. Mine certainly wasn’t. But the framework is production-ready, the ecosystem is mature, and the developer experience is excellent.
Just please, for the love of all that is good, don’t use setState for global state.
Want to discuss Flutter architecture for your project? Reach out—I love talking about this stuff.