Upgrading to Flutter 3.47 Broke 4 Things in My App: The Checklist I Wish I'd Had

Three failed loudly, one failed silently, and one of the loud ones was hiding in generated code

On 2026-10-03 I opened my Flutter app under Flutter 3.47.6 stable for the first time. The app had last been built on an older SDK, and the first thing I saw was not a new feature. It was 111 test files that no longer compiled.

The app is CAMI, a pet-personality app I maintain: Riverpod, typed go_router routes, easy_localization, and a lot of generated code. Over the next two days, the upgrade broke four things. Two came from the SDK itself, one came from a package upgrade that rode along with it, and one appeared when I raised the Dart constraint to match. None of them is exotic. The first two were checkable before the upgrade landed, which is the annoying part, and the other two have a check you can run now.

Here they are, in the order they hit me, each with the fix that shipped and the check I'd run first next time.

1. An icon package that subclassed IconData

The 111 broken test files all had one thing in common: somewhere down the import graph, they pulled in lucide_icons 0.257.0. That package declares a class that extends IconData. In the SDK I'm on now (3.47.7, read 2026-10-12), this is the declaration in packages/flutter/lib/src/widgets/icon_data.dart:

final class IconData {

Because IconData is a final class, a package that extends it stops compiling, and so does every test that transitively imports it.

The fix turned out to be boring, which is the best kind. lucide_icons_lite 1.50.0 defines its icons as IconData constants instead of subclassing, so the swap was a dependency line plus an import line per file (commit 23f070f0):

-  lucide_icons: ^0.257.0
+  lucide_icons_lite: ^1.50.0
-import 'package:lucide_icons/lucide_icons.dart';
+import 'package:lucide_icons_lite/lucide_icons_lite.dart';

One search of your pub cache before you upgrade finds this:

grep -rl --include='*.dart' 'extends IconData' ~/.pub-cache/hosted/pub.dev/

Any hit in a package you depend on is a compile failure waiting for you.

2. A ListTile sitting on a colored box

With the icons fixed, the tests compiled, and a different batch failed. This time the message came from the framework itself. In Flutter 3.47.7, packages/flutter/lib/src/material/list_tile.dart reports it like this:

ErrorSummary('ListTile background color or ink splashes may be invisible.'),

ListTile paints its background and ink splashes on the nearest Material ancestor. If a Container or DecoratedBox with a background color sits between the two, the box covers those effects. The SDK now asserts on that in debug builds, and widget tests run with asserts on, so that's where it showed up.

My bottom sheets, a search view and a settings screen did exactly this. The fix (commit 4793fdb5) moves the color and the shape onto a Material and leaves the outer widget for layout only. Here is the choice sheet:

-            DecoratedBox(
-              decoration: ShapeDecoration(
-                color: AppColors.gray100,
-                shape: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(16),
-                ),
+            Material(
+              color: AppColors.gray100,
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(16),
               ),
+              clipBehavior: Clip.antiAlias,

clipBehavior: Clip.antiAlias keeps the splash inside the rounded corners, which matters on a rounded list container or on the first and last items of a grouped sheet. A border-only BoxDecoration with no fill does not trigger the assertion.

According to my notes from that day, six tests went from failing (17 assertions) to passing. Two screens that had no tests got the same change, and for those, flutter analyze is the only thing that checked it. (That's "probably fine", which is not the same as "verified".)

Next time I'd search for ListTile inside a colored Container or DecoratedBox first, and run the widget tests before the SDK bump merges, not after.

3. go_router 18 and the app that wasn't a MaterialApp anymore

This one is my favorite, because nothing failed to compile and the analyzer said nothing at all.

A dependency refresh (merry hard) on the same day brought in go_router 18. Its changelog entry opens with one short line, and I read right past it:

## 18.0.0

- Migrates to material_ui and cupertino_ui.
- Updates minimum supported SDK version to Flutter 3.44/Dart 3.12.

Here's what that means in the source (go_router 18.0.2, lib/src/pages/material.dart, which imports package:material_ui/material_ui.dart):

bool isMaterialApp(BuildContext context) =>
    context.findAncestorWidgetOfExactType<MaterialApp>() != null;

That MaterialApp is package:material_ui's class, not the one from package:flutter/material. My app was still built on flutter/material, so as far as go_router was concerned, it was a plain WidgetsApp. Every route without its own pageBuilder became a NoTransitionPage: no page transition, no iOS swipe-back. Unknown routes rendered the ErrorScreen, which has no Scaffold.

The app ran. It just quietly lost its transitions. The only reason I noticed is that a few snackbar tests navigated to an unregistered route with .go(), and the snackbar had no Scaffold to show up in. Without those tests, the first place to notice would have been a real phone, on a screen that no longer swiped back.

On the day, I pinned go_router back to ^17.5.0 (17.3 or later is needed if your go_router_builder 4.4+ generates hasOverriddenOnExit). The real fix landed in PR #5 (c7b2c685), which moved the app itself to material_ui:

-  go_router: ^17.1.0
+  go_router: ^18.0.2
+  material_ui: ^1.5.0

Not every dependency has migrated yet, so the app's builder in lib/app/my_app.dart keeps a bridge for the stragglers:

// Packages that still import package:flutter/material.dart (croppy, dropdown_button2,
// flutter_markdown, ...) read the legacy Theme and MaterialLocalizations; the bridge
// provides both until those packages migrate to material_ui.

Two other packages from that refresh, cached_network_image 4 and smooth_page_indicator 3, also depend on material_ui. They don't look up Material ancestors, so I left them upgraded.

This one takes reading rather than compiling, so for every package that switched to material_ui, look through its source for Theme.of, findAncestor…, or Scaffold before you accept the upgrade. If your app still uses flutter/material, any hit is a behavior change the analyzer won't report.

4. Dart 3.13 and final parameters in generated code

The next day (2026-10-04, PR #7), I raised the SDK constraint to match the new toolchain:

-  sdk: ">=3.10.0 <4.0.0"
+  sdk: ">=3.13.5 <3.14.0-z"
+  flutter: ">=3.47.6 <4.0.0"

Dart 3.13 rejects final on a parameter (extraneous_modifier). My checked-in freezed outputs had exactly that, so the constraint bump alone produced 106 analyzer errors, every one of them in a *.freezed.dart file. One of them, from base_event.freezed.dart:

-  const _EmptyEvent({final String? $type}) : $type = $type ?? 'empty';
+  const _EmptyEvent({String? $type}) : $type = $type ?? 'empty';

The twist is how that fix got into generated files at all. It came from merry format, which runs dart fix --apply and dart format, and which reaches generated code because these two excludes are commented out in analysis_options.yaml:

# - "**/*.g.dart"
# - "**/*.freezed.dart"

So the constraint bump has to ship in the same commit as the final removal. The commit message also warned me about myself: the outputs "were edited by merry format … not regenerated by build_runner, so a later regeneration may reintroduce final parameters until the generator emits Dart 3.13-compatible code."

So far, that hasn't happened. PR #14 (2026-10-09) lists dart run build_runner build and flutter analyze among its validation steps, and on 2026-10-12 a grep of lib/ on main found no final String? $type parameter left. The note I left myself still says to run flutter analyze after any regeneration.

If you commit generated code too, bump the SDK constraint and run flutter analyze in the same change, then run it again after every regeneration.

What I took away from this

The loud failures were the cheap ones. A compile error and a debug assertion both stop you on the spot. The go_router change cost the most, because the app kept running. Behavior changes without a compile error are the ones to look for on purpose.

A package upgrade during an SDK upgrade is its own upgrade. I went in thinking "Flutter 3.47," and one of the four breakages had nothing to do with Flutter's release at all. Read the changelog of every major bump in the lockfile diff, even the one-line ones.

Tests found three of the four. The icon package, the ListTile assertion, and the go_router regression all surfaced through widget tests. The two screens without tests are the two I'm least sure about.

If you're about to make the same jump, run the four checks above before you touch pubspec.yaml. And if your upgrade broke something that isn't on this list, tell me in the comments. I'd rather add a fifth item from your app than find it in mine.