+1 (415) 480-3939

Upgrading a Legacy App from Flutter 2 to Flutter 3.44

Most of the Flutter upgrade work we do in 2026 is the same job: an app built in the Flutter 2.x era that has not been touched in two or three years, now blocked by a store requirement or a security fix that needs a newer SDK. This tutorial is the process we use. It assumes a Flutter 2.x app with a handful of third-party packages and at least one platform plugin; adjust the scale, not the order.

0. Before you touch anything

Take a baseline. You need a known-good build and a way to tell whether you broke it.

git checkout -b upgrade/flutter-3-44
flutter --version            # record the starting Flutter and Dart versions
flutter pub deps --style=compact > deps-before.txt
flutter analyze > analyze-before.txt
flutter test                 # note what passes; it may not be everything

If there is no test suite, write the safety net before the upgrade, not after. The cheapest high-value coverage is a handful of golden tests over your most important screens:

// test/goldens/home_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/screens/home_screen.dart';

void main() {
  testWidgets('home screen matches golden', (tester) async {
    await tester.binding.setSurfaceSize(const Size(390, 844));
    await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
    await tester.pumpAndSettle();
    await expectLater(
      find.byType(HomeScreen),
      matchesGoldenFile('goldens/home_screen.png'),
    );
  });
}

Run flutter test --update-goldens once on the old SDK to record the baseline images, commit them, and you have a pixel-level regression check for every step that follows.

1. Triage dependencies first

The upgrade timeline is decided by your dependency tree, not by Flutter. For every package in pubspec.yaml, check on pub.dev:

  • Does the latest version support Dart 3 (sound null safety is mandatory)?
  • When was it last published? Anything silent since 2022 is a candidate for replacement.
  • Does it have a platform implementation that will need Kotlin, Gradle, or Swift changes?

Sort the list into upgrade, replace, fork and patch, and remove. Do the replacements on the old SDK where you can, so each change is isolated and testable against the baseline.

flutter pub outdated gives a quick first pass:

flutter pub outdated --mode=null-safety   # still useful for pre-null-safety trees
flutter pub outdated

2. Migrate to null safety on the last 2.x release

If the app is not yet null safe, do that on the newest Flutter 2.x release (2.10) rather than on 3.x. Dart 3 removed the unsound legacy mode entirely, so the dart migrate tool no longer exists in current SDKs. Use a 2.x SDK via FVM or a direct install:

flutter upgrade                 # from your current channel to latest
# or pin explicitly:
fvm install 2.10.5 && fvm use 2.10.5
dart migrate                    # interactive null-safety migration
flutter analyze
flutter test

Commit the null-safe app as its own milestone. Everything after this is mechanical by comparison.

3. Step through releases, not over them

Jumping straight from 2.10 to 3.44 produces one enormous pile of errors with no way to attribute them. Stepping through stable releases (3.0, 3.3, 3.7, 3.10, 3.16, 3.22, 3.27, 3.44) lets you read one release's breaking changes at a time. Each step:

fvm use 3.10.6         # or the next stable on your path
flutter pub get
flutter analyze
flutter test
flutter build apk --debug && flutter build ios --debug --no-codesign

Fix what breaks, run the goldens, commit, move on. Typical findings at each stage:

  • 3.0 / Dart 2.17Object? defaults in generic APIs, TextTheme renames (headline6 becomes titleLarge and friends, removed later).
  • 3.7–3.10 / Dart 3 — legacy mode removed; switch exhaustiveness now enforced for enum and sealed types; class modifiers (final, base, interface) in packages you extend.
  • 3.16 and later — Material 3 becomes the default (useMaterial3: true), which changes colors, typography, and component shapes across the app. Either adopt it or pin useMaterial3: false as a transitional step.
  • 3.22–3.27 — Skia to Impeller transition on Android; deprecated Color accessor changes (red/green/blue to r/g/b doubles, withOpacity to withValues).
  • 3.44 — the first steps of decoupling Material and Cupertino from the core framework; read the release notes for any direct imports of internal library paths.

dart fix automates a surprising amount of this:

dart fix --dry-run
dart fix --apply

4. Adopt Dart 3 features where they simplify code

Upgrading is the right moment to remove boilerplate. Records and patterns replace a lot of hand-written result classes:

// Before: a one-off class for a pair of values
class LoadResult {
  final List<Item> items;
  final bool hasMore;
  LoadResult(this.items, this.hasMore);
}

// After: a record, destructured at the call site
Future<(List<Item>, bool)> loadPage(int page) async { /* ... */ }

final (items, hasMore) = await loadPage(1);

Sealed classes plus exhaustive switch make state handling safer:

sealed class AuthState {}
class SignedOut extends AuthState {}
class SignedIn extends AuthState { final User user; SignedIn(this.user); }
class Expired extends AuthState {}

Widget buildFor(AuthState state) => switch (state) {
  SignedOut() => const SignInScreen(),
  SignedIn(:final user) => HomeScreen(user: user),
  Expired() => const ReauthScreen(),
};

The compiler now refuses to build if you add a fourth state and forget a branch — exactly the kind of bug that used to surface in production.

5. Impeller regression pass

With the app building on 3.44, run it on real devices, not just simulators. Impeller is the default renderer on iOS and Android, and it is stricter than Skia about a few things:

  • Custom fragment shaders must be declared in pubspec.yaml under flutter: shaders: and compiled through the Flutter tooling.
  • CustomPainter code that relied on Skia-specific blend or anti-aliasing behavior may render differently.
  • Heavy Opacity and saveLayer usage shows up clearly in the DevTools frame timeline.

Open Dart DevTools, enable the performance overlay, and scroll through every list-heavy screen watching for frames over 16 ms (or 8 ms on 120 Hz devices). Our DevTools profiling tutorial covers the workflow in detail.

6. Finish the job

  • Update CI to the new SDK and make flutter analyze a required check (see our GitHub Actions pipeline tutorial).
  • Delete deps-before.txt and the temporary useMaterial3: false pins once the team has reviewed the Material 3 look.
  • Write down the cadence: upgrading one or two stable releases behind, every quarter, is far cheaper than this exercise every three years.

If you would rather have someone who has done this twenty times run it with your team, our Flutter app modernization service starts with an audit and a fixed-scope estimate.