Flutter upgrades rarely fail loudly. More often the app compiles, the tests pass, and then something is quietly wrong: a slider looks different, content hides behind the status bar, or an Android build breaks on a Gradle mechanism you never touched. That is because a large share of Flutter’s changes between versions are runtime and build-config changes, not compile errors — so they slip straight through a green build. This diagnostic walks a production Flutter upgrade the way we run it: capture a baseline, upgrade in the right order, auto-migrate what can be automated, then hunt the silent breakers by the exact version they landed in. If you are jumping several minor versions at once — say 3.16 to the current 3.44 — you inherit every change in between, which is why upgrades that skip releases hurt the most. Every version claim below is from Flutter’s official breaking-changes guides.
1. Why Flutter upgrades break silently
A compile error is the easy case: the analyzer points at the line, you fix it, you move on. The expensive failures are the ones the compiler cannot see:
- A default changed (a theme, a system-UI mode) and the app now looks or behaves differently with identical code.
- A build-tooling contract changed (Gradle, the Android embedding) and only the Android build breaks, on a machine that is not yours.
- A rendering engine changed underneath you and a custom painter draws slightly differently.
None of these throw at compile time. The only defence is to know which versions introduced them and to test the runtime, not just the build. That is what the rest of this guide is for.
2. Capture a known-good baseline first
Before you change anything, record exactly what “working” is. You cannot diagnose a regression without a before.
# Record the current SDK, tooling, and channel
flutter --version | tee flutter-baseline.txt
flutter doctor -v | tee -a flutter-baseline.txt
flutter channel
# Confirm a clean tree so the only diff is the upgrade
git status --short
git switch -c chore/flutter-upgrade
Also pin your current dependency resolution by committing pubspec.lock. When something breaks after the upgrade, the diff between the old and new lockfile is often the fastest way to see which transitive package moved.
3. Upgrade in the right order
Upgrade the SDK and the dependencies as separate, reviewable steps — not in one blur.
# 1) Move the Flutter SDK on your current channel
flutter upgrade
# 2) See which dependencies are behind, and by how much
flutter pub outdated
# 3) Move dependencies — including major bumps, which is where APIs change
flutter pub upgrade --major-versions
Review the pubspec.yaml and pubspec.lock diffs before you build. A major-version bump of a package is a breaking change you are opting into, and it belongs in the same risk conversation as the SDK bump.
4. Auto-migrate what can be automated
Flutter ships a migration tool that handles many (though explicitly not all) breaking changes. Run it before you start fixing things by hand.
# Preview the automated migrations, then apply them
dart fix --dry-run
dart fix --apply
# Surface everything dart fix could not handle
flutter analyze
dart fix resolves a large fraction of deprecation migrations mechanically. Treat flutter analyze output as your manual to-do list: whatever remains after dart fix is the work that needs a human. The sections below are the changes most likely to appear there — or worse, not appear there because they are not analyzer-visible at all.
5. The silent visual breakers
These change how the app looks at runtime with no code error. They are the single most common source of “it built fine but QA says the UI is wrong.”
- Material 3 became the default (Flutter 3.16).
ThemeData.useMaterial3now defaults totrue. Colors, shapes, elevations, and component styling change. Fix: either adopt Material 3 intentionally, or setuseMaterial3: falseto keep the old look while you migrate deliberately. - Edge-to-edge became the default
SystemUiMode(Flutter 3.27). Content can now render under the status and navigation bars. Symptom: clipped headers, controls hidden behind system bars. Fix: applySafeAreawhere needed, or set the system UI mode explicitly. - Material 3
Sliderand progress indicators were restyled (Flutter 3.29). Symptom: sliders and spinners look different. Fix: re-theme intentionally, or pin the styling you want. textScaleFactorwas deprecated in favour ofTextScaler(Flutter 3.16). Symptom: text-scaling logic behaves differently or warns. Fix: migrate to theTextScalerAPI.
Diagnostic: none of these are caught by the compiler. Catch them with golden-image tests and a manual pass over every screen on a real device.
6. The Android build breakers
These typically fail only the Android build, often on CI rather than a developer machine.
- Imperative apply of Flutter’s Gradle plugins was deprecated (Flutter 3.19). The old
apply plugin:pattern gives way to the declarativeplugins { }blocks insettings.gradle. Symptom: Gradle warnings, then failures on newer AGP. Fix: follow Flutter’s Gradle-plugin migration to the declarative form. - The v1 Android embedding was removed (Flutter 3.29). Old plugins and
MainActivitycode that relied on v1 embedding APIs stop compiling. Symptom: Android build errors referencing removed embedding classes. Fix: migrate to v2 embedding; update or replace plugins still on v1. - Java/Gradle configuration drift. As AGP, Gradle, and the JDK move, the Android toolchain versions in your project can fall out of the supported window. Diagnostic:
# Check the Android toolchain Flutter sees
flutter doctor -v
# Inspect the versions your project actually pins
grep -R "com.android.tools.build:gradle\|AGP\|distributionUrl" android/ 2>/dev/null
Fix: align AGP, the Gradle wrapper (gradle-wrapper.properties), and your JDK to versions the new Flutter release supports.
7. The API renames and removals
These are usually compile errors or analyzer warnings — the good case — but they are easy to underestimate in volume when skipping versions.
MaterialStaterenamed toWidgetState(Flutter 3.22). EveryMaterialState*reference updates. Often auto-fixable withdart fix.RawKeyEvent/RawKeyboardreplaced byKeyEvent/HardwareKeyboard(Flutter 3.19). Keyboard-handling code must migrate to the new event system.- Color API wide-gamut changes (Flutter 3.27). The
ColorAPI moved to wide-gamut support;withOpacityis deprecated in favour ofwithValues, and per-channel access changed. Symptom: deprecation warnings and, in edge cases, subtly different colors. Fix: migrate towithValuesand the updated component accessors. Radiowidget redesigned (Flutter 3.35). Constructor and property changes; grouped radios move to the new API. Fix: follow the Radio redesign migration guide.- Rolling removal of deprecated APIs. Flutter removes previously deprecated APIs on a schedule (for example, APIs deprecated before 3.13 were removed in 3.16). Anything you left on a deprecation warning eventually becomes a hard error.
Run dart fix --apply first; it clears many of these. Whatever flutter analyze still reports is genuine manual work.
8. Impeller: the rendering change that isn’t in your code
The subtlest upgrade change is the one you cannot grep for. Impeller is now the default rendering engine on iOS and Android, replacing the older Skia backend. Impeller precompiles a smaller set of shaders at build time, which removes the “shader compilation jank” that caused first-run stutter — a real win. But a different renderer can draw blends, shadows, blurs, and custom shaders subtly differently.
Diagnostic: treat the renderer as its own test pass. Screenshot the same set of screens before and after the upgrade on real devices — especially anything using CustomPainter, fragment shaders, BackdropFilter, or heavy visual effects — and compare. If you find a genuine regression, capture it with a minimal repro; do not assume your own painter code is wrong before ruling out an engine difference.
9. Validate before you ship the upgrade
An upgrade is not done when it compiles. Because the dangerous failures are runtime and visual, validation must exercise the app:
- Run the full test suite, including golden-image tests — these are what catch the silent visual breakers in section 5.
- Manually walk every screen on both a real Android and a real iOS device.
- Explicitly check the known default changes: edge-to-edge layout (status/nav bar overlap), Material 3 theming, sliders and progress indicators.
- Exercise Android build on CI, not just locally, to catch Gradle and embedding breakage on a clean machine.
- Profile a representative screen before and after with DevTools to confirm the upgrade did not regress performance.
10. Performance and security notes
- Performance: the Impeller move should reduce first-run jank, but validate frame times on your lowest-end target device rather than assuming. A major-version dependency bump can change performance characteristics independently of Flutter itself — profile after the dependency upgrade, not just the SDK upgrade.
- Security: upgrading is also a security action. Staying on an old Flutter and old packages means unpatched transitive dependencies. Review the
pubspec.lockdiff for dependencies that jumped several versions, and check the changelogs of any package handling networking, auth, crypto, or serialization for security-relevant fixes. - Regression safety: Flutter maintains a test registry you can submit your tests to, so future SDK changes are checked against your app’s behaviour before they land.
Where MaboaSoft fits
Version upgrades are where a lot of Flutter technical debt comes due at once, especially on apps that skipped several releases. If you have a production Flutter app that is several versions behind and you want the upgrade done without shipping a regression, book a 20-minute call or look at how we work. For the broader picture of how AI-generated code compounds this kind of drift, see AI technical debt in mobile.
FAQ
How do I safely upgrade a production Flutter app across several versions? Capture a known-good baseline first (record flutter —version and a clean git state), then upgrade the SDK with flutter upgrade and dependencies with flutter pub outdated and flutter pub upgrade —major-versions. Run dart fix —apply and flutter analyze to catch and auto-migrate deprecations, then work through the breaking changes for every version between your old and new SDK — not just the target version, because you inherit all of them. Finish with device QA and golden-image tests, because the most common upgrade regressions are visual, not compile errors.
Why does my Flutter app look different after upgrading without any code change? Because several Flutter defaults changed between versions. Material 3 became the default theme in Flutter 3.16, so colors, shapes, and components restyle unless you set useMaterial3 to false. The default SystemUiMode became edge-to-edge in 3.27, so content can now render under the status and navigation bars. Material 3 Slider and progress indicators were restyled in 3.29. None of these are compile errors — they change how the app looks at runtime, which is why they slip through a build that passes.
What Flutter commands help find breaking changes during an upgrade? Use flutter upgrade to move the SDK, flutter pub outdated to see which dependencies are behind, and flutter pub upgrade —major-versions to move them. Run dart fix —dry-run then dart fix —apply to auto-migrate many deprecations, and flutter analyze to surface the rest. Flutter’s official breaking-changes guides list each change by version, and dart fix supports many but not all of them, so plan for manual work on the remainder.
Does upgrading Flutter change how my app renders? It can, independently of your code. Impeller is now the default rendering engine on iOS and Android, replacing the older Skia backend. Impeller precompiles shaders to remove shader-compilation jank, but blends, shadows, and custom shaders can render subtly differently. Treat a rendering-engine change as its own test pass: compare screens before and after on real devices, and pay special attention to custom painters, shaders, and heavy visual effects.
Sources (accessed 14 July 2026):
- Flutter, Breaking changes — Material 3 default (3.16), textScaleFactor→TextScaler (3.16), Gradle plugin declarative apply (3.19), RawKeyEvent→KeyEvent (3.19), MaterialState→WidgetState (3.22), Color wide gamut (3.27), edge-to-edge SystemUiMode (3.27), v1 embedding removal (3.29), Material 3 slider/progress (3.29), Radio redesign (3.35), and dart fix coverage.
- Flutter, Upgrading Flutter — flutter upgrade, flutter pub outdated, flutter pub upgrade —major-versions, channels, and the test registry.
- Flutter, Impeller rendering engine — Impeller as the default renderer on iOS and Android and shader precompilation.