Flutter 3.50 & Dart 3.14 新鲜出炉。
官方博客给这版起的副标题是 "Investing in Flutter's next decade",口气不小。不过把更新内容翻完,我的感受更接近大扫除:一把甩掉了不少包袱。
- SDK 的 Material 与 Cupertino 正式废弃,独立包
material_ui与cupertino_ui正式彻底稳坐宝座了。 - iOS带来了不少的优化,比如触控响应优化直接快一整帧(在 60 Hz 的 iOS 设备上快 16 毫秒,在采用 120 Hz ProMotion 显示屏的设备上快 8 毫秒。)Android release 禁止了外部 Intent 启动参数注入的高危漏洞。扩展了 Platform Views 对 Hybrid Composition++ 的支持,并新增了对 Android Gradle Plugin 9 全新 DSL 的可选支持。
material_ui 和 cupertino_ui登上王座
3.47 的时候 material_ui 和 cupertino_ui 出了 1.0,就这意味着二者迟早要登上王座。3.50 落了:package:flutter/material.dart、package:flutter/cupertino.dart,连同 flutter_localizations 里对应的 delegate,全部标记为 deprecated。现在 flutter create 出来的新项目,默认 import 的已经是 package:material_ui。
老项目迁移只要一条命令:
dart fix --apply --code=migrate_design_widgets
它会往 pubspec.yaml 里加依赖,把旧的 import 路径改掉,本地化相关的配置也一并更新。
废弃不等于删除,旧写法眼下还能编译,不用今天就改。只是三方库会陆续切过去,拖得越久,项目里新旧两套混用的情况越难看,找个不赶版本的下午跑一遍比较省心。
有一点要留意:鸿蒙那边的 Flutter 还没适配
material_ui。项目要跑鸿蒙的,先别急着迁。
iOS:什么都不用改,触控快一帧
这是我觉得这版最实在的一条。
引擎重新安排了触摸事件之后的出帧时机,触摸事件到了立刻交给 framework,在同一个刷新周期里把这一帧做出来,不用再等下一个 vsync。换算下来,60Hz 设备快 16ms,120Hz 的 ProMotion 屏快 8ms。官方一直测到了 2015 年的 iPhone 6s。
为什么以前要多等一帧?早期 iOS 的触摸事件在平台线程派发,驱动渲染的 CADisplayLink 在另一个线程,为了抹平跨线程的抖动,引擎中间垫了好几层缓冲调度。后来线程合并了,这些缓冲就成了纯粹的历史包袱,这次由社区贡献者 Matej Knopp 一把清掉。官方说后面还会继续压 iOS 的输入延迟。
3.50 也是 Xcode 27 和 iOS 27 之后的第一个稳定版,几个烦人的问题一起修了:
- 新配对或者刚升完系统的 iPhone,不用先开一次 Xcode 才能跑起来,以前那个白屏卡住的问题没了
- iOS 27 设备上
flutter run偶发的启动崩溃修了 - 用 Xcode 27 构建时,
flutter screenshot在真机上又能用了,这功能从 iOS 17 起就是坏的
还有两个时间点。CocoaPods trunk 今年 12 月转为只读,从 3.50 开始,只要你关着 Swift Package Manager,每敲一条 Flutter 命令都会收到一次警告。当初为了绕过构建问题关掉 SPM 的同学,可以重新打开试试了。另外 Flutter 已经不在 Intel Mac 上跑测试,用 Intel Mac 构建会提示一次,彻底移除支持只是时间问题。
Android:一个安全补丁,一个躲不开的迁移
先说安全。以前 Flutter 打出来的 Android 包,会接受设备上任何 App 通过 Intent extras 传进来的运行时配置。3.50 的 release 包把这条路堵上了:外部 Intent 带来的 route、dart_entrypoint、cached_engine_id 一律忽略,--dart-flags 这类引擎参数直接拒绝。正常的 deep link 不受影响。如果你的业务恰好依赖从外部 Intent 指定入口或路由,升级前一定要测。
Hybrid Composition++(HCPP)这版继续打磨。它把原生 View 的合成交给 Android 系统本身,地图、WebView 嵌在列表里滚动时的撕裂,还有输入框的各种抽风,都是冲着这个去的。目前 beta 渠道已经默认开启,stable 上还得手动:
flutter config --enable-hcpp
条件是 Android 14 以上,并且 Impeller 跑在 Vulkan 上。不满足的设备会自动退回原来的方案,所以打开它没什么风险。
插件作者要注意 Built-in Kotlin。还在手动 apply Kotlin Gradle Plugin 的插件,以后 pub.dev 评分会被扣,到 AGP 10 干脆编不过。官方已经把 flutter/packages 里的一方插件全迁完了,等你项目里所有插件都支持之后,就可以切到 AGP 9 的新 DSL。
新建工程的模板默认值也跟着涨了一轮:AGP 9.3.1、Gradle 9.5.0、KGP 2.4.20,Java 最低 17,compileSdk 和 targetSdk 是 36,minSdk 是 24。
原生互操作转正
去年官方画的那张饼,直接调原生 API、不走 platform channel,这版到了 stable:Objective-C 用 FFIgen,Java 和 Kotlin 用 JNIgen。
好处很具体。不用序列化,不用靠字符串对方法名,可以同步调用,也可以在后台 isolate 里调。系统出了新 API,生成一份绑定就能用,不必等某个插件更新。
Pigeon 也出了新的 native interop 模式,底层就是这两个生成器。不过官方的措辞是 preview,要不要上生产自己掂量。
Web:Safari 和 Firefox 也能跑 Wasm 了
之前只有 Chrome 和 Edge 默认跑 WebAssembly。3.50 里 Safari 26.2+(包括 iOS)和 Firefox 147+ 也支持了,官方给的数据是重度 widget 场景下帧率是 JS 的两倍以上。暂时还要手动加白名单,在 web/flutter_bootstrap.js 里:
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
config: {
wasmAllowList: { blink: true, webkit: true, gecko: true },
},
});
然后 flutter build web --wasm,老浏览器会自动回退到 JS。
做过 Flutter Web 发版的人,大概都遇到过"发了新版用户还在跑旧 JS"。现在有官方方案了:构建时加 --web-content-hash,JS、Wasm 和 assets 的文件名都会带上内容哈希。这些文件可以放心缓存一年,只需要让 index.html 和 flutter_bootstrap.js 保持 no-cache。
还有一个实验性的 WebParagraph,和 Chrome 团队一起做的,让浏览器自己负责文字排版。CanvasKit 不用再打包一整套字体解析器,体积从 5.46 MB 降到 3.75 MB 左右,也不用再联网下载 fallback 字体。中文页面上那些一闪而过的豆腐块,以后有希望彻底消失。
顺带一提,dart:html、dart:js、dart:js_util、package:js 都废弃了。想知道项目里还有没有依赖在用,构建时加上 --no-deprecated-js-interop,有的话会直接失败。
Framework 里几个顺手的东西
这部分很多来自社区贡献,都不大,但都是平时写到一半会骂一句的地方。
SliverClipRect 和 SliverClipRRect。以前想给 CustomScrollView 里的一段内容加圆角,ClipRRect 根本套不上 sliver。现在有专门的组件,还能通过 clipOverlap 控制内容滚到吸顶 header 下面时怎么裁:
SliverClipRRect(
borderRadius: BorderRadius.circular(16),
clipOverlap: ClipOverlapBehavior.preserveShape,
sliver: SliverList.builder(
itemCount: 20,
itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
),
)
其余几个:
ListView.separated支持itemExtentBuilder了,带分割线的长列表也能按需算高度ScrollPhysics.shouldUpdate,自定义滚动物理的参数变了,不用再靠换 Key 重建整个列表showDialog多了barrierBuilder,做毛玻璃遮罩不用自己手搓路由TextInputType.numberWithOptions加了password选项,Android 上输 PIN 码能弹安全数字键盘- Android 可以用
AutofillHints.emailOTPCode自动填邮箱验证码 DecorationImage支持placeholder,容器背景图加载时不再是一片空白MediaQueryData.reduceMotion可以读到系统的"减弱动态效果"开关
一些老 bug 也收了尾:ensureVisible 不会再把目标滚到吸顶 header 底下,在 PageView 里拖选文字不会误翻页。
路由和弹窗
路由这块有两个改动值得单拎出来。
一个是 showDialog 多了 barrierBuilder。以前遮罩层只能改个 barrierColor,想要毛玻璃背景,得自己写 RawDialogRoute 或者 PageRouteBuilder。现在一个回调就够:
dart
showDialog<void>(
context: context,
barrierBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: child,
),
);
},
builder: (context) => const AlertDialog(title: Text('提示')),
);
回调里的 child 是默认遮罩,点击关闭的逻辑还在,包一层就行。
另一个是 Navigator.popUntilWithResult。多步流程里很常见的需求:从 A 一路 push 到 D,在 D 完成后直接退回 A,同时把结果带回去。popUntil 不能带返回值,这个 issue 2019 年就提了,这些年大家靠全局变量、EventBus 或者循环手动 pop 绕过去。现在可以直接写:
// A 页面
final result = await Navigator.pushNamed<OrderResult>(context, '/step1');
// D 页面
Navigator.popUntilWithResult<OrderResult>(
context,
ModalRoute.withName('/cart'),
OrderResult(success: true),
);
官方博客在这一条上强调的是:即使目标路由自己带有 local history,结果也能正确交回去。
键盘和无障碍方面也有几处和导航相关的改动:
- 按 Esc 会先关掉打开着的
Tooltip,焦点不会跟着跑,符合 Web 无障碍规范的要求 FocusableActionDetector新增skipTraversal,自定义的可交互组件可以选择不参与 Tab 键遍历- Web 上 VoiceOver 和
WidgetsApp.router的焦点问题修了,浮层里的焦点不会莫名其妙被重置 MediaQueryData.reduceMotion可以读到系统的"减弱动态效果"开关,页面切换可以据此把滑动换成淡入淡出- 自定义滑块可以用
SemanticsRole.slider,读屏软件能正确播报数值
渲染和桌面
Impeller 在桌面端的 SDF 渲染器,这版把线条、细边框、圆角和渐变都接了过去,这些形状直接在 GPU 上算,卡片和阴影多的页面受益最明显。
Android 这边修的是具体机型上的问题:部分 Mali GPU 加载纹理时的崩溃修了,PowerVR B 系列自动从 Vulkan 回退到 OpenGL。线上有这类设备崩溃记录的,值得升上去看看数据。
Windows 有一条对国内开发者很友好:路径里带非 ASCII 字符也能正常加载资源了。用户名是中文的那批用户,终于不会再因为这个打不开应用。此外 Windows 默认请求 OpenGL ES 3,文字对比度调过,比之前清楚。
多窗口 API 仍然是实验性的,继续在和 Canonical 合作推进。Linux 上能自己画标题栏、做透明窗口了。API 也改了名,RegularWindow 变成 Window,sizedToContent 变成 shrinkWrap。
桌面端:这版改动不少
桌面一直是 Flutter 里存在感偏弱的一块,3.50 给的东西比往常多,分平台说。
Windows
对国内开发者最有用的一条:路径里带非 ASCII 字符也能正常加载资源和文件了。用户名是中文的那批用户,装完打不开应用的老问题到此为止。
渲染上有三处变化。引擎默认请求 OpenGL ES 3,老机器会自动回退到 ES 2,不用担心兼容。每帧少做一次全屏拷贝,GPU 占用降了。文字对比度调高了,之前切到新渲染管线后字发虚发细的问题有改善。
macOS
窗口和弹窗可以在显示之前先按内容算好尺寸。以前做工具小窗、设置弹窗,窗口先按默认大小弹出来再跳一下,这个闪烁现在没了。
Impeller 在 macOS 上的渐变和重复文字阴影也更快了。
另外 Xcode 27 不再要求 macOS 应用带 Intel 架构,想只打 Apple Silicon 的包,现在就可以:
bash
flutter config --enable-macos-arm64-only
官方说后面的版本会把它变成默认。
Linux
WindowControllerLinux 开放了一批底层能力:关掉系统标题栏和边框、窗口背景透明、把拖动和缩放交还给窗口管理器处理。也就是说自绘标题栏、做异形透明窗口,都不需要再装第三方插件。
三个平台共同的部分
Impeller 的 SDF 渲染器这版接管了线条、细边框、圆角和渐变,这些形状直接在 GPU 上算,不再由 CPU 生成几何。卡片、边框、阴影堆得多的界面受益最明显。Windows 和 Linux 上叠层 UI 和自定义形状的锯齿也有改善。
多窗口 API 仍然是实验性的,和 Canonical 合作在推。这版有三个变化,用过的人需要改代码:
RegularWindow改名为WindowsizedToContent改名为shrinkWrap,和 Flutter 布局里的叫法统一WindowManager提到了WidgetsApp上面,副窗口可以有自己独立的 UI 和导航状态,不必和主窗口挂在同一棵树下
还有个小细节:RawScrollbar.mouseCursor 现在只在鼠标落到滑块和轨道上时才变光标,以前靠近滚动条附近就会变。
工具链
Dart 3.14 带来一个新命令 dart migrate,用来处理升级时的破坏性变更,可以先用 --dry-run 预览会改哪些地方。
Analysis Server 在多包工作区里的冷启动 rename 快了一截,官方拿 DevTools 的仓库测,从 2.0 秒降到 0.9 秒。
AI 相关的,官方的 agent 插件里多了一条规则:改完 lib/ 下的代码,会自动通过 MCP 触发 hot reload。包作者也可以把 agent skills 放在包的 skills/ 目录里随包发布,使用方执行 dart run skills@ get 就能装上。
升不升
我的看法是值得升,尤其是 iOS 用户占比高的项目,那一帧的触控延迟是白拿的。
升之前过一遍这几项:
- Android 是否依赖外部 Intent 传入路由或入口参数
IgnorePointer、AbsorbPointer上的ignoringSemantics这版已经删除,还在用的会直接编不过- 带软连字符的文本,换行处现在会显示出连字符
- 移动端浏览器设置了
viewport-fit=cover的页面,SafeArea的表现会变 - iOS 工程如果关着 SPM,先打开看能不能过
material_ui 的迁移不用和升级绑在一起做。先升 SDK,跑稳了再迁。
新版本肯定还有坑,等一两个 hotfix 再上生产也不丢人。
参考资料
- Flutter 官方博客:What's new in Flutter 3.50(flutter.dev/blog/whats-new-in-flutter-3-50)