Migrating a Mobile Application to a New Flutter SDK Version
Flutter changes its public API more often than it appears. Upgrading from Flutter 2.x to 3.x introduced null safety as a mandatory requirement, removal of WidgetsFlutterBinding.ensureInitialized() in some cases, new Navigator 2.0, and changes from MaterialState → WidgetState. Migrating a production application isn't just running flutter pub upgrade.
What Actually Breaks in Major Upgrades
Null Safety: Beyond dart migrate
The dart migrate tool does 70–80% of the work—adds ? and ! where the analyzer can infer nullability. The remaining 20–30% is manual work, and that's where bugs hide.
Typical case: a pub.dev plugin hasn't migrated to null safety and is frozen. Options: fork with patch, switch to alternative, write a wrapper. With 40+ dependencies, this becomes a week of work on dependencies alone.
More dangerous: code that passes migration without errors but changes behavior. A late variable without initialization throws LateInitializationError at runtime where there used to be just null. Caught only by tests or in production.
Breaking Changes in Material 3
Flutter 3.16+ switched useMaterial3: true by default. If your app has custom ThemeData, parts of components look different: button sizes changed, AppBar padding changed, TextTheme hierarchy shifted. Apps rejected for visual changes after upgrade—not rare.
Solution: set useMaterial3: false during migration, then incrementally move to M3 component by component.
Changes in Navigator and Routing
If your app uses go_router, each major release breaks the API differently. Moving from go_router 6.x to 10.x requires complete reconfiguration: GoRoute + ShellRoute instead of nested GoRoute, redirect callback signature changes, new StatefulShellRoute for persistent navigation.
Rendering Changes: Impeller
Flutter 3.10+ enabled Impeller by default on iOS (Android optional). Impeller removes jank from shader compilation, but breaks custom CustomPainter implementations using non-standard BlendMode or ImageFilter. After enabling Impeller, all animations and custom widgets need testing on real devices.
Migration Process
Dependency Audit—first step. flutter pub outdated shows what's stale but not breaking changes. Review CHANGELOG manually for each package's major versions. Divide dependencies into three categories:
- updates without code changes
- require code changes (API changes)
- no compatible version—need replacement or fork
Feature-flag approach for large apps. Create a migration branch, upgrade SDK and packages, fix all compilation errors. Then fix incrementally, starting with core layer (models, repositories) and ending with UI.
Testing after migration:
-
flutter analyze—no static analysis warnings -
flutter test—all existing suite must pass - Golden tests for UI components (if used)—regenerate, Impeller renders pixels differently
- Smoke test on physical devices: iOS + Android, budget Android mandatory
Real-World Problems
In one project (e-commerce, Flutter 2.8 → 3.19), the main difficulty wasn't code but flutter_local_notifications. Between versions 9.x and 16.x, the entire Android side changed: new FlutterLocalNotificationsPlugin.initialize() with InitializationSettings, mandatory onDidReceiveNotificationResponse instead of deprecated callback. Plus Android 13 requires explicit POST_NOTIFICATIONS permission—silently fails without it.
Another case: image_picker after updating started returning XFile instead of File. All uses of File(imagePicker.path) needed changing to File(xFile.path)—23 usages across different screens.
Timeline
| App Scale | Typical Migration Time |
|---|---|
| Small (< 20 screens, < 15 deps) | 3–5 days |
| Medium (20–60 screens, 15–40 deps) | 1–3 weeks |
| Large (60+ screens, complex architecture) | 3–6 weeks |
Cost calculated individually after repository audit and dependency list review.







