在Flutter中如何实现材料组件与Cupertino设计风格的解耦[完整指南]
今年早些时候,我发表了《在Flutter中分离Material和Cupertino设计库》,这篇文章介绍了当时还处于预览阶段的功能:Flutter计划将Material和Cupertino设计库从核心SDK中分离出来,作为独立的包发布在pub.dev上。 在当时,这一功能仍处于预览阶段,迁移工具还不完善,相关生态系统也尚未做好相应准备。这篇文章主要是为了说明Flutter的发展方向及其原因。 2026年8月12日发布的Flutter 3.47彻底改变了这一状况。 独立的`material_ui`和`cupertino_ui`包已经更新到了1.0版本,迁移工具也已准备就绪,兼容性过渡方案也正式投
今年早些时候,我发表了《在Flutter中分离Material和Cupertino设计库》,这篇文章介绍了当时还处于预览阶段的功能:Flutter计划将Material和Cupertino设计库从核心SDK中分离出来,作为独立的包发布在pub.dev上。
在当时,这一功能仍处于预览阶段,迁移工具还不完善,相关生态系统也尚未做好相应准备。这篇文章主要是为了说明Flutter的发展方向及其原因。
2026年8月12日发布的Flutter 3.47彻底改变了这一状况。
独立的`material_ui`和`cupertino_ui`包已经更新到了1.0版本,迁移工具也已准备就绪,兼容性过渡方案也正式投入使用,而旧版本的导入方式也已正式进入淘汰阶段。
这不再只是一个预览功能或发展方向,而是当前Flutter开发中必须遵循的标准。这一变化影响着每一位Flutter开发者。
这本手册全面详细地介绍了所有这些变化,解释了Flutter团队为何做出这样的设计决策,新的包包含了哪些内容,它们与旧版本有何不同,如何自动或手动完成迁移过程,如何处理那些尚未完成迁移的依赖项,当前的本地化功能是如何运行的,你的项目中的现有组件会发生什么变化,以及完整的淘汰时间表,这样你就能清楚地知道何时旧的开发方式将不再被支持。
如果你之前读过那篇文章,那么这本书就是你一直在等待的后续内容;如果你是初次接触这个主题,这里也包含了你所需的所有信息。
目录
先决条件
在开始阅读本指南之前,请确保满足以下要求。
Flutter 3.47或更高版本:本指南介绍的内容仅适用于此版本及后续版本。请在终端中运行flutter upgrade进行升级,然后通过flutter --version验证版本是否正确。
Dart SDK 3.10或更高版本:Dart 3.10与Flutter 3.47同步发布。请使用dart --version检查SDK版本。
已有的Flutter项目,或愿意在沙箱环境中按照迁移步骤操作:无论项目的规模大小,这些迁移方法都适用于所有Flutter应用。
对Flutter项目结构有基本了解:您需要知道pubspec.yaml的作用、flutter pub get的用途,以及Dart中的导入语句格式。
无需具备关于解耦功能的先验知识:本指南会从基础开始讲解所有内容。但如果您先阅读《在Flutter中分离Material和Cupertino组件》,将有助于您更好地理解这一变更的背景原因。
发生了哪些变化?为什么这些变化如此重要?
在Flutter 3.47之前,当您编写import 'package:flutter/material.dart'时,实际上是在导入直接集成到Flutter SDK中的Material组件库。如果不升级整个Flutter SDK,就无法使用更新版本的Material组件。此时您没有任何选择余地。
从Flutter 3.47开始,Material和Cupertino组件成为了独立的包,分别位于pub.dev平台上:material_ui和cupertino_ui。您可以独立于Flutter SDK来升级这些组件,它们会按照自己的时间表发布漏洞修复和新功能。而且Flutter SDK不再负责这些组件的开发规划。
为什么Flutter团队要做出这样的改变?
在2018年Flutter刚推出时,这种将Material和Cupertino组件直接集成到SDK中的设计方式确实非常合理。这种方式使得开发者无需进行任何配置即可使用这些组件,入门十分便捷,也没有任何障碍。
但随着Flutter的发展,这种集成方式逐渐成为了束缚。Material Design 3的更新进度比预期要慢,因为每次对Material组件的修改都必须等到SDK的季度版本发布才能应用。社区贡献者也发现,想要将新的组件功能合并到项目中变得更加困难,因为修改核心SDK代码的门槛较高。那些使用Flutter来构建自定义设计系统的团队,无论是否需要,仍然不得不将Material和Cupertino组件作为依赖项纳入项目。
通过解耦这些组件,这三个问题都得到了解决:使用Material组件的团队可以每周获得漏洞修复和新功能;而构建自定义设计系统的团队则无需再将其作为依赖项。这样的设计方向使得Flutter的核心框架真正实现了风格上的中立性——框架负责处理布局、渲染和平台交互,而各种设计库则完全可以根据需求进行选择和替换。
对您当前代码的影响
您现有的代码在Flutter 3.47版本中仍然可以正常编译。旧的packageflutter/material.dart和package/flutter Cupertino.dart导入语句目前依然有效。因此,在升级到Flutter 3.47后,您的代码不会出现任何问题。
这些旧导入语句被标记为“废弃功能”的时间定为2026年秋季的稳定版本发布时,预计会在11月进行更新。届时,这些旧的导入方式将正式被弃用。不过在它们被弃用之后,并不会立即被从代码中移除,但这一进程已经开始了。
了解旧架构
在Flutter 3.47版本之前,Flutter的主要UI组件都是被集成在同一个SDK中一起发布的。Material Design、Cupertino组件、渲染机制、绘图功能、平台服务以及本地化功能都包含在同一份SDK发布包中。
这种设计方式带来了一些限制:修复Material中的错误可能需要等待整个SDK的更新;那些开发自定义设计系统的团队仍然会受到Material的限制;向核心SDK贡献代码的难度较大,从而导致改进进展缓慢;Material无法独立于Flutter SDK进行版本更新;即使只有其中一个组件需要紧急更新,Material和Cupertino组件的更新周期也必须保持一致。
旧架构使得Flutter的UI组件与SDK紧密绑定在一起,因此各个组件无法像在更模块化的架构中那样独立地进行开发与发布。
任何使用packageflutter/material.dart的Flutter项目都会受到SDK发布计划的直接影响。如果Material中出现了视觉方面的问题,即使Flutter引擎本身没有问题,您也必须等待下一次季度性的SDK更新才能得到修复。正是这种紧密的绑定关系,才导致了当前需要采取分离这些组件的措施。
新架构:独立包机制
从Flutter 3.47版本开始,其架构将Flutter的设计系统与核心SDK分离开来。Material UI和Cupertino UI是通过pub.dev发布的独立包。每个包都可以拥有自己的版本,并且可以独立地进行更新发布。
这两个包都依赖于Flutter SDK核心,该核心包含了底层组件、渲染机制、绘图功能、平台相关服务以及基础架构。核心SDK仍然遵循常规的季度发布周期,而UI相关的包则可以更频繁地推出更新。
Flutter的核心架构正变得越来越模块化。Material UI和Cupertino UI可以在不需要整个Flutter SDK同步更新的情况下独立发展。
这种架构设计的关键在于实现了职责的分离。现在,Flutter SDK负责管理渲染引擎、基础组件层以及平台抽象层;而设计系统(如material_ui和cupertino-ui)则是由Flutter团队开发但在pub.dev平台上发布的独立包,它们的版本更新也是独立进行的。
## 设置:添加新的包
### 添加material_ui
```bash
flutter pub add material_ui
```
这条命令会将material_ui添加到你的pubspec.yaml文件中的dependencies部分,并自动执行flutter pub get命令。执行完成后,你的pubspec.yaml文件将会包含以下内容:
```yaml
dependencies:
flutter:
sdk: flutter
material_ui: ^1.0.0
```
`flutter pub add material_ui`是添加包的标准化方式。它会自动选择最新且兼容的版本,并正确地设置依赖关系格式。^1.0.0这个版本约束表示“1.0.0版本或任何更高版本,只要它们与1.x系列版本兼容即可”,这符合Dart的半版本号命名规则。
这样的配置能够确保当你运行flutter pub upgrade时,补丁更新和次要升级可以自动应用,同时也能避免假设中的2.0.0版本所带来的破坏性变更影响到你的项目。
### 添加cupertino_ui
```bash
flutter pub add cupertino-ui
``>
只有当你的项目使用了Cupertino风格的组件时,才需要添加这个包。那些仅针对Android平台开发的应用程序,或者那些完全使用自定义设计系统的应用程序,可能并不需要这个包。
在pubspec.yaml文件中,添加cupertino_ui后的依赖关系配置如下:
```yaml
dependencies:
flutter:
sdk: flutter
material-ui: ^1.0.0
cupertino_ui: ^1.0.0
```
## 同时添加两个包
```bash
flutter pub add material_ui cupertino_ui
```
在一条flutter pub add命令中同时列出这两个包的名称,可以一次性完成它们的添加操作,并自动构建完整的依赖关系图。这样比分别执行两条命令要快得多。迁移您的项目:自动化迁移方案
Flutter团队提供了一款迁移工具,能够自动处理最常见的情况。对于大多数项目而言,使用这款工具就可以完成全部的迁移工作。
步骤1:运行迁移工具
dart fix --apply --code=migrate_design_widgets
dart fix是Dart内置的自动代码修复工具。参数--apply表示它会直接应用所有建议的修复方案,而不会在每次修复前询问用户确认。参数则专门用于执行migrate_design_widgets这个修复脚本,该脚本专门用于处理解耦相关的迁移操作。它会在你的项目中查找对packageflutter/material.dart和package:flutter Cupertino.dart的引用,并将它们分别更新为正确的新的导入路径,即package:material-ui/material_ui.dart和package:cupertino-ui Cupertino_ui.dart。
该工具还会尝试自动更新你的pubspec.yaml文件,以便在其中添加新的依赖包信息。不过需要注意的是,目前存在一个已知的缺陷,在某些情况下,pubspec.yaml的更新可能无法正确执行。
步骤2:处理已知的pubspec.yaml问题
如果迁移工具未能成功更新你的pubspec.yaml文件,请运行以下命令:
flutter pub add material_ui
flutter pub add cupertino-ui
dart fix --apply
通过运行flutter pub add material_ui和flutter pub add cupertino-ui,可以手动将这两个依赖包添加到pubspec.yaml文件中,然后再次运行包解析命令。之后再执行dart fix --apply(这次不要使用--code参数),这样就可以应用那些在第一次运行时被遗漏的修复方案了。
在将依赖包添加到pubspec.yaml文件之后,再次运行dart fix命令,可以让系统根据实际安装的依赖包来验证导入路径是否正确。
步骤3:验证迁移结果
flutter analyze
flutter analyze会扫描你的整个项目,并报告任何剩余的问题。如果迁移成功,你应该不会看到与缺失导入或过时API相关的错误。如果仍然存在错误,那么这些错误通常属于以下两类之一:要么是迁移工具无法自动更新的导入路径(具体处理方法见下文的手动配置部分);要么是对第三方依赖包的引用尚未完成迁移(相关内容请参见兼容性桥接章节)。
该工具实际会做出哪些更改
下面详细说明了自动化迁移工具会对你的导入语句进行哪些修改:
// 修改前的代码:所有Flutter应用程序过去都是这样写的
import 'packageflutter/material.dart';
import 'package/flutter Cupertino.dart';
// 迁移后的结果:迁移工具会产生什么变化
import 'package:material-ui/material_ui.dart';
import 'package:cupertino/ui Cupertino_ui.dart';
import 'package:flutter/material.dart'这条语句是从Flutter SDK内置的包中导入Material库的;而import 'package:material_ui/material_ui.dart'这条语句则是从你在pubspec.yaml文件中指定的独立包中导入相关组件的。
各种部件的名称、类名以及API接口都保持不变。Scaffold依然是Scaffold,ThemeData依然是ThemeData,AppBar也依然是 AppBar。没有任何部件被重新命名或结构上发生改变,唯一的变化就是导入路径而已。
之所以能够通过简单地修改导入路径来完成这种迁移操作,是因为Flutter团队特意将material_ui设计成可以替代内置的Material库的组件。它的API接口被固定在了与内置库相同的状态之下。这也是为什么该包的README文件中会提到开发工作在4月份就被暂停了,目的是为了确保迁移过程能够顺利进行。
material-ui 1.0版本中所包含的内容,实际上与Flutter 3.44版本中packageflutter/material.dart包里的内容是完全相同的。今后,这些组件会以更快的节奏继续得到优化和升级。
手动迁移项目
虽然自动化工具能够处理绝大多数迁移任务,但在某些特定情况下,还是需要人工进行干预。
混合导入的文件
如果你有一个文件,在同一行中从多个Flutter子库中导入组件,或者这些导入方式的格式无法被工具识别:
// 一个包含多处Flutter导入语句的文件
import 'packageflutter/material.dart';
import 'package:flutter/rendering.dart';
import 'packageflutter/services.dart';
import 'packageflutter/gestures.dart';
在这种情况下,工具只会更新material.dart这个库的导入路径,而其他库的导入路径仍然会保持不变,因为rendering.dart、services.dart和gestures.dart》这些都是核心框架库,它们并不会被分离成独立的包。只有与设计系统相关的导入路径才会发生变化。
// 迁移后的结果:所有导入路径都正确无误
import 'package:material-ui/material_ui.dart'; // 已更新
import 'packageflutter/rendering.dart'; // 保持不变
import 'package:flutter/services.dart'; // 保持不变
import 'packageflutter/gestures.dart'; // 保持不变
packageflutter/rendering.dart这类核心框架库的导入路径不会发生改变,因为它们属于SDK的核心功能模块,涉及布局、渲染、绘图以及平台相关服务。这种分离措施主要是针对设计系统而言的,并不涉及底层框架的基本组件。理解这一区别非常重要,这样你就不会误以为存在某个名为rendering_ui的独立包了。
条件导入与平台特定文件
// 使用条件导入的平台特定文件
export 'packageflutter/material.dart'
if (dart.library.html) 'packageflutter/material.dart';
请手动更新条件导入语句中的两段代码:
// 迁移后的代码
export 'package:material_ui/material_ui.dart'
if (dart.library(html)) 'package:material/ui/material-ui.dart';
使用`if (dartlibrary...)`进行的条件导入会在编译时根据平台选择不同的导入路径。不过,迁移工具可能并非在所有情况下都能正确处理这类条件导入语句。因此,请务必手动检查项目中所有包含`if (dart.library.html)`或类似条件语句的文件。
生成的文件
那些以`.g.dart`、`.freezed.dart`或其他后缀结尾的文件是由`build_runner`生成的,因此绝对不能手动修改它们。当你运行以下命令时,这些文件会自动重新生成,并且其中的导入语句也会被更新为正确的版本:
dart run buildrunner build --delete-conflicting-outputs
`dart run build_runner build`命令会针对你的源代码文件执行所有的代码生成任务(比如生成`json_serializable`、`freezed`等格式的文件)。`--delete-conflicting-outputs`选项会在重新生成文件之前删除之前生成的文件,这样就可以避免旧文件与新文件之间产生冲突。
由于迁移工具已经更新了源代码文件中的导入语句,因此生成器在重新处理这些文件时会产生内容一致的生成文件。在迁移完成后,只需再次运行生成器即可,无需对生成的文件进行任何额外的操作。
MaterialUiCompatibilityBridge:弥合兼容性差距
生态系统的升级不可能一蹴而就。当你将应用程序更新为使用`material_ui`包时,某些第三方依赖包可能仍然在内部使用`packageflutter/material.dart`。这种情况下,你的应用程序中的组件树中就会同时包含来自两个不同来源的Material组件。
正是为了应对这种情况,才出现了`MaterialUiCompatibilityBridge`。它提供了一层兼容性机制,使得来自这两个不同来源的Material组件能够在同一个组件树中共存,而不会导致运行时错误。
import 'package:material_ui/material-ui.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
),
),
builder: (BuildContext context, Widget? child) {
return MaterialUiCompatibilityBridge(child: child!);
},
home: const HomeScreen(),
);
}
}
import 'package:material-ui/material_ui.dart'这种导入方式是新的。通过这一条导入语句,就可以使用所有的Material组件了,包括MaterialApp、ThemeData、ColorScheme以及MaterialUiCompatibilityBridge。
MaterialApp(...)在名称和功能上与之前使用的版本完全相同。构造函数的参数和行为都没有变化。这个类现在来自material_ui包,而不是原来的集成SDK,但使用它的代码本身并不需要做任何修改。
builder: (BuildContext context, Widget? child) { return MaterialUiCompatibilityBridge(child: child!); }这一行代码用于添加兼容性层。MaterialApp的builder参数会包裹MaterialApp创建的整个组件树。通过在这一层级插入MaterialUiCompatibilityBridge>,它就会位于应用程序中所有组件的上方。这样一来,无论树结构中的哪个组件——无论是你自己编写的代码中使用material_ui创建的,还是通过依赖项引入的(这些依赖项仍然使用package:flutter/material.dart>),都会在这个兼容性层的范围内运行。
child!这里使用了空值断言,这是因为只要应用程序配置了home、routes或initialRoute>,MaterialApp就会为builder>提供一个非空的组件作为参数。
何时使用兼容性层
首先回答这样一个问题:你的项目是否包含使用Material组件的依赖项?
如果答案是“否”:你不需要使用兼容性层,可以直接继续开发。
如果答案是“是”:请检查这些依赖项是否都已经更新为使用material_ui>。
如果全部都已更新:那么就不需要使用兼容性层了,可以继续正常开发。
如果部分或全部没有更新:在这些依赖项得到更新之前,请先使用兼容性层。
只有当你的项目仍然依赖于那些使用旧版Material组件的依赖项时,才需要使用兼容性层。如果所有的依赖项都已经升级为使用material_ui>,那么就可以移除或避免使用这个兼容性层了。
这是一个过渡性的工具。随着整个开发生态系统的逐步迁移,你可以通过运行相应的检查命令来确认你的依赖项是否已经更新为使用material-ui>。
flutter pub outdated
当你的所有依赖项都在使用 `material_ui` 时,就应该删除那个过渡性代码。它本来就不是为了成为你应用程序的永久组成部分而设计的。
本地化设置:发生了哪些变化以及如何进行更新
在这次迁移中,本地化设置是其中最重要的实际变更之一。之前,`flutter_localizations` 包会将 Material 和 Cupertino 组件的翻译及本地化配置整合在一个包中提供;现在这些功能已经被分拆到了两个独立的包中。
旧的本地化设置方式
// 旧的方式:使用 flutter_localizations
import 'packageflutter_localizations/flutter_localizations.dart';
import 'package:flutter/material.dart';
MaterialApp(
localizationsDelegates: const > [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizationsdelegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('ar'),
Locale('fr'),
],
// ...
)
旧的方法要求明确列出三个本地化配置代理:`GlobalCupertinoLocalizations.delegate` 用于处理 Cupertino 组件的文本,`GlobalMaterialLocalizationsdelegate` 用于处理 Material 组件的文本,而 `GlobalWidgetsLocalizations.delegate` 则用于处理其他普通组件的文本。此外还必须单独导入 `flutter_localizations` 包。这种写法显得繁琐,而且开发人员需要清楚了解每个代理负责处理哪些组件。
新的本地化设置方式
// 新的方式:使用 material_ui
import 'package:material/ui/material-ui.dart';
MaterialApp(
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const [
Locale('en'),
Locale('ar'),
Locale('fr'),
],
// ...
)
`GlobalMaterialLocalizations.delegates` 是一个获取器,它将 Material、Cupertino 和其他组件的本地化配置代理全部合并在一起。只需将这个获取器赋值给 `localizationsDelegates`,就能实现与旧方法相同的功能,而且代码量更少。
即使你没有单独导入 `cupertino_ui`,Cupertino 组件的文本也会被自动包含进来,因为 `material-ui` 在内部实际上依赖于 `cupertino_ui`,并且将这些本地化配置代理都整合在了它的获取器中。
现在不再需要单独导入 `flutter_localizations` 包了。虽然这个包仍然存在(也没有被标记为过时),但对于那些正在迁移到 `material ui` 的项目来说,可以直接从导入语句和 `pubspec.yaml` 中删除对它的引用。
本地化设置架构图
该图示展示了Flutter在架构变更前后本地化配置的差异。
变更前: 本地化功能是通过独立的`flutter_localizations`包来实现的,开发者需要手动添加`Material`、`Cupertino`和`Widgets`相关的本地化代理。
变更后: 通过`material_ui`包,本地化配置得到了简化。`GlobalMaterialLocalizations.delegates`自动包含了所需的`Cupertino`和`Widgets`代理,开发者无需再进行额外配置。
这种新的设计方式大大减少了开发者需要编写的本地化配置代码量,同时也使得相关配置更易于维护。
变更前后的对比:代码示例
基本应用的配置流程
// 变更前:标准的Flutter应用入口代码
import 'package:flutter/material.dart';
import 'packageflutter_localizations/flutter_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizationsdelegate,
GlobalWidgetsLocalizationsdelegate,
],
supportedLocales: const [Locale('en')]
home: const HomeScreen(),
);
}
}
// 变更后:迁移后的应用入口代码
import 'package:material_ui/material-ui.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const [Locale('en')]
home: const HomeScreen(),
);
}
}
这里的差异主要体现在三处:首先,导入语句从`packageflutter/material.dart`变为了`package:material_ui/material-ui.dart`;其次,原本用于引入本地化代理的`flutter_localizations`包被移除了;最后,`localizationsDelegates`列表也从三个独立的代理对象简化为了一个统一的获取器函数。其余部分(如`MaterialApp`、`ThemeData`、`ColorScheme.fromSeed`、`useMaterial3`以及`home`)由于API没有发生变化,因此保持不变。
使用Material Widgets构建的界面
// 修改前的代码
import 'package:flutter/material.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('个人资料'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const CircleAvatar(child: IconIcons.person)),
title: const Text('Ade Mensah'),
subtitle: const Text('Flutter开发者'),
trailing: const Icon(Icons.chevron_right),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {},
child: const Text('编辑个人资料'),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const IconIcons.add),
),
);
}
}
修改后的代码:迁移后的界面
import 'package:material/ui/material_ui.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('个人资料'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const CircleAvatar(child: IconIcons.person)),
title: const Text('Ade Mensah'),
subtitle: const Text('Flutter开发者'),
trailing: const Icon(Icons.chevron_right),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {},
child: const Text('编辑个人资料'),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const IconIcons.add),
),
);
}
}
组件结构完全相同。Scaffold、AppBar、Card、ListTile、CircleAvatar、FilledButton以及FloatingActionButton》:这些组件的名称、参数和功能都没有发生任何变化。
唯一不同的地方就是代码顶部的导入语句。这是有意为之的。Flutter团队的目标就是确保这次迁移仅仅涉及导入语句的修改,而不会对任何组件API造成任何影响。
使用Cupertino样式构建的界面
修改前的代码
import 'package:flutter Cupertino.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('设置'),
),
child: SafeArea(
child: CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
title: const Text('通知'),
leading: const Icon(CupertinoIcons.bell),
trailing: CupertinoSwitch(
value: true,
onChanged: (value) {},
),
),
],
),
),
);
}
}
// 修改前的代码
import 'package:cupertino_ui Cupertino_ui.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('设置'),
),
child: SafeArea(
child: CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
title: const Text('通知'),
leading: const Icon(CupertinoIcons.bell),
trailing: CupertinoSwitch(
value: true,
onChanged: (value) {},
),
),
],
),
),
);
}
}
情况还是一样的。CupertinoPageScaffold、CupertinoNavigationBar、CupertinoListSection、CupertinoListTile、CupertinoSwitch以及CupertinoIcons这些组件都可以从package:cupertino_ui Cupertino_ui.dart中导入,和使用方式与从package:flutter/cupertino.dart中导入时完全相同。只需要修改一行导入语句,其他代码部分根本不需要进行任何更改。
同时使用Material和Cupertino设计风格的应用程序
有些应用程序会混合使用不同的设计风格。一种常见的做法是在主要以Material风格设计的应用程序中,使用Cupertino提供的对话框和选择器组件。这两个库可以同时被导入到同一个项目中,而且不会导致任何冲突:
// 修改前的代码
import 'package:flutter/material.dart';
import 'packageflutter Cupertino.dart';
class DatePickerButton extends StatelessWidget {
const DatePickerButton({super.key});
void _showDatePicker(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (context) => Container(
height: 216,
color: CupertinoColors.systemBackground,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) {},
),
),
);
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => _showDatePicker(context),
child: const Text('选择日期'),
);
}
}
// 修改后的代码:同时导入了两个包
import 'package:material_ui/material-ui.dart';
import 'package:cupertino/ui Cupertino_ui.dart';
class DatePickerButton extends StatelessWidget {
const DatePickerButton({super.key});
void _showDatePicker(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (context) => Container(
height: 216,
color: CupertinoColors.systemBackground,
child: CupertinoDatePicker(
mode: CupertinoPickerMode.date,
onDateTimeChanged: (DateTime newDate) {},
),
),
);
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => _showDatePicker(context),
child: const Text('选择日期'),
);
}
}
material-ui和cupertino_ui都可以在同一文件中被导入,而不会引发任何命名空间冲突。需要注意的是,material-ui在内部就已经依赖于cupertino_ui,因此在实际使用中,你可能会发现大多数文件并不需要显式导入cupertino_ui——因为通过导入material-ui,就可以使用Cupertino相关的组件。不过,如果同时显式导入这两个库,会更清楚地体现你的意图,对于那些需要同时使用这两种系统中的组件的文件来说,这也是推荐的做法。
迁移包的作者信息
如果你维护的是一个Flutter包(而不仅仅是一个Flutter应用程序),那么在进行迁移时还需要考虑一些额外的因素。Flutter团队明确指出:将包转换为独立发布的形式应被视为该包的一次重大版本更新。
作为包的作者应该怎么做
# 迁移之前的包的pubspec.yaml文件
name: myflutter_package
version: 1.5.0
dependencies:
flutter:
sdk: flutter
# 迁移之后的包的pubspec.yaml文件
name: myflutter_package
version: 2.0.0
dependencies:
flutter:
sdk: flutter
material_ui: ^1.0.0
将版本号升级到2.0.0是必要的,因为这一变更会对使用你的包的用户产生影响。在之前的版本中,用户在使用你的包时并不需要单独导入material-ui(因为它已经包含在了你的包中);而迁移后的版本中,你的包明确声明了对material_ui的依赖,这就改变了你的包的依赖关系结构。因此,那些将包升级到2.0.0的用户也需要确保他们的系统中已经安装了material-ui,而如果他们也在同时进行迁移的话,这个问题自然就会得到解决。通过将版本号中的主版本号提高,可以清晰地传达这一变更信息。
在过渡期间保持向后兼容性
如果你希望在过渡期(2026年11月之前)同时支持旧版本的Flutter环境和新版本的Flutter环境,你可以使用Dart中的条件导入功能:
// lib/src/widgets.dart文件
// 这是一个用于处理条件导入的内部文件
export 'package:material_ui/material_ui.dart'
if (dart.library.nonexistent) 'packageflutter/material.dart';
不过,这种做法比较复杂,而且很少有必要。Flutter团队的建议更为简单:将你的包升级到material-ui,然后提高主版本号,让用户们按照自己的节奏进行升级。在material-ui中提供的兼容性机制,可以帮助那些正在迁移自己应用程序的用户顺利实现新旧环境的共存。
查看你的pub.dev评分
在将你的包升级到material-ui之后,用于计算pub.dev评分的静态分析工具会识别出这一变更,并相应地给予奖励。现在,那些还没有进行迁移的包会在pub.dev评分中得到较低的分数。这种设计是故意设置的,目的是为了鼓励更多人采用这些新的开发标准。
Flutter 3.47中还发生了哪些变化
解耦是 Flutter 3.47最显著的特点,但该版本还带来了其他几项会对实际项目产生影响的重大变更。
Impeller现已成为桌面平台的默认渲染引擎
Flutter的下一代渲染引擎Impeller,在iOS和Android平台上早已被设置为默认选项,现在它也成为了macOS、Windows和Linux平台的默认渲染引擎。Impeller通过在构建时而不是运行时来编译着色器,从而避免了动画首次播放时出现的卡顿现象。
对于大多数项目来说,这一变更几乎不会产生任何明显的影响——你的动画从第一帧开始就会显得更加流畅。如果你遇到了渲染方面的问题,需要暂时禁用Impeller,可以参考以下代码进行设置:
<!-- macOS: ios/Runner/Info.plist -->
<key>>FLTEnableImpeller<>/key>>
<false/>
// Windows: windows/runner/main.cpp
project.set_impeller_switch(flutter::ImpellerSwitch::Disabled);
// Linux: linux/my_application.cc
fl_dart_project_set_enable_impeller(project, FALSE);
这些机制是为那些在新默认设置中遇到问题的项目提供的。未来版本会移除对Skia的回退选项,因此如果你必须选择禁用Impeller,请向Flutter团队提交错误报告,以便他们能够修复相关问题。
最低支持的iOS和macOS版本已提高
由于新增了对Xcode 27的支持,最低支持的操作系统版本也发生了变化:
平台 之前的最低版本 新的最低版本(Flutter 3.47及以上)
iOS 13 15
macOS 10.15 (Catalina) 12 (Monterey)
如果你的应用的ios/Runner.xcodeproj或macos/Runner.xcodeproj文件中指定的部署目标低于这些新规定的最低版本,那么构建过程将会失败。请在Xcode中更新这些部署目标,或者运行flutter build ios让Flutter CLI自动处理这一设置——该命令会提示你是否存在不匹配的情况。
iOS的UIScene生命周期要求
使用Xcode 27构建的应用程序,如果仍然采用旧的UIApplication委托生命周期模型(而非新的UIScene生命周期模型),那么在iOS 27系统上将无法正常启动。对于大多数Flutter应用程序来说,CLI会在构建过程中自动完成这一迁移工作。
但如果你的应用中包含自定义的本地代码(位于AppDelegate.swift或AppDelegate.m文件中),或者使用了依赖于旧生命周期模型的插件,那么你需要按照Flutter文档中的指南手动进行迁移操作。
Widget预览功能已正式稳定发布
以前需要先构建整个应用程序才能查看单个组件的预览效果的功能,现在已经正式稳定发布了。在项目根目录下创建一个.widget_preview/文件夹,可以缓存预览状态,从而加快应用程序的启动速度。如果你们的团队经常对组件界面进行反复修改,那么启用这一功能会非常有用。
WebAssembly正逐渐成为默认选择
虽然Wasm目前还不是Flutter Web的默认开发方案,但这一趋势正在逐步显现。你现在就可以选择使用它:
flutter build web --release --wasm
--wasm选项会让你构建针对WebAssembly而非JavaScript的Flutter Web应用。对于那些需要大量计算资源的UI界面来说,这种设置带来的性能提升非常显著。不过,你的代码及依赖库必须使用package:web而不是dart:html,因为Wasm不支持传统的HTML库。目前大多数流行的开发包都已经完成了迁移。
弃用时间表:旧导入方式何时将不再可用
了解这一时间表对于规划你的迁移计划至关重要。
这个时间表清楚地展示了 Flutter逐渐淘汰旧版material_ui和cupertino-ui包的进程。
2026年8月,Flutter 3.47版本:新的material_ui和cupertino/ui包升级到1.0版本,dart fix迁移工具也已可用。此时使用旧导入方式仍然可以正常编译,且不会产生任何警告,整个开发生态系统也开始逐渐采用这些新包。
2026年11月,Flutter Fall Stable版本:旧的package:flutter/material.dart和packageflutter Cupertino.dart导入方式被正式弃用。使用这些方式的开发者会在分析工具中看到警告信息,但现有应用程序在此阶段仍然可以正常编译和运行。
2027年的某个版本:这些旧导入方式将从Flutter SDK中完全移除,未完成迁移的项目将无法再使用它们进行编译。
最安全的迁移时机就是现在,在2026年11月之前,因为此时使用旧导入方式仍然可以顺利编译通过。如果在弃用警告期(2026年11月至正式移除期间)进行迁移,虽然仍能完成迁移,但分析工具会发出警告信息;而如果在这些旧导入方式被完全移除之后再进行迁移,则需要采取紧急措施才能解决问题,因此最好提前做好规划。
最佳实践
尽早迁移,一次完成迁移
自动迁移工具已经可以用于生产环境。现在就使用它,你可以立即享受到更快的material_ui和cupertino-ui包更新带来的好处,完全避免进入弃用警告期,从而让你的开发进度领先于整个生态系统。
那些较早进行迁移的团队也可以避免这样一种情况:在他们仍在使用旧版本的时候,依赖关系的升级会不小心引入新版本包中存在的错误修改。
迁移后删除flutter_localizations包
在迁移到material_ui之后,你在pubspec.yaml文件中指定的flutter_localizations包就变得多余了。因为material-ui已经包含了该包所提供的本地化功能,所以不需要再保留这个包了。请按照以下步骤将其删除:
# 迁移完成后,请从pubspec.yaml文件中删除以下内容
# flutter_localizations:
# sdk: flutter
同时,还需要在所有Dart文件中删除对flutter_localizations的导入语句,例如:
# 从所有Dart文件中删除以下导入语句
# Remove: import 'package:flutter_localizations/flutter_localizations.dart';
虽然在项目中保留flutter_localizations包不会导致任何错误,但它属于多余的组件,而且会在查看项目依赖关系时造成混淆。
暂时使用兼容性桥接工具,但不要永久依赖它
MaterialUiCompatibilityBridge只是一个过渡性的工具。在设计系统架构时,不应该以它的存在为依据。在迁移过程中添加这个工具,等所有依赖关系都迁移到material_ui之后,再及时将其删除。你可以定期使用以下命令检查依赖关系的迁移状态:
flutter pub outdated
在持续集成环境中固定Material和Cupertino包的版本
由于material_ui和cupertino_ui都会每周发布更新版本,因此你可能需要在持续集成环境中固定这些包的具体版本,以确保构建过程的可重复性。具体操作方法如下:
# 为确保生产环境的稳定性,请在pubspec.yaml文件中指定以下版本
dependencies:
material_ui: 1.2.0 # 为持续集成环境固定特定版本
cupertino_ui: 1.1.0
在开发环境中,使用^这种版本约束即可确保代码始终使用最新版本。而对于持续集成和生产环境的构建来说,固定具体版本并有意识地控制版本的更新顺序,可以帮助你更好地管理不同构建版本之间的差异。
常见的错误
在同一文件中混合使用旧版和新版的导入语句
// 错误做法:在同一文件中同时使用旧版和新版的导入语句
import 'packageflutter/material.dart';
import 'package:material_ui/material_ui.dart'; // 这是重复导入
在同一文件中同时使用这两种导入语句是多余的,而且可能会导致分析工具提示类型定义重复的错误。迁移完成后,每个文件都应该只包含一个针对material-ui的导入语句,即新的package:material_ui/material_ui.dart。你可以运行flutter analyze命令来检查是否存在这样的问题。
在需要时忘记使用兼容性桥接工具
如果你迁移了应用程序中的导入依赖项,但没有添加 `MaterialUiCompatibilityBridge`,而你的某些依赖项仍然使用旧版本的 Material 库,那么在运行时你可能会遇到错误——这些组件会因为查找位置不正确而找不到继承的主题数据。出现这种问题的症状通常是“主题值为 null”,或者出现“无法找到类型为 `MaterialLocalizations` 的祖先”这样的错误。解决这个问题的方法就是一定要添加 `MaterialUiCompatibilityBridge`。
在修复代码问题后直接运行 `pub get`,而无需先添加相应的包
# 错误的顺序
dart fix --apply --code=migrate_design_widgets
# 如果 `pubspec.yaml` 没有更新,分析错误仍然会存在
# 如果工具无法更新 `pubspec.yaml`,正确的顺序应该是这样
flutter pub add material_ui
flutter pub add cupertino_ui
dart fix --apply
在执行 `dart fix` 命令时,确保项目中的这些依赖包已经可以被正确解析,这样才能使导入关系的更新生效。如果你在这些包还没有被添加到 `pubspec.yaml` 中时就运行这个命令,虽然导入字符串可能会被更新,但那些无法被解析的依赖项仍然会存在,从而导致分析工具报错。
在迁移依赖包时不要提升其版本号
如果你维护着一个依赖包,并将其迁移到 `material_ui` 版本,但没有提升其版本号,那么那些还没有在 `pubspec.yaml` 中添加 `material-ui` 依赖的消费者,在更新你的依赖包时就会遇到解析失败的问题。
每当你的依赖包新增了外部依赖项时,都必须提升其版本号——因为从使用内置的 SDK 库转变为明确指定外部包作为依赖项,实际上就意味着版本号的变更。
迁移后组件的行为可能会发生变化
有些开发者认为,在将应用程序迁移到 Material 3 Expressive 或其他 Material Design 更新版本的过程中,组件的行为也会随之改变。但实际上并非如此。`material_ui` 1.0 版本实际上只是在代码冻结阶段对 `packageflutter/material.dart` 的简单复制而已;这些组件依然是相同的,它们的行为和视觉样式也没有任何变化。这种分离机制只是一种架构上的调整,并不是视觉设计上的重新设计。未来 Material 3 Expressive 中带来的视觉改进将会在 `material_ui` 1.0 之后的后续版本中逐步体现出来。
结论
将 Material 和 Cupertino 从 Flutter SDK 的核心部分分离出来,这是 Flutter 自诞生以来所进行的最为重要的架构调整之一。早在之前的文章 《在 Flutter 中分离 Material 和 Cupertino》 中就提出了这一设想,而现在这一目标已经完全实现,并且已经在 Flutter 3.47 版本中正式投入使用。
Flutter团队所建立的迁移路径堪称完美——任何可能引发架构变动的情况都得到了妥善处理:自动化工具负责处理相关导入文件的更新工作,兼容性解决方案则用于弥补不同组件之间的生态差异;API接口的设计保持不变,因此无需修改任何 widget 代码;本地化功能的配置也变得更加简单。而这些努力带来的回报是立竿见影的:你的设计系统能够每周都得到更新,而这一切都与季度发布的SDK版本周期无关。从这次发布开始,旧功能的弃用进程就已经正式启动了。到2026年11月,这些旧功能将正式被弃用。对于任何团队来说,这样的时间安排都足以完成迁移工作,但这并不是拖延迁移的借口——每推迟一周,就会错过一周的Material组件更新。
目前可以采取以下三个实际步骤:首先运行`flutter upgrade`命令来安装Flutter 3.47版本;接着运行`dart fix --apply --code=migrate_design_widgets`命令来迁移相关的导入依赖;最后运行`flutter analyze`命令来验证迁移结果。对于大多数项目来说,这三个命令就已经完成了所有的迁移工作。如果某些依赖库需要兼容性桥接方案,可以在迁移过程中添加该方案,完成后再将其移除。
Flutter 3.47是一个重要的里程碑。这次更新所带来的解耦机制、更快的迭代速度、更便捷的贡献流程、风格中立的核心架构,以及独立的组件版本管理系统,使得Flutter真正具备了模块化的设计特性。因此,现在就进行迁移是非常值得的。
参考资料
- Flutter 3.47的新功能:Flutter官方博客中发布的文章,介绍了此次更新中引入的独立UI组件包、桌面版Impeller引擎、稳定的组件预览功能以及其他各项变化。
- Flutter版本变更说明:详细列出了每个Flutter版本中的重大变更内容,包括解耦迁移的相关信息。
- material-ui库:由flutter.dev发布的官方独立Material Design组件库,替代了之前的`package:flutter/material.dart`。
- cupertino_ui库:官方独立的Cupertino设计组件库,同样替代了之前的`package/flutter Cupertino.dart`。
- material-ui的GitHub仓库:该独立组件的源代码、问题跟踪信息以及贡献指南。
- Flutter中Material与Cupertino组件的解耦机制:我在freeCodeCamp网站上发表的文章,解释了这一设计的背景、决策过程以及在Flutter 3.47版本发布之前的开发进展。
- 解耦项目的GitHub页面:公开的项目管理页面,记录了整个解耦工作的进展情况,包括已完成的任务和仍在进行中的工作。
- Impeller渲染引擎文档:关于Impeler渲染引擎的完整说明,包括如何在各种平台上临时禁用该引擎,以及如何报告渲染相关的问题。
相关文章
TanStack Table V9测试版:支持调整数据结构、可保存应用状态,并且能够降低内存使用量
TanStack Table V9是一个针对各种JavaScript框架开发的无头UI库的测试版。该版本在状态管理、内存使用效率以及扩展性方面都得到了优化。最显著的变化是采用了可选组件机制,这使得开发者可以仅加载所需的组件。对于旧版本的兼容性,也提供了相应的迁移工具。这个库仍然保持免费且以开发者为中心的设计理念。 作者:Daniel Curtis
阅读全文
在React中处理高频实时数据:从环形缓冲区到离屏canvas技术
React在很多方面都表现得非常出色。但如果你曾经尝试过每秒向它传输数千个数据点,你就会很快意识到:React并不像一根能输送大量水流的消防水管,而更像是一根普通的花园浇水软管。 如果强迫它处理过多的数据,要么会导致“草坪被淹没”(即DOM结构变得混乱),要么会使得“管道爆裂”(也就是应用程序运行出现严重问题)。 还有另一个与上述观点相关的观察结果:你的笔记本电脑通常拥有8到16个CPU核心,而你的React应用程序几乎总是只使用其中的一个核心。主线程负责处理JavaScript代码、DOM操作、布局计算以及绘制工作;而其他核心则处于闲置状态,因为主线程实在难以维持每秒60帧的渲染速度。 这两
阅读全文
React Router v8:一个刻意被设计得“乏味”的版本——它仅提供ESM格式的构建文件,并且默认使用了某种中间件。
React Router v8于2026年6月17日正式发布,此次更新带来的变动很小,同时也引入了一些新的功能。主要的更新内容包括仅支持ESM格式的构建版本以及默认的中间件设置。React Router v6和Remix v2已经进入了生命周期终止阶段。开发者应遵循相应的迁移指南来更新自己的应用程序;同时,也有人正在考虑使用其他替代方案,比如TanStack Router。 作者:Daniel Curtis
阅读全文
演讲主题:了解渐进性坍塌现象:如何避免连锁故障的发生
Sam Newman探讨了土木工程中“渐进性崩溃”这一概念,以及它如何应用于分布式系统。通过列举一些现实世界中的案例——从1968年Ronan Point大厦的坍塌事故到AWS系统的故障——他为软件领域的从业者提供了重要的韧性工程策略。了解如何加强系统组件的稳定性、隔离故障节点,并减少组件之间的相互依赖关系,从而有效防止灾难性的连锁反应。 作者:Sam Newman
阅读全文