Flutter

Flutter material_ui Migration: What Breaks When Material Leaves

Material and Cupertino no longer live inside Flutter. Since 3.47 they ship on pub.dev as material_ui and cupertino_ui, and the in-framework copies are frozen and about to be deprecated. This post is for Flutter developers who maintain an app or a package and need to plan the material_ui migration before the November stable release forces the issue. The official guide calls it a drop-in change, and for a fresh template it is one. However, real apps have localizations, third-party widgets and design systems built on ThemeExtension. So every claim below comes from a probe project run on Flutter 3.47.6 and 3.35.2 on the same day, with the commands and raw output included. It covers what dart fix changes, what the compatibility bridge carries across and what it drops, and the compile errors nobody mentions.

Versions Checked, and How

Version facts come from the Flutter blog, the breaking-change page and the pub.dev API on the day of writing. Behaviour comes from a stock flutter create app, migrated with the official command, plus a small local package that still imports package:flutter/material.dart. That package stands in for every dependency in your pubspec.yaml that has not migrated yet.

Checked 2026-10-08
Windows 11 Pro 25H2 (build 26200), AMD Ryzen 5 8600G, 15.2 GB RAM
Flutter 3.47.6 stable (Dart 3.13.5, framework 5fc346839b, engine 692136cb65), current stable, released 2026-10-01
Flutter 3.35.2 stable (Dart 3.9.0), for the old-SDK resolution check
material_ui 1.6.0 (published 2026-10-06), cupertino_ui 1.1.2 (published 2026-10-06)
Android release builds: android-arm64, 3.47.6 template (Gradle 9.3.1, AGP 9.1.0, Kotlin 2.4.0), JDK 21
Breaking-change page  https://docs.flutter.dev/release/breaking-changes/material-ui-and-cupertino-ui
Package versions      https://pub.dev/packages/material_ui/versions
Next scheduled re-check: 2027-01-08, or when the deprecation ships in a stable release

Most results here are compiler and analyzer output, which does not vary between runs. The one size measurement was built twice per variant and produced byte-identical files, as shown later. Two limitations apply throughout. First, the widget behaviour comes from flutter test, not from a device. Second, the probe package covers the patterns that commonly break, not every widget a real dependency might use.

Where the Decoupling Stands in October 2026

The short version: the packages are at 1.x, migration is optional on 3.47, and the deprecation has merged to master but has not reached stable. The timeline below uses official sources only.

DateWhat happenedSource
2026-02-18material_ui and cupertino_ui 0.0.1 published as empty placeholderspub.dev versions
2026-04-07Code freeze announced for Material and Cupertino inside flutter/flutter, effective with 3.44Code freeze post
2026-05-18Flutter 3.44 stable. The in-framework design libraries stop receiving changesBreaking-change page
2026-08-12Flutter 3.47 stable and both packages at 1.0.0, copied from the frozen codeWhat’s new in 3.47
2026-09-22material_ui 1.4.0 raises its minimum to Flutter 3.47 and Dart 3.13material_ui changelog
2026-10-03PR 192957 deprecates package:flutter/material.dart and package:flutter/cupertino.dart on masterflutter/flutter
November 2026Deprecation expected in the Fall stable release. Removal has no date3.47 release post

After 1.0.0, material_ui shipped eight more releases in eight weeks, one of them retracted. Version 1.5.0 added Material 3 Expressive support for IconButton, and 1.6.0 fixed a web crash in showAboutDialog. None of that reaches an app that still imports package:flutter/material.dart, because the copy inside the SDK is frozen. That is the practical cost of waiting. Every fix now lands only in the package.

What the Deprecation Will Do to Your CI

The deprecation message on master reads: “Use package:material_ui/material_ui.dart instead. This feature was deprecated after v3.47.0-0.0.pre.” Once it ships, every file that imports the old library gets a deprecation diagnostic. That matters more than it sounds. flutter analyze treats info-level issues as fatal by default:

flutter analyze -h | grep -A1 "fatal-infos"
    --[no-]fatal-infos        Treat info level issues as fatal.
                              (defaults to on)

So a pipeline that runs flutter analyze with default flags goes red on the day someone bumps the SDK, unless the imports are already migrated or the team passes --no-fatal-infos. Notably, this happens without any change to the app’s own code.

What dart fix Does in the material_ui Migration

The official migration command is dart fix --apply --code=migrate_design_widgets. The fix ships with the Dart 3.13 analyzer, so it already works on 3.47.6, before the deprecation lands. Here it is on an untouched 3.47.6 template:

flutter create --platforms=android,ios fix_probe > /dev/null && cd fix_probe
git init -q && git add -A && git commit -qm "before migration"
dart fix --dry-run --code=migrate_design_widgets
dart fix --apply --code=migrate_design_widgets
git diff -U0 -- lib pubspec.yaml test
Computing fixes in fix_probe (dry run)...

3 proposed fixes in 3 files.

lib\main.dart
  migrate_design_widgets - 1 fix

pubspec.yaml
  missing_dependency - 1 fix

test\widget_test.dart
  migrate_design_widgets - 1 fix

To fix an individual diagnostic, run one of:
  dart fix --apply --code=migrate_design_widgets 
  dart fix --apply --code=missing_dependency 

To fix all diagnostics, run:
  dart fix --apply 
Computing fixes in fix_probe...
Applying fixes...

lib\main.dart
  migrate_design_widgets - 1 fix

pubspec.yaml
  missing_dependency - 1 fix

test\widget_test.dart
  migrate_design_widgets - 1 fix

3 fixes made in 3 files.
diff --git a/lib/main.dart b/lib/main.dart
index 244a702..b9e22f2 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1 +1 @@
-import 'package:flutter/material.dart';
+import 'package:material_ui/material_ui.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index c64bc49..44df2c4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -37,0 +38 @@ dependencies:
+  material_ui: any
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 82bedaf..f950806 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -8 +8 @@
-import 'package:flutter/material.dart';
+import 'package:material_ui/material_ui.dart';

Two details matter here. First, the new flutter create template on 3.47.6 still imports package:flutter/material.dart, so new projects start unmigrated. Second, the fix adds material_ui: any, with no version constraint at all. Pub resolved it to 1.6.0, but any lets a future 2.0 in on the next flutter pub upgrade. Replace it with a caret constraint right away:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  material_ui: ^1.6.0
  # Only if you import package:cupertino_ui directly; material_ui already depends on it.
  cupertino_ui: ^1.1.2

After that, flutter analyze reported no issues and the template’s widget test passed. For the stock template, the material_ui migration really is three lines.

The Localization Import That Stops Compiling

Most production apps are not the stock template. They register localization delegates from flutter_localizations, and dart fix does not touch that import. The probe file below is the standard setup from the Flutter internationalization guide, after dart fix rewrote line 1:

// lib/app_l10n.dart, as left by dart fix
import 'package:material_ui/material_ui.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

const List<LocalizationsDelegate<dynamic>> appDelegates = <LocalizationsDelegate<dynamic>>[
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
  GlobalCupertinoLocalizations.delegate,
];

Widget buildApp(Widget home) => MaterialApp(localizationsDelegates: appDelegates, home: home);
flutter analyze lib/app_l10n.dart
  error - Const variables must be initialized with a constant value. Try changing the initializer to be a constant expression - lib\app_l10n.dart:5:3 - const_initialized_with_non_constant_value
  error - The name 'GlobalMaterialLocalizations' is defined in the libraries 'package:flutter_localizations/src/material_localizations.dart (via package:flutter_localizations/flutter_localizations.dart)' and 'package:material_ui/src/global_material_localizations.dart (via package:material_ui/material_ui.dart)'. Try using 'as prefix' for one of the import directives, or hiding the name from all but one of the imports - lib\app_l10n.dart:5:3 - ambiguous_import
  error - The values in a const list literal must be constants. Try removing the keyword 'const' from the list literal - lib\app_l10n.dart:5:3 - non_constant_list_element

material_ui exports its own GlobalMaterialLocalizations, so the name now exists twice. The tempting fix is to hide one of them. Do not hide the wrong one, because the next section shows what happens when a migrated app keeps the old delegates. The correct change drops the flutter_localizations import from that file and uses the combined list:

// lib/app_l10n.dart, after the material_ui migration
import 'package:material_ui/material_ui.dart';

// One list covering Material, Cupertino and Widgets strings for every supported locale.
final List<LocalizationsDelegate<dynamic>> appDelegates = <LocalizationsDelegate<dynamic>>[
  AppLocalizations.delegate, // your generated gen-l10n class, if you have one
  ...GlobalMaterialLocalizations.delegates,
];

Keep flutter_localizations in pubspec.yaml if your generated AppLocalizations class or anything else still needs it. The delegate list is the only part that has to move.

Old Delegates Fail Quietly in a Migrated App

If the ambiguity gets resolved by keeping the old flutter_localizations delegates, the code compiles. Even so, the migrated widgets cannot find their strings. The old delegate registers the old MaterialLocalizations type, and material_ui widgets look up a different class with the same name. This probe runs a migrated app in German with each set of delegates:

// test/l10n_probe_test.dart
import 'package:flutter_localizations/flutter_localizations.dart' as old_l10n;
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';

Future<void> probe(WidgetTester tester, String name, List<LocalizationsDelegate<dynamic>> delegates) async {
  String? label;
  await tester.pumpWidget(MaterialApp(
    locale: const Locale('de'),
    supportedLocales: const <Locale>[Locale('en'), Locale('de')],
    localizationsDelegates: delegates,
    home: Builder(builder: (BuildContext context) {
      // The lookup every material_ui widget performs for its labels and tooltips.
      label = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations)?.cancelButtonLabel;
      return const Scaffold(body: SizedBox());
    }),
  ));
  final Object? error = tester.takeException();
  print('== $name');
  print('  material_ui MaterialLocalizations.cancelButtonLabel: $label');
  print('  exception: ${error == null ? 'none' : error.toString().split('\n').first}');
}

void main() {
  testWidgets('old delegates kept after dart fix', (WidgetTester tester) async {
    await probe(tester, 'flutter_localizations delegates (unchanged by dart fix)', const <LocalizationsDelegate<dynamic>>[
      old_l10n.GlobalMaterialLocalizations.delegate,
      old_l10n.GlobalWidgetsLocalizations.delegate,
      old_l10n.GlobalCupertinoLocalizations.delegate,
    ]);
  });

  testWidgets('material_ui delegates', (WidgetTester tester) async {
    await probe(tester, 'GlobalMaterialLocalizations.delegates from material_ui', GlobalMaterialLocalizations.delegates);
  });
}
flutter test test/l10n_probe_test.dart | grep -E "^(==|  material_ui|  exception)"
== flutter_localizations delegates (unchanged by dart fix)
  material_ui MaterialLocalizations.cancelButtonLabel: null
  exception: Warning: This application's locale, de, is not supported by all of its localization delegates.
== GlobalMaterialLocalizations.delegates from material_ui
  material_ui MaterialLocalizations.cancelButtonLabel: Abbrechen
  exception: none

With the old delegates, the migrated app has no Material strings for German at all. In practice, any Material widget that checks for localizations, such as a date picker or an AppBar back button tooltip, asserts the first time it needs a label. Moreover, English-only testing never shows it, because MaterialApp falls back to its built-in English strings. Run at least one widget test in a second locale after the migration.

What Happens to Packages That Still Import flutter/material.dart

This is the part of the material_ui migration that dart fix cannot do for you. Your app can migrate today. Your dependencies migrate on their own schedule. The official guide even asks package authors to treat the change as a major version bump. Until they do, your app runs two copies of Material side by side. Their classes share names but not identities.

The probe package below behaves like a typical unmigrated dependency. It reads the theme, ships a list row, and shows a SnackBar:

// packages/legacy_widgets/lib/legacy_widgets.dart
// Stand-in for an unmigrated third-party package.
import 'package:flutter/material.dart';

class BrandColors extends ThemeExtension<BrandColors> {
  const BrandColors({required this.accent});
  final Color accent;

  @override
  BrandColors copyWith({Color? accent}) => BrandColors(accent: accent ?? this.accent);

  @override
  BrandColors lerp(BrandColors? other, double t) =>
      BrandColors(accent: Color.lerp(accent, other?.accent, t)!);
}

/// Reports what a legacy widget resolves from context, without painting anything.
Map<String, String> readLegacyContext(BuildContext context) {
  final ThemeData theme = Theme.of(context);
  final ButtonStyle? buttonStyle = theme.elevatedButtonTheme.style;
  final MaterialLocalizations? l10n =
      Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
  String hex(Color? c) =>
      c == null ? 'null' : '0x${c.toARGB32().toRadixString(16).padLeft(8, '0')}';
  return <String, String>{
    'colorScheme.primary': hex(theme.colorScheme.primary),
    'elevatedButtonTheme bg': hex(buttonStyle?.backgroundColor?.resolve(<WidgetState>{})),
    'extension<BrandColors>': hex(theme.extension<BrandColors>()?.accent),
    'MaterialLocalizations': l10n == null ? 'null' : l10n.okButtonLabel,
  };
}

/// A list row a package might ship. It needs a Material ancestor for its ink.
class LegacyTile extends StatelessWidget {
  const LegacyTile({super.key});
  @override
  Widget build(BuildContext context) =>
      ListTile(title: const Text('legacy tile'), onTap: () {});
}

/// A package helper that shows a SnackBar through ScaffoldMessenger.
void showLegacySnackBar(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('saved')));
}

/// A package API with a Material type in its public signature.
Widget themedPreview(ThemeData theme) => Theme(data: theme, child: const SizedBox());

Four App Setups, One Unmigrated Package

The widget test pumps the same probe page under four app setups. Every app uses a teal seed colour (0xFF00796B) and an orange ElevatedButton background (0xFFFF5722). Case A is the baseline, an app that has not migrated. Cases B to D use material_ui. Here is case C, which follows the official bridge instructions. The other cases differ only in the builder and home lines:

// test/bridge_probe_test.dart, case C (abridged; helpers runCase and Probe omitted)
// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart' as legacy;
import 'package:flutter_test/flutter_test.dart';
import 'package:legacy_widgets/legacy_widgets.dart';
import 'package:material_ui/material_ui.dart';

testWidgets('C: migrated app, MaterialUiCompatibilityBridge', (WidgetTester tester) async {
  final Map<String, String> out = <String, String>{};
  await runCase(tester, 'C: app on material_ui, with bridge', MaterialApp(
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00796B)),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFFF5722))),
    ),
    builder: (BuildContext context, Widget? child) =>
        MaterialUiCompatibilityBridge(child: child!),
    home: Scaffold(body: Probe(out: out)),
  ), out);
});
// Case B drops the builder. Case D wraps the child in legacy.ScaffoldMessenger
// and the body in legacy.Scaffold, so legacy widgets find their own ancestors.

runCase pumps the app, records tester.takeException() after the build, then calls showLegacySnackBar and checks that the text “saved” is on screen. Each result line starts with PROBE, which keeps Flutter’s own error dumps out of the filtered output:

flutter test test/bridge_probe_test.dart | grep "^PROBE"
PROBE == A: app on flutter/material.dart (baseline)
PROBE   colorScheme.primary      0xff006b5e
PROBE   elevatedButtonTheme bg   0xffff5722
PROBE   extension<BrandColors>   0xff3f51b5
PROBE   MaterialLocalizations    OK
PROBE   LegacyTile build         ok
PROBE   legacy showSnackBar      ok, SnackBar visible: true
PROBE == B: app on material_ui, no bridge
PROBE   colorScheme.primary      0xff6750a4
PROBE   elevatedButtonTheme bg   null
PROBE   extension<BrandColors>   null
PROBE   MaterialLocalizations    null
PROBE   LegacyTile build         Multiple exceptions (2) were detected during the running of the current test, and at least one was unexpected.
PROBE   legacy showSnackBar      not reached
PROBE == C: app on material_ui, with bridge
PROBE   colorScheme.primary      0xff006b5e
PROBE   elevatedButtonTheme bg   null
PROBE   extension<BrandColors>   null
PROBE   MaterialLocalizations    OK
PROBE   LegacyTile build         Multiple exceptions (2) were detected during the running of the current test, and at least one was unexpected.
PROBE   legacy showSnackBar      not reached
PROBE == D: bridge + legacy Material + legacy ScaffoldMessenger
PROBE   colorScheme.primary      0xff006b5e
PROBE   elevatedButtonTheme bg   null
PROBE   extension<BrandColors>   null
PROBE   MaterialLocalizations    OK
PROBE   LegacyTile build         ok
PROBE   legacy showSnackBar      ok, SnackBar visible: true

The extension<BrandColors> row reads null in B to D for a reason covered below. A migrated app cannot register the package’s extension at all.

Cases B and C throw the same pair of exceptions. First comes the assertion below, from the ListTile inside the package. After it, a layout assertion follows from the broken tile:

The following assertion was thrown building ListTile(onTap: Closure: () => void, dirty):
No Material widget found.
ListTile widgets require a Material widget ancestor within the closest LookupBoundary.

Without the bridge (case B), the unmigrated package sees the default Material 3 purple, 0xff6750a4, instead of your teal. It finds no localizations and crashes on its first ListTile, even though that tile sits inside a perfectly good Scaffold. The Scaffold belongs to material_ui, and the package’s debugCheckHasMaterial looks for the other library’s Material widget.

What MaterialUiCompatibilityBridge Carries, and What It Drops

With the bridge (case C), colours and localizations come back. Component themes do not, and the ListTile still crashes. The reason is in the bridge’s source, lib/src/migration_utility.dart in material_ui 1.6.0. It builds a fresh legacy ThemeData from four fields of your theme:

// material_ui 1.6.0, MaterialUiCompatibilityBridge._mapToLegacy (field list abridged)
return legacy.ThemeData(
  platform: modernTheme.platform,
  visualDensity: legacy.VisualDensity(...),
  colorScheme: legacy.ColorScheme(/* all 35 colour roles copied */),
  textTheme: legacy.TextTheme(/* all 15 text styles copied */),
);

Everything else falls back to the default for that colour scheme. That includes elevatedButtonTheme, inputDecorationTheme, appBarTheme and every other component theme, pageTransitionsTheme, and extensions. The bridge also injects only Theme and Localizations. It does not provide a legacy Material, ScaffoldMessenger, or any other ancestor that the old library’s widgets search for. CupertinoUiCompatibilityBridge in cupertino_ui 1.1.2 follows the same pattern for CupertinoThemeData. Both classes are marked @Deprecated from day one, as temporary migration utilities.

Case D shows the workaround. Wrapping the package’s subtree in legacy.Scaffold (or legacy.Material) and the app in legacy.ScaffoldMessenger makes the package’s widgets work again. Your component themes are still missing on the package’s buttons, though. If a package’s screens must match your button and input styling, the bridge alone is not enough. Either wait for the package to migrate, or wrap its subtree in a legacy.Theme that you build yourself with the component themes you need.

Type Errors the Bridge Cannot Fix

The bridge works through BuildContext. It cannot help when a package exposes Material types in its public API. Two common patterns fail at compile time:

// lib/type_probe.dart
import 'package:legacy_widgets/legacy_widgets.dart';
import 'package:material_ui/material_ui.dart';

// Passing material_ui types into a package API that still speaks flutter/material types.
final ThemeData appTheme = ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00796B)),
  extensions: const <ThemeExtension<dynamic>>[BrandColors(accent: Color(0xFF3F51B5))],
);

final Widget preview = themedPreview(appTheme);
flutter analyze lib/type_probe.dart
  error - The element type 'BrandColors' can't be assigned to the list type 'ThemeExtension<dynamic>' - lib\type_probe.dart:7:47 - list_element_type_not_assignable
  error - The argument type 'ThemeData (where ThemeData is defined in C:\Users\User\AppData\Local\Pub\Cache\hosted\pub.dev\material_ui-1.6.0\lib\src\theme_data.dart)' can't be assigned to the parameter type 'ThemeData (where ThemeData is defined in C:\Users\User\AppData\Local\Temp\flutter-sdk\flutter\packages\flutter\lib\src\material\theme_data.dart)'.  - lib\type_probe.dart:10:38 - argument_type_not_assignable
2 issues found. (ran in 7.7s)

The first error hits design-system packages hardest. A shared ThemeExtension subclass defined in an unmigrated package cannot sit in a migrated app’s ThemeData.extensions. Teams that split reusable UI components into an internal package use this pattern all the time. The second error hits any package that accepts a ThemeData, ColorScheme, TextTheme or ButtonStyle as a parameter. Official guidance states the same limit: dependencies with API-signature coupling must migrate before your app can pass modern types to them. So the order is fixed. Internal design-system packages migrate first, then the apps that consume them.

Does material_ui Work on Older Flutter Versions?

No, not in any useful sense, and pub will not tell you. Every material_ui release from 0.0.2 to 1.3.0 requires Flutter 3.44 or later. Since 1.4.0 the floor is 3.47. Version 0.0.1 has no such floor, so an older SDK quietly resolves to it. Here is what that looks like on Flutter 3.35.2, the SDK many teams still pin:

flutter --version | head -1
flutter create --platforms=android old_probe > /dev/null && cd old_probe
flutter pub add material_ui | grep material_ui
grep -A1 "^  material_ui:" pubspec.yaml
# Print the resolved package's library file, found through package_config.json.
cat "$(grep -A1 '"name": "material_ui"' .dart_tool/package_config.json | grep -o 'file://[^"]*' | sed -E 's#^file://##; s#^/([A-Za-z]:)#\1#')/lib/material_ui.dart" | grep -v "^//"
Flutter 3.35.2 • channel stable • https://github.com/flutter/flutter.git
+ material_ui 0.0.1 (1.6.0 available)
  material_ui: ^0.0.1


library material_ui;

export 'package:flutter/material.dart';

Version 0.0.1 is one line: a re-export of the old library. A project on 3.35.2 can therefore “migrate” its imports, pass flutter analyze with no issues, and still run the frozen in-framework code. MaterialUiCompatibilityBridge and GlobalMaterialLocalizations.delegates do not exist in it. The trap is the ^0.0.1 constraint pub writes, which never upgrades to 1.x on its own.

The resolution rules on the day of writing are:

Your Flutter SDKNewest material_ui pub resolvesWhat you get
3.35 to 3.410.0.1A re-export of flutter/material.dart, no new APIs
3.44.x1.2.0 (1.3.0 is retracted)The real package, three minor releases behind
3.47.x1.6.0Current

For an app, the fix is to upgrade Flutter to 3.47 first and migrate second. For a package, add an explicit SDK floor so resolution fails loudly instead of silently:

# pubspec.yaml of a package that has migrated to material_ui
environment:
  sdk: ^3.13.0
  flutter: ">=3.47.0"

dependencies:
  flutter:
    sdk: flutter
  material_ui: ^1.4.0

The package publishing guide covers how to announce a breaking release like this one, with a changelog entry and a major version bump.

How Much Does an Unmigrated Dependency Add to App Size?

An app that has migrated but still uses one unmigrated package compiles both Material libraries. Tree shaking removes the classes nobody references. Even so, the bridge itself references a lot of the old library. To put a number on it, three entry points were built as release APKs for android-arm64 from the same probe project:

  • main_legacy.dart: the 3.47.6 template, unmigrated
  • main_migrated.dart: the same template after dart fix
  • main_mixed.dart: the migrated template plus one LegacyTile from the probe package, inside legacy.Material, with MaterialUiCompatibilityBridge in MaterialApp.builder
for v in legacy migrated mixed; do
  flutter build apk --release --target-platform android-arm64 -t lib/main_$v.dart > ../build_$v.log 2>&1
  echo "$v exit=$? $(tail -1 ../build_$v.log)"
  unzip -l build/app/outputs/flutter-apk/app-release.apk | grep -E "libapp.so"
  ls -l build/app/outputs/flutter-apk/app-release.apk | awk '{print "apk bytes", $5}'
done
legacy exit=0 √ Built build\app\outputs\flutter-apk\app-release.apk (14.8MB)
  3212168  1981-01-01 01:01   lib/arm64-v8a/libapp.so
apk bytes 15493027
migrated exit=0 √ Built build\app\outputs\flutter-apk\app-release.apk (14.8MB)
  3277704  1981-01-01 01:01   lib/arm64-v8a/libapp.so
apk bytes 15558563
mixed exit=0 √ Built build\app\outputs\flutter-apk\app-release.apk (15.2MB)
  3605384  1981-01-01 01:01   lib/arm64-v8a/libapp.so
apk bytes 15886243

A second build of mixed and legacy produced the same libapp.so sizes to the byte, 3605384 and 3212168. The migrated template’s Dart code is 65,536 bytes larger than the unmigrated one. Adding one legacy widget plus the bridge adds another 327,680 bytes. Both differences are exact multiples of 16 KiB, so the shared object grows in whole aligned pages. Treat them as rounded up, not as precise per-class costs.

The source explains most of the 320 KiB. The bridge constructs a legacy ThemeData and installs the legacy flutter_localizations delegates for every locale they support. That keeps the old theme system and the old translation tables alive in the binary. For most apps this is a temporary cost that disappears once the last dependency migrates. Nevertheless, it is worth knowing when a size budget is tight. The Flutter app size measurements put it in context against typical package costs.

How to Do the material_ui Migration Safely

Here is the order that turns the material_ui migration into one planned release instead of a string of surprises:

  1. Upgrade to Flutter 3.47 or later first. On older SDKs, pub resolves a re-export or an outdated release
  2. Run dart fix --dry-run --code=migrate_design_widgets and read the list of files before applying anything
  3. Apply the fix, then replace material_ui: any in pubspec.yaml with ^1.6.0 or newer
  4. Delete the GlobalMaterialLocalizations import from flutter_localizations wherever the analyzer reports ambiguous_import, and switch to ...GlobalMaterialLocalizations.delegates
  5. List every resolved dependency whose source still imports package:flutter/material.dart or package:flutter/cupertino.dart, with the script below
  6. Migrate internal design-system packages before the apps that depend on them, because shared ThemeExtension classes and ThemeData parameters cannot cross the boundary
  7. Add MaterialUiCompatibilityBridge in MaterialApp.builder only if step 5 found third-party packages that read the theme from context
  8. Wrap each unmigrated package’s screens in legacy.Material or legacy.Scaffold, and supply a legacy.Theme if they need your component themes
  9. Run widget tests in a second locale and golden tests on screens that host package widgets

Step 5 is the one most teams skip. The script below reads .dart_tool/package_config.json, which lists every resolved package with its location, and searches each package’s lib/ for the old imports. It works the same with hosted, git and path dependencies:

# Lists every resolved package whose lib/ still imports the in-framework design libraries.
# Run from the app root after `flutter pub get`.
grep -E '"(name|rootUri)"' .dart_tool/package_config.json \
  | sed -E 's/.*": "(.*)",?$/\1/' | paste - - \
  | while IFS=$'\t' read -r name uri; do
      case "$name" in
        flutter|flutter_test|flutter_localizations|flutter_web_plugins|material_ui|cupertino_ui) continue ;;
      esac
      dir=$(echo "$uri" | sed -E 's#^file://##; s#^/([A-Za-z]:)#\1#')
      case "$dir" in /*|[A-Za-z]:*) ;; *) dir=".dart_tool/$dir" ;; esac
      grep -rlqE "package:flutter/(material|cupertino)\.dart" "$dir/lib" 2>/dev/null && echo "$name"
    done

Run against the probe project, it printed:

leak_tracker_flutter_testing
legacy_widgets
design_probe

legacy_widgets is the stand-in package, as expected. design_probe is the app itself, because the probe keeps an unmigrated entry point for the size comparison. That line is useful in a real project too, since it means some of your own files still use the old import. leak_tracker_flutter_testing arrives through flutter_test as a dev dependency. It never ships in your app, so it needs no action. Every other name the script prints is a package whose widgets need the bridge, a legacy.Material ancestor, or a newer release. Golden tests catch the visual half of this cheaply. The widget and integration testing guide shows how to set them up for exactly this kind of regression.

Real-World Scenario: A Date Picker Package Turns Purple

Consider a mid-sized B2B app with 25 to 35 screens, maintained by a small team, localized into English and German. It uses an internal design_system package with a ThemeExtension for brand colours, plus a third-party form package whose date and dropdown fields import package:flutter/material.dart. The team upgrades to 3.47.6 and runs the official command, expecting a quick change.

First, the app stops compiling on the GlobalMaterialLocalizations ambiguity. That takes minutes to fix once the cause is clear. Next, the design-system package breaks the build, because its ThemeExtension subclass is a legacy type. So the team migrates design_system first and publishes it as a new major version. With that done, the app compiles and every unit test passes. QA then finds the settings screen showing default purple date fields, and a red error screen in debug builds on the one page where a package ListTile sits inside the app’s Scaffold.

The bridge in MaterialApp.builder fixes the colours and the German labels. A legacy.Material around the form package’s subtree fixes the crash. The package’s buttons still ignore the app’s elevatedButtonTheme, because the bridge does not carry component themes. The trade-off the team faces is concrete: ship with slightly off-brand buttons on two screens, build a legacy.Theme by hand for that subtree, or wait for the package’s migrated major release. For most teams, the hand-built legacy.Theme is a few hours of work and is deleted later. The bigger lesson is the order: design-system packages before apps, and a dependency audit before the first dart fix --apply.

When to Do the material_ui Migration Now

  • Your app is on Flutter 3.47 or later and you want fixes and features that now ship only in material_ui
  • Your dependencies that render Material widgets have already migrated, or you have very few of them
  • You own internal packages that share theme types, and you can migrate them in the same release cycle
  • Your CI runs flutter analyze with default flags and you want to be done before the November deprecation turns it red

When NOT to Migrate to material_ui Yet

  • Your app is still on Flutter 3.44 or older, where pub resolves an outdated release or a plain re-export
  • A dependency passes ThemeData, ColorScheme or a ThemeExtension through its public API and has no migrated release
  • Your product relies on component themes inside third-party widgets, and the bridge would silently drop them
  • You maintain a package whose users span many SDK versions and you are not ready to publish a major version with a 3.47 floor

Common Mistakes with the material_ui Migration

  • Accepting the material_ui: any constraint that dart fix writes, which lets a future major version in unnoticed
  • Resolving the GlobalMaterialLocalizations ambiguity by keeping the flutter_localizations delegate, which leaves migrated widgets with no translations
  • Testing only in English, where MaterialApp‘s built-in strings hide missing delegates
  • Expecting MaterialUiCompatibilityBridge to carry component themes and ThemeData.extensions, when it copies colours, text styles, density and platform only
  • Assuming a material_ui Scaffold satisfies an unmigrated widget’s Material ancestor check
  • Running flutter pub add material_ui on an older SDK and treating the ^0.0.1 result as a finished migration
  • Migrating an app before the internal design-system package it shares theme types with
  • Publishing a migrated package as a minor version, when the official guidance calls for a major bump

Next Steps for the material_ui Migration

The material_ui migration is three changed lines for a template and a dependency-ordering problem for a real app. dart fix handles the imports. It does not handle localization delegates, version constraints, shared theme types, or the unmigrated packages that now see a different Theme. The compatibility bridge closes part of that gap, colours and strings, and leaves component themes and Material ancestors to you. Start today by running the dry-run command and the dependency audit above on your current project. That shows the size of the work before November makes it urgent. If 3.47 also changed rendering on your Android builds, the Impeller on Android migration guide covers the other half of this upgrade.