This commit is contained in:
guoshuyu 2023-05-17 14:34:31 +08:00
parent a9bf64ef03
commit 4674894d49
10 changed files with 2178 additions and 0 deletions

266
Dart-300.md Normal file
View File

@ -0,0 +1,266 @@
# Google I/O 2023 - Dart 3 发布,快来看看有什么更新吧
> 核心原文链接: https://medium.com/dartlang/announcing-dart-3-53f065a10635
自从 Flutter Forword 发布了 [Dart 3α 预览](https://juejin.cn/post/7194741144482218045) 之后,大家对 Dart 3 的正式发布就一直翘首以待,这不仅仅是 Dart 版本号追上了 Flutter 版本号,更是 Dart 在 2.0 之后迎来的最大一次更新,主要包括了:
- 100% 空安全
- records
- patterns
- class modifiers
- Wasm 对 Web 的增加支持,可以预览 dart wasm native 了
# 100% 空安全支持
如下图所示Dart 的 null safety 历经三年的时间,如今 Dart 终于有用了完善的类型系统,现在的 Dart 3 下,如果一个类型说一个值不是 `null`,那么它永远不可能是 `null`
![](http://img.cdn.guoshuyu.cn/20230511_D3/image1.png)
> 说起来,还真有不少用户的项目没升级到 null safety ,这次就不能再等了。
另外,目前 pub.dev 上排名前 1000 的包中有 99% 支持空安全,所以官方预计升级到 Dart 3 的兼容问题并不大少数情况下Dart 3 中的对一些历史代码的相关清理可能会影响某些代码的运行,例如
- 一些旧的核心库 API 已被删除([#34233](https://github.com/dart-lang/sdk/issues/34233)、[#49529](https://github.com/dart-lang/sdk/issues/49529)
- 一些工具已被调整([#50707](https://github.com/dart-lang/sdk/issues/50707))。
> 如果你在迁移到到 Dart 3 时遇到问题,可以查阅 https://dart.dev/resources/dart-3-migration
# Record, patterns 和 class modifiers
关于万众期待的 record 和 patterns 其实在之前的 [Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](https://juejin.cn/post/7194741144482218045)上已经有个详细解释,这里主要重新根据官方内容简诉一些这些变化。
## 使用 record 构建结构化数据
在此之前 Dart 函数只能返回一个值,如果需要返回多个值,必须将这些值打包成其他数据类型,例如 Map 或 List或者定义可以保存这些值的新类。
使用非类型化数据结构削弱了类型安全性,而定义新类来传输数据会增加编码过程中的工作量,但是现在,通过 record 就可以简洁明地构建结构化数据:
```dart
(String, int) userInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['height'] as int);
}
```
在 Dart 中record 是一个通用功能,它们不仅可以用于函数返回值,还可以将它们存储在变量中,例如将它们放入 List 中或者它们用作 Map 中的键,或创建包含其他 record 的 record。
另外还可以添加未命名字段,就像我们在前面的示例中所做的那样,也可以添加命名字段,例如 `(42, description: Meaning of life)`
record 是值类型,没有标识,这让编译器能够在某些情况下完全擦除记录对象,记录还带有自动定义的 `==` 运算符和 `hashCode` 函数。
> 详细可以参考官方文档https://dart.dev/language/records 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045
## 使用具有 pattern 和 pattern 匹配的结构化数据
record 简化了构建结构化数据的方式,这不会取代使用类来构建正式的类型层次结构的方式,它只是提供了另一种选择。
在任何一种情况下,你可能希望将结构化数据分解为单独的元素,这就是 pattern 匹配发挥作用的地方。
考虑 pattern 的基本形式,以下记录 pattern 将 record 解构为两个新变量 `name``height` ,然后可以像任何其他变量一样使用这些变量:
```dart
var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
print('User $name is $height cm tall.');
```
List 和 Map 存在类似的 pattern ,都可以使用下划线模式跳过单个元素:
```dart
var (String name, _) = userInfo(…);
```
在 switch 语法中, Dart 3 扩展了语句 switch 的支持,现在支持在这些情况下进行 pattern 匹配:
```dart
switch (charCode) {
case slash when nextCharCode == slash:
skipComment();
case slash || star || plus || minus:
operator(charCode);
case >= digit0 && <= digit9:
number();
default:
invalid();
}
```
还可以通过新的表达式进行微调,以下示例函数返回 switch 表达式的值以计算今天工作日的描述:
```dart
String describeDate(DateTime dt) =>
switch (dt.weekday) {
1 => 'Feeling the Monday blues?',
6 || 7 => 'Enjoy the weekend!',
_ => 'Hang in there.'
};
```
**模式的一个强大功能是检查 “exhaustiveness” 的能力,此功能可确保 switch 处理所有可能的情况**。
在前面的示例中,我们正在处理工作日的所有可能值,这是一个`int` ,所以我们通过针对特定值 `1``6 `/`7` 的匹配语句的组合来穷尽所有可能的值,然后通过 `_` 对其余情况使用默认情况。
要对用户定义的数据层次结构(例如类层次结构)启用该能力,请在类层次结构的顶部使用 `sealed` 修饰符,如下例所示:
```dart
sealed class Animal { … }
class Cow extends Animal { … }
class Sheep extends Animal { … }
class Pig extends Animal { … }
String whatDoesItSay(Animal a) =>
switch (a) { Cow c => ' $c says moo' , Sheep s => ' $s says baa' };
```
这将返回以下错误,提醒我们错过了最后一个可能的子类型 Pig 的处理:
```
line 6 • The type 'Animal' is not exhaustively matched by the switch cases
since it doesn't match 'Pig()'.
```
最后,`if ` 语句也可以使用 pattern ,在下面的例子里,我们使用 *if-case* 匹配映射模式来解构 JSON 映射,这里匹配常量值(字符串如 `'name'` and `'Michael' `)和类型测试模式 `int h` 以读出 JSON 值如果模式匹配失败Dart 将执行该 `else` 语句。
```dart
final json = {'name': 'Michael', 'height': 180};
// Find Michael's height.
if (json case {'name': 'Michael', 'height': int h}) {
print('Michael is $h cm tall.');
} else {
print('Error: json contains no height info for Michael!');
}
```
> 详细可以参考官方文档http://dart.dev/language/patterns 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045
# classes with class modifiers
Dart 3 的第三个语言特性是类修饰符,与前两个支持不同的是,这更像是一个高级用户功能,它主要是为了满足了 Dart 开发人员制作大型 API 或构建企业级应用时的需求。
> 目前是基于 *constructed**extended**implemented* 来实现处理,关键词有
类修饰符使 API 作者能够仅支持一些特定的功能,而默认值保持不变,例如:`abstract`、`base` 、`final`、`interface`、`sealed`、`mixin` 。
> 只有`base `修饰符可以出现在 mixin 声明之前,修饰符不适用于其他声明如 `enum`、`typedef`或 `extension`
```dart
class Vehicle {
String make; String model;
void moveForward(int meters) { … }
}
// Construct.
var myCar = Vehicle(make: 'Ford', model: 'T',);
// Extend.
class Car extends Vehicle {
int passengers;
}
// Implement.
class MockVehicle implements Vehicle {
@override void moveForward …
}
```
例如要强制继承类或 mixin 的实现,就可以使用 `base` 修饰符。 `base` 不允许在其自己的库之外实现,这保证:
- 每当创建类的子类型的实例时,都会调用基类构造函数
- 所有实现的私有成员都存在于子类型中
- 类中新实现的成 员`base `不会破坏子类型,因为所有子类型都继承了新成员
```dart
// Library a.dart
base class Vehicle {
void moveForward(int meters) { ... }
}
// Library b.dart
import 'a.dart';
var myCar = Vehicle(); // Can be constructed
base class Car extends Vehicle { // Can be extended
int passengers;
// ...
}
base class MockVehicle implements Vehicle { // ERROR: Cannot be implemented
@override
void moveForward { ... }
}
```
如果要创建一组已知的、可枚举的子类型,就可以使用修饰符 `sealed` [sealed 允许在那些静态](https://dart.dev/language/branches#exhaustiveness-checking)子类型上创建一个 switch 。
```dart
sealed class Vehicle { ... }
class Car extends Vehicle { }
class Truck implements Vehicle { }
class Bicycle extends Vehicle { }
// ...
var vehicle = Vehicle(); // ERROR: Cannot be instantiated
var vehicle = Car(); // Subclasses can be instantiated
// ...
// ERROR: The switch is missing the Bicycle subtype or a default case.
return switch (vehicle) {
Car() => 'vroom',
Truck() => 'VROOOOMM'
};
```
类修饰符存在一些添加限制,例如:
- 使用 `interface class` ,可以定义 contract 给其他人去实现,但不能扩展接口类。
- 使用 `base class`,可以确保类的所有子类型都继承自它,而不是实现它的接口,这确保私有方法在所有实例上都可用。
- 使用 `final class`,可以关闭类型层次结构,以防止自己的库之外的任何子类。这样的好处是允许 API 所有者添加新成员,而不会出现破坏 API 使用者更改的风险。
> 是不是没看明白?有关详细信息,可以参考 https://dart.dev/language/class-modifiers
# 展望未来
Dart 3 不仅仅是是在这些新功能上向前迈出了重要的一步,还为大家提供了下一步的预览。
## Dart language
Records, patterns 和 class modifiers 是非常庞大的新功能,因此它们的某些设计可能还需要改进,所以接下来还会有一些更小、更增量的功能更新,这些功能完全不会中断,并且专注于在没有迁移成本的情况下提高开发人员的工作效率。
目前正在探索的还有 [primary constructors](https://github.com/dart-lang/language/issues/2364) 和 [inline classes](https://github.com/dart-lang/language/issues/2727) 包装,另外之前讨论过的宏(也称为[元编程](https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md))也在进行探索,因为元编程的规模和固有风险,目前正在采取一种更有效和彻底的方法进行探索,因此没有具体的时间表可以分享,即使是最终确定的设计决策。
## native interop
移动和桌面上的应用通常依赖于 native 平台提供的大量 API无论是通知、支付还是获取手机位置等。
在之前 Flutter 中,这些是通过构建插件来访问的,这需要为 API 编写 Dart 代码和一堆特定于平台的代码来提供实现。
目前已经支持与使用 `dart:ffi` 直接和原生语言进行交互我们目前正在努力扩展它在Android 上的支持,再次之前可以看 [Java 和 Kotlin interop ](https://dart.dev/guides/libraries/java-interop) 以及 [Objective-C 和 Swift interop](https://juejin.cn/post/7137874832988831751) 。
> 请查看新的 Google I/O 23 的 [Android interop 视频](https://io.google/2023/program/2f02692d-9a41-49c0-8786-1a22b7155628/)。
## 编译为 WebAssembly——使用 native 代码定位 web
[WebAssembly (缩写为 Wasm作为跨](https://webassembly.org/)[所有浏览器的](https://caniuse.com/wasm)平台的二进制指令格式其可用性度一直在增长Flutter 框架使用 Wasm 有一段时间了,这就是我们如何通过 Wasm 编译模块将用 C++ 编写的 SKIA 图形渲染引擎交付给浏览器的实现。
Flutter 也一直对使用 Wasm 来部署 Dart 代码很感兴趣,但是在此之前该实现被阻止了,与许多其他面向对象的语言一样,因为 Dart 需要使用垃圾回收。
在过去的一年里Flutter 和 Wasm 生态系统中的多个团队合作,将新的 WasmGC 功能添加到 WebAssembly 标准中,目前在 Chromium 和 Firefox 浏览器中已经接近稳定。
将 Dart 编译为 Wasm 模块的工作有两个针对 Web 的高级目标:
- **加载时间:**我们希望我们可以使用 Wasm 交付部署有效负载,使浏览器可以更快地加载,从而缩短到达用户可以与 Web 交互的时间。
- **性能:**由 JavaScript 提供支持的 Web 应用需要即时编译才能获得良好的性能Wasm 模块更底层,更接近机器代码,因此我们认为它们可以提供更高的性能、更少的卡顿和更一致的帧率。
- **语义一致性**Dart 在我们支持的平台之间保持高度一致而自豪。但是,在 web 上有一些例外情况,例如 Dart web 目前在[数字表示](https://dart.dev/guides/language/numbers)方式上有所不同,而使用 Wasm 模块,我们可以将 web 视为具有与其他原生目标相似语义的“原生”平台。
**跟随 Dart3 的发布, Dart 到 Wasm 编译的第一个预览也一起发布**,这是最初的 Flutter Web 重点支持。虽然现在还早,后续还有很多工作要完成,但已经可以通过 https://flutter.dev/wasm 开始测试。

View File

@ -76,6 +76,10 @@
* [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md)
* [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md)
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)
* [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md)
* [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md)
* [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md)
* [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md)

441
Flutter-310.md Normal file
View File

@ -0,0 +1,441 @@
# Google I/O 2023 - Flutter 3.10 发布,快来看看有什么更新吧
> 核心部分原文链接https://medium.com/flutter/whats-new-in-flutter-3-10-b21db2c38c73
虽然本次 I/O 的核心 keynote 主要是 AI ,但是按照惯例依然发布了新的 Flutter 稳定版,不过并非大家猜测的 4.0,而是 3.10 Flutter 的版本号依然那么的出人意料。
**Flutter 3.10 主要包括有对 Web、mobile、graphics、安全性等方面的相关改进**,核心其实就是:
- iOS 默认使用了 Impeller
- 一堆新的 Material 3 控件袭来
- iOS 新能优化Android 顺带可有可无的更新
- Web 可以无 iframe 嵌套到其他应用
# Framework
## Material 3
看起来谷歌对于 Material 3 的设计规范很上心,根据最新的 [Material Design spec](https://m3.material.io/components) 规范 Flutter 也跟进了相关的修改,其中包括有**新组件和组件主题和新的视觉效果等**。
目前依然是由开发者可以在 `MaterialApp` 主题配置下,通过 `useMaterial3` 标志位选择是否使用 Material 3**不过从下一个稳定版本开始,`useMaterial3` 默认会被调整为 `true`** 。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image1.png)
> 对于 Material 3 ,可以通过 https://flutter.github.io/samples/material_3.html 上的相关 Demo 预览。
## ColorScheme.fromImageProvider
所有 M3 组件配置主题的默认颜色 `ColorScheme`**默认配色方案使用紫色 shades这有区别于之前默认的蓝色**。
除了可以从单一 “seed” 颜色来定制配置方案之后,通过 `fromImageProvider` 图像也可以创建自定义配色方案。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image2.gif)
## NavigationBar
本次还增加了一个 M3 版本的 `BottomNavigationBar` 控件效果,虽然 [M3](https://m3.material.io/components/navigation-bar/overview) 使用不同的颜色、highlighting 和 elevation但它的工作方式其实还是和以前一样。
如果需要调整 `NavigationBars` 的默认外观,可以使用使用 `NavigationBarTheme` 来覆盖修改,虽然目前你不需要将现有 App 迁移到 `NavigationBars` ,但是官方建议还是尽可能在新项目里使用 `NavigationBars` 作为导航控件。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image3.gif)
## NavigationDrawer
M3 针对 Drawer 同样提供了新的 `NavigationDrawer `,它通过 `NavigationDestinations` 显示单选列表,也可以在该列表中包含其他控件。
> 同步M3下 `Drawer` 也更新了颜色和高度,同时对布局进行了一些小的更改。
`NavigationDrawer` 需要时可以滚动,如果要覆盖 `NavigationDrawer` 的默认外观,同样可以使用 `NavigationDrawerTheme` 来覆盖。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image4.gif)
## SearchBar 和 SearchAnchor
这是 Flutter 为搜索查询和提供预测效果新增的控件。
当用户在输入搜索查询时,会在 “search view” 中计算匹得到一个配响应列表,用户选择一个结果或调整匹配结果。
如果要覆盖 `SearchBarTheme` 的默认外观,同样可以使用 `SearchAnchorTheme` 来覆盖。
| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image5.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image6.gif) |
| ------------------------------------------------------ | ------------------------------------------------------ |
## Secondary Tab Bar
M3 下 Flutter 现在默认提供创建第二层选项卡式内容的支持,针对二级 Tab 可以使用 `TabBar.secondary`
![](http://img.cdn.guoshuyu.cn/20230511_F3/image7.gif)
## DatePicker 和 TimePicker 更新
M3下 `DatePicker `更新了控件的日历、文本字段的颜色、布局和形状等,对应 API 没有变动,但会个新增了 `DatePickerTheme` 用于调整控件样式。
`TimePicker` 和`DatePicker` 一样,更新了控件的常规版本和紧凑版本的颜色、布局和形状。
| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image8.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image9.gif) |
| ------------------------------------------------------ | ------------------------------------------------------ |
## BottomSheet 更新
M3 下 `BottomSheet` 除了颜色和形状更新之外,还添加了一个可选的拖动手柄,当设置 `showDragHandle``true` 时生效。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image10.gif)
## ListTile 更新
M3下 `ListTile` 更新了定位和间距,包括 content padding、leading 和 trailing 控件的对齐、minimum leading width, 和 vertical spacing 等,但是 API 保持不变。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image11.gif)
# TextField 更新
M3 更新了所有 `TextField` 对原生手势支持。
用鼠标双击或三次点击 `TextField` 和在触摸设备上双击或三次点击效果相同,默认情况下 `TextField` 和`CupertinoTextField ` 都可以使用该功能。
### `TextField` double click/tap 手势
- Double click + drag扩展字块中的选择。
- Double tap + drag扩展字块中的选择。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image12.gif)
### `TextField` triple click/tap 手势
Triple click
- 在多行 `TextField`(Android/Fuchsia/iOS/macOS/Windows) 中选择点击位置的段落块。
- 在多行 `TextField` (Linux) 内部时,在 click 位置选择一个行块。
- 选择单行中的所有文本 `TextField`
Triple tap
- 在 multi-line `TextField` 中选择点击位置的段落块 。
- 选择单行 `TextField` 中的所有文本
Triple click+拖动
- 扩展段落块中的选择 (Android/Fuchsia/iOS/macOS/Windows)。
- 扩展行块中的选择 (Linux)。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image13.gif)
> 简单来说,就是手势和鼠标在双击和三击下,会触发不同的选择效果,并且 Linux 在三击效果下会有点差异
## Flutter 支持 SLSA 级别 1
Flutter Framework 现在使用软件工件供应链级别 ( [SLSA](https://slsa.dev/) ) 级别 1 进行编译,这里面支持了许多安全功能的实现,包括:
- **脚本化构建过程**Flutter 的构建脚本现在允许在受信任的构建平台上自动构建,建立在受保护的架构上有助于防止工件篡改,从而提高供应链安全性。
- **带有审计日志的多方批准**Flutter 发布工作流程仅在多个工程师批准后执行,所有执行都会创建可审计的日志记录,这些更改确保没有人可以在源代码和工件生成之间引入更改。
- **出处**Beta 和稳定版本现在使用 [provenance](https://slsa.dev/provenance/v0.1) 构建,这意味着具有预期内容的可信来源构建了框架发布工件,每个版本都会发布链接以查看和验证 [SDK 存档](https://docs.flutter.dev/release/archive) 的出处。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image14.png)
这项工作还在朝着 SLSA L2 和 L3 合规性迈进,这两个级别侧重于在构建过程中和构建之后提供 artifacts 保护。
# Web
## 改进了加载时间
3.10 减小了图标字体的文件大小,它会从 Material 和 Cupertino 中删除了未使用的字形,从而提供了更快加载。
## CanvasKit 变小
基于 Chromium 的浏览器可以使用更小的自定义 CanvasKit 渠道,托管在 Google [gstatic.com ](http://gstatic.com/) 上的 CanvasKit 可以进一步提高性能。
## Element 嵌入
现在可以 [从页面中的特定 Element 来加载 Flutter Web](https://docs.flutter.dev/deployment/web#embedding-a-flutter-app-into-an-html-page) ,不需要 `iframe`,在这个版本之前 fluter web 是需要填充整个页面主体或显示在 `iframe` 标记内,简单说就是把 flutter web 嵌套到其他 Web 下更方便了。
> 具体 Demo 可见https://github.com/flutter/samples/tree/main/web_embedding
## **着色器支持**
Web 应用可以使用 Flutter 的 [fragment shader](https://docs.flutter.dev/development/ui/advanced/shaders)
```yaml
flutter:
shaders:
- shaders/myshader.frag
```
# Engine
## Impeller
在 3.7 稳定版中 iOS 提供了 [Impeller ](https://docs.flutter.dev/perf/impeller) 预览支持,从那时起 Impeller 就收到并解决了用户的大量反馈。
在 3.10 版本中,我们对 Impeller 进行了 250 多次提交,**现在我们将 Impeller 设置为 iOS 上的默认渲染器**。
默认情况下,所有使用 Flutter 3.10 为 iOS 构建的应用都使用 Impeller这样 iOS 应用预计将会有更少的卡顿和更一致的性能。
自 3.7 版本以来iOS 上的 Impeller 改进了内存占用,可以使用较少的渲染通道和中间渲染目标。
在较新的 iPhone 上,**启用有损纹理压缩可在不影响保真度的情况下减少内存占用,这些进步也显着提高了 iPad 的性能**。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image15.png)
比如 [Wonderous](https://flutter.gskinner.com/wonderous/) 应用中的 “pull quote” 页面,**这些改进是的当前页面下的内存占用量减少了近一半**。
内存使用量的减少也适度降低了 GPU 和 CPU 负载Wondrous 应用可能不会记录这些负载下降,它的框架之前已经优化的不错,但这一变化应该会延长续航能力。
Impeller 还释放了团队可以更快地交付流行功能请求的能力,例如在 iOS 上支持更广泛的 P3 色域。
> 社区贡献加速了我们的进步,特别是 GitHub 用 户[ColdPaleLight](https://github.com/ColdPaleLight) 和 [luckysmg](https://github.com/luckysmg ) ,他们编写了多个与 Impeller 相关的补丁,提高了保真度和性能。
虽然 Impeller 满足大多数 Flutter 应用的渲染需求,但你可以选择关闭 Impeller。如果选择退出请考虑在[ GitHub 上提交问题](https://github.com/flutter/flutter/issues/new/choose)以告诉我们原因。
> ```
> <key>FLTEnableImpeller</key>
> <false/>
> ```
用户可能会注意到 Skia 和 Impeller 在渲染时存在细微差别,这些差异可能是错误,所以请勿在 Github 上提出问题,**在未来的版本中,我们将删除适用于 iOS 的旧版 Skia 渲染器以减小 Flutter 的大小**。
另外Impeller 的 Vulkan 后端然在支持当中Android 上的 Impeller 仍在积极开发中,但尚未准备好进行预览。
> 要了解 Impeller 进展,请查看 https://github.com/orgs/flutter/projects/21。
# Performance
3.10 版本涵盖了除 Impeller 之外还有更多性能改进和修复。
## 消除卡顿
这里要感谢 [luckysmg](https://github.com/luckysmg) 他们发现可以缩短从 Metal 驱动获取下一个可绘制层的时间,而方式就是需要将 `FlutterViews` 背景颜色设置为非零值。
此更改消除了最近 iOS 120Hz 显示器上的低帧率问题,**在某些情况下它会使帧速率增加三倍**,这帮助我们解决了六个 GitHub issue。
**这一变化具有意义重大,以至于我们向后移植了一个修补程序到 3.7 版本中**。
在 3.7 稳定版中,我们将本地图像的加载从平台线程转移到 Dart 线程,以避免延迟来自平台线程的 vsync 事件。但是[用户](https://github.com/flutter/flutter/issues/121525)注意到 Dart 线程上的这项额外工作也导致了一些卡顿。
在 3.10 中,**我们将本地图像的打开和解码从 Dart 线程移至[后台线程](https://github.com/flutter/engine/pull/39918)**,这个更改消除了具有大量本地图像的屏幕上潜在的长时间停顿,同时避免了延迟 vsync 事件,在我们的本地测试和自动化基准测试中,这个更改将多个同步图像的加载时间缩短了一半。
![](http://img.cdn.guoshuyu.cn/20230511_F3/image16.png)
我们继续在 Flutter 新的内部 DisplayList 结构之上构建优化,在 3.10 中,我们添加了 [R-Tree based culling](https://github.com/flutter/engine/pull/38429) 机制。
这种机制在我们的渲染器中更早地移除了绘制操作的处理。[例如](https://github.com/flutter/flutter/issues/92366) 优化加速了输出在屏幕外失败的自定义painter。
我们的 [microbenchmarks](https://flutter-engine-perf.skia.org/e/?begin=1671661938&end=1671754421&keys=X789f7ff76f30f8ccc672464f335fe09b&num_commits=50&request_type=1&xbaroffset=31974) 显示 DisplayList 处理时间最多减少了 50%,具有裁剪自定义绘画的 App 可能会看到不同效果的改进,改进的程度取决于隐藏绘制操作的复杂性和数量。
## 减少 iOS 启动延迟
之前应用中标识符查找的[低效策略](https://github.com/flutter/flutter/issues/37826)增加了应用启动延迟,这个启动延迟的增长与应用的大小成正比。
而在 3.10 中,[我们修复了 bundle identifier lookup](https://github.com/flutter/engine/pull/39975),这将大型应用的启动延迟减少了 100 毫秒或大约 3050%。
## 缩小尺寸
Flutter 使用 `SkParagraph` 作为文本、布局和渲染的默认库,之前我们包括了一个标志以支持回退到遗留 `libtxt``minikin`
由于我们对 `SkParagraph` 有充分的信心,[我们](https://github.com/flutter/engine/pull/39499)在 3.10 中删除了 `libtxt``minikin` 以及它们的标志,这将 Flutter 的压缩大小减少了 30KB。
> 看起来信心十足了。
## 稳定性
在 3.0 版本中,我们在渲染管道后期启用了一项 Android 功能,该功能使用高级 GPU 驱动当只有一个“dirty” 区域发生变化时,这些驱动功能会重新绘制较少的屏幕内容。
我们之前已经将它添加到早期的优化中以达到类似的效果,尽管我们的基准测试结果不错,但还是出现了两个问题:
- 首先,改进最多的基准可能不代表实际用例。
- 其次,[事实证明很难找到](https://github.com/flutter/engine/pull/37493)支持此 GPU 驱动功能的设备和 Android 版本集
鉴于有限的进步和支持,我们在 Android 上[禁用了](https://github.com/flutter/engine/pull/40898)部分重绘功能。
而使用 Skia 后端时,该功能在 iOS 上依然保持启用状态,我们希望在未来的版本中可以[通过 Impeller 启用它。](https://github.com/flutter/flutter/issues/124526)
# API 改进
## APNG解码器
Flutter 3.10 [解决了一个我们最受关注的问题](https://github.com/flutter/flutter/issues/37247),它增加了 `APNG` 解码图像的[能力](https://github.com/flutter/engine/pull/31098),现在可以使用 Flutter 现有的图片加载 API 来加载 `APNG` 图片。
## 图片加载 API 改进
3.10 添加了一个[新方法](https://master-api.flutter.dev/flutter/dart-ui/instantiateImageCodecWithSize.html) `instantiateImageCodecWithSize`,该方法满足以下三个条件的[用例支持:](https://github.com/flutter/flutter/issues/118543)
- 加载时宽高比未知
- 边界框约束
- 原始纵横比约束
# Mobile
## iOS
### 无线调试
**现在可以在无线的情况下运行和热重新加载的 Flutter iOS 应用**。
在 Xcode 中成功无线配对 iOS 设备后,就可以使用 flutter run 将应用部署到该设备,**如果遇到问题,请在 Window > Devices** 和 **Simulators > Devices**下验证网络图标是否出现在设备旁边。
> 要了解更多信息,可以查阅 https://docs.flutter.dev/get-started/install/macos#ios-setup。
### 宽色域图像支持
iOS 上的 Flutter 应用现在可以支持宽色域图像的精确渲染,要使用宽色域支持,应用必须使用 Impeller 并在 `Info.plist` 文件添加 `FLTEnableWideGamut ` 标志。
### 拼写检查支持
`SpellCheckConfiguration()` 控件现在默认支持 [Apple](https://developer.apple.com/documentation/uikit/uitextchecker) 在 iOS 上的拼写检查服务,可以使用 `spellCheckConfiguration` 中的参数对其进行设置 `CupertinoTextField`
![](http://img.cdn.guoshuyu.cn/20230511_F3/image17.gif)
### 自适应复选框和单选
3.10 将 `CupertinoCheckBox``CupertinoRadio` 添加到库中 `Cupertino` ,他们创建符合 Apple 样式的复选框和单选按钮组件。
Material 复选框和单选控件添加了 `.adaptive` 构造函数,在 iOS 和 macOS 上,这些构造函数使用相应的 Cupertino 控件,在其他平台上使用 Material 控件。
### 优化 Cupertino 动画、过渡和颜色
Flutter 3.10 改进了一些动画、过渡和颜色以匹配 SwiftUI这些改进包括
- [更新](https://github.com/flutter/flutter/pull/122275) `CupertinoPageRoute`
- [添加](https://github.com/flutter/flutter/pull/110127)标题放大动画 `CupertinoSliverNavigationBar`
- 添加几种[新的 iOS 系统颜色 ](https://github.com/flutter/flutter/pull/118971)`CupertinoColors`
![](http://img.cdn.guoshuyu.cn/20230511_F3/image18.gif)
### PlatformView 性能
`PlatformViews `出现在屏幕上时Flutter会限制 iOS 上的[刷新率以减少卡顿](https://github.com/flutter/engine/pull/39172),当应用显示动画或可滚动时,用户可能会在应用出现 `PlatformViews` 时注意到这一点。
### macOS 和 iOS 可以在插件中使用共享代码
Flutter 现在支持插件 `pubspec.yaml` 文件中的 `sharedDarwinSource` ,这个 key 表示 Flutter 应该共享 iOS 和 macOS 代码。
```
ios:
pluginClass: PathProviderPlugin
dartPluginClass: PathProviderFoundation
sharedDarwinSource: true
macos:
pluginClass: PathProviderPlugin
dartPluginClass: PathProviderFoundation
sharedDarwinSource: true
```
### 应用扩展的新资源
我们为 Flutter 开发人员添加了使用 iOS 应用扩展文档,这些扩展包括实时活动、主屏幕控件和共享扩展。
为了简化创建主屏幕控件和共享数据,我们向 `path_provider``homescreen_widget` 插件添加了新方法。
> 具体可见https://docs.flutter.dev/development/platform-integration/ios/app-extensions
### 跨平台设计的新资源
该文档现在包括针对特定 [UI 组件](https://docs.flutter.dev/resources/platform-adaptations#ui-components)的跨平台设计注意事项,要了解有关这些 UI 组件的更多信息请查看Flutter UX GitHub 存储库中的讨论: https://github.com/flutter/uxr/discussions
> 具体可见https://docs.flutter.dev/resources/platform-adaptations#ui-components
## Android
### Android CameraX 支持
[Camera X ](https://developer.android.com/training/camerax)是一个 Jetpack 库,可简化向 Android 应用添加丰富的相机功能。
该功能适用于多种 Android 相机硬件,在 3.10 中,我们为 Flutter Camera 插件添加了对 CameraX 的初步支持,此支持涵盖以下用例:
- 图像捕捉
- 视频录制
- 显示实时相机预览
```
Dependencies:
camera: ^0.10.4 # 最新相机版本
camera_android_camerax: ^0.5.0
```
# 开发者工具
我们继续改进了 DevTools这是一套用于 Dart 和 Flutter 的性能和调试工具,一些亮点包括:
- DevTools UI 使用 Material 3这让外观现代化又增强了可访问性。
- DevTools 控制台支持在调试模式下评估正在运行的应用,在 3.10 之前,只能在暂停应用时执行此操作。
- 嵌入式 [Perfetto 跟踪查看器](https://perfetto.dev/)取代了以前的时间线跟踪查看器。
Perfetto 可以处理更大的数据集,并且比传统的跟踪查看器表现得更好,例如:
- 允许固定感兴趣的线程
- 单击并拖动以从多个帧中选择多个时间轴事件
- 使用 SQL 查询从时间轴事件中提取特定数据
![](http://img.cdn.guoshuyu.cn/20230511_F3/image19.png)
# 弃用和重大更改
## 弃用的 API
3.10 中的重大更改包括在 v3.7 发布后过期的弃用 API。
要查看所有受影响的 API 以及其他上下文和迁移指南,请查看[之前版本的弃用指南](https://docs.flutter.dev/release/breaking-changes/3-7-deprecations)。
> [Dart Fix ](https://docs.flutter.dev/development/tools/flutter-fix)可以修复其中的许多问题,包括在 IDE 中快速修复和使用`dart fix`命令批量应用。
## Android Studio Flamingo 升级
将 Android Studio 升级到 Flamingo 后,你可能会在尝试 `flutter run``flutter build` Flutter Android 应用时看到错误。
[发生此错误是因为 Android Studio Flamingo 将其捆绑的 Java SDK 从 11 更新到 17](https://docs.gradle.org/current/userguide/compatibility.html#java)使用 Java 17 时,之前的 7.3 Gradle 版本无法运行。
我们[更新](https://github.com/flutter/flutter/pull/123916)来了 `flutter analyze --suggestions` 以验证是否由于 Java SDK 和 Gradle 版本之间的不兼容而发生此错误。
> 要了解修复此错误的不同方法请查看我们的迁移指南https://docs.flutter.dev/go/android-java-gradle-error。
## Window singleton 弃用
改版本弃用了 Window singleton依赖它的应用和库需要开始[迁移](https://docs.flutter.dev/release/breaking-changes/window-singleton)。
当你的应用在未来版本的 Flutter 中做支持时,这会可以为你的应用提前做好多窗口准备支持。
> PS还可以关注下本次 I/O 基于 Flutter 发布的新小游戏:[I/O FLIP 小游戏](https://juejin.cn/post/7231378331139997757)

120
Flutter-310Win.md Normal file
View File

@ -0,0 +1,120 @@
# Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher
Flutter 3.10 发布之后,大家可能注意到,在它的 [release note](https://juejin.cn/post/7231565908631633979#heading-46) 里提了一句: **Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现**
> 所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。
如下图所示,如果具体到对应的 API 场景,主要就是涉及 `WidgetsBinding.instance.window``MediaQueryData.fromWindow` 等接口的适配,因为 `WidgetsBinding.instance.window` 即将被弃用。
![](http://img.cdn.guoshuyu.cn/20230517_310/image1.png)
> 你可以不适配,还能跑,只是升级的技术债务往后累计而已。
那首先可能就有一个疑问,为什么会有需要直接使用 `WidgetsBinding.instance.window` 的使用场景?简单来说,具体可以总结为:
- 没有 `BuildContext` ,不想引入 `BuildContext`
- 不希望获取到的 ` MediaQueryData` 受到所在 `BuildContext` 的影响,例如键盘弹起导致 padding 变化重构和受到 `Scaffold` 下的参数影响等
> 这部分详细可见:[《MediaQuery 和 build 优化你不知道的秘密》 ](https://juejin.cn/post/7114098725600903175)。
那么从 3.10 开始,针对 `WidgetsBinding.instance.window` 可以通过新的 API 方式进行兼容:
- 如果存在 `BuildContex` 可以通过 `View.of` 获取 `FlutterView`,这是官方最推荐的替代方式
- 如果没有 `BuildContex` 可以通过 `PlatformDispatcher``views` 对象去获取
这里注意到没有,现在用的是 `View.of` ,获取的是 `FlutterView` ,对象都称呼为 View 而不是 「Window」对应的 `MediaQueryData.fromWindow` API 也被弃用,修改为 `MediaQueryData.fromView` ,这个修改的依据在于:
> 起初 Flutter 假定了它只支持一个 Window 的场景,所以会有 `SingletonFlutterWindow` 这样的 instance window 对象存在,同时 `window` 属性又提供了许多和窗口本身无关的功能,在多窗口逻辑下会显得很另类。
那么接下来就让我们用「长篇大论」来简单介绍下这两个场景的特别之处。
# 存在 BuildContext
回归到本次的调整,首先是存在 BuildContext 的场景,如下代码所示,对于存在 `BuildContex` 的场景, `View.of` 相关的调整为:
```dart
/// 3.10 之前
double dpr = WidgetsBinding.instance.window.devicePixelRatio;
Locale locale = WidgetsBinding.instance.window.locale;
double width =
MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width;
/// 3.10 之后
double dpr = View.of(context).devicePixelRatio;
Locale locale = View.of(context).platformDispatcher.locale;
double width =
MediaQueryData.fromView(View.of(context)).size.width;
```
可以看到,这里的 `View` 内部实现肯定是有一个 `InheritedWidget` ,它将 `FlutterView` 通过 `BuildContext` 往下共享,从而提供类似 「window」 的参数能力,而通过 `View.of` 获取的参数:
- **当 `FlutterView` 本身的属性值发生变化时,是不会通知绑定的 `context` 更新,这个行为类似于之前的 ` WidgetsBinding.instance.window`**
- 只有当 `FlutterView` 本身发生变化时,比如 `context` 绘制到不同的 `FlutterView` 时,才会触发对应绑定的 `context` 更新
可以看到 `View.of` 这个行为考虑的是「多 `FlutterView`」 下的更新场景,如果是需要绑定到具体对应参数的变动更新,如 `size` 等,还是要通过以前的 `MediaQuery.of` / `MediaQuery.maybeOf` 来实现。
而对于 `View` 来说,**每个 `FlutterView` 都必须是独立且唯一的**,在一个 Widget Tree 里,一个 `FlutterView` 只能和一个 `View` 相关联,这个主要体现在 `FlutterView` 标识 `GlobalObjectKey` 的实现上。
![](http://img.cdn.guoshuyu.cn/20230517_310/image2.png)
简单总结一下:**在存在 `BuildContex` 的场景,可以简单将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,不用担心绑定了 `context` 导致重构,因为 `View.of` 只对 `FlutterView` 切换的场景生效**。
# 不存在 BuildContext
对于不存在或者不方便使用 `BuildContext` 的场景,官方提供了 `PlatformDispatcher.views` API 来进行支持,不过因为 `get views` 对应的是 `Map``values` ,它是一个 `Iterable` 对象,**那么对于 3.10 我们需要如何使用 `PlatformDispatcher.views` 来适配没有 `BuildContext``WidgetsBinding.instance.window` 场面**
![](http://img.cdn.guoshuyu.cn/20230517_310/image3.png)
> `PlatformDispatcher` 内部的` views` 维护了中所有可用 `FlutterView` 的列表,用于提供在没有 `BuildContext` 的情况下访问视图的支持。
你说什么情况下会有没有 `BuildContext` ?比如 Flutter 里 的 `runApp` 如下图所示3.10 目前在 `runApp` 时会通过 `platformDispatcher.implicitView` 来塞进去一个默认的 `FlutterView`
![](http://img.cdn.guoshuyu.cn/20230517_310/image4.png)
`implicitView` 又是什么?其实 `implicitView` 就是 `PlatformDispatcher._views` 里 id 为 0 的 `FlutterView` ,默认也是 `views` 这个 `Iterable` 里的 `first` 对象。
![](http://img.cdn.guoshuyu.cn/20230517_310/image5.png)
也就是在没有 `BuildContext` 的场景, 可以通过 `platformDispatcher.views.first` 的实现迁移对应的 `instance.window` 实现。
```dart
/// 3.10 之前
MediaQueryData.fromWindow(WidgetsBinding.instance.window)
/// 3.10 之后
MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)
```
为什么不直接使用 `implicitView` 对象? 因为 `implicitView` 目前是一个过渡性方案,官方希望在多视图的场景下不应该始终存在 implicit view 的概念,而是应用自己应该主动请求创建一个窗口,去提供一个视图进行绘制。
![](http://img.cdn.guoshuyu.cn/20230517_310/image6.png)
所以对于 `implicitView` 目前官方提供了 `_implicitViewEnabled` 函数,后续可以通过可配置位来控制引擎是否支持 `implicitView` ,也就是 **`implicitView` 在后续更新随时可能为 null ,这也是我们不应该在外部去使用它的理由**,同时它是在 `runApp` 时配置的,所以它在应用启动运行后永远不会改变,如果它在启动时为空,则它永远都会是 null。
> `PlatformDispatcher.instance.views[0]` 在之前的单视图场景中,无论是否有窗口存在,类似的 `implicitView` 会始终存在;而在多窗口场景下,`PlatformDispatcher.instance.views` 将会跟随窗口变化。
另外我们是通过 `WidgetsBinding.instance.platformDispatcher.views` 去访问 `views` ,而不是直接通过 `PlatformDispatcher.instance.views` ,因为通常官方更建议在 Binding 的依赖关系下去访问 `PlatformDispatcher`
> 除了需要在 `runApp()` 或者 `ensureInitialized()` 之前访问 PlatformDispatcher 的场景。
另外,如下图所示,通过 Engine 里对于 window 部分代码的实现,可以看到我们所需的默认` FlutterView` 是 id 为 0 的相关依据,所以这也是我们通过 `WidgetsBinding.instance.platformDispatcher.views` 去兼容支持的逻辑所在。
| ![](http://img.cdn.guoshuyu.cn/20230517_310/image7.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image9.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image10.png) |
| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------- |
# 最后
最后总结一下,说了那么多,其实不外乎就是将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,如果还有一些骚操作场景,可以使用 `WidgetsBinding.instance.platformDispatcher.views` ,如果不怕后续又坑,甚至可以直接使用 `WidgetsBinding.instance.platformDispatcher.implicitView`
整体上解释那么多,**主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解**,相信下一个版本多窗口应该就可以和大家见面了。
更多讨论可见:
- https://github.com/flutter/flutter/issues/120306
- https://github.com/flutter/engine/pull/39553
- https://github.com/flutter/flutter/issues/116929
- https://github.com/flutter/flutter/issues/99500
- https://github.com/flutter/engine/pull/39788

122
Flutter-IOW.md Normal file
View File

@ -0,0 +1,122 @@
# Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC
随着 [Flutter 3.10 发布](https://juejin.cn/post/7231565908631633979)Flutter Web 也引来了它最具有「里程碑」意义的更新,**这里的「里程碑」不是说这次 Flutter Web 有多么重大的更新,而是 Flutter 官方对于 Web 终于有了明确的定位和方向**。
# 提升
首先我们简单聊提升,这不是本篇的重点,只是顺带。
本次提升主要在于两个大点:**Element 嵌入支持和 fragment shaders 支持 **
首先是 Element 嵌入,**Flutter 3.10 开始,现在可以将 Flutter Web嵌入到网页的任何 HTML 元素中,并带有 `flutter.js` 引擎和 `hostElement` 初始化参数**。
简单来说就是不需要 `iframe` 了,如下代码所示,只需要通过 `initializeEngine ``hostElement` 参数就可以指定嵌入的元素,**灵活度支持得到了提高**。
```html
<html>
<head>
<!-- ... -->
<script src="flutter.js" defer></script>
</head>
<body>
<!-- Ensure your flutter target is present on the page... -->
<div id="flutter_host">Loading...</div>
<script>
window.addEventListener("load", function (ev) {
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: async function(engineInitializer) {
let appRunner = await engineInitializer.initializeEngine({
// Pass a reference to "div#flutter_host" into the Flutter engine.
hostElement: document.querySelector("#flutter_host")
});
await appRunner.runApp();
}
});
});
</script>
</body>
</html>
```
> PS :如果你的项目是在 Flutter 2.10 或更早版本中创建的,要先从目录中删除 `/web` 文件 ,然后通过 `flutter create . --platforms=web` 重新创建模版。
fragment shaders 部分一般情况下大家可能并不会用到shaders 就是以 `.frag` 扩展名出现的 GLSL 文件,在 Flutter 里是在 `pubspec.yaml` 文件下的 `shaders` 中声明,现在它支持 Web 了:
```yaml
flutter:
shaders:
- shaders/myshader.frag
```
> 一般运行时会把 frag 文件加载到 `FragmentProgram ` 对象中,通过 program 可以获取到对应的 `shader `,然后通过 `Paint.shader` 进行使用绘制, 当然 Flutter 里 shaders 文件是存在限制的,比如不支持 UBO 和 SSBO 等。
**当然,这里不是讲解 shaders 而是宣告一下Flutter Web 支持 shaders 了**。
# 未来
**其实未来才是本篇的重点**,我们知道 Flutter 在 Web 领域的支持上一直在「妥协」Flutter Web 在整个 Flutter 体系下一直处于比较特殊的位置,因为它一直存在两种渲染方式:[html 和 canvaskit](https://juejin.cn/post/7095294020900880420)。
简单说 html 就是转化为 [JS + Html Element](https://juejin.cn/post/7095294020900880420) 渲染,而 canvaskit 是采用 [Skia + WebAssembly](https://skia.org/docs/user/modules/canvaskit/) 的方式,**而 html 的模式让 Web 在 Flutter 中显得「格格不入」,路径依赖和维护成本也一直是 Flutter Web 的头痛问题**。
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image1.png)
面对这个困境,官方在年初的 [Flutter Forword](https://juejin.cn/post/7192646390948823098) 大会上提出重新规划 Flutter Web 的未来,而随着 Flutter 3.10 的发布,官方终于对于 Web 的未来有了明确的定位:
> **“Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架。”**
Flutter 团队表示,**Flutter Web 的定位不是设计为通用 Web 的框架**,类似的 Web 框架现在有很多,比如 Angular 和 React 等在这个领域表现就很出色,而 Flutter 应该是围绕 CanvasKit 和 [WebAssembly](https://webassembly.org/) 等新技术进行架构设计的框架。
所以 Flutter Web 未来的路线更多会是 CanvasKit ,也就是 WebAssembly + Skia ,同时在这个领域 Dart 也在持续深耕:**从 Dart 3 开始,对于 Web 的支持将逐步演进为 WebAssembly 的 Dart native 的定位**。
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image2.png)
什么是 WebAssembly 的 dart native ?一直以来 Flutter 对于 WebAssembly 的支持都是:使用 Wasm 来处理CanvasKit 的 runtime而 Dart 代码会被编译为 JS而这对于 Dart 团队来时,其实是一个「妥协」的过渡期。
而随着官方与 WebAssembly 生态系统中的多个团队的深入合作,**Dart 已经开始支持直接编译为原生的 wasm 代码,一个叫 [WasmGC]((https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md)) 的垃圾收集实现被引入标准**,该扩展实现目前在基于 Chromium 的浏览器和 Firefox 浏览器中在趋向稳定。
> 目前在基准测试中,执行速度提高了 3 倍
要将 Dart 和 Flutter 编译成 Wasm你需要一个支持 [WasmGC ](https://github.com/WebAssembly/gc/tree/main/proposals/gc) 的浏览器,目前 [Chromium V8](https://chromestatus.com/feature/6062715726462976) 和 Firefox 团队的浏览器都在进行支持,比如 Chromium 下:
> 通过结构和数组类型为 WebAssembly 增加了对高级语言的有效支持,以 Wasm 为 target 的语言编译器能够与主机 VM 中的垃圾收集器集成。在 Chrome 中启用该功能意味着启用类型化函数引用,它会将函数引用存储在上述结构和数组中。
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image3.png)
现在在 Flutter master 分支下就可以提前尝试 wasm 的支持,运行 `flutter build web --help` 如果出现下图所示场, 说明支持 wasm 编译。
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image4.png)
之后执行 `flutter build web --wasm` 就可以编译一个带有 native dart wasm 的 web 包,命令执行后,会将产物输出到 `build/web_wasm` 目录下。
之后你可以使用 pub 上的 [`dhttpd`](https://pub.dev/packages/dhttpd) 包在 `build/web_wasm`目录下执行本地服务,然后在浏览器预览效果。
```
> cd build/web_wasm
> dhttpd
Server started on port 8080
```
目前需要版本 112 或更高版本的 Chromium 才能支持,同时需要启动对应的 Chrome 标识位:
- `enable-experimental-webassembly-stack-switching`
- `enable-webassembly-garbage-collection`
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image5.png)
当然,目前阶段还存在一些限制,例如:
> Dart Wasm 编译器利用了 [ JavaScript-Promise Integration (JSPI) ](https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md)特性Firefox 不支持 JSPI 提议,所以一旦 Dart 从 JSPI 迁移出来Firefox 应启用适当的标志位才能运行。
另外还需要 JS-interop 支持,因为为了支持 WasmDart 改变了它针对浏览器和 JavaScript 的 API 支持方式, 这种转变是为了防止把 `dart:html ``package:js` 编译为 Wasm 的 Dart 代码,大多数特定于平台的包如 url_launcher 会使用这些库。
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image6.png)
最后,**目前 DevTools 还不支持 `flutter run` 去运行和调试 Wasm**。
# 最后
很高兴能看到 Flutter 团队最终去定了 Web 的未来路线,这让 Web 的未来更加明朗,当然,正如前面所说的,**Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架**。
**所以 Flutter Web不是为了设计为通用 Web 的框架去 Angular 和 React 等竞争,它是让你在使用 Flutter 的时候,可以将能力很好地释放到 Web 领域**,而 CanvasKit 带来的一致性更符合 Flutter Web 的定位,当然,解决加载时长问题会是任重道远的需求。

347
Flutter-N21.md Normal file
View File

@ -0,0 +1,347 @@
# Flutter 小技巧之霓虹灯文本的「故障」效果的实现
如下图所示,最近通过群友的问题在 [codepen.io ](https://codepen.io/mattgrosswork/pen/VwprebG) 上看到了一个文本「抽动」的动画实现,看起来就像是生活中常见的「霓虹灯招牌」故障时的「抽动」效果,而本篇的目标通过「抄袭」这个实现,帮助大家理解 Flutter 里的一些实现小技巧。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image1.gif)
这个效果在 codepen 上是通过 CSS 实现的,实现思路 codepen 上的 [Glitch Walkthrough](https://codepen.io/mattgrosswork/pen/VwprebG) 大致有提示,但是 Flutter 没有强大的 CSS那么如何将它「复刻」到 Flutter 上就是本篇的核心要点。
> 不得不说 CSS 很强大,要在 Flutter 上实现类似的效果还是比较「折腾」。
而要在 Flutter 上实现类似 Glitch Walkthrough 的效果,大致上我们需要处理:
- 类似霓虹灯效果的文本
- 文本内容撕裂的效果
- 文本变形闪动的效果
那么接下来我们就按照这个流程来实现一个 Flutter 上的 Glitch Walkthrough 。
# 霓虹灯文本
这一步其实相对简单Flutter 的 `TextStyle` 提供了 `shadows` 配置,通过它可以快速实现一个「会发光」的文本。
我们这里通过两个 `Shadow` 来实现「发光」的视觉效果,核心就是利用 `Shadow``blurRadius` 来让背景出现一定程度的模糊发散,然后两个 `Shadow` 形成不一样的颜色深度和发散效果,从而达到看起来「发亮」的效果。
> 如下图是没有填充文本颜色时 `Shadow` 的效果。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image2.png)
最后,如下代码所示,我们只需要通过 `foreground` 给文本补充下颜色,就可以看到如下图所示的类似「霓虹灯」效果的文本。
> 当然这里你不想用 `foreground` ,只用简单的 `color` 也可以。
```dart
Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 5
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
)
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image3.png)
这里提个题外话,其实类似的思路用在图片上也可以实现「发光」的效果,如下代码所示,通过 Stack 嵌套两个 `Image` ,然后中间通过 `BackdropFilter``ImageFilter` 做一层模糊,让底下的图片模糊后发散产生类似「发光」的效果。
```dart
var child = Image.asset(
'static/test_logo.png',
width: 250,
);
return Stack(
children: [
child,
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurRadius,
sigmaY: blurRadius,
),
child: Container(color: Colors.transparent),
),
),
child,
],
)
);
```
如下图所示,图片最终可以通过自己的色彩产生类似「发光」的效果,当然这部分只是额外的拓展内容,和我们要实现的效果无关。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image4.png)
# 文本撕裂
这部分可以说是需求效果的核心,这里我们需要用到 `ClipPath``Polygon` ,通过 `Polygon` 来实现随机的多边形路径,然后利用 `ClipPath` 对文本内容进行随机的路径裁剪。
虽然说用 `Polygon` 但是 Flutter 官方并没有直接提供类似前端 CSS 的 `Polygon` 多边形 API 支持,但是社区总有「好心人」,我们可以直接使用 Flutter 上类似的第三方库: `polygon: ^0.1.0`
>简单说 `Polygon` 就是按照 step 对 `Path``moveTo``quadraticBezierTo` 等 API 进行了封装。
Flutter 上的 `Polygon` 取值范围是 -1 1 ,也就是按照比例决定位置,比如 - 1 就是起始点, 1 就是最大宽高, 更具体如下面的代码所示,这里利用 `Polygon` 添加了三个点,最终这三个点形成的 Path 会绘制出一个三角形。
```dart
List<Offset> generatePoint() {
List<Offset> points = [];
points.add(Offset(-1, -1));
points.add(Offset(-1, 0));
points.add(Offset(0, -1));
return points;
}
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image5.png)
如下代码所示,那如果如果 point 的数量多了,就可以形成一系列不规则的形状,比如下面代码随机添加了 60 个点的位置,可以看到此时屏幕上的白色 `Container` 被裁剪成「凌乱」的形状。
```dart
List<Offset> generatePoint() {
List<Offset> points = [];
points.add(Offset(-1.00, -0.76));
points.add(Offset(0.06, -0.76));
points.add(Offset(0.06, -0.48));
points.add(Offset(-0.50, -0.48));
points.add(Offset(-0.50, 0.72));
points.add(Offset(-0.38, 0.72));
points.add(Offset(-0.38, -1.00));
points.add(Offset(0.06, -1.00));
points.add(Offset(0.06, 0.67));
points.add(Offset(0.84, 0.67));
points.add(Offset(0.84, 0.63));
points.add(Offset(0.39, 0.63));
points.add(Offset(0.39, -0.42));
points.add(Offset(0.56, -0.42));
points.add(Offset(0.56, 0.30));
points.add(Offset(0.37, 0.30));
points.add(Offset(0.37, 0.32));
points.add(Offset(0.54, 0.32));
points.add(Offset(0.54, -0.09));
points.add(Offset(0.70, -0.09));
points.add(Offset(0.70, -0.48));
points.add(Offset(0.94, -0.48));
points.add(Offset(0.94, -0.43));
points.add(Offset(0.67, -0.43));
points.add(Offset(0.67, -0.31));
points.add(Offset(0.08, -0.31));
points.add(Offset(0.08, 0.78));
points.add(Offset(-0.40, 0.78));
points.add(Offset(-0.40, 0.15));
points.add(Offset(0.65, 0.15));
points.add(Offset(0.65, 0.00));
points.add(Offset(0.36, 0.00));
points.add(Offset(0.36, -0.28));
points.add(Offset(0.24, -0.28));
points.add(Offset(0.24, -0.80));
points.add(Offset(-0.76, -0.80));
points.add(Offset(-0.76, -0.31));
points.add(Offset(0.19, -0.31));
points.add(Offset(0.19, 0.13));
points.add(Offset(0.96, 0.13));
points.add(Offset(0.96, 0.65));
points.add(Offset(-0.80, 0.65));
points.add(Offset(-0.80, 0.06));
points.add(Offset(0.82, 0.06));
points.add(Offset(0.82, 0.67));
points.add(Offset(0.60, 0.67));
points.add(Offset(0.60, 0.65));
points.add(Offset(-0.19, 0.65));
return points;
}
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image6.png)
如果这时候把白色 `Container` 换成文本内容,那么我们就可以如下图所示的效果,看起来像不像一帧状态下文本的「错乱」效果?后面我们只需要每次生成一帧这样的 Path ,就可以实现文本动态「撕裂」的需求。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image7.png)
> 我们只需要把这个实现做成随机输出,然后每次生成一个 `Path` 就可以了。
如下代码所示,我们通过 `generatePoint` 方法,每次随机生成 60 个点,然后将这些点通过 `computePath` 转化为 Path然后继承 `CustomClipper` 配置到 `getClip` 方法里,在需要的时候(`tear` )对 child 按 Path 进行裁剪。
> 注意这里的 `i % 2` ,为的是让上次的 x 或者 y 可以是同一个位置,在连接上能连续。
```dart
class RandomTearingClipper extends CustomClipper<Path> {
bool tear;
RandomTearingClipper(this.tear);
List<Offset> generatePoint() {
List<Offset> points = [];
var x = -1.0;
var y = -1.0;
for (var i = 0; i < 60; i++) {
if (i % 2 != 0) {
x = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
} else {
y = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
}
points.add(Offset(x, y));
}
return points;
}
@override
Path getClip(Size size) {
var points = generatePoint();
var polygon = Polygon(points);
if (tear)
return polygon.computePath(rect: Offset.zero & size);
else
return Path()..addRect(Offset.zero & size);
}
@override
bool shouldReclip(RandomTearingClipper oldClipper) => true;
}
```
接着,我们只需要设置一个定期器,然后将前面的「霓虹灯文本」和「故障裁剪效果」配置到 `ClipPath` 上,如下图所示,我们就可以看到文本的随机撕裂效果。
```dart
timer = Timer.periodic(Duration(milliseconds: 400), (timer) {
tearFunction();
});
return ClipPath(
child: Center(
child: Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
),
),
clipper: RandomTearingClipper(tear),
);
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image8.gif)
> 此时看起来还不够形象。
# 变形闪动
为了达到我们预期的效果,最后我们还需要做一些特殊处理,比如再实现两个形状、颜色和位置不一样「霓虹灯文本」,为的就是实现「变形和闪动」的效果替换。
比如如下代码所示,通过 `ShaderMask` 可以实现一个渐变效果的的文本,这是用来在闪动的时候,提供一个短暂替换和色彩加深的作用。
```dart
ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return LinearGradient(
colors: [Colors.blue, Colors.green, Colors.red],
stops: [0.0, 0.5, 1.0],
).createShader(bounds);
},
child:
```
![](http://img.cdn.guoshuyu.cn/20230322_N21/image9.png)
类似的我们还可以实现一个「变形」的文本,在之前的白色「霓虹灯」文本基础上增加「斜体」和「颜色变淡」等处理,用来闪动的时候提供「变形」的作用。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image10.png)
最后我们再将之前的 ` ClipPath`添加到它们上面,并增加一个 `transform` 实现文本四周随意移动的效果支持,如下图所示,此时的效果已经肉眼可见的接近我们的需求。
```dart
transform:
Matrix4.translationValues(randomPosition(4), randomPosition(4), 0),
double randomPosition(position) {
return Random().nextInt(position).toDouble() *
(Random().nextBool() ? -1 : 1);
}
```
| ![](http://img.cdn.guoshuyu.cn/20230322_N21/image11.gif) | ![](http://img.cdn.guoshuyu.cn/20230322_N21/image12.gif) |
| -------------------------------------------------------- | -------------------------------------------------------- |
最后我们将这几个文本效果用 `Stack` 组合起来,然后再在定时器里不停去切换「故障」和「正常」的文本状态,并且随机选择展示不同的 「故障」状态。
```dart
timer = Timer.periodic(Duration(milliseconds: 400), (timer) {
tearFunction();
});
timer2 = Timer.periodic(Duration(milliseconds: 600), (timer) {
tearFunction();
});
tearFunction() {
count++;
tear = count % 2 == 0;
if (tear == true) {
setState(() {});
Future.delayed(Duration(milliseconds: 150), () {
setState(() {
tear = false;
});
});
}
}
@override
Widget build(BuildContext context) {
var status = Random().nextInt(3);
return Stack(
children: [
if (tear && (status == 1)) renderTearText1(RandomTearingClipper(tear)),
if (!tear || (tear && status != 2))
renderMainText(RandomTearingClipper(tear)),
if (tear && status == 2) renderTearText2(RandomTearingClipper(tear)),
],
);
}
```
最终效果如下图所示,这里还额外对后面两个文本做了一个 `ClipRect` 处理,闪动切换的时候只展示部分内容,这样在「故障」时的切换不会显得太过生硬,可以看到简单的 CSS 效果在 Flutter 上的实现成本其实并不低。
![](http://img.cdn.guoshuyu.cn/20230322_N21/image13.gif)
当然,这里的实现没考虑性能问题,所以代码也比较糙,不过这里主要是为了展示了 `ClipPath ``Shadow` 的使用技巧,相信通过这个例子,可以帮助大家更好地发掘 Flutter 里对于路径绘制和阴影的使用场景,这才是本篇的主要目的。
那么本篇小技巧到这里就结束了,如果你还有什么想说的,欢迎留言评论。
> 完整代码可见https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/tear_text_demo_page.dart

860
Flutter-N24.md Normal file
View File

@ -0,0 +1,860 @@
# Flutter 小技巧之横竖列表的自适应大小布局支持
今天这个主题看着是不是有点抽象?又是列表嵌套?之前不是分享过[《 ListView 和 PageView 的各种花式嵌套》](https://juejin.cn/post/7116267156655833102)了么?那这次的自适应大小布局支持有什么不同?
> 算是某些奇特的场景下才会需要。
首先我们看下面这段代码,基本逻辑就是:我们希望 `vertical``ListView` 里每个 Item 都是根据内容自适应大小,并且 Item 会存在有 `horizontal``ListView` 这样的 child。
`horizontal``ListView` 我们也希望它能够根据自己的 `children` 去自适应大小。**那么你觉得这段代码有什么问题?它能正常运行吗?**
```dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
ListView(
scrollDirection: Axis.horizontal,
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "TEST\n")
.toString()),
),
);
}),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
);
}
```
答案是不能,因为这段代码里 `vertical``ListView` 嵌套了 `horizontal``ListView` ,而横向的 `ListView` 并没有指定高度,并且垂直方向的 `ListView` 也没有指定 `itemExtent` ,所以我们会得到如下图所示的错误:
![](http://img.cdn.guoshuyu.cn/20230425_N24/image1.png)
为什么会有这样的问题,简单说一下,我们都知道 Flutter 是从上往下传递约束,从上往上返回 `Size` 的一个布局过程,也就是需要 child 通过通过 parent 的约束来决定自己的大小,然后 parent 根据 child 返回的 `Size` 决定自己的尺寸。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image2.png)
> 对这部分感兴趣的可以看 [《带你了解不一样的 Flutter》](https://juejin.cn/post/7053777774707736613)
但是对于可滑动控件来说有点特殊,因为可滑动控件在其滑动方向的主轴上,理论是需要「无限大」的,所以对于可滑动控件来说,就需要有一个「窗口」的固定大小,也就是 `ViewPort` 这个「窗口」需要有一个主轴方向的大小。
比如 `ListView` ,一般情况下就是有一个 `ViewPort` ,然后内部的 `SliverList` 构建一个列表,然后通过手势在 `ViewPort` 「窗口」下相应产生移动,从而达到列表滑动的效果。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image3.png)
> 如果感兴趣可以看 [《不一样角度带你了解 Flutter 中的滑动列表实现》](https://juejin.cn/post/6956215495440007175)
那么我们再回到上面 `vertical``ListView` 嵌套 `horizontal``ListView ` 的问题:
- 因为垂直的 `ListView` 没有设置 `itemExtent` ,所以它的每个 child 不会有一个固定高度,因为我们的需求是每个 Item 根据自己的需要自适应高度。
- 横向的 `ListView` 没有设置明确高度,作为 parent 的垂直 `ListView` 高度理论又是「无限高」,所以横向的 `ListView` 无法计算得到一个有效的高度。
另外,由于 `ListView` 不像 `Row`/`Column `等控件,它拥有的 `children` 理论也是「无限」的,并且没有展示的部分一般是不会布局和绘制,所以不能像 `Row`/`Column ` 一样计算出所有控件的高度之后,来决定自身的高度。
那么破解的方式有哪些呢?目前情况下可以提供两种解决方式。
# SingleChildScrollView
如下代码所示,首先最简单的就是把横向的 `ListView` 替换成 `SingleChildScrollView` ,因为不同于 `ListView` `SingleChildScrollView` 只有一个 child ,所以它的 `ViewPort` 也比较特殊。
```dart
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "TEST\n")
.toString()),
),
);
}),
),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
)
```
`SingleChildScrollView``_RenderSingleChildViewport` 里,布局时可以很简单的通过 `child!.layout` 之后得到 child 的大小,然后配合 `Row` 就计算出所有 child 的综合高度,这样可以实现横向的列表效果。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image4.png)
运行之后结果入下图所示,可以看到此时在垂直的 `ListView `里,横向的 `SingleChildScrollView` 被正确渲染出来,但是此时出现「参差不齐」的高度布局。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image5.png)
如下代码所示,这时候我们只需要在 `Row` 嵌套一个 `IntrinsicHeight` ,就可以让其内部高度对齐,因为 `IntrinsicHeight` 在布局时会提前调用 child 的 `getMaxIntrinsicHeight` 获取 child 的高度,修改 parent 传递给 child 的约束信息。
```dart
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: IntrinsicHeight(
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
alignment: Alignment.bottomCenter,
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "TEST\n")
.toString()),
),
);
}),
),
),
),
```
运行效果如下所示,可以看到此时所有横向 Item 的高度都一致,但是这个解决方法也有两个比较致命的问题:
- `SingleChildScrollView` 里是通过 `Row` 计算的高度,也就是布局时会需要一次性计算所有 child ,如果列表太长就会产生性能损耗
- `IntrinsicHeight` 推算布局的过程会比较费时,可能会到 O虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image6.png)
# UnboundedListView
第二个解决思路就是基于 `ListView` 去自定义,前面我们不是说 `ListView` 不会像 `Row` 那样去统计 children 的大小么?那我们完全可以自定义一个 `UnboundedListView` 来统计。
> 这部分思路最早来自 Github https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c
首先我们基于 `ListView` 定义一个 `UnboundedListView ` ,通过 `mixin` 的方式` override` 对应的 `Viewport``Sliver` ,也就是:
- 把 `buildChildLayout` 里的 `SliverList` 替换成我们自定义的 `UnboundedSliverList`
- 把 `buildViewport` 里的 `Viewport` 替换成我们自定义的 `UnboundedViewport`
- 在 `buildSlivers` 里处理` Padding` 逻辑,把 `SliverPadding` 替换为自定义的 `UnboundedSliverPadding`
```dart
class UnboundedListView = ListView with UnboundedListViewMixin;
/// BoxScrollView 的基础上
mixin UnboundedListViewMixin on ListView {
@override
Widget buildChildLayout(BuildContext context) {
return UnboundedSliverList(delegate: childrenDelegate);
}
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
return UnboundedViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
);
}
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry? effectivePadding = padding;
if (padding == null) {
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver =
UnboundedSliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[sliver];
}
}
```
接下来首先是实现 `UnboundedViewport` ,一样的套路:
- 首先基于 `Viewport` 的基础上,通过 `createRenderObject``RenderViewPort` 修改为我们的 `UnboundedRenderViewport`
- 基于 `RenderViewport` 增加 `performLayout``layoutChildSequence` 的自定义逻辑,实际上就是增加一个 `unboundedSize` 参数,这个参数通过 child 的 `RenderSliver` 里去统计得到
```dart
class UnboundedViewport = Viewport with UnboundedViewportMixin;
mixin UnboundedViewportMixin on Viewport {
@override
RenderViewport createRenderObject(BuildContext context) {
return UnboundedRenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ??
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
}
}
class UnboundedRenderViewport = RenderViewport
with UnboundedRenderViewportMixin;
mixin UnboundedRenderViewportMixin on RenderViewport {
@override
bool get sizedByParent => false;
double _unboundedSize = double.infinity;
@override
void performLayout() {
BoxConstraints constraints = this.constraints;
if (axis == Axis.horizontal) {
_unboundedSize = constraints.maxHeight;
size = Size(constraints.maxWidth, 0);
} else {
_unboundedSize = constraints.maxWidth;
size = Size(0, constraints.maxHeight);
}
super.performLayout();
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
@override
double layoutChildSequence({
required RenderSliver? child,
required double scrollOffset,
required double overlap,
required double layoutOffset,
required double remainingPaintExtent,
required double mainAxisExtent,
required double crossAxisExtent,
required GrowthDirection growthDirection,
required RenderSliver? advance(RenderSliver child),
required double remainingCacheExtent,
required double cacheOrigin,
}) {
crossAxisExtent = _unboundedSize;
var firstChild = child;
final result = super.layoutChildSequence(
child: child,
scrollOffset: scrollOffset,
overlap: overlap,
layoutOffset: layoutOffset,
remainingPaintExtent: remainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: growthDirection,
advance: advance,
remainingCacheExtent: remainingCacheExtent,
cacheOrigin: cacheOrigin,
);
double unboundedSize = 0;
while (firstChild != null) {
if (firstChild.geometry is UnboundedSliverGeometry) {
final UnboundedSliverGeometry childGeometry =
firstChild.geometry as UnboundedSliverGeometry;
unboundedSize = math.max(unboundedSize, childGeometry.crossAxisSize);
}
firstChild = advance(firstChild);
}
if (axis == Axis.horizontal) {
size = Size(size.width, unboundedSize);
} else {
size = Size(unboundedSize, size.height);
}
return result;
}
}
```
接下来我们继承 `SliverGeometry` 自定义一个 `UnboundedSliverGeometry` ,主要就是增加了一个 `crossAxisSize` 参数,用来记录当前统计到的副轴高度,从而让上面的 `ViewPort` 可以获取得到。
```dart
class UnboundedSliverGeometry extends SliverGeometry {
UnboundedSliverGeometry(
{SliverGeometry? existing, required this.crossAxisSize})
: super(
scrollExtent: existing?.scrollExtent ?? 0.0,
paintExtent: existing?.paintExtent ?? 0.0,
paintOrigin: existing?.paintOrigin ?? 0.0,
layoutExtent: existing?.layoutExtent,
maxPaintExtent: existing?.maxPaintExtent ?? 0.0,
maxScrollObstructionExtent:
existing?.maxScrollObstructionExtent ?? 0.0,
hitTestExtent: existing?.hitTestExtent,
visible: existing?.visible,
hasVisualOverflow: existing?.hasVisualOverflow ?? false,
scrollOffsetCorrection: existing?.scrollOffsetCorrection,
cacheExtent: existing?.cacheExtent,
);
final double crossAxisSize;
}
```
如下代码所示,最终我们基于 `SliverList` 实现一个 `UnboundedSliverList` ,这也是核心逻辑,主要是实现 `performLayout` 部分的代码,我们需要在原来代码的基础上,在某些节点加上自定义的逻辑,用于统计参与布局的每个 Item 的高度,从而得到一个最大值。
> 代码看起来很长,但是其实我们新增的很少。
```dart
class UnboundedSliverList = SliverList with UnboundedSliverListMixin;
mixin UnboundedSliverListMixin on SliverList {
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element =
context as SliverMultiBoxAdaptorElement;
return UnboundedRenderSliverList(childManager: element);
}
}
class UnboundedRenderSliverList extends RenderSliverList {
UnboundedRenderSliverList({
required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
// See RenderSliverList::performLayout
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double scrollOffset =
constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
BoxConstraints childConstraints = constraints.asBoxConstraints();
int leadingGarbage = 0;
int trailingGarbage = 0;
bool reachedEnd = false;
if (constraints.axis == Axis.horizontal) {
childConstraints = childConstraints.copyWith(minHeight: 0);
} else {
childConstraints = childConstraints.copyWith(minWidth: 0);
}
double unboundedSize = 0;
// should call update after each child is laid out
updateUnboundedSize(RenderBox? child) {
if (child == null) {
return;
}
unboundedSize = math.max(
unboundedSize,
constraints.axis == Axis.horizontal
? child.size.height
: child.size.width);
}
unboundedGeometry(SliverGeometry geometry) {
return UnboundedSliverGeometry(
existing: geometry,
crossAxisSize: unboundedSize,
);
}
// This algorithm in principle is straight-forward: find the first child
// that overlaps the given scrollOffset, creating more children at the top
// of the list if necessary, then walk down the list updating and laying out
// each child and adding more at the end if necessary until we have enough
// children to cover the entire viewport.
//
// It is complicated by one minor issue, which is that any time you update
// or create a child, it's possible that the some of the children that
// haven't yet been laid out will be removed, leaving the list in an
// inconsistent state, and requiring that missing nodes be recreated.
//
// To keep this mess tractable, this algorithm starts from what is currently
// the first child, if any, and then walks up and/or down from there, so
// that the nodes that might get removed are always at the edges of what has
// already been laid out.
// Make sure we have at least one child to start from.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = unboundedGeometry(SliverGeometry.zero);
childManager.didFinishLayout();
return;
}
}
// We have at least one child.
// These variables track the range of children that we have laid out. Within
// this range, the children have consecutive indices. Outside this range,
// it's possible for a child to get removed without notice.
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
RenderBox? earliestUsefulChild = firstChild;
// A firstChild with null layout offset is likely a result of children
// reordering.
//
// We rely on firstChild to have accurate layout offset. In the case of null
// layout offset, we have to find the first child that has valid layout
// offset.
if (childScrollOffset(firstChild!) == null) {
int leadingChildrenWithoutLayoutOffset = 0;
while (earliestUsefulChild != null &&
childScrollOffset(earliestUsefulChild) == null) {
earliestUsefulChild = childAfter(earliestUsefulChild);
leadingChildrenWithoutLayoutOffset += 1;
}
// We should be able to destroy children with null layout offset safely,
// because they are likely outside of viewport
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
// If can not find a valid layout offset, start from the initial child.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = unboundedGeometry(SliverGeometry.zero);
childManager.didFinishLayout();
return;
}
}
}
// Find the last child that is at or before the scrollOffset.
earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
// We have to add children before the earliestUsefulChild.
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
));
return;
}
}
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
// firstChildScrollOffset may contain double precision error
if (firstChildScrollOffset < -precisionErrorTolerance) {
// Let's assume there is no child before the first child. We will
// correct it on the next layout if it is not.
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
final SliverMultiBoxAdaptorParentData childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
final SliverMultiBoxAdaptorParentData childParentData =
earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
// If the scroll offset is at zero, we should make sure we are
// actually at the beginning of the list.
if (scrollOffset < precisionErrorTolerance) {
// We iterate from the firstChild in case the leading child has a 0 paint
// extent.
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
// We correct one child at a time. If there are more children before
// the earliestUsefulChild, we will correct it once the scroll offset
// reaches zero again.
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
assert(earliestUsefulChild != null);
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
final SliverMultiBoxAdaptorParentData childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
// We only need to correct if the leading child actually has a
// paint extent.
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
return;
}
}
}
// At this point, earliestUsefulChild is the first child, and is a child
// whose scrollOffset is at or before the scrollOffset, and
// leadingChildWithLayout and trailingChildWithLayout are either null or
// cover a range of render boxes that we have laid out with the first being
// the same as earliestUsefulChild and the last being either at or after the
// scroll offset.
assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
// Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
// Here, earliestUsefulChild is still the first child, it's got a
// scrollOffset that is at or before our actual scrollOffset, and it has
// been laid out, and is in fact our leadingChildWithLayout. It's possible
// that some children beyond that one have also been laid out.
bool inLayoutRange = true;
RenderBox? child = earliestUsefulChild;
int index = indexOf(child!);
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
bool advance() {
// returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout) inLayoutRange = false;
child = childAfter(child!);
if (child == null) inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(
childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
updateUnboundedSize(child);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child!.layout(childConstraints, parentUsesSize: true);
updateUnboundedSize(child!);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData =
child!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent =
childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
geometry = unboundedGeometry(
SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
),
);
return;
}
}
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// Finally count up all the remaining children and label them as garbage.
if (child != null) {
child = childAfter(child!);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child!);
}
}
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
collectGarbage(leadingGarbage, trailingGarbage);
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedMaxScrollOffset;
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild!),
lastIndex: indexOf(lastChild!),
leadingScrollOffset: childScrollOffset(firstChild!),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >=
endScrollOffset - childScrollOffset(firstChild!)!);
}
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild!)!,
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild!)!,
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint =
constraints.scrollOffset + constraints.remainingPaintExtent;
geometry = unboundedGeometry(
SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
),
);
// We may have started the layout while scrolled to the end, which would not
// expose a new child.
if (estimatedMaxScrollOffset == endScrollOffset)
childManager.setDidUnderflow(true);
childManager.didFinishLayout();
}
}
```
别看上面这段代码很长,其实很多都是 `RenderSliverList` 自己的源码,如下图所示,真正我们修改添加的只有这么点:
- 在开始前增加 `updateUnboundedSize``unboundedGeometry` 用于记录布局高度和生成 `UnboundedSliverGeometry`
- 将所有原来的 `SliverGeometry ` 修改为 `UnboundedSliverGeometry`
- 在所有涉及 `layout` 的位置后面调用 `updateUnboundedSize` ,因为 child 在布局之后我们就可以获取到它的 `Size` ,然后我们统计得到他们的最大值,就可以通过 `UnboundedSliverGeometry` 返回给 `ViewPort`
![](http://img.cdn.guoshuyu.cn/20230425_N24/image7.png)
最后如下代码所示,将 `UnboundedListView` 添加到一开始的垂直 `ListView `里,运行之后可以看到,随着横向滑动,列表的自身高度在发生变化。
```dart
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: IntrinsicHeight(
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
alignment: Alignment.bottomCenter,
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "TEST\n")
.toString()),
),
);
}),
),
),
),
UnboundedListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 100,
itemBuilder: (context, index) {
print('$index');
return Padding(
padding: EdgeInsets.all(2),
child: Container(
height: index * 1.0 + 10,
width: 50,
color: Colors.blue,
),
);
}),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
);
```
![](http://img.cdn.guoshuyu.cn/20230425_N24/image8.gif)
那么这是否达到了我们的需求?如下代码所示,假如我将代码修改成如下所示,运行之后可以看到,此时的横向列表变成了参差不齐的状态。
```dart
UnboundedListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 100,
itemBuilder: (context, index) {
print('$index');
return Container(
padding: EdgeInsets.all(2),
child: Container(
width: 50,
color: Colors.blue,
alignment: Alignment.bottomCenter,
child: Text(List.generate(
math.Random().nextInt(15), (index) => "TEST\n")
.toString()),
),
);
}),
```
![](http://img.cdn.guoshuyu.cn/20230425_N24/image9.png)
但是这时候我们无法用类似 `IntrinsicHeight` 的方式来解决,因为 `ListView` 里的 Item 都是动态处理的,**也就是布局时需要处理特定便宜范围内的 Item 添加和销毁**,具体在 `performLayout` 里会通过 `scrollOffset``targetEndScrollOffset` 等来确定布局 Item 的范围。
> 这样就导致我们通过 `firstChild` 链表结构去访问的时候,我们无法在 `layout` 之前获取到 child ,因为此时它还没有被 add 到链表里,同时也受限于 `insertAndLayoutLeadingChild``insertAndLayoutChild` 的耦合实现和私有方法限制,这里不方便简单重写支持。
但是「天无绝人之路」,既然我们不能在 child `layout` 之前处理,那么我们可以在 `layout` 之后做多一次冗余布局,如下代码所示:
- 我们首先将 `unboundedSize` 提取为 `UnboundedRenderSliverList` 里的全局变量
- 在 `didFinishLayout` 之前,通过 `firstChild` 链表结构,重新通过 `layout(childConstraints.tighten(height: unboundedSize)` 布局多一次
```dart
double unboundedSize = 0;
// See RenderSliverList::performLayout
@override
void performLayout() {
····
var tmpChild = firstChild;
while (tmpChild != null) {
tmpChild.layout(childConstraints.tighten(height: unboundedSize),
parentUsesSize: true);
tmpChild = childAfter(tmpChild);
}
childManager.didFinishLayout();
····
}
```
运行之后可以看到,此时列表已经全部对齐,而损耗就是 child 会有 double 布局的情况,对于此处性能损耗,对比 `SingleChildScrollView` 的实现,可以根据实际场景来取舍使用哪种逻辑,**当然,为了性能考虑非必要还是给横向 `ListView` 一个高度,这样的实现才是最优解**。
![](http://img.cdn.guoshuyu.cn/20230425_N24/image10.png)
好了,本篇小技巧到这里就解决了,不知道对于类似实现,你是否还有什么想法,如果你有更好的解决方案,欢迎留言讨论。
> 完整代码可见https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart

View File

@ -93,6 +93,7 @@
- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md)
- [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md)
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
@ -101,6 +102,7 @@
- [Dart 2.17 发布的新特性](Dart-217.md)
- [Dart 2.18 发布Objective-C 和 Swift interop](Dart-218.md)
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)
* [番外](FWREADME.md)
@ -179,6 +181,10 @@
* [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md)
* [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md)
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)
* [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md)
* [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md)
* [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md)
* [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md)
[Flutter 工程化选择](GCH.md)

View File

@ -60,6 +60,7 @@
- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md)
- [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md)
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
@ -68,6 +69,7 @@
- [Dart 2.17 发布的新特性](Dart-217.md)
- [Dart 2.18 发布Objective-C 和 Swift interop](Dart-218.md)
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)
* [番外](FWREADME.md)
@ -219,6 +221,14 @@
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)
* [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md)
* [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md)
* [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md)
* [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md)
* [Flutter 工程化选择](GCH.md)
* [Flutter 工程化框架选择——搞定 Flutter 动画](Z1.md)
* [Flutter 工程化框架选择 — 搞定 UI 生产力](Z3.md)

View File

@ -13,6 +13,7 @@
- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md)
- [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md)
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
@ -27,4 +28,5 @@
- [Dart 2.17 发布的新特性](Dart-217.md)
- [Dart 2.18 发布Objective-C 和 Swift interop](Dart-218.md)
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)