•5 min read

Flutter 3.47: Migrating to the Standalone material_ui Package

A heap of loose building bricks, separated from the set they came in

I put the material_ui migration off for a month because the release blog made it sound like one command. It is one command. I finally ran it last week, and the month of waiting turned out to be the interesting part, because the package did a lot of moving while I did nothing.

Flutter 3.47 shipped on August 12 and pulled Material and Cupertino out of the core SDK. They live on pub.dev now as material_ui and cupertino_ui, with weekly releases planned instead of the quarterly SDK train. Nothing breaks today. The in-framework libraries still ship in 3.47 and are scheduled for formal deprecation in the Fall stable release in November, which is the deadline sitting under all of this.

The command works, the pubspec is the loose brick

The automated path is a dart fix rule that rewrites your imports. The class names do not change, so in theory that is the whole job.

dart fix --apply --code=migrate_design_widgets
flutter pub add material_ui
flutter pub add cupertino_ui   # only if you use Cupertino widgets
dart fix --apply

Two things about that sequence. The pub add lines exist because the fix rule has a known early bug where it fails to add the new dependencies to your pubspec.yaml, so add them by hand and re-run. The second dart fix --apply is also deliberate: the first pass rewrites imports, the second cleans up what the rewrite disturbed, mostly import sorting and lint warnings.

The bridge is a theme bridge, not a type bridge

Your own code is the easy half. The other half is the third-party packages still importing package:flutter/material.dart. That is where MaterialUiCompatibilityBridge comes in. Wrap your app inside MaterialApp.builder:

import 'package:material_ui/material_ui.dart';

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4)),
  ),
  builder: (BuildContext context, Widget? child) {
    return MaterialUiCompatibilityBridge(child: child!);
  },
  home: const HomeScreen(),
)

You can wrap individual subtrees instead, which I did for one stubborn package.

The bridge injects theme and localization data down the tree, so an unmigrated widget calling Theme.of(context) or MaterialLocalizations.of(context) still resolves and still looks right. What it does not do is reconcile types, and that is the part that bites.

import 'package:flutter/material.dart' as legacy;
import 'package:material_ui/material_ui.dart';

// Same names, two unrelated types. This will not compile.
legacy.ColorScheme toOldPath(ColorScheme modern) => modern;

Dart enforces static typing across distinct libraries, so a ColorScheme from the old path and a ColorScheme from material_ui have no common supertype. Your own code fails loudly, which is fine. The quiet case is a dependency whose public API takes or returns those types, say a TextTheme parameter or a callback returning a FloatingActionButtonLocation. The bridge cannot reach into that signature. Sort your dependencies into two buckets: ones that only style their own output and work fine behind the bridge, and ones that put design types in their API. The second bucket is your critical path.

Read the version list before trusting the older advice

A lot of write-ups, including the ones I read in August, point out that material_ui 1.0.0 needed only Dart 3.12. The conclusion was appealing: migrate the UI packages on Flutter 3.44 now and upgrade the SDK later, as two separate jobs.

That was true in August. It stopped being true. Here is the history, which pub.dev shows plainly and the release blog does not mention.

1.0.0   Aug 12   Flutter 3.44 / Dart 3.12
1.2.0   Sep 08   Flutter 3.44 / Dart 3.12
1.3.0   Sep 15   retracted
1.4.0   Sep 22   Flutter 3.47 / Dart 3.13

1.4.0 bumped the floor, and its changelog leads with that line. Run flutter pub add material_ui on Flutter 3.44 and nothing complains. It resolves down to 1.2.0 and says nothing, so you quietly miss six weeks of the fixes that justified decoupling in the first place.

The retracted row deserves a mention. Retraction is only available for seven days after publication, and the version is not picked for new resolutions. If one is already in your pubspec.lock it keeps working until you run dart pub upgrade material_ui, and holding onto it on purpose means dependency_overrides. Use a caret constraint rather than a pin and you will never notice.

Meanwhile the package shipped real fixes: SearchAnchor stopped letting slow suggestions overwrite newer ones, StyleVariant arrived for Material 3 Expressive, and DataTable got a sortIconBuilder. Under the old model each of those would have waited for a quarterly SDK release. Your builds now track two release trains instead of one.

Localizations got shorter while you were reading

If you use the global localization delegates, the setup collapses from three delegates to one. GlobalMaterialLocalizations.delegates now includes the Cupertino and Widgets delegates for you.

import 'package:material_ui/material_ui.dart';

MaterialApp(
  localizationsDelegates: GlobalMaterialLocalizations.delegates,
)

If you never touched GlobalMaterialLocalizations or GlobalCupertinoLocalizations, there is nothing to do here. RTL support is not affected, which was my first worry.

The rest of the same upgrade

While you are in there anyway: minimum supported versions moved to iOS 15 and macOS 12, so check your analytics before assuming nobody is on iOS 13. iOS 27 requires the UIScene lifecycle, and apps built with Xcode 27 that skip it fail to launch rather than warn, though the CLI handles most projects.

What I would actually do, and did: branch, run the fix, then count two numbers. How many files the rewrite touched, and how many dependencies still import the old package:flutter/material.dart. Those tell you whether this is a Tuesday afternoon or something worth planning. I keep the commands and the dependency grep as a snippet in Snippet Ark, because we have three more Flutter apps and I am not going through this from memory again.

Our app came through clean. One package needs its own migration before I can pass it a modern ColorScheme, and the bridge holds that corner until it ships. Honest verdict: this is a one-command job for your own code and a dependency audit for everything else.

It stays optional until November, which is why most of us will treat it as urgent around late October. If you want more Flutter upgrade pain to go with it, the scroll jank checklist and the state management post-mortem are the two I keep re-reading.