Flutter 3.x upgrade guide: what changed and how to migrate
By Ann Tech · 31 May 2026
Flutter 3.x introduced several significant changes over the 2.x era. This guide covers the most impactful ones and how to handle them in an existing app.
Dart 3: null safety is now mandatory
Dart 3 requires sound null safety — all packages must be null-safe. The migration era is over.
# Check if any of your dependencies are not null-safe
dart pub outdated --mode=null-safety
If you have packages that aren't null-safe, check if there are newer versions, fork them, or replace them.
Material 3 as the default
Flutter 3.16+ enables Material 3 by default. You can opt out:
MaterialApp(
theme: ThemeData(useMaterial3: false), // Opt out temporarily
)
But the better path is migrating to M3. The main visual changes:
- New color roles (
colorScheme.surfacereplacescolorScheme.background) - New component shapes (more rounded by default)
- New typography scale
// M2 color access (still works but deprecated):
Theme.of(context).colorScheme.background
// M3 replacement:
Theme.of(context).colorScheme.surface
Check your theme for deprecated properties:
flutter analyze # Will flag deprecated M2 tokens
Dart 3 records and patterns
// Records — new in Dart 3
(String, int) getCoordinates() => ('New York', 40);
final (city, lat) = getCoordinates();
// Named records
({String name, double price}) getProduct() => (name: 'Shoes', price: 49.99);
final product = getProduct();
print(product.name); // 'Shoes'
// Exhaustive switch with patterns
Widget buildIcon(IconData? icon) => switch (icon) {
null => const SizedBox.shrink(),
Icons.home => const Icon(Icons.home, color: Colors.blue),
final i => Icon(i), // Default case
};
Sealed classes (Dart 3)
// Sealed classes are exhaustively checkable
sealed class ApiResult<T> {}
class ApiSuccess<T> extends ApiResult<T> {
final T data;
ApiSuccess(this.data);
}
class ApiError<T> extends ApiResult<T> {
final String message;
ApiError(this.message);
}
class ApiLoading<T> extends ApiResult<T> {}
// Switch is exhaustive — compiler errors if you miss a case
Widget build(ApiResult<List<Product>> result) => switch (result) {
ApiLoading() => const CircularProgressIndicator(),
ApiSuccess(:final data) => ProductList(products: data),
ApiError(:final message) => ErrorView(message: message),
};
Navigator 2.0 / go_router changes
go_router 10+ has a simplified API:
// Old (still works):
GoRouter(routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
])
// New: StatefulShellRoute for bottom nav with state persistence
GoRouter(routes: [
StatefulShellRoute.indexedStack(
builder: (_, __, shell) => ScaffoldWithBottomNav(shell: shell),
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/orders', builder: (_, __) => const OrdersScreen()),
]),
],
),
])
Running the upgrade
# Upgrade Flutter
flutter upgrade
# Upgrade all packages
flutter pub upgrade --major-versions
# Fix most deprecation warnings automatically
dart fix --apply
# Check remaining issues
flutter analyze
Golden test failures
Expect golden test failures after upgrading. Impeller, M3 defaults, and typography changes all affect pixel output:
flutter test --update-goldens
Review the diff for each golden before committing — confirm that the new output looks correct, not just different.
Common pitfalls
Upgrading without reviewing the CHANGELOG. Flutter's CHANGELOG lists breaking changes per release. Always read it before upgrading a production app: flutter.dev/docs/release/breaking-changes.
Upgrading packages and Flutter simultaneously. If you upgrade both at once and something breaks, you won't know which change caused it. Upgrade Flutter first, fix issues, then upgrade packages.
Ignoring deprecation warnings. Deprecated APIs are removed in future releases. Fix them when upgrading, not later — warnings accumulate and become a large migration project.
Sign in to like, dislike, or report.