Fetching latest headlines…
Can you migrate to material_ui yet?

Dev

Can you migrate to material_ui yet?

Dev.toUnited States · NORTH AMERICA

What changed, and why For years, package:flutter/material.dart came with the SDK. You installed Flutter, and Material came with it, same version, same release schedule, no decision to make. That is...

0 views0 likes0 comments

What changed, and why

For years, package:flutter/material.dart came with the SDK. You installed Flutter, and Material came with it, same version, same release schedule, no decision to make.

That is changing. Since Flutter 3.47, Material and Cupertino also exist as two packages on pub.dev,material_ui and cupertino_ui, published by flutter.dev and versioned on their own. The SDK still ships its own copy for now, but that copy is frozen.

The three architectural layers are untouched. What moved is the top of the framework layer.

Flutter architecture before the split: Material and Cupertino sit inside the framework layer, above Widgets

Flutter architecture after the split: material_ui and cupertino_ui are packages above the framework, whose top layer is now Widgets
Material and Cupertino used to sit inside the framework, on top of widgets.
You can now use them above it, as ordinary packages you add to pubspec.yaml.

Why now

Three reasons, all from the announcement by Craig Labenz on 9 September 2026.

1. Upgrading Flutter meant redoing your UI

Material shipped inside the SDK. So a Flutter upgrade, even one you wanted only for a security patch, brought Material changes with it. You could not take one without the other.

The team is blunt about what that cost:

a major source of developer friction that kept many teams on older Flutter
releases

and

the task could consume multiple weeks of developer effort and bottleneck
entire repositories

Teams stayed on old Flutter versions on purpose, because moving up meant weeks of fixing screens. Staying behind also meant missing the security patches they wanted in the first place.

2. A fix could wait sixteen weeks

Flutter ships a stable release every three months, and Material shipped with it.
So a Material fix had to catch the release cutoff. The post gives the worst case:

landing code one minute after the stable release cutoff can introduce a 16
week delay until that change sees the light of a stable release

One minute late meant sixteen weeks of waiting.

As separate packages, material_ui and cupertino_ui are planned to release weekly. And the post says work is already underway on official implementations of Liquid Glass and Material 3 Expressive, Apple's and Google's current design languages.

3. Material was keeping the useful parts to itself

This reason is less obvious than the other two, but its effect lasts the longest.

Flutter's core is the widgets library. Material is built on top of it.

When the Material team needed something the core did not have, they added it to Material. That was faster, and it worked. But only Material got it.

it proved too tempting to build foundational functionality in those design
libraries

Other design systems exist. The post names seven: macos_ui, fluent_ui,yaru, arna, chicago, VSP, shadcn. They needed some of the same pieces, and could not reuse Material's.

Now that Material is a package like the others, the missing pieces have to go into the core instead. The official migration guide puts it this way:

Decoupling lays the groundwork for a style-neutral Flutter core widget
catalog, making it easier to build and maintain custom design systems.

In plain terms: make the core work for any design system, not just Material and Cupertino.

The team's own description of the old arrangement is the clearest line in the post. Material and Cupertino had started to feel:

more soldered-on than just included

And for years, improving this system was "a top request from Flutter
developers"
.

What you actually get

Version 1.0.0 of material_ui was a copy of what was already in the SDK. The announcement says so directly:

released exact replicas on pub.dev as the 1.0.0 versions of
pkg:material_ui and pkg:cupertino_ui

It had the same widgets, with the same names and the same behaviour. Migrating is not meant to change how your app looks.

Here is how the team describes the move:

Our expectation is that this will be a drop-in maneuver, but please file an
issue if you encounter any trouble.

Note the word "expectation". We will come back to it.

What changes in your code

One line per file:

// before
import 'package:flutter/material.dart';

// after
import 'package:material_ui/material_ui.dart';

For most files, that is the whole change. Scaffold, AppBar, TextField,
ThemeData all keep their names.

Are they really shipping weekly?

Speed was the reason for the split. So it is worth checking whether the speed arrived. Counting from pub.dev on 8 October 2026:

Package Latest Releases after 1.0.0 One every
material_ui 1.6.0 7, over 55 days 8 days
cupertino_ui 1.1.2 4, over 55 days 14 days

material_ui is shipping close to the weekly pace it promised, and a long way from a three-month cycle. cupertino_ui moves more slowly.

So on paper, migrating is one import line and nothing else. The rest of this article is what happened when I ran it.

The migration, step by step

The app under test

A small app with no third-party UI package. If something breaks, the migration is the only suspect.

It contains the pieces the migration touches: Material and Cupertino widgets, translations generated from .arb files, a language switcher, a barrel file that exports both design libraries, and two widgets that take a ColorScheme or a CupertinoThemeData as a constructor argument. Ten widget tests cover it. All ten pass before we start.

The code is in the repository,
with a tag for each state: first-app-without-migration, after-dart-fix and after-localizations-fix. Every command output quoted below is saved in its measurements/ folder.

Step 1: ask before you apply

dart fix --dry-run --code=migrate_design_widgets
17 proposed fixes in 14 files.

--dry-run changes nothing. It only lists what the tool plans to do. Sixteen fixes come from the migration rule. One comes from a second rule, missing_dependency, which targets pubspec.yaml.

Step 2: apply

dart fix --apply --code=migrate_design_widgets
17 fixes made in 14 files.

The same seventeen. Every change in the Dart files is an import line, like this one:

-import 'package:flutter/material.dart';
+import 'package:material_ui/material_ui.dart';

No widget, no constructor, no type was touched.

The pubspec.yaml was updated too, with any as the version:

+  cupertino_ui: any
+  material_ui: any

any accepts every version, including a future one that breaks your app. I replaced it with real constraints:

  cupertino_ui: ^1.1.2
  material_ui: ^1.6.0

What the tool did not touch

export directives. The barrel file is not in the diff:

// lib/core/ui.dart, unchanged after the migration
export 'package:flutter/cupertino.dart' hide RefreshCallback;
export 'package:flutter/material.dart';

The tool rewrites import lines, not export lines. This was reported in dart-lang/sdk#63968. The issue is marked as closed, but with Dart 3.13.5 the exports were still left as they were. The next part, Where it stops working, shows what this costs.

The localization import. lib/app/app.dart had two SDK imports:
material.dart and flutter_localizations.dart. Only the first one was rewritten. That is where the only error comes from.

The only error

This is the top of app.dart after the tool:

import 'package:material_ui/material_ui.dart';                      // rewritten
import 'package:flutter_localizations/flutter_localizations.dart';  // not touched

Both libraries define a class called GlobalMaterialLocalizations.
flutter_localizations still has the old one, and material_ui brought its own copy of the Material translations. The file imports both, so Dart cannot tell which one you mean:

error - lib/app/app.dart:66:9 - The name 'GlobalMaterialLocalizations' is defined in the libraries
'package:flutter_localizations/src/material_localizations.dart (via ...)' and 'package:material_ui/src/global_material_localizations.dart (via ...)' - ambiguous_import

A second error follows on the same line, non_constant_list_element. It disappears with the first one.

The fix, in two steps.

  1. Remove the flutter_localizations import from app.dart.
  2. Replace the three old delegates with the list from material_ui:
// before
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
  AppLocalizations.delegate,
  GlobalMaterialLocalizations.delegate,
  GlobalCupertinoLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
],

// after
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
  AppLocalizations.delegate,
  ...GlobalMaterialLocalizations.delegates,
],

GlobalMaterialLocalizations.delegates, with an s, is a list. The migration guide says it contains the Material, Cupertino and Widgets delegates, so the three old lines become one. The ... spreads that list into yours. The app's French tests confirm it: Material and Cupertino texts are translated.

AppLocalizations.delegate stays first. It carries your own app's texts, generated from your .arb files. The guide's example does not show this line, because its example app has no texts of its own. If yours does, keep it.

One more point if you use .arb files: keep flutter_localizations in your pubspec.yaml. Your own code no longer imports it, but the file that gen-l10n generates still does. Without the line, the app still builds, because material_ui brings the package along. But your project would then use a package it never asked for, and the lint that would warn you is switched off in generated files.

So keep it declared in pubspec.yaml, and never import it again in your own files. The import is what caused the error, not the declaration.

Did anything break?

Analyzing material_ui_migration... No issues found!
00:00 +10: All tests passed!

I also ran the same ten tests on the code from before the migration, in a separate git worktree, with the test imports switched back. Ten green there as well.

So: ten tests green before, ten green after. Seventeen automatic fixes, and one manual fix to make it compile. For an app without third-party UI packages, the documented path works.

The second pass

The guide says to run dart fix a second time to sort the imports. They do need it, because the migration breaks the alphabetical order:

import 'package:material_ui/material_ui.dart';
import 'package:flutter_test/flutter_test.dart';

But the second pass returned Nothing to fix!. Import order is checked by the lint rule directives_ordering, which is not part of flutter_lints. Without that rule, dart fix does not sort your imports.

Your dependencies stay the same

flutter pub outdated prints the same list before and after the migration. For example, material_color_utilities stays at 0.13.0 when 0.13.1 exists. In Flutter 3.47 the old Material is still inside the SDK, and the SDK holds this version. Migrating does not change that.

That is the good news. Here are the three places where it goes wrong:
a file left on the old import, an export the tool skipped, and the translation list that gen-l10n generates for you.

Where it stops working

The migration guide has a tool for apps that cannot migrate everything at once:
the compatibility bridge. You add it in MaterialApp.builder:

builder: (BuildContext context, Widget? child) {
  return MaterialUiCompatibilityBridge(child: child!);
},

dart fix does not add it for you. The guide describes what it does:

This allows unmigrated child widgets and dependencies that read design state
from context (such as Theme.of(context) or MaterialLocalizations.of(context)) to continue functioning properly.

and what it cannot do:

the compatibility bridge cannot resolve type mismatches when a dependency
exposes, accepts, or returns in-framework SDK types in its public API
signatures (such as passing a FloatingActionButtonLocation, ColorScheme,
or TextTheme as a parameter or callback return value).

One thing the guide does not say: in material_ui 1.6.0, the bridge is already marked @Deprecated, as "a temporary migration utility" that "will be removed in a future release". The changelog does not mention it either. Only the analyzer warns you, when you add it. So whatever it fixes below, it fixes for now.

I broke the migrated app in three ways, on purpose, and checked whether the bridge helps.

1. A file left on the old import

This happens when one file stays on the old import, and with packages that have not migrated yet. To reproduce it, I put one file back on its old import:

git checkout first-app-without-migration -- lib/features/legacy/legacy_summary.dart

That file reads the theme and passes its ColorScheme to StatCard, a widget that is now migrated:

The argument type 'ColorScheme (where ColorScheme is defined in .../flutter/lib/src/material/color_scheme.dart)'
can't be assigned to the parameter type 'ColorScheme (where ColorScheme is defined in .../material_ui-1.6.0/lib/src/color_scheme.dart)'.

"ColorScheme can't be assigned to ColorScheme." Same name, two classes. The compiler has to show the file paths, because the name no longer tells them apart.

The bridge cannot help here, and the guide says so: it works through the context, and this value travels as a constructor argument. The only fix is to migrate the file. At least the compiler tells you.

2. An export the tool skipped

After the migration, the app's theme belongs to material_ui. But the barrel file, lib/core/ui.dart, still exports the old Material from the SDK. And one widget, Section, imports that barrel.

So when Section asks for the theme with Theme.of(context), it asks the old Material. The old Material cannot see the app's theme, because that theme now belongs to material_ui. Instead of failing, it gives back its own default theme.

No error, no warning, and all ten tests still pass. A test measured the colors:

Primary color Title color
the app's theme #465D91, my blue #1A1B20
what Section gets #6750A4, the default purple #1D1B20

The test also checked the color on screen: Section painted its title with the default one, #1D1B20.

In this app you cannot see the difference: Section only uses the title color, and the two are almost identical. But any widget behind the barrel that uses colorScheme.primary would show the default purple instead of your blue.

With the bridge: it works. The old Material now gets the app's colors, #465D91 and #1A1B20.

But it only fixes part of the theme. Its source (lib/src/migration_utility.dart) builds the old ThemeData from four values only: platform, visualDensity, colorScheme and textTheme. Everything else keeps the old Material's defaults. Component themes, like inputDecorationTheme, and theme extensions are not passed on.

The fix: migrate the two export lines by hand.

export 'package:cupertino_ui/cupertino_ui.dart' hide RefreshCallback;
export 'package:material_ui/material_ui.dart';

After that, dart analyze is clean and Section paints its title with the app's color, #1A1B20. No bridge needed. After running dart fix, search your project for export 'package:flutter/: the tool will not do it for you.

3. The translation list gen-l10n writes for you

gen-l10n generates a ready-made list of delegates in lib/l10n/app_localizations.dart. After the migration, it still writes the old ones:

import 'package:flutter_localizations/flutter_localizations.dart';

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

Those three come from flutter_localizations, the SDK. Passing this list straight to MaterialApp is a common shortcut: Zulip and kitchenowl both used it. So I did the same:

localizationsDelegates: AppLocalizations.localizationsDelegates,

It compiles. Eight tests pass, three fail, and the three that fail are the three that run in French. The app registers French texts for the SDK's Material widgets, but its widgets now come from material_ui, and they look for material_ui's texts. In English the app falls back on built-in English texts. In French, there is nothing to fall back on.

The messages point the wrong way:

- A MaterialLocalizations delegate that supports the fr locale was not found.

There is one. It is the SDK's.

No MaterialLocalizations found.
To introduce a MaterialLocalizations, either use a MaterialApp at the root of your application...

There is a MaterialApp. Neither message can say what is wrong, which is that two classes are called MaterialLocalizations.

It breaks in the TextField on the first screen, the same widget as in the kitchenowl report. My tests run in debug mode, where an assertion catches it. The kitchenowl report says that release builds only showed a generic null-check crash. I did not measure release mode.

With the bridge: no change. The same three French tests fail, with the same messages. The bridge registers the old flutter_localizations delegates, for widgets that still use the old Material. The migrated widgets need the new ones.

Does your app speak more than one language?

  1. Search your code: grep -rn "localizationsDelegates" lib/. If you see AppLocalizations.localizationsDelegates, you are affected.
  2. Run the app with locale: const Locale('fr') (or any language other than English) in your MaterialApp. An English-only run will not show the bug.
  3. Replace the generated list with: [AppLocalizations.delegate, ...GlobalMaterialLocalizations.delegates]. If that file still imports flutter_localizations, remove the import.
  4. Never fix the generated file by hand. It is rewritten every time you add a text to your .arb files.
  5. The compatibility bridge does not fix this one.

So, can you migrate yet?

For an app like the one tested here, with no third-party UI package: yes. Remember the word "expectation": the team expected "a drop-in maneuver", and for this app it almost is. Seventeen automatic fixes, one manual fix to make it compile, and the same ten tests green before and after. Almost, because of the three things below.

But dart fix does not finish the job. After running it, check three things yourself:

  1. Your export lines. The tool skips them. A forgotten one gives no error and brings back the default theme without any warning.
  2. Your translation list, if your app speaks more than English. The list that gen-l10n generates still points to the old Material, and it breaks every other language.
  3. Your pubspec.yaml. Replace any with real version constraints. The compatibility bridge will not do this for you. It fixes the theme read through Theme.of(context), but not types passed as arguments, and not the generated translations. And it is already deprecated.

Should you migrate now, or wait? The SDK copy of Material is frozen. The package is not: it has released a new version about every week since August, with fixes the SDK copy will not get, like the showAboutDialog crash on the web (changelog). And the
Flutter 3.47 announcement schedules the old libraries for deprecation in the
November stable release. If you wait, you will migrate anyway, only closer to that deadline.

Which leaves the real question for most apps: can you migrate if your app depends on packages that have not migrated yet?

The first case above shows what such a package looks like from your side. If it passes a ColorScheme or a ThemeData through its API, the compiler refuses, and the bridge cannot help. The guide asks package authors to treat the migration as a major version bump. In one real app, only one of the eighteen dependencies that import Material had migrated when its team started (flutter_server_box#1591).

That is the next article. I will add real packages to this app and sort them: the ones that migrate on their own, the ones the bridge saves, and the ones that break.

Comments (0)

Sign in to join the discussion

Be the first to comment!