This commit is contained in:
guoshuyu 2023-03-20 17:10:51 +08:00
parent a0992ef01b
commit 6ea34c1df8
13 changed files with 3190 additions and 66 deletions

574
Dart-300a.md Normal file
View File

@ -0,0 +1,574 @@
# Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解
> 由于 Dart 3 还处于 alpha ,某些细节可能还会有所变化,但是总体设定和大部分细节应该不会变太多,大家可以提前尝鲜。
>
> 更多更新也可以关注官方的 [records-feature-specification](https://github.com/dart-lang/language/blob/master/accepted/future-releases/records/records-feature-specification.md) 和 [feature-specification.md](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#patterns) 相关进展。
Record 和 Patterns 作为 Dart 3 的 Big Things ,无疑是 Flutter 和 Dart 开发者都十分关注的新特性。
简单来说,**Records 支持高效简洁地创建匿名复合值,不需要再声明一个类来保存,而在 Records 组合数据的地方Patterns 可以将复合数据分解为其组成部分**。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image14.png)
众所周知 Dart 语言本身一直都 “相对保守”,而这次针对 Records 和 Patterns 的支持却很“彻底”,属于全能力的模式匹配,能递归匹配,有 condition guards ,对于 Flutter 开发者来说无疑是生产力的大幅提升。
> 当然,也可能是 Bug 的大幅度提升。
# Records
如下方代码所示,**Records 属于是一种匿名的不可变聚合类型** ,类似于 Map 和 List ,但是 Records 固定大小,组合更灵活,并且支持不同类型存储。
```dart
var record = (1, a: 2, 3, b: 4);
```
> 除了大小固定之外Records 和 Map 和 List 最大不同就是它支持不同类型聚合存储,也就是你不用再写 `List<Object>` 之类的代码来承载数据多样性。
当然,可能你会觉得,这和我定义一个 Class 来承载不同数据对象有什么区别?其实还是有很大区别的:
- 定义了类,也就是说你的数据集合需要和特定类耦合
- 使用 Records 就不必声明对应类型,**只要具有相同字段集的记录, Dart 就会认为它们是相同类型**(这个后面会介绍)
> 所以从上面可以看到, Records 的出现对于Dart 来说是很重要的能力拓展,尽管对于其他语言这也许并不是什么新鲜特性。
## 简单介绍
对于 Records ,我们拓展前面的代码,通过打印对应的数值,可以清晰看到 Records 内数值的获取方式:**通过 `$` 位置字段或者命名字段的方式获取数据**。
```dart
var record = (1, a: 2, 3, b: 4);
print(record.$1); // Print "1"
print(record.a); // Print "2"
print(record.$2); // Print "3"
print(record.b); // Print "4"
```
> 在 Records 的变更记录里:**现在 Records 开始位置记录是从 `$1` 开始,而不是 `$0`** ,但是 DartPad 上你可能还会遇到需要从 `$0` 开始。
而定义 Records 是通过 `()` 和 "`,`" 实现,为什么要有 "`,`" ,如下代码所示:
```dart
var num = (123); // num
var records = (123,); // record
```
- 如果没有 "`,`" ,那么 `(123)` 就是一个 num 类型的对象
- 有 "`,`" 之后 `(123,)` 才会被识别为是一个 Records 类型
所以作为一个集合类型Records 也是可以用来声明变量,比如:
```dart
(bool, num, {int n, String s}) records;
records = (false, 1, n: 12, s : "xxx");
print(records);
```
当然,如果你如下代码一样赋值就会收获一个 ` can't be assigned to a variable of type` 的错误因为它们类型不相同Records 是固定大小的:
```dart
records = (false, 1, s : "xxx2");
records = (false, 1, n : 12);
```
而 Records 上的命名字段主要在于可以如下这样赋值:
```dart
records = (false, 1, s : "xxx2", n : 12);
records = (s : "xxx2", n : 12, false, 1, );
print(records);
```
最后,在 Records 的定义里需要遵循以下规则:
- 同一命名字段名称只能出现一次,这个不难理解,比如上面代码你不可能定义两个 `s`
- `(,)` 这样的表达式是不允许的,但是 `()` 可以是没有任何字段的常量空 Records
- 有参数但是只有 `()` 没有 "`,`" 也不是 Records ,如 `(6)`
- 命令为 `hashCode``runtimeType``noSuchMethod`, 、`toString` 的字段是不允许的
- 以下划线开头的命令字段是不允许的
- 与位置字段名称冲突的命令字段,比如 *`('pos', $1: 'named')`* 这样是不行的,但是 `($1: 'records')` 这样可以
知道了 Records 的大概逻辑之后,这里面有个有趣的设定,比如:
```dart
var t = (int, String);
print(t);
print(t.$0.runtimeType);
print(t.$1.runtimeType);
```
通过打印你会发现 `t` 里面的 `$0``$1``_Type` 类型,也就是如果后面再写 ` t = (1, "fff");` ,就会收获这样的错误
![](http://img.cdn.guoshuyu.cn/20230131_D3/image1.png)
> 其实这个例子没什么实际意义,注意强调一下 `var t = (int, String);``(int, String) t` 的区别。
最后简单介绍下 Records 的类型关系:
- **`Record``Object``dynamic` 的子类和 `Never` 的父类**
- **所有的 Records 都是 `Record` 的子类和 `Never` 的父类**
如果拓展到 Records 之间进行比较,假设有 A、B 两个都是 Records 对象,而 **B 在和 A 具有相同 shape 的前提下,所有的字段都是 A 里字段的子类**,那么 Records B 可以认为是 Records A 的子类。
![](http://img.cdn.guoshuyu.cn/20230131_D3/image2.png)
## 进阶探索
前面我们介绍过,**在 Records 里,只要具有相同字段集的记录, Dart 就会认为它们是相同类型**,这怎么理解呢?
首先需要确定的是,**Records 类型里命名字段的顺序并不重要**,就是 `{int a, int b}` 与`{int b, int a} ` 的类型系统和 runtime 会完全相同。
> 另外位置字段不仅仅是名为 `$1` 、`$2` 这样的字段语法糖,`('a', 'b')` 和 `($1: 'a', $2: 'b') ` 从外部看是具有相同的 *members* ,只是具有不同的 *shapes*
例如 `(1.2, name: 's', true, count: 3) ` 的签名大概会是这样:
```dart
class extends Record {
double get $1;
String get name;
bool get $2;
int get count;
}
```
> **Records 里每个字段都有 getter ,并且字段是不可变的,所以不会又 Setter**
所以由于 Records 本身数据复杂性等原因,所以设定上 Records 的标识就是它的内容,**也就是具有相同 shape 和字段的两条 Records 是相等的值**。
```dart
print((a: 1, b: 2) == (b: 2, a: 1)); // true
```
当然,如果是以下这种情况,因为位置参数顺序不一样,所以它们并不相等,因为 shape 不同,会输出 `false`
```dart
print((true, 2, a: 1, b: 2,) == (2, true, b: 2, a: 1)); // false
```
同时,**Records 运行时的类型由其字段的运行时的类型确定**,例如:
```dart
(num, Object) pair = (1, 2.3);
print(pair is (int, double)); // "true".
```
这里**运行时 `pair``(int, double)`,不是`(num, Object)`** ,虽然官方文档是这么提供的,但是 Dartpad 上验证目前却很有趣,大家可以自行体会:
![](http://img.cdn.guoshuyu.cn/20230131_D3/image3.png)
![](http://img.cdn.guoshuyu.cn/20230131_D3/image4.png)
我们再看个例子,如下代码所示, Records 是可以作为用作 Map 里的 key 值,因为它们的 shape 和 value 相等,所以可以提取出 Map 里的值。
```dart
var map = {};
map[(1, "aa")] = "value";
print(map[(1, "aa")]); //输出 "value"
```
如果我们定义一个 `newClass` 如下代码所示,可以预料到输出结果会是 `null` ,因为两个 `newClass` 并不相等。
```dart
class newClass {
}
var map = {};
map[(1, new newClass())] = "value";
print(map[(1, new newClass())]); //输出 "null"
```
但是如果给 `newClass``==``hashCode `进行` override `,就可以又看到输出 `"value"` 的结果。
```dart
class newClass {
@override
bool operator ==(Object other) {
return true;
}
@override
int get hashCode => 1111111;
}
```
所以到这里,你应该就理解了“**只要具有相同字段集的记录, Dart 就会认为它们是相同类型**”这句话的含义。
最后再介绍一个 Runtime 时的特性, **Records 中的字段是从左到右计算的**,即使后续实现选择了重新排序命名字段也是如此,例如:
```dart
int say(int i) {
print(i);
return i;
}
var x = (a: say(1), b: say(2));
var y = (b: say(3), a: say(4));
```
上门结果一定是打印 *“1”、“2” / “3”、“4”* 就算是下面代码的排列,也是输出 *“0”、“1”、“2” / “3”、“4”、“5”*
```dart
var x = (say(0), a: say(1), b: say(2));
var y = (b: say(3), a: say(4), say(5));
```
## Records 带来的语法歧义
因为 Dart 3 的 Records 是在以前版本的基础上升级的,那么一些语法兼容就是必不可少的,这里整理一下目前官方罗列出来的常见调整。
### try/on
首先是 `try/on` 相关语法, 如果按照以前的设定,第二行的 `on` 应该是被识别为一个局部函数,但是在增加了 Records 之后,现在它是可以匹配的 `on` Records 类型。
```dart
void recordTryOn() {
try {
} on String {
}
on(int, String) {
}
}
```
> 这里声明的类型其实没什么意义,只是为了形象展示对比
鉴于消除歧义的目的,如果在早于 Records 支持版本里,`on `关键字后带 `()` 这样的类型,将直接被语法解析为 Records 类型,提示为语法错误,因为该 Dart 版本不支持 Records 类型。
![](http://img.cdn.guoshuyu.cn/20230131_D3/image5.png)
### metadata 注解
如下代码所示,因为多了 Records 之后,注解的理解上可能就会多了一些语法歧义:
```dart
@metadata (a, b) function() {}
```
如果不约定好理解,这可能是:
- `@metadata(a, b)` 与没有返回类型的函数声明关联的metadata 注解
- `@metadata`与返回类型为 Records 类型的函数关联的metadata 注解 `(a, b)`
所以这里主要通过空格来约定,尽管这样很容易出现纰漏:
```dart
@metadata(a, b) function() {}
@metadata (a, b) function() {}
```
- 前者由于 `@metadata` 之后没有空格,所以表示为 `(a, b)` 的 metadata 注解
- 前者由于有空格,所以表示为 Records 返回类型
它们的不同之处可以参考下面的两种类型:
```dart
// Records 和 metadata 是一起作用在 a
@metadata(x, y) a;
@metadata<T>(x, y) a;
@metadata <T>(x, y) a;
// Records 是直接作用在 a ,和 metadata 无关
@metadata (x, y) a;
@metadata
(x, y) a;
@metadata/* comment */(x, y) a;
@metadata // Comment.
(x,) a;
```
举个例子,比如下面这种情况 `@TestMeta(1, "2")` 没有空格,所以不会有语法错误
```dart
@TestMeta(1, "2")
class C {}
class TestMeta {
final String message;
final num code;
const TestMeta(this.code, this.message);
@override
String toString() => "feature: $code, $message";
}
```
但是如果是 `@TestMeta (1, "2")` ,就会有 `Annotations can't have spaces or comments before the parenthesis.` 这样的错误提示。
```dart
@TestMeta (1, "2") //Error
class C {}
```
> 所以有无空格对于 metadata 注解来说将会变得完全不一样,可能这对一些第三方插件的适配使用上会有一定 breaking change。
### toString
在 Debug 版本中Records 的 `toString()` 方法会通过调用每个字段的 `toString()`值,并在其前面加上字段名称,后续是否添加 `: ` 字符取决于字段是否为命名字段,最终会将每个字段转换为字符串。
> 看下面例子可能会更形象。
每个字段会利用 `, ` 作为分隔符连接起来,并返回用括号括起来的结果,例如:
```
print((1, 2, 3).toString()); // "(1, 2, 3)".
print((a: 'str', 'int').toString()); // "(a: str, int)".
```
**Debug 版本中,命名字段出现的顺序以及它们如何与位置字段进行排列是不确定的,只有位置字段必须按位置顺序出现**
> 所以 toString 内部实现可以自由地为命名字段选择规范顺序,而与创建记录的顺序无关。
而在发布或优化构建中,`toString()` 行为是更不确定的, 所以可能会有选择地丢弃命名字段的全名以减少代码大小等操作。
> **所以用户最好只将 Records 的 `toString()` 用于调试**,强烈建议不要解析调用结果 `toString()` 或依赖它来获得某些逻辑判断,避免产生歧义。
# Patterns
如果只是单纯 Records 可能还看不到巨大的价值,但是如果配合上 Patterns ,那开发效率就可以得到进一步提升,**其中最值得关注的就是多个返回值的支持**。
![](http://img.cdn.guoshuyu.cn/20230131_D3/image6.png)
## 简单介绍
**关于 Patterns 这里不会有太长的篇幅**,首先目前 Patterns 在 DartPad 上还是 disabled 的状态,其次 Patterns 的复杂度和带来的语法歧义问题实在太多,它目前还具有太多未确定性。
![](http://img.cdn.guoshuyu.cn/20230131_D3/image7.png)
> 从[提案](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#summary)上看,未来感觉也不会一次性所有能力全部发布。
### 多返回值
回到主题,我们知道,使用 Records 可以让我们的方法实现多个返回值,例如下面代码的实现
```dart
(double, double) geoCode(String city) {
var lat = // Calculate...
var long = // Calculate...
return (lat, long); // Wrap in record and return.
}
```
但是当我们需要获取这些值的时候,就需要 **Patterns 的解构赋值**,例如:
```dart
var (lat, long) = geoCode('Aarhus');
print('Location lat:$lat, long:$long');
```
**当然 Patterns 下的解构赋值不只是针对 Records** ,例如对 `List` 或者 `Map` 也可以:
```dart
var list = [1, 2, 3];
var [a, b, c] = list;
print(a + b + c); // 6.
var map = {'first': 1, 'second': 2};
var {'first': a, 'second': b} = map;
print(a + b); // 3.
```
更近一步还可以解构并分配给现有变量:
```dart
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap!
print('$a $b'); // Prints "right left".
```
> 有没有觉得代码变得难阅读了?哈哈哈哈
### 代数数据类型
就如 Flutter Forward 介绍那样现在类层次结构基本上已经可以对代数数据类型进行建模Patterns 下提供了新的模式匹配结构,例如代码可以变成这样:
```dart
///before
double calculateArea(Shape shape) {
if (shape is Square) {
return shape.length + shape.length;
} else if (shape is Circle) {
return math.pi * shape.radius * shape.radius;
} else {
throw ArgumentError("Unexpected shape.");
}
}
//after
double calculateArea(Shape shape) =>
switch (shape) {
Square(length: var l) => l * l,
Circle(radius: var r) => math.pi * r * r
};
```
> 甚至 `switch `都不需要添加 `case` 关键字,并且用上了后面会简单介绍的可变模式。
### Patterns
目前 Dart 上 Patterns 的设定还挺复杂,简单来说是:
> **通过一些简洁、可组合的符号,排列后确定一个对象是否符合条件,并从中解构出数据,然后仅当所有这些都为 true 时才执行代码**
也就是你会看到一系列充满操作符的简短代码,如 `"||"``" && "``"=="``"<"``"as"``"?"``"_"`、`"[]"`、`"()"`、`"{}"`等的排列组合,并尝试逐个去理解它们,例如:
```dart
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false
};
```
`"||" `可以在 switch 中让多个 case 共享一个主体,`"_"` 表示默认,甚至如下代码所示,你还可以在绑定 `s` 之后,多个共享一个 `when` 条件:
```dart
switch (shape) {
case Square(size: var s) || Circle(size: var s) when s > 0:
print('Non-empty symmetric shape');
case Square() || Circle():
print('Empty symmetric shape');
default:
print('Asymmetric shape');
}
```
这种写法可以大大优化 `switch` 的结构 ,如下所示可以看到,类似写法代码得到了很大程度的精简:
```dart
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;
return switch (char) {
< space => 'control',
== space => 'space',
> space && < zero => 'punctuation',
>= zero && <= nine => 'digit'
// Etc...
}
}
```
当然,还有一些很奇葩的设定,比如利用 `? `匹配非空值,很明显这样的写法很反直觉,最终是否这样落地还是要看社区讨论的结果:
```dart
String? maybeString = ...
switch (maybeString) {
case var s?:
// s has type non-nullable String here.
}
```
更进一步还有在解构的 position 赋值时通过 `!` 强制转为非空,还有在 switch 匹配时第一个列为 `'user'``name` 不为空。
```dart
(int?, int?) position = ...
// We know if we get here that the coordinates should be present:
var (x!, y!) = position;
List<String?> row = ...
// If the first column is 'user', we expect to have a name after it.
switch (row) {
case ['user', var name!]:
// name is a non-nullable string here.
}
```
如果搭配上 Records 就更难理解了,比如下代码,可变 pattern 将匹配值绑定到新变量,这里的 `var a ``var b ` 是可变模式,最终分别绑定到 `1``2` 上。
```dart
switch ((1, 2)) {
case (var a, var b): ...
}
switch (record) {
case (int x, String s):
print('First field is int $x and second is String $s.');
}
```
其实就类似于 Flutter Forword 介绍的能力,`case` 下可以做对应的绑定,如上 `switch (record)` 也是类似这种绑定。
![](http://img.cdn.guoshuyu.cn/20230131_D3/image6.png)
> 如果使用变量的名称是 `_`,那么它不绑定任何变量
更多的可能还有如 List、 Map 、 Records、 Object 等相关的 pattern 匹配等,**可以看到 Patterns 将很大程度改变 Dart 代码的编写和逻辑组织风格**
```dart
var list = [1, 2, 3];
var [_, two, _] = list;
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
print('$a $b $rest $c $d'); // Prints "1 2 [3, 4, 5] 6 7".
// Variable:
var (untyped: untyped, typed: int typed) = ...
var (:untyped, :int typed) = ...
switch (obj) {
case (untyped: var untyped, typed: int typed): ...
case (:var untyped, :int typed): ...
}
// Null-check and null-assert:
switch (obj) {
case (checked: var checked?, asserted: var asserted!): ...
case (:var checked?, :var asserted!): ...
}
// Cast:
var (field: field as int) = ...
var (:field as int) = ...
class Rect {
final double width, height;
Rect(this.width, this.height);
}
display(Object obj) {
switch (obj) {
case Rect(width: var w, height: var h): print('Rect $w x $h');
default: print(obj);
}
}
```
> 从目前看来,**这会是一种自己写起来很爽,别人看起来可能很累的特性**,同时也可能会带来不少的 breaking change ,更多详细可见:[patterns-feature-specification](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md)
好了,关于 Patterns 的这里就不再继续展开,它落地会如何最终还不完全确定,但是从我的角度来看,它绝对会是一把双刃剑,希望 Patterns 到来的同时不会引入太多的 Bug。
# 最后
其实我相信大多数人可能都只关心 Records 和解构赋值,从而实现函数的多返回值能力,这对我们来说是最直观和最实用的。
至于 switch 如何匹配和 Patterns 如何精简代码结构,这都是后话了。
现在,或者你可以选择 Dart 3 尝尝鲜了~

View File

@ -69,6 +69,13 @@
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)
* [Android 开发者的跨平台 - Flutter or Compose ](SQS.md)
* [Flutter 小技巧之快速理解手势逻辑](N15.md)
* [2023 Flutter Forward 大会回顾,快来看看 Flutter 的未来会有什么](Flutter-FF2023.md)
* [Flutter 2023 Roadmap 解析](Flutter-roadmap2023.md)
* [Flutter 小技巧之 3.7 性能优化background isolate](Flutter-N16.md)
* [Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?](Flutter-N18.md)
* [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md)
* [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md)
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)

361
Flutter-370.md Normal file
View File

@ -0,0 +1,361 @@
# 2023 年第一弹, Flutter 3.7 发布啦,快来看看有什么新特性
> 核心内容原文链接: https://medium.com/flutter/whats-new-in-flutter-3-7-38cbea71133c
2023 年新春之际, Flutter 喜提了 3.7 的大版本更新,在 Flutter 3.7 中主要有**改进框架的性能,增加一些很棒的新功能,例如:创建自定义菜单栏、级联菜单、更好地支持国际化的工具、新的调试工具等等**。
另外 Flutter 3.7 还**改进了 Global selection、使用 Impeller提升渲染能力、DevTools 等功能,以及一如既往的性能优化**。
> PS 3.7 版本包含大量,大量,大量更新内容,感觉离 4.0 不远了。
# 提升 Material 3 支持
随着以下 Widget 的迁移Material 3 支持在 3.7 中得到了极大提升:
- `Badge`
- `BottomAppBar`
- `Filled ``Filled Tonal` 按键
- `SegmentedButton`
- `Checkbox`
- `Divider`
- `Menus`
- `DropdownMenu`
- `Drawer`和`NavigationDrawer`
- `ProgressIndicator`
- `Radio ` 按键
- `Slider`
- `SnackBar`
- `TabBar`
- `TextFields`和`InputDecorator`
- `Banner`
要使用这些新功能只需打开 `ThemeData ``useMaterial3`标志即可。
**要充分利用 M3 的特性支持,还需要完整的 M3 配色方案,可以使用新的 [theme builder](https://m3.material.io/theme-builder#/custom) 工具,或者使用构造函数的 `colorSchemeSeed` 参数生成一个`ThemeData`**
```dart
MaterialApp (
theme : ThemeData (
useMaterial3 : true,
colorSchemeSeed : Colors.green,
),
// …
);
```
> 使用这些组件,可以查看展示所有新 M3 功能的 [interactive demo](https://flutter-experimental-m3-demo.web.app/#/)
![](http://img.cdn.guoshuyu.cn/20230125_F37/image1.gif)
# 菜单栏和级联菜单
Flutter 现在可以创建菜单栏和级联 context 菜单。
**对于 macOS 可以使用 `PlatformMenuBar` 创建一个菜单栏,它定义了由 macOS 而不是 Flutter 渲染的原生菜单栏支持**。
而且,对于所有平台可以定义一个 [Material Design menu](https://m3.material.io/components/menus/overview) ,它提供级联菜单栏 ( `MenuBar`) 或由用户界面触发的独立级联菜单( `MenuAnchor`) 。
这些菜单可完全自主定制,菜单项可以是自定义 Widget或者是使用新的菜单项 Widget ( `MenuItemButton`, `SubmenuButton`)。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image2.png)
# Impeller 预览
这里很高兴地宣布新的 [Impeller 渲染引擎](https://github.com/flutter/engine/tree/main/impeller) 已经[可以在 ](https://github.com/flutter/engine/tree/main/impeller#try-impeller-in-flutter) Stable Channel 上的 iOS 进行预览。
**Flutter 团队相信 Impeller 的性能将达到或超过大多数应用的 Skia 渲染器并且在保真度方面Impeller 实现几乎覆盖了少数极端下的使用场景**。
> 未来在即将发布的稳定版本中可能会让 Impeller 成为 iOS 上的默认渲染器,如果有任何问题,欢迎在 GitHub 的 [Impeller Feedback](https://github.com/flutter/flutter/issues) 上提交反馈。
虽然目前期待的结果是 iOS 上的 Impeller 可以满足几乎所有现有 Flutter 应用的渲染需求,但 API 覆盖率仍然存在一些差距:
在 [Flutter wiki ](https://github.com/flutter/flutter/wiki/Impeller#status)上列出了少量剩余的未覆盖情况,用户可能还会注意到 Skia 和 Impeller 之间在渲染中的细微视觉上存在差异,而这些细微差别可能会导致错误,所以如果有任何问题,请不要犹豫,欢迎在 Github [提出问题](https://github.com/flutter/flutter/issues)。
> 社区的贡献大大加快了 Impeller 上的进展。特别是 GitHub 用户 [ColdPaleLight](https://github.com/ColdPaleLight)、[guoguo338](https://github.com/guoguo338)、[JsouLiang](https://github.com/JsouLiang) 和 [magicianA ](https://github.com/magicianA)为该版本贡献了 291 个 Impeller 相关补丁中的 37 个(>12%)。
另外 Flutter 将继续在 Impeller 的 Vulkan 上继续推进支持(在旧设备上回退到 OpenGL但 Android 上的 Impeller 目前还未准备好Android 上的支持正在积极开发中,希望可以在未来的版本中分享更多关于它的信息——以及未来更多关于 desktop 和 web 上的支持
> 在 GitHub 上的 [Impeller 项目板上](https://github.com/orgs/flutter/projects/21) 可以关注进展。
# iOS 版本验证
当开发者发布 iOS 应用时, [checklist of settings to update](https://docs.flutter.dev/deployment/ios#review-xcode-project-settings) 可确保开发者的应用已准备好提交到 App Store。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image3.png)
`flutter build ipa` 命令现在会验证其中一些设置,并在发布前通知开发者是否需要对应用进行更改。
# 开发工具更新
在 3.7 版本中,有几个关于新的工具和功能方面的改进。
DevTools 内存调试工具新增了三个功能选项卡,**Profile**、**Trace **和 **Diff**,它们支持所有以前支持的内存调试功能,并添加了更多功能以方便调试。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image4.png)
新功能包括:
- 按 class 和 memory 类型分析应用的当前内存分配
- 调查哪些代码路径在运行时为一组 class 分配内存
- 差异内存快照以了解两个时间点之间的内存管理
> 所有这些新的内存功能都记录在 [docs.flutter.dev](https://docs.flutter.dev/development/tools/devtools/memory) 上
Performance 页面还有一些值得注意的新功能,性能页面顶部的**Frame Analysis** 提供了对所选 Flutter frame 的分析:
可能包括有关跟踪到的 frame 的 expensive 操作的建议,或有关在 Flutter 框架中检测到的 expensive 操作的警告。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image5.png)
这些只是 3.7 里 DevTools 的几个亮点, 3.7 版本还包含几个错误修复和更多功能改进,包括 Inspector、Network profiler 和 CPU profiler 的一些重要错误修复。
> 如需更深入的更新列表,请查看 Flutter 3.7 中 DevTools 更改的发行说明。
# 自定义 Context 菜单
**3.7 开始可以在 Flutter 应用的任何位置创建自定义 Context 菜单,还可以使用它们来自定义内置的 Context 菜单**。
例如,开发者可以将 “发送电子邮件” 按钮添加到默认文本选择工具栏,当用户选择电子邮件地址 ([code](https://github.com/flutter/samples/blob/main/experimental/context_menus/lib/email_button_page.dart)) 时,该工具栏就会显示。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image6.gif)
通过 `contextMenuBuilder `参数,该参数已添加到默认情况下显示 Context 菜单的 Widget例如 `TextField`
> 现在开发者可以从 `contextMenuBuilder` 返回任何想要的 Widget包括修改默认的平台自适应的 Context 菜单。
这个新功能也适用于文本选择之外,例如创建一个 `Image`,然后在右键单击或长按时显示 “**Save**” 按钮([code](https://github.com/flutter/samples/blob/main/experimental/context_menus/lib/image_page.dart)),通过 `ContextMenuController` 在应用的任何位置显示当前平台的默认 Context 菜单或自定义菜单。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image7.gif)
> 更多可见 [Flutter Demo context_menus](https://github.com/flutter/samples/tree/main/experimental/context_menus)中的全套示例。
# CupertinoListSection 和 CupertinoListTile 小部件
Cupertino 新增了两个新的 Widget`CupertinoListSection` 和`CupertinoListTile`,用于显示 iOS 风格的可滚动小部件列表。
> 它们是Material `ListView``ListTile` 的 Cupertino 版本。
| ![](http://img.cdn.guoshuyu.cn/20230125_F37/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230125_F37/image9.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
# 滚动改进
3.7 版本带来了多项 [滚动更新](https://github.com/flutter/flutter/issues?page=1&q=is%3Aissue+is%3Aclosed+closed%3A2022-07-11..2022-11-30+label%3A"f%3A+scrolling"+reason%3Acompleted)
- 触控板交互改进
- 新的 Widget`Scrollbars` 和`DraggableScrollableSheet`
- 滚动 Context 文本选择的改进处理
> 值得注意的是, [MacOS 应用现在将通过添加新的滚动 ](https://github.com/flutter/flutter/pull/108298)physics 来体验更高的保真度以匹配桌面平台。
另外还有新的 `AnimatedGrid``SliverAnimatedGrid` 动画。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image10.gif)
最后,本次还[修复了](https://github.com/flutter/flutter/pull/108706)几个滚动 Widget 的构造函数中的问题,例如`ListView`
> 在 Flutter 框架的 NNBD 迁移过程中,原本 `itemBuilder` 允许用户按需提供 widgets 类型,但是在迁移到 `IndexedWidgetBuilder` 时不允许用户返回 null。
这意味着 `itemBuilder` 不能再返回 `null`,而本次跟新该设定已经通过 `NullableIndexedWidgetBuilder` 修复。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image11.png)
# 国际化工具和文档
国际化支持已经全面改进3.7 版本通过完全重写了 `gen-l10n `工具来实现支持:
- 描述性的语法错误
- 涉及嵌套/多个复数、选择和占位符的复杂消息
![](http://img.cdn.guoshuyu.cn/20230125_F37/image12.png)
> 有关更多信息,可参阅更新的 [国际化 Flutter 应用](https://docs.flutter.dev/development/accessibility-and-localization/internationalization)页面。
# 全局选择改进
`SelectionArea` 现在支持键盘选择,开发者可以使用键盘快捷键扩展现有选择,例如 `shift+right`
![](http://img.cdn.guoshuyu.cn/20230125_F37/image13.png)
# 后台 isolates
3.7 开始 [Platform Channels](https://docs.flutter.dev/development/platform-integration/platform-channels) 可以从任何 `Isolate` invoked 以前用户只能从 Flutter 提供的主 Isolate 调用平台通道,而现在 [Plugins](https://docs.flutter.dev/development/packages-and-plugins/developing-packages) 或 [Add-to-app](https://docs.flutter.dev/development/add-to-app) 能更好地使用 Isolate 和主机平台代码进行交互。
> 有关更多信息,请查看在 flutter.dev 上的 [platform-specific code](https://docs.flutter.dev/development/platform-integration/platform-channels) 和 [Introducing background isolate channels](https://medium.com/flutter/introducing-background-isolate-channels-7a299609cad8)。
# 文本放大镜
3.7 开始在 Android 和 iOS 上选择文本时出现的放大镜。
对于所有带有文本选择的应用,这是开箱即用的能力,但如果你想禁用或自定义它,请参阅 [magnifierConfiguration](https://master-api.flutter.dev/flutter/material/TextField/magnifierConfiguration.html) 属性。
| ![](http://img.cdn.guoshuyu.cn/20230125_F37/image14.gif) | ![](http://img.cdn.guoshuyu.cn/20230125_F37/image15.gif) |
| -------------------------------------------------------- | -------------------------------------------------------- |
# 插件的快速迁移
由于 Apple 现在专注于使用 Swift 作为他们的 APIs ,我们希望开发参考资料以帮助 Flutter 插件开发人员使用 Swift 迁移或创建新插件。
> [quick_actions](https://pub.dev/packages/quick_actions) 插件已从 Objective-C 迁移到 Swift可用作最佳实践的演示。如果有兴趣成为帮助我们迁移插件的一员请参阅wiki[的 Swift 迁移部分](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#swift-migration-for-1p-plugins)。
**适用于 iOS 开发人员的资源**,我们为 iOS 开发者发布了一些新资源,包括:
- [面向 SwiftUI 开发者的 Flutter](https://docs.flutter.dev/get-started/flutter-for/ios-devs?tab=swiftui)
- [面向 Swift 开发人员的 Dart](https://dart.dev/guides/language/coming-from/swift-to-dart)
- [Swift 开发者的 Flutter 并发](https://docs.flutter.dev/resources/dart-swift-concurrency)
- [将 Flutter 添加到现有的 SwiftUI 应用](https://docs.flutter.dev/development/add-to-app/ios/add-flutter-screen)
- [使用 Flutter 创建 flavors ](https://docs.flutter.dev/deployment/flavors)(适用于 Android 和 iOS
# Bitcode deprecation
[从 Xcode 14 开始watchOS 和 tvOS 应用不再需要 bitcodeApp Store 也不再接受来自 Xcode 14 的 bitcode 提交。](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes)
> 因此Flutter 已删除对 bitcode 的支持。
默认情况下Flutter 应用不启用位码,我们预计这不会影响许多开发人员。
但是如果你在 Xcode 项目中手动启用了 bitcode请在升级到 Xcode 14 后立即禁用它。
你可以通过打开 `ios/Runner.xcworkspace` 并将 **Enable Bitcode** 设置为 **No** 来实现Add-to-app 的开发人员可以在宿主 Xcode 项目中禁用它。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image16.png)
# iOS PlatformView BackdropFilter
我们添加了在有 blurred 效果的 Flutter Widget 下方呈现时使原生 iOS 视图模糊的功能,并且 `UiKitView` 现在可以包装在 `BackdropFilter`
![](http://img.cdn.guoshuyu.cn/20230125_F37/image17.png)
> 有关详细信息,请参考 [iOS PlatformView BackdropFilter ](http://flutter.dev/go/ios-platformview-backdrop-filter-blur)设计文档。
# 内存管理
3.7 版本对内存管理进行了一些改进,具体有:
- 减少垃圾收集暂停导致的卡顿
- 由于分配速度和后台 GC 线程而降低 CPU 利用率
- 减少内存占用
作为一个例子Flutter 扩展了现有的手动释放支持某些 `dart:ui` 对象。
> 以前Native 资源由 Flutter 引擎持有,直到 Dart VM 垃圾回收 Dart 对象。
通过对用户应用的分析和我们自己的基准测试,我们确定该策略不足以避免不合时宜的 GC 和过度使用内存。
**因此在此版本中Flutter 引擎添加了显式释放用于 `Vertices` 、`Paragraph` 和 `ImageShader ` 对象持有的原生资源的 API** 。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image18.png)
> 在迁移到的 Flutter 框架基准测试中,这些改进将 90% 的帧构建时间减少了 30% 以上,最终用户将体验到更流畅的动画和更少的卡顿。
此外Flutter 引擎不再[将 GPU 图像的大小注册到 Dart VM](](https://github.com/flutter/engine/pull/35473)),这些图像在不再需要时会由框架手动释放。
沿着类似的思路,现在 Flutter 引擎的策略是仅向 Dart VM 报告支持 `dart:ui` 的 Dart 对象部分的 Native 的 [shallow size](https://github.com/flutter/engine/pull/35813) 。
![](http://img.cdn.guoshuyu.cn/20230125_F37/image19.png)
> 在基准测试中,本次更改消除了在 Widget 创建 GPU 驻留图像时构建帧的同步 GC 。
在此版本中Flutter Engine 还更好地利用了有关 Flutter 应用状态的信息来动态更新 Dart VM。
> Flutter 现在使用 Dart VM 的 [RAIL](https://web.dev/rail/) Style [API ](https://github.com/dart-lang/sdk/commit/c6a1eb1b61844b2d733f9e2f4c7754f1920325d7)在路由转换动画期间进入 [低延迟模式](https://github.com/flutter/flutter/pull/110600)。
在低延迟模式下Dart VM 的内存分配器会倾向堆增长而不是垃圾收集,以避免因 GC 暂停而中断过渡动画。
> 虽然类似更改不会带来任何显着的性能改进,但 Flutter 团队计划在未来的版本中扩展此模型的使用,以进一步消除不合时宜的 GC 暂停。
此外,本次还 修复了 Flutter 引擎空闲时通知 Dart VM 的 逻辑[错误](https://github.com/flutter/engine/pull/37737),修复这些错误可以防止与 GC 相关的卡顿。
最后,对于 add-to-app 的 Flutter 应用,当 Flutter 视图不再显示时 Flutter [会通知 Dart VM ](https://github.com/flutter/engine/pull/37539)引擎,当没有 Flutter 视图可见时Dart VM 为与视图关联的对象触发 GC ,此更改可以减少了 Flutter 的内存占用。
# 停用 macOS 10.11 到 10.13
Flutter 不再支持 macOS 10.11 和 10.12 版本3.7 版本发布后,也取消对 10.13 的支持,这可以并将帮助团队大大简化代码库。
这也意味着在 3.7 版本及以后版本中针对稳定的 Flutter SDK 构建的应用将不再适用于这些版本,并且 Flutter 支持的最低 macOS 版本增加到 10.14 Mojave。
因此,由于 Flutter 支持的所有 iOS 和 macOS 版本都包含 Metal 支持OpenGL 后端已从 iOS 和 macOS 嵌入器中删除删除这些后Flutter 引擎的压缩大小减少了大约 100KB。
# toImageSync
3.7 版本在 `dart:ui` 里 [添加了](https://github.com/flutter/engine/pull/33736) `Picture.toImageSync``Scene.toImageSync` 方法。
> 类似于异步 `Picture.toImage`,从 `Picture` 转化为 `Image` 时会从 `Scene.toImage.Picture.toImageSync ` 同步返回一个句柄,并在后台异步进行 `Image` 光栅化。
**当 GPU 上下文可用时,图像将保持为 GPU 常驻状态**,这意味着会比 `toImage` 具有更快的渲染速度(生成的图像也可以保留在 GPU 中,但这种优化尚未在该场景中实现。)
新的`toImageSync`API 支持用例,例如:
- 快速实现光栅化成本高昂的图片,以便在多个帧中重复使用。
- 对图片应用多通道滤镜。
- 应用自定义着色器。
例如Flutter 框架 [现在使用该 API](https://github.com/flutter/flutter/pull/106621) 来提高 Android 上页面转换的性能,这几乎将帧光栅化时间减半,减少卡顿,并允许动画在支持这些刷新率的设备上达到 90/120fps。
# 自定义 shader 改进
3.7 版本包含了对 Flutter 对自定义片段着色器支持的大量改进。
**Flutter SDK 现在包含一个着色器编译器,可将 `pubspec.yaml` 文件中列出的 GLSL 着色器编译为目标平台的正确特定格式**。
此外自定义着色器现在可以热加载iOS 上的 Skia 和 Impeller 后端现在也支持自定义着色器。
> [更多可见 docs.flutter.dev 上编写和使用自定义片段着色器](https://docs.flutter.dev/development/ui/advanced/shaders)文档,以及 pub.dev 上的 `flutter_shaders` 包。
# 字体热重载
以前向 `pubspec.yaml` 文件添加新字体需要重新运行应用才能看到它们,这个行为这与其他可以热加载的 asset 不同。
现在,对字体清单的更改(包括添加新字体)可以热加载到应用中。
# 减少 iOS 设备上的动画卡顿
感谢 [luckysmg ](https://github.com/luckysmg)的开源贡献改进减少了 iOS 上的动画卡顿,特别是手势期间在主线程上[添加虚拟](https://github.com/flutter/engine/pull/35592) `CADisplayLink` 对象,现在会强制以最大刷新率进行刷新。
此外,[键盘动画]((https://github.com/flutter/engine/pull/34871))现在将刷新率设置为 `CADisplayLink` ,与 Flutter 引擎动画使用的刷新率相同。
由于这些变化,用户应该注意到 120Hz iOS 设备上的动画更加一致流畅。
# 最后个人感想
以上就是来自 Flutter 团队关于 Flutter 3.7 的主要更新内容,可以看到本次更新内容相当丰富:
- 最显眼的莫过于 Impeller 在 iOS 可以预览,性能提升未来可期
- 关于菜单相关的更新,也极大丰富了 Flutter 在编辑和本次选择中的疲弱态势
- 全局选择的改进和文本放大镜也进一步完善了 Flutter 文本操作的生态
- 性能、内存优化老生常谈,特别是 iOS 上的优化
- 开发工具进一步提升
当然本次大版本更新设计的内容范围很广,可以预见会有各式各样的坑在等大家,特别本次更新很多涉及底层 Framework 部分,所以按照惯例,等三个小版本会更稳。

198
Flutter-FF2023.md Normal file
View File

@ -0,0 +1,198 @@
# 2023 Flutter Forward 大会回顾,快来看看 Flutter 的未来会有什么
[Flutter Forward](https://flutter.dev/events/flutter-forward) 作为一场 Flutter 的突破性发布会,事实上 [Flutter 3.7 在大会前已经发布](https://juejin.cn/post/7192468840016511034) ,所以本次大会更多是介绍未来的可能,核心集中于 *come on soon* 的支持,所以值得关注的内容很多,特别是一些 Feature 让人十分心动。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image1.png)
# 开始之前
按照惯例,在展望未来之前需要先总结过去,首先,到目前为止已经超过 700,000 个已发布应用使用了 Flutter例如腾讯知名的 PUBG 再次登上了大会 PPT。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image2.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image3.png) |
| ------------------------------------------------------ | ------------------------------------------------------ |
另外,如 [Google Classroom](https://edu.google.com/workspace-for-education/classroom/) 团队也分享了他们使用 Flutter 开发的经历和收获,包括了代码复用率和开发效率等。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image4.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image5.png) |
| ------------------------------------------------------ | ------------------------------------------------------ |
> “使用 Flutter我们将相同功能的代码大小减少了 66%……这意味着每个平台的错误更少未来的技术债务也更少。”Kenechi UfonduGoogle 课堂软件工程师)
另外从 Flutter 目前的用户数据情况看,当前阶段 Flutter 还是很受欢迎的。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image6.png)
而关于 Flutter 3.7 部分这里就不再赘述,感兴趣可以看前面已经发布的 [Flutter 3.7 的更新说明](https://juejin.cn/post/7192468840016511034) 。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image7.png)
**本次 Flutter 还安利了两个低代码的友商平台:[FlutterFlow](https://flutterflow.io/) 和 [WidgetBook](https://www.widgetbook.io/)** 。
不得不说它们的成熟度都挺高的,例如 FlutterFlow 的在线调试运行和翻译支持就相当惊艳。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image9.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image10.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image11.png) |
另外 WidgetBook 作为开源项目,它支持 Flutter 开发者针对自己的控件进行分类归纳,同时可以在使用 Widgetbook Cloud 的情况下,将 Widget 与 Figma 同步并和团队共享,为设计和开发人员提供更灵活的协作工具。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image12.png)
> FlutterFlow 并不是完全免费哦。
# Dart 3 alpha
本次大会的另外一个重点就是 Dart 3 alpha ,其实在此之前官方就有提前预热过,在[《Flutter 的下一步, Dart 3 重大变更即将在 2023 到来》](https://juejin.cn/post/7174985128799076389) 里我们就提前预览过对应更新,其中大家最关注的莫过于 [Records](https://github.com/dart-lang/language/blob/master/accepted/future-releases/records/records-feature-specification.md) 和 [Patterns](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#patterns ) 。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image13.png)
**Records 支持高效简洁地创建匿名复合值,不需要再声明一个类来保存,而在 Records 组合数据的地方Patterns 可以将复合数据分解为其组成部分**。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image14.png)
> 例如要将 `geoLocation` 上面的返回值(由一对整数组成的记录)解构为两个单独的 `int` 变量 `lat``long`,就可以使用这样的 Patterns 声明。
Patterns 是完全类型安全的支持,并且会在开发过程中进行错误检查。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image15.png)
你还可以对值的类型进行 Patterns 匹配,通过 `switch`可以使用匹配类型的 Patterns ,以及每种类型的字段。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image16.png)
当然Dart 3 还有一个重点就是 100% 空安全的要求,也就是不再支持非空安全的代码,这对于旧项目来说是很大的挑战,相信还是有相当一大部分人的 Flutter 项目一直维持在低版本。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image17.png)
Dart 3 还进行了很大程度的优化, 例如 Dart 3 进行了清理一些不必要的 API ,同时对编译做了很大的优化,例如下图是变异后的代码对比。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image18.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image19.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
另外 Dart 3 将支持更多的平台架构,例如 [RISC-V](https://en.wikipedia.org/wiki/RISC-V) ,同时还在覆盖 Windows 上的 ARM64 支持,而 Web 上 Dart 3 也将可以脱离 Flutter 直接支持 [WebAssembly (Wasm)](https://webassembly.org/) 。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image20.png)
最后在新工具的支持下Dart 可以根据 C/ObjC/Swift/Java/Kotlin 代码的头文件/接口文件,自动创建具有 Dart 接口的绑定,以及那些跨语言调用所需的自动绑定,也就是 FFIgen + JNIgen。
> 具体可见https://github.com/flutter/samples/blob/main/experimental/pedometer/README.md
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image21.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image22.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
# Web
本次还有一个惊喜就是 add-to-web 要来了, 一个叫做 **element embedding** 的支持即将到来。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image23.png)
**element embedding 允许将 Flutter 添加到任何 Web `<div>`中** 当以这种方式嵌入时Flutter 就变成了一个 Web 组件与 Web DOM 完全集成,甚至可以使用 CSS 来设置父 Flutter 对象的样式。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image24.gif)
> 例如将 Flutter 嵌入到基于 HTML 的网页中,然后使用 CSS 旋转效果,并且在旋转时 Flutter 内容仍可以交互。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image25.png)
同时 Dart 3 还对 Pub 上的 [js 包](http://pub.dev/packages/js)进行了一些重大更改,从而实现 **JavaScript 和 Dart 之间可以直接调用**,如里使用 `@JSExport` 属性注释 Dart 代码中的任何函数,然后从 JS 代码中调用它。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image26.png)
除此之外 Flutter Web 也有一系列的优化计划,其中针对体积大小的优化是最重要的指标之一。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image27.png)
从官方提供的数据下看,未来 Flutter Web 的加载速度将会不断提升。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image28.png)
最后,现在 Flutter 支持在 Web 上的使用 Pixel shaders ,从而实现各种炫酷的视觉效果。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image29.png)
![](http://img.cdn.guoshuyu.cn/20230126_FF/image30.gif)
# Flutter 新闻工具包
本次还有一个有意思但是对国内比较鸡肋的介绍: [Flutter News Toolkit](https://github.com/flutter/news_toolkit),一个用来加速新闻应用开发的免费 Flutter 应用模板。
这是 Flutter 团队和 [GNI](https://newsinitiative.withgoogle.com/) 合作的项目,官方宣称与 iOS 和 Android 上的传统双端开发相比,在该领域使用 FNT 可以节省高达 80% 的时间。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image31.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image32.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
# 使用 Wonderous 适应大屏幕
Wonderous 早在去年 9 月份官方就[推荐过一次](https://mp.weixin.qq.com/s/cAwU2RmG-VtTBjPLweoobg) ,这一次主要是介绍了 Wonderous 的下一个版本,**增加了对可折叠设备、平板电脑和平板电脑横向的支持**。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image33.png)
> 此次迭代同时测试了 Flutter 对不同设备格式的适配能力, 具体可见https://github.com/gskinnerTeam/flutter-wonderous-app
![](http://img.cdn.guoshuyu.cn/20230126_FF/image34.png)
# Impeller
**随着 3.7 的发布Impeller 现在已经可以在 iOS 上进行预览**。
Impeller 针对 Flutter 进行了优化,提供了更大的灵活性和对图形管道的控制支持。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image35.png)
例如使用预编译着色器,减少运行时由着色器编译引起的丢帧,利用 Metal 和 Vulkan 中的原始支持等等。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image36.png)
除了让 UI 更流畅Impeller 还可以在某些极端情况下显着提高性能,比如大会介绍的一个例子:
> 左边是默认渲染器,右边是 Impeller可以看到滚动是左侧因为性能问题导致帧速率为 7-10 fps而右侧 Impeller 可以稳定在 60 fps 。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image37.png)
# 3D
**本次最后一个亮点就是 Flutter 未来将正式支持 3D 渲染**,同时也代表着 Flutter 在游戏领域的更进一步。
> 其实从去年的 I/O 也好,还有本次 [Flutter Forward](https://flutter.dev/events/flutter-forward) 提前预热的相关内容,可以看到 Flutter 进军游戏领域一直没有停歇。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image38.png)
在本次演示中,除了支持 3D 渲染之外,还支持对 3D 文件资源进行 hotload 、添加动画支持。
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image39.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image40.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
可以看到,在演示中多个 3D 模型同时渲染动画的情况下,画面依然可以流畅运行,这绝对是本次 Flutter Forward 最让人期待的特性。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image41.gif)
最后官方还演示了在低端 iPhone 上的 3d 游戏场景(有指纹解锁的老 iPhone ),可以看到画面还是相当流畅。
![](http://img.cdn.guoshuyu.cn/20230126_FF/image42.gif)
# 最后
**看完之后你是不是蠢蠢欲动?但是这里面绝大多的都还只是开发中,可能会在未来可能还会有其他变动**,而本次 Flutter Forward 展示它们的目的,相信也是官方想让大家更直观地了解 Flutter 未来的方向。
最后总结一下,本次 Flutter Forward 主要的核心内容有:
- Impeller
- 3D 支持
- add-to-web 支持
- Dart 3
让我们期待未来 Flutter 的更新能让这些 Feature 都能用上吧,在没有坑的情况下~

449
Flutter-GPT.md Normal file
View File

@ -0,0 +1,449 @@
# 面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯” 到 “觉悟” ,未来又如何落地
对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等而其中不乏一些玩的很「花」的场景例如
- [ChatPDF](https://www.chatpdf.com/) :使用 ChatPDF 读取 PDF 之后,你可以和 PDF 文件进行「交谈」,就好像它是一个完全理解内容的「人」一样,通过它可以**总结中心思想,解读专业论文,生成内容摘要,翻译外籍,并且还支持中文输出等**。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image1.png)
- [BiBiGPT](https://b.jimmylv.cn/video/BV1uM411P7oA?spm_id_from=333.1007.tianma.2-1-4.click) : **一键总结视频内容**,主要依赖字幕来做总结,绝对是「二创」作者的摸鱼利器。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image2.png)
所以把 ChatGPT 理解为「搜索引擎」其实并不正确,从上述介绍的两个落地实现上看, **ChatGPT 不是单纯的统计模型,它的核心并不是完全依赖于它的「语料库」,更多来自于临场学习的能力「 in-context learning」这就是 ChatGPT 不同于以往传统 NLP「一切都从语料的统计里学习」的原因**
> 当然,我本身并非人工智能领域的开发者,而作为一个普通开发者,我更关心的是 ChatGPT 可以如何提升我的开moyu效率只是没想到随手一试我会被 ChatGPT 的 「 in-context learning」 给「逼疯」。
# ChatGPT & UI
相信大家平时「面向」 ChatGPT 开发时,也是通过它来输出「算法」或者「 CURD」 等逻辑居多,因为这部分输出看起来相对会比较直观,而用 ChatGPT 来绘制前端 UI 的人应该不多,因为 UI 效果从代码上看并不直观 ,而且 ChatGPT 对与 UI 的理解目前还处于 「人工智障」的阶段。
>但是我偏偏不信邪。。。。。
因为近期开发需求里恰好需要绘制一个具有动画效果的 ⭐️ 按键,面对这么「没有挑战性」的工作我决定尝试交给 ChatGPT 来完成,所以我向 ChatGPT 发起了第一个命令:
> 「用 Flutter 画一个黄色的五角星」
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image3.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image4.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
结果不负众望关键部分如下代码所示Flutter 很快就提供了完整的 Dart 代码,并且还针对代码提供了代码相关实现的讲解,不过运行之后可以看到,这时候的 ⭐️ 的样式并不满足我们的需求。
> 此时顶部的角也太「肥」了 。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image5.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image6.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
所以我随着提出了调整,希望五角星的五个角能够一样大,**只是没想到我的描述,开始让 ChatGPT 放飞自我** 。
> 也许是我的描述并不准确?
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image7.png)
在我满怀期待的 `cv` 代码并运行之后,**猝不及防的「五角星」差点没让我喷出一口老血**,虽然这也有五个角,但是你管这个叫 「五角星」
> 这难道不是某个红白机游戏里的小飞机??
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image9.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
甚至于在看到后续 ChatGPT 关于代码的相关讲解时,**我觉得它已经开始在「一本正经的胡说八道」,像极了今天早上刚给我提需求的产品经理**。
> 哪里可以看出五个角相同了???
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image10.png)
接着我继续纠正我的需求,表示我要的是 `「一个五个角一样大的黄色五角星」` ,我以为这样的描述应过比较贴切,须不知·····
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image11.png)
如下代码所示,其实在看到代码输出 `for` 循环时我就觉得不对了,但是秉承着「一切以实物为准」的理念,在运行后不出意外的发生了意外,确实是五个角一样大,不过是一个等边五边形。
> 算一个发胖的 ⭐️ 能解jiaobian过去不
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image12.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image13.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
再看 ChatGPT 对于代码的描述,我发现我错了,**原来它像的是「理解错需求还在嘴硬的我」,只是它在说「这是一个五角星」的时候眼皮都不会眨一下**。
> AI确实五个角一样大五个角一样大的五边形为什么就不能是五角星你这是歧视体型吗
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image14.png)
所以我继续要求:`「我要的是五角星,不是五边形」`,还好 ChatGPT 的临场学习能力不错,他又一次「重新定义五角星」,**不过我此时我也不抱希望,就是单纯想看看它还能给出什么「惊喜」**。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image15.png)
不出意外,这个「离谱」的多边形让我心头一紧,就在我想着是否放弃的时候,身为人类无法驯服 AI 「既爱又恨」的复杂情绪,让我最终坚持一定要让 ChatGPT 给我画出一个 ⭐️。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image16.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image17.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
不过心灰意冷之下,我选择让 ChatGPT 重新画一个黄色五角星,没想道这次却有了意外的惊喜,从下面的图片可以看到,此时的 ⭐️ 除了角度不对,形状已经完全满足需求。
> 所以一个问题我多问几遍,也许就能接近我要的答案?
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image18.png)
事实上这也是目前 ChatGPT 的现状,因为「临场学力」能力等因素影响,**同一个问题它可能会给出不同的答案,而有的答案其实和我们要的根本不沾边**。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image19.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image20.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
那么,接下来只要让 ChatGPT 把 ⭐️ 旋转一下角度,应该就可以完成需求····了吧?所以我提出`「帮我旋转 180度」`的要求。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image21.png)
结果不出意外的还是发生了意外,可能 ChatGPT 理解的 180 度和我不大一样,如下图所示,确实旋转了,只是还是歪的,而基于前面的尝试,我觉得有必要再给它一次机会。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image22.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image23.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
终于在我换了以下描述之后ChatGPT 似乎「开窍」了,终于让我如愿以偿的得到了我想要的 ⭐️ 代码。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image24.png)
可以看到,其实这段代码并不复杂,你自己写或者去搜索引擎扒一扒大概会比用 ChatGPT 更快,但是 ChatGPT 的魅力就在于:**它也许会错,但是总有一次它会「蒙对」,如果现在还不对,那么未来也会有算对的时候,这就是我感受到的 AI 魅力所在**。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image25.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image26.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
既然五角星都出来,那么接下来我们来完成最后一步的动画效果,我们希望的是点击 ⭐️ 的时候可以有一个爆炸粒子的动画,所以我提出了:
> 「希望加上一个动画效果,就是点击的时候星星爆炸成颗粒的动画效果」
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image27.png)
结果还是「一口老血」,如下图所示,「星星确实爆炸了」,**只是这个爆炸动画看起来是那么的「一言难尽」**,感兴趣的可以 cv 下面的源代码自己体验下,不出意外接下来又是新的一轮人类和 AI 的「斗智斗勇」。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image28.gif)
```dart
class YellowStar extends StatefulWidget {
@override
_YellowStarState createState() => _YellowStarState();
}
class _YellowStarState extends State<YellowStar> with TickerProviderStateMixin {
bool _isExploded = false;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTapDown(TapDownDetails details) {
setState(() {
_isExploded = true;
_animationController.forward(from: 0.0);
});
}
void _resetStar() {
setState(() {
_isExploded = false;
_animationController.reset();
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTap: _resetStar,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: _isExploded
? BorderRadius.circular(50)
: BorderRadius.zero,
),
child: _isExploded
? _buildParticles()
: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: YellowStarPainter(),
),
),
),
);
}
Widget _buildParticles() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: 100,
height: 100,
child: Stack(
children: List.generate(
50,
(index) {
double radius = _animation.value * 50;
double angle = 2 * pi * index / 50;
double x = 50 + cos(angle) * radius;
double y = 50 + sin(angle) * radius;
return Positioned(
left: x,
top: y,
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.yellow,
),
),
);
},
),
),
);
},
);
}
}
class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;
@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;
Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;
for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```
经过一轮「鏖战」如下图所示面对同一个问题ChatGPT 总能找到不同的实现方式来回答你,只是这些实现里有的无法运行的,有的是运行没有效果的,有的甚至是给你输出了一个 `//TODO 爆炸动画` ,总之就是让你「血压飙升」。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image29.png)
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image30.png)
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image31.png)
当然,「胜利终将属于人类」,如下图所示,在经历了无数次「血压飙升」的场景之后,最终我还是得到了我想要的:点击 ⭐️,然后出现了💥的动画效果,尽管还是不完美,但是基本满足了我后续开发的需求。
> **这个代码完完全全由 ChatGPT 输出,我一句没改**,完整代码贴在了后面。
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image32.gif)
```dart
class YellowStarPage extends StatefulWidget {
@override
_YellowStarPageState createState() => _YellowStarPageState();
}
class _YellowStarPageState extends State<YellowStarPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
bool _isExploded = false;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleStarTap() {
if (!_isExploded) {
_isExploded = true;
_animationController.forward(from: 0);
Future.delayed(Duration(milliseconds: 500), () {
setState(() {
_isExploded = false;
});
});
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Yellow Star')),
body: Center(
child: GestureDetector(
onTap: _handleStarTap,
child: Container(
width: 300,
height: 300,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: YellowStarPainter(_animationController.value,
isExploded: _isExploded),
);
},
),
),
),
),
);
}
}
class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;
final double animationValue;
final bool isExploded;
YellowStarPainter(this.animationValue, {this.isExploded = false});
@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;
Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;
if (isExploded) {
double particleSize = starSize / 30;
paint.strokeWidth = 1;
paint.style = PaintingStyle.fill;
paint.color = Colors.yellow;
Random random = Random();
for (int i = 0; i < 30; i++) {
double dx = random.nextDouble() * starSize - starSize / 2;
double dy = random.nextDouble() * starSize - starSize / 2;
double x = centerX + dx * (1 + animationValue);
double y = centerY + dy * (1 + animationValue);
canvas.drawCircle(Offset(x, y), particleSize, paint);
}
} else {
for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}
path.close();
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```
最后,给大家欣赏一下我让 ChatGPT 画一只米老鼠的「心路历程」,**很明显这一次「人类一败涂地」**,从目前的支持上看,让 ChatGPT 输出复杂图像内容并不理想,因为它不的笔画「不会拐弯」。
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image33.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image34.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image35.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image36.png) |
| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
> 真的是又爱又恨。
# 最后
经过上面的一系列「折腾」,**可以看到 ChatGPT 并没有我们想象中智能,如果面向 GPT 去开发,甚至可能并不靠谱,因为它并不对单一问题给出固定答案,甚至很多内容都是临场瞎编的**,这也是因为大语言模型本身如何保证「正确」是一个复杂的问题,但是 ChatGPT 的魅力也来自于此:
> **它并不是完全基于语料来的统计来给答案**
当然这也和 ChatGPT 本身的属性有关系, ChatGPT 目前的火爆有很大一部分属于「意外」,目前看它不是一个被精心产品化后的 2C 产品,反而 [ChatPDF](https://www.chatpdf.com/) 和 [BiBiGPT](https://b.jimmylv.cn/video/BV1uM411P7oA?spm_id_from=333.1007.tianma.2-1-4.click) 这种场景化的包装落地会是它未来的方向之一。
而现在 OpenAI 发布了多模态预训练大模型 [CPT-4](https://mp.weixin.qq.com/s/kA7FBZsT6SIvwIkRwFS-xw) **GPT-4 按照官方的说法是又得到了飞跃式提升:强大的识图能力;文字输入限制提升至 2.5 万字;回答准确性显著提高;能够生成歌词、创意文本,实现风格变化等等**
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image37.gif)
所以我很期待 ChatGPT 可以用 Flutter 帮我画出一只米老鼠, 尽管 ChatGPT 现在可能会让你因为得到 `1+1=3` 这样的答案而「发疯”」,**但是 AI 的魅力在于,它终有一天能得到准确的结果** 。

192
Flutter-N16.md Normal file
View File

@ -0,0 +1,192 @@
# Flutter 小技巧之 3.7 性能优化background isolate
Flutter 3.7 的 background isolate 绝对是一大惊喜,尽管它在 [release note](https://juejin.cn/post/7192468840016511034#heading-10) 里被一笔带过 ,但是某种程度上它可以说是 3.7 里最实用的存在:**因为使用简单,提升又直观**。
> Background isolate YYDS
## 前言
我们知道 Dart 里可以通过新建 isolate 来执行”真“异步任务,而本身我们的 Dart 代码也是运行在一个独立的 isolate 里(简称 root isolate而 isolate 之间不共享内存,只能通过消息传递在 isolates 之间交换状态。
> 所以 Dart 里不像 Java 一样需要线程锁。
而在 Dart 2.15 里新增了 isolate groups 的概念,**isolate groups 中的 isolate 共享程序里的各种内部数据结构**,也就是虽然 isolate groups 还是不允许 isolate 之间共享可变对象,但 groups 可以通过共享堆来实现结构共享,例如:
> Dart 2.15 后可以将对象直接从一个 isolate 传递到另一 isolate而在此之前只支持基础数据类型。
那么如果使用场景来到 Flutter Plugin **在 Flutter 3.7 之前,我们只能从 root isolate 去调用 Platform Channels** ,如果你尝试从其他 isolate 去调用 Platform Channels ,就会收获这样的错误警告:
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image1.png)
> 例如,在 Flutter 3.7 之前Platform Channels 是和 `_DefaultBinaryMessenger ` 这个全局对象进行通信,但是一但切换了 isolate ,它就会变为 null ,因为 isolate 之间不共享内存。
而从 Flutter 3.7 开始简单地说Flutter 会通过新增的 BinaryMessenger 来实现非 root isolate 也可以和 Platform Channels 直接通信,例如:
> 我们可以在全新的 isolate 里,通过 Platform Channels 获取到平台上的原始图片后,在这个独立的 isolate 进行一些数据处理,然后再把数据返回给 root isolate ,这样数据处理逻辑既可以实现跨平台通用,又不会卡顿 root isolate 的运行。
# Background isolate
现在 Flutter 在 Flutter 3.7 里引入了 `RootIsolateToken``BackgroundIsolateBinaryMessenger` 两个对象,当 background isolate 调用 Platform Channels 时, background isolate 需要和 root isolate 建立关联,所以在 API 使用上大概会是如下代码所示:
```dart
RootIsolateToken rootIsolateToken =
RootIsolateToken.instance!;
Isolate.spawn((rootIsolateToken) {
doFind2(rootIsolateToken);
}, rootIsolateToken);
doFind2(RootIsolateToken rootIsolateToken) {
// Register the background isolate with the root isolate.
BackgroundIsolateBinaryMessenger
.ensureInitialized(rootIsolateToken);
//......
}
```
通过 `RootIsolateToken` 的单例,我们可以获取到当前 root isolate 的 Token ,然后在调用 Platform Channels 之前通过 `ensureInitialized` 将 background isolate 需要和 root isolate 建立关联。
> 大概就是 token 会被注册到 `DartPluginRegistrant` 里,然后 `BinaryMessenger``_findBinaryMessenger` 时会通过 `BackgroundIsolateBinaryMessenger.instance` 发送到对应的 `listener`
完整代码如下所示,逻辑也很简单,就是在 root isolate 里获取 `RootIsolateToken` ,然后在调用 Platform Channels 之前 `ensureInitialized` 关联 Token 。
```dart
InkWell(
onTap: () {
///获取 Token
RootIsolateToken rootIsolateToken =
RootIsolateToken.instance!;
Isolate.spawn(doFind, rootIsolateToken);
},
////////////////
doFind(rootIsolateToken) async {
/// 注册 root isolaote
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
///获取 sharedPreferencesSet 的 isDebug 标识位
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
.then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
/// 获取本地目录
final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
/// 合并执行
var values = await Future.wait([sharedPreferencesSet, tempDirFuture]);
final Directory? tempDir = values[1] as Directory?;
final String dbPath = path.join(tempDir!.path, 'database.db');
File file = File(dbPath);
if (file.existsSync()) {
///读取文件
RandomAccessFile reader = file.openSync();
List<int> buffer = List.filled(256, 0);
while (reader.readIntoSync(buffer) == 256) {
List<int> foo = buffer.takeWhile((value) => value != 0).toList();
///读取结果
String string = utf8.decode(foo);
print("######### $string");
}
reader.closeSync();
}
}
```
> 这里之所以可以在 isolate 里直接传递 `RootIsolateToken` ,就是得益于前面所说的 Dart 2.15 的 isolate groups
其实入下代码所示,上面的实现换成 `compute` 也可以正常执行,当然,**如果是 `compute` 的话,有一些比较特殊情况需要注意**。
```dart
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
compute(doFind, rootIsolateToken);
```
如下代码所示, `doFind2` 方法在 `doFind` 的基础上,将 `Future.wait``await` 修改为 `.then` 去执行,如果这时候你再调用 `spawn``compute` ,你就会发现 **`spawn` 下代码依然可以正常执行,但是 `compute` 却不再正常执行**。
```dart
onTap: () {
RootIsolateToken rootIsolateToken =
RootIsolateToken.instance!;
compute(doFind2, rootIsolateToken);
},
onTap: () {
RootIsolateToken rootIsolateToken =
RootIsolateToken.instance!;
Isolate.spawn(doFind2, rootIsolateToken);
},
doFind2(rootIsolateToken) async {
/// 注册 root isolaote
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
///获取 sharedPreferencesSet 的 isDebug 标识位
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
.then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
/// 获取本地目录
final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
///////////////////// Change Here //////////////////
/// 合并执行
Future.wait([sharedPreferencesSet, tempDirFuture]).then((values) {
final Directory? tempDir = values[1] as Directory?;
final String dbPath = path.join(tempDir!.path, 'database.db');
///读取文件
File file = File(dbPath);
if (file.existsSync()) {
RandomAccessFile reader = file.openSync();
List<int> buffer = List.filled(256, 0);
while (reader.readIntoSync(buffer) == 256) {
List<int> foo = buffer.takeWhile((value) => value != 0).toList();
String string = utf8.decode(foo);
print("######### $string");
}
reader.closeSync();
}
}).catchError((e) {
print(e);
});
}
```
为什么会这样?`compute` 不就是 Flutter 针对 `Isolate.spawn` 的简易封装吗?
> 其实原因就在这个封装上,**`compute` 现在不是直接执行 `Isolate.spawn` 代码,而是执行 `Isolate.run`** ,而 `Isolate.run` 针对 `Isolate.spawn` 做了一些特殊封装。
`compute` 内部会将执行对象封装成 `_RemoteRunner` 再交给 `Isolate.spawn` 执行,而 `_RemoteRunner` 在执行时,会在最后强制调用 `Isolate.exit` ,这就会导致前面的 `Future.wait` 还没执行,而 `Isolate` 就退出了,从而导致代码无效的原因。
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image2.png)
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image3.png)
另外在 Flutter 3.7 上 ,如果 background isolate 调用 Platform Channels 没有关联 root isolate也能看到错误提示你初始化关联所以这也是为什么我说它使用起来很简单的原因。
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image4.png)
除此之外,最近刚好遇到有“机智”的小伙伴说 background isolate 无法正常调用,看了下代码是把 `RootIsolateToken.instance!;` 写到了 background isolate 执行的方法里。
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image5.png)
> 你猜如果这样有效,为什么官方不直接把这个获取写死在 framewok
其实这也是 isolates 经常引起歧义的原因isolates 是隔离,内存不共享数据,所以 root isolate 里的 `RootIsolateToken` 在 background isolate 里直接获肯定是 null ,所以这也是 isolate 使用时需要格外注意的一些小细节。
> 另外还有如 [#36983](https://github.com/dart-lang/sdk/issues/36983 ) 等问题,也推动了前面所说的 `compute` 相关的更改。
最后,如果需要一个完整 Demo 的话,可以参考官方的 [background_isolate_channels](https://github.com/flutter/samples/tree/294ea4ff8fce588f332e82e3ee97fa3d3429c9a4/background_isolate_channels) ,项目里主要通过 `SimpleDatabase``_SimpleDatabaseServer` 的交互,来模拟展示 root isolate 和 background isolate 的调用实现。
# 最后
总的来说 background isolate 并不难理解,自从 2018 年在 [issue #13937](https://github.com/flutter/flutter/issues/13937) 被提出之后就饱受关注,甚至官方还建议过大家通过 ffi 另辟蹊径去实现,当时的 issue 也被搭上了` P5` 的 Tag。
> 相信大家都知道 P5 意味着什么。
所以 background isolate 能在 Flutter 3.7 看到是相当难得的,当然这也离不开 Dart 的日益成熟的支持,同时 background isolate 也给我们带来了更多的可能性,其中最直观就是性能优化上多了新的可能,代码写起来也变得更顺畅。
期待 Flutter 和 Dart 在后续的版本中还能给我们带来更多的惊喜。

264
Flutter-N18.md Normal file
View File

@ -0,0 +1,264 @@
# Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?
随着 Flutter 3.7 的更新, `dart:ui` 下多了 `Picture.toImageSync``Scene.toImageSync` 这两个方法,和`Picture.toImage` 以及 `Scene.toImage` 不同的是 `toImageSync` 是一个同步执行方法,所以它不需要 `await` 等待,而调用 `toImageSync` 会直接返回一个 Image 的句柄,并在 Engine 后台会异步对这个 Image 进行光栅化处理。
# 前言
`toImageSync ` 有什么用?不是有个 `toImage` 方法了,为什么要多一个 Sync 这样的同步方法?
- **目前 `toImageSync ` 最大的特点就是图像会在 GPU 中常驻** ,所以对比 `toImage` 生成的图像,它的绘制速度会更快,并且可以重复利用,提高效率。
> `toImage` 生成的图像也可以实现 GPU 常驻,但目前没有未实现而已。
- `toImageSync ` 是一个同步方法,在某些场景上弥补了 `toImage` 必须是异步的不足。
![](http://img.cdn.guoshuyu.cn/20230207_sync/image1.png)
`toImageSync ` 的使用场景上,官方也列举了一些用途,例如:
- 快速捕捉一张昂贵的栅格化图片,用户支持跨多帧重复使用
- 应用在图片的多路过滤器上
- 应用在自定义着色器上
具体在 Flutter Framework 里,目前 `toImageSync ` 最直观的实现,就是被使用在 Android 默认的页面切换动画 `ZoomPageTransitionsBuilder ` 上,得意于 `toImageSync ` 的特性Android 上的页面切换动画的性能,**几乎减少了帧光栅化一半的时间**,从而减少了掉帧和提高了刷新率。
> 当然,这是通过牺牲了一些其他特性来实现,后面我们会讲到。
# SnapshotWidget
前面说了 `toImageSync ` 让 Android 的默认页面切换动画性能得到了大幅提升,那究竟是如何实现的呢?这就要聊到 Flutter 3.7 里新增加的 `SnapshotWidget`
其实一开始 `SnapshotWidget` 是被定义为 `RasterWidget` ,从初始定义上看它的 Target 更大,但是最终在落地的时候,被简化处理为了 `SnapshotWidget` ,而从使用上看确实 Snapshot 更符合它的设定。
![](http://img.cdn.guoshuyu.cn/20230207_sync/image2.png)
## 概念
**`SnapshotWidget` 的作用是可以将 Child 变成的快照(`ui.Image`)从而替换它们进行显示,简而言之就是把子控件都变成一个快照图片**,而 `SnapshotWidget` 得到快照的办法就是 `Scene.toImageSync`
> 那么到这里,你应该知道为什么 `toImageSync ` 可以提高 Android 上的页面切换动画的性能了吧?因为 `SnapshotWidget` 会在页面跳转时把 Child 变成的快照,而 `toImageSync ` 栅格化的图片还可以跨多帧重复使用。
那么问题来了,`SnapshotWidget` 既然是通过 `toImageSync ` 将 Child 变成的快照(`ui.Image`)来提高性能,那么带来的副作用是什么?
答案是动画效果,**因为子控件都变成了快照,所以如果 Child 控件带有动画效果,会呈现“冻结”状态**,更形象的对比如下图所示:
| FadeUpwardsPageTransitionsBuilder | ZoomPageTransitionsBuilder |
| -------------------------------------------------------- | -------------------------------------------------------- |
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image3.gif) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image4.gif) |
默认情况下 Flutter 在 Android 上的页面切换效果使用的是 `ZoomPageTransitionsBuilder` ,而 `ZoomPageTransitionsBuilder` 里在页面切换时会开启 `SnapshotWidget` 的截图能力,所以可以看到,它在页面跳转时,对比 `FadeUpwardsPageTransitionsBuilder` 动图, `ZoomPageTransitionsBuilder` 的红色方块和掘金动画会停止。
> 因为动画很短,所以可以在代码里设置 **` timeDilation = 40.0;`** 和 `SchedulerBinding.resetEpoch` 来全局减慢动画执行的速度,另外可以配置 `MaterialApp ``ThemeData` 下对应的 `pageTransitionsTheme` 来切换页面跳转效果。
所以在官方的定义中,**`SnapshotWidget` 是用来协助执行一些简短的动画效果**,比如一些 scale 、 skew 或者 blurs 动画在一些复杂的 child 构建上开销会很大,而使用 `toImageSync ` 实现的 `SnapshotWidget` 可以依赖光栅缓存:
> 对于一些简短的动画,例如 `ZoomPageTransitionsBuilder` 的页面跳转, `SnapshotWidget` 会将页面内的 children 都转化为快照(`ui.Image`),尽管页面切换时会导致 child 动画“冻结”,但是实际页面切换时长很短,所以看不出什么异常,**而带来的切换动画流畅度是清晰可见的**。
再举个更直观的例子,如下代码所示,运行后我们可以看到一个旋转的 logo 在屏幕上随机滚动,这里分别使用了 `AnimatedSlide``AnimatedRotation` 执行移动和旋转动画。
```dart
Timer.periodic(const Duration(seconds: 2), (timer) {
final random = Random();
x = random.nextInt(6) - 3;
y = random.nextInt(6) - 3;
r = random.nextDouble() * 2 * pi;
setState(() {});
});
AnimatedSlide(
offset: Offset(x.floorToDouble(), y.floorToDouble()),
duration: Duration(milliseconds: 1500),
curve: Curves.easeInOut,
child: AnimatedRotation(
turns: r,
duration: Duration(milliseconds: 1500),
child: Image.asset(
'static/test_logo.png',
width: 100,
height: 100,
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20230207_sync/image5.gif)
如果这时候在 `AnimatedRotation` 上层加多一个 `SnapshotWidget` ,并且打开 `allowSnapshotting` ,可以看到此时 logo 不再转动,因为整个 child 已经被转化为快照(`ui.Image`)。
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image6.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image7.gif) |
| -------------------------------------------------------- | -------------------------------------------------------- |
>所以 `SnapshotWidget` 不适用于子控件还需要继续动画或有交互响应的地方,例如轮播图。
## 使用
如之前的代码所示,使用 `SnapshotWidget` 也相对简单,你只需要配置 `SnapshotController` ,然后通过 `allowSnapshotting `控制子控件是否渲染为快照即可。
```dart
controller.allowSnapshotting = true;
```
`SnapshotWidget` 在捕获快照时,会生成一个全新的 `OffsetLayer``PaintingContext`,然后通过 `super.paint` 完成内容捕获(这也是为什么不支持 PlatformView 的原因之一),之后通过 `toImageSync` 得到完整的快照(`ui.Image`)数据,并交给 `SnapshotPainter` 进行绘制。
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image9.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
所以 `SnapshotWidget` 完成图片绘制会需要一个 `SnapshotPainter` ,默认它是通过内置的 `_DefaultSnapshotPainter` 实现,当然我们也可以自定义实现 `SnapshotPainter` 来完成自定义逻辑。
> 从实现上看,`SnapshotPainter` 用来绘制子控件快照的接口,正如上面代码所示,会根据 child 是否支持捕获(`_childRaster == null`),从而选择调用 `paint``paintSnapshot` 来实现绘制。
另外,目前受制于 `toImageSync ` 的底层实现, `SnapshotWidget` 无法捕获 PlatformView 子控件,如果遇到 PlatformView`SnapshotWidget` 会根据 `SnapshotMode` 来决定它的行为:
| normal | 默认行为,如果遇到无法捕获快照的子控件,直接 thrown |
| ---------- | ---------------------------------------------------------- |
| permissive | 宽松行为,遇到无法捕获快照的子控件,使用未快照的子对象渲染 |
| forced | 强制行为,遇到无法捕获快照的子控件直接忽略 |
另外 `SnapshotPainter` 可以通过调用 `notifyListeners` 触发 `SnapshotWidget` 使用相同的光栅进行重绘,简单来说就是:
> **你可以在不需要重新生成新快照的情况下,对当然快照进行一些缩放、模糊、旋转等效果,这对性能会有很大提升**
所以在 `SnapshotPainter` 里主要需要实现的是 `paint``paintSnapshot` 两个方法:
- paintSnapshot 是绘制 child 快照时会被调用
- paint 方法里主要是通过 `painter` (对应 `super.paint`)这个 Callback 绘制 child ,当快照被禁用或者 `permissive` 模式下遭遇 PlatformView 时会调用此方法
![](http://img.cdn.guoshuyu.cn/20230207_sync/image10.png)
举个例子,如下代码所示,在 `paintSnapshot` 方法里,通过调整 `Paint ..color` ,可以在前面的小 Logo 快照上添加透明度效果:
```dart
class TestPainter extends SnapshotPainter {
final Animation<double> animation;
TestPainter({
required this.animation,
});
@override
void paint(PaintingContext context, ui.Offset offset, Size size,
PaintingContextCallback painter) {}
@override
void paintSnapshot(PaintingContext context, Offset offset, Size size,
ui.Image image, Size sourceSize, double pixelRatio) {
final Rect src = Rect.fromLTWH(0, 0, sourceSize.width, sourceSize.height);
final Rect dst =
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
final Paint paint = Paint()
..color = Color.fromRGBO(0, 0, 0, animation.value)
..filterQuality = FilterQuality.low;
context.canvas.drawImageRect(image, src, dst, paint);
}
@override
void dispose() {
super.dispose();
}
@override
bool shouldRepaint(covariant TestPainter oldDelegate) {
return oldDelegate.animation.value != animation.value;
}
}
```
![](http://img.cdn.guoshuyu.cn/20230207_sync/image11.gif)
其实还可以把移动的动画部分挪到 `paintSnapshot` 里,然后通过对 animation 的状态进行管理,然后通过 `notifyListeners` 直接更新快照绘制这样在性能上会更有优势Android 上的 `ZoomPageTransitionsBuilder` 就是类似实现。
```dart
animation.addListener(notifyListeners);
animation.addStatusListener(_onStatusChange);
void _onStatusChange(_) {
notifyListeners();
}
@override
void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
_drawMove(context, offset, size);
}
@override
void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
switch (animation.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
return painter(context, offset);
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
....
}
```
> 更多详细可以参考系统 `ZoomPageTransitionsBuilder` 里的代码实现。
# 拓展探索
其实除了 `SnapshotWidget` 之外,`RepaintBoundary` 也支持了 `toImageSync ` 因为 `toImageSync ` 获取到的是 GPU 中的常驻数据,所以在**实现类似控件截图和高亮指引等场景绘制上**,理论上应该可以得到更好的性能预期。
```dart
final RenderRepaintBoundary boundary =
globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final ui.Image image = boundary.toImageSync();
```
除此之外,`dart:ui `里的 `Scene` 和 `_Image` 对象其实都是 `NativeFieldWrapperClass1` ,以前我们解释过:**`NativeFieldWrapperClass1` 就是它的逻辑是由不同平台的 Engine 区分实现** 。
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image12.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image13.png) |
| --------------------------------------------------------- | --------------------------------------------------------- |
> 所以如果你直接在 `flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart `下去断点 `toImageSync` 是无法成功执行到断点位置的,因为它的真实实现在对应平台的 Engine 实现。
![](http://img.cdn.guoshuyu.cn/20230207_sync/image14.png)
另外,前面我们一直说 `toImageSync` 对比 `toImage` 是 GPU 常驻,那它们的区别在哪里?从上图我们就可以看出:
- `toImageSync` 执行了 `Scene:RasterizeToImage` 并返回 `Dart_Null` 句柄
- `toImage` 执行了 `Picture:RasterizeLayerTreeToImage` 并直接返回
简单展开来说,就是:
- `toImageSync` 最终是通过 `SkImage::MakeFromTexture` 通过纹理得到一个 GPU `SkImage` 图片
- `toImage` 是通过 `makeImageSnapshot``makeRasterImage` 生成 `SkImage` `makeRasterImage` 是一个复制图像到 CPU 内存的操作。
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image15.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image16.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image17.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image18.png) |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
其实一开始 `toImageSync` 是被命令为 `toGpuImage` ,但是为了更形象通用,最后才修改为 `toImageSync`
![](http://img.cdn.guoshuyu.cn/20230207_sync/image19.png)
`toImageSync` 等相关功能的落地可以说同样历经了漫长的讨论,关于是否提供这样一个 API 到最终落地,其执行难度丝毫不比 [background isolate ](https://juejin.cn/post/7195825738472620087) 简单比如是否定义异常场景遇到错误是否需要在Framwork 层消化,是否真的需要这样的接口来提高性能等等。
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image20.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image21.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image22.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image23.png) |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
`toImageSync` 等相关功能最终能落地,其中最重要的一点我认为是:
> `toGoulmage` gives the framework the ability to take performance into their own hands, which is important given that our priorities don't always line up.
# 最后
`toImageSync` 只是一个简单的 API ,但是它的背后经历了很多故事,同时 `toImageSync` 和它对应的封装 `SnapshotWidget` ,最终的目的就是提高 Flutter 运行的性能。
也许目前对于你来说 `toImageSync` 并不是必须的,甚至 `SnapshotWidget` 看起来也很鸡肋,但是一旦你需要处理复杂的绘制场景时, `toImageSync` 就是你必不可少的菜刀。

118
Flutter-N19.md Normal file
View File

@ -0,0 +1,118 @@
# Flutter 小技巧之 3.7 更灵活的编译变量支持
今天我们聊个简单的知识点,在 Flutter 3.7 的 [release-notes](https://docs.flutter.dev/development/tools/sdk/release-notes/release-notes-3.7.0) 里,有一个没有出现在 announcement 说明上的 change log ,可能对于 Flutter 团队来说这个功能并不是特别重要,但是对于我个人而言,这是一个十分重要的能力补充:
- [flutter_tools] Fix so that the value set by `--dart-define-from-file` can be passed to Gradle by @blendthink in https://github.com/flutter/flutter/pull/114297
> 翻到这个小功能,纯属是意外之喜。
# Dart
在 3.7 版本之前,如果我们需要在编译时动态给 Flutter 添加变量信息,那么我们会用到 `--dart-define` ,例如:
```dart
flutter run --dart-define=APP_CHANNEL=Offical
const APP_CHANNEL = String.fromEnvironment('APP_CHANNEL');
```
我们可以通过 `--dart-define` 在命令行指定一个变量,然后在 Flutter 里通过 `String.fromEnvironment` 读取它,一般场景下它是满足需求的,但是:
- 如果当你需要定义多个变量时,命令就会变得冗长且不好维护
- 如果你是混合开发,变量还需要同步修改到原生项目的配置里,就会变得麻烦
在此之前,针对同步修改到不同原生项目的配置,我是通过自定义脚本去实现:
- Android 上利用 gradle 脚本,参考 RN 上的 `dotenv ` 读取某个脚本配置,修改 `project.env`
- iOS 上通过读取脚本配置,然后利用系统的 `PlistBuddy` 命令在编译时插入和修改某些参数
而现在,**从 Flutter 3.7 开始,它变得更简单了,因为你可以使用 `--dart-define-from-file`**
```dart
flutter run --dart-define-from-file=config.json
////// config.json //////
{
"TEST_KEY1": "test key 1",
"TEST_KEY2": "test key 2"
}
```
同样是 dart define ,但是 `--dart-define-from-file` 可以直接从一个 json 文件上读取配置,然后转成一个 `Map`,之后配置到 Environment 里,同样是可以在 dart 里通过 `String.fromEnvironment` 去读取参数,而 json 文件的配置方式,可以让你在需要配置多个变量时参数管理变得更好维护。
![](http://img.cdn.guoshuyu.cn/20230209_df/image1.png)
那到这里就结束了吗?显然不是,前面我们说过同步修改到不同原生项目的配置,而 Flutter 3.7 下官方也正式支持。
# Android
首先是 Android ,我们可以在 `app/build.gradle` 文件下定义一个 `dartEnvVar` 变量,它主要是用来读取前面 json 文件注入到 `project` 的参数。
![](http://img.cdn.guoshuyu.cn/20230209_df/image2.png)
然后我们就可以在 `app/build.gradle ` 下直接通过 `dartEnvVar` 引用对应参数,比如定义 `resValue` ,可以看到 `dartEnvVar` 在编译时,成功读取到 json 文件里的参数。
| ![](http://img.cdn.guoshuyu.cn/20230209_df/image3.png) | ![](http://img.cdn.guoshuyu.cn/20230209_df/image4.png) |
| ------------------------------------------------------ | ------------------------------------------------------ |
如下图所示,能通过 `project` 读取 dart 的环境变量配置之后,我们就可以定义有 `resValue ` 去修改 `AndroidManifest` 文件,甚至定义插入到 `BuildConfig` 里在原生代码引用,而对于配置我们只需要维护一份 json 文件即可。
![image-20230208182506190](http://img.cdn.guoshuyu.cn/20230209_df/image5.png)
那它是如何实现的?简单来说,在 *flutter/packages/flutter_tools/lib/src/build_info.dart* 脚本下,之前读取的 json 文件可以得到一个 `dartDefineConfigJsonMap` 对象,它会被转化为一个 Gradle 参数列表,在之后的 `assembleTask` 里被作为参数执行。
> 这里需要注意,定义的 key 不能和与定制的 key 冲突,比如 `dart-obfuscation` 等。
![](http://img.cdn.guoshuyu.cn/20230209_df/image6.png)
如下图所示,最终执行的时候就会是 -PTEST_KEY1=test key 1 -PTEST_KEY2=test key 2 这样的效果。
![](http://img.cdn.guoshuyu.cn/20230209_df/image7.png)
# iOS
iOS 上同样也很简单,你只需要在 `Info.plist` 上定义好 key-value 的引用即可,因为 iOS 上在 `--dart-define-from-file` 编译时,同样会生成对应的 `xcconfig` 配置信息。
![](http://img.cdn.guoshuyu.cn/20230209_df/image8.png)
` ios/Flutter` 目录下,编译时会产生两个忽略文件,分别是 `flutter_export_environment.sh``Generated.xcconfig` ,可以看到编译后这两个文件下都产生了对应的 key-value 。
| ![](http://img.cdn.guoshuyu.cn/20230209_df/image9.png) | ![](http://img.cdn.guoshuyu.cn/20230209_df/image10.png) |
| ------------------------------------------------------ | ------------------------------------------------------- |
> 这里需要注意,在 iOS 上 `xcconfig` 格式会将 `// ` 读取为注释分隔符 ,也就是 `//` 之后的内容会被忽略,也就是说,你不能通过它来传递 url ,比如 `https://xxxx` ,因为 `//` 后会被忽略。
当然,如果你需要默认值,那么你也可以在 ` ios/Flutter` 目录下的 `Debug.xcconfig``Release.xcconfig` 上进行定制配置。
![](http://img.cdn.guoshuyu.cn/20230209_df/image11.png)
和 Android 一样, iOS 在编译时会对 `--dart-define-from-file` 的参数进行转化变成 `xcconfig` 参数,从而实现 dart 和 iOS 端公用一份变量配置的效果。
| ![](http://img.cdn.guoshuyu.cn/20230209_df/image12.png) | ![](http://img.cdn.guoshuyu.cn/20230209_df/image13.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
# 最后
可以看到 `--dart-define-from-file` 的使用和实现并不复杂,在没有它之前我们也可以通过一些手段来实现类似的效果。
但是 `--dart-define-from-file` 命令的出现简化了整个构建流程,让编译动态配置的链条变得更加灵活可靠,所以它无疑是 3.7 里最容易被忽略的实用更新。
不得不说Flutter 3.7 给我们带来了不少的惊喜,例如 [toImageSync ](https://juejin.cn/post/7197326179933372476) 和 [background isolate](https://juejin.cn/post/7195825738472620087) 都是期待已久的功能,而类似 `--dart-define-from-file` 的支持,也在不断完善 Flutter 的开发体验。
最后,从 3.7 开始的小版本更新有两个特征:
- 1、impeller 确实还有不少问题
- 2、impeller 真的来了,就算预览功能也要 fix 到稳定分支
![](http://img.cdn.guoshuyu.cn/20230209_df/image14.png)
期待下个版本 impeller 能给我们带来更好的体验。

823
Flutter-N20.md Normal file
View File

@ -0,0 +1,823 @@
# Flutter 小技巧之实现一个精美的动画相册效果
今天的小技巧主要是「抄袭」一个充满设计感的相册控件,如下图所示是 [gskinner](https://gskinner.com/) 开源应用 [wonderous](https://github.com/gskinnerTeam/flutter-wonderous-app) 里一个相片集的实现效果,可以看到**相册支持上下左右滑动,并带有高亮展示的动画效果,而且相册整体布局可以超出屏幕滚动**,因为是开源的 App 我们只需要「照搬」就可以实现一摸一样的效果,那么如果要实现这样的效果,你第一反应是用什么基础控件?
![](http://img.cdn.guoshuyu.cn/20230317_W/image1.gif)
因为需要支持上下左右自由滑动,可能大家第一反应会是 `Table` ,还是嵌套两个 `ListView` ?但是从上面的效果体验上看,控件滑动的过程并不是一个正常 Scroll 控件的线性效果,因为它并不是「跟随手指滑动」的状态。
既然是开源代码,我们通过源码可以发现它是用了 `GridView` 来实现,这也是这个效果里最有趣的点,一个 `GridView` 如何变成一个带有动画的 Photo Gallery 。
> **所以本篇的核心是分析 wonderous 里的 Photo Gallery 是如何实现的,并剥离出简单代码**
# Photo Gallery
要实现上述的 Photo Gallery 效果,主要需要解决三个方面核心的要点:
- 1、`GridView` 所在区域的上下左右要超出屏幕
- 2、`GridView` 如何实现上下左右自由切换
- 3、高亮展示选中 Item 的动画效果
首先是第一点的方案肯定是 `OverflowBox` ,因它支持解放 Child 的布局约束,允许 Child 溢出父布局,因为前面的 Photo Gallery 在水平方向设定是 5 个 Item`GridView` 是默认是上下滑动,所以可以简单的设定一个 `maxWidth``maxHeight` 来作为 Child 超出屏幕后大小。
```dart
OverflowBox(
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
alignment: Alignment.center,
child:
```
可以看到「超出屏幕」这个需求还是比较简单,接下里就是 「`GridView` 如何实现上下左右自由切换」这个问题。
> **小技巧 1 :在合适场合使用 OverflowBox 可以溢出屏幕**
默认情况下 `GridView` 肯定只支持一个方向滑动,所以干脆我们禁止 `GridView` 的滑动逻辑,让 `GridView` 只管布局,后面滑动逻辑通过自定义的 `GestureDetector` 来实现。
```dart
GridView.count(
physics: NeverScrollableScrollPhysics(),
```
如下代码所示,我们通过封装 `GestureDetector` 来实现手势识别,这里核心的要点就是 `_maybeTriggerSwipe` 的实现,它的作用就是得到手势滑动的方向结果,对于滑动具体大于 `threshold` 的参数,通过「采样」将数据变成 -1、 0 、 1 这样的结果来代表方向:
- Offset(1.0, 0.0) 是手指右滑
- Offset(-1.0, 0.0) 是手指左滑
- Offset(0.0, 1.0) 是手指下滑
- Offset(0.0, -1.0) 是手指上滑
```dart
class _EightWaySwipeDetectorState extends State<EightWaySwipeDetector> {
Offset _startPos = Offset.zero;
Offset _endPos = Offset.zero;
bool _isSwiping = false;
void _resetSwipe() {
_startPos = _endPos = Offset.zero;
_isSwiping = false;
}
///这里主要是返回一个 -1 1 之间的数值,具体用于判断方向
/// Offset(1.0, 0.0) 是手指右滑
/// Offset(-1.0, 0.0) 是手指左滑
/// Offset(0.0, 1.0) 是手指下滑
/// Offset(0.0, -1.0) 是手指上滑
void _maybeTriggerSwipe() {
// Exit early if we're not currently swiping
if (_isSwiping == false) return;
/// 开始和结束位置计算出移动距离
// Get the distance of the swipe
Offset moveDelta = _endPos - _startPos;
final distance = moveDelta.distance;
/// 对比偏移量大小是否超过了 threshold ,不能小于 1
// Trigger swipe if threshold has been exceeded, if threshold is < 1, use 1 as a minimum value.
if (distance >= max(widget.threshold, 1)) {
// Normalize the dx/dy values between -1 and 1
moveDelta /= distance;
// Round the dx/dy values to snap them to -1, 0 or 1, creating an 8-way directional vector.
Offset dir = Offset(
moveDelta.dx.roundToDouble(),
moveDelta.dy.roundToDouble(),
);
widget.onSwipe?.call(dir);
_resetSwipe();
}
}
void _handleSwipeStart(d) {
_isSwiping = true;
_startPos = _endPos = d.localPosition;
}
void _handleSwipeUpdate(d) {
_endPos = d.localPosition;
_maybeTriggerSwipe();
}
void _handleSwipeEnd(d) {
_maybeTriggerSwipe();
_resetSwipe();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onPanStart: _handleSwipeStart,
onPanUpdate: _handleSwipeUpdate,
onPanCancel: _resetSwipe,
onPanEnd: _handleSwipeEnd,
child: widget.child);
}
}
```
> **小技巧 2Offset.distance 可以用来作为判断偏移量的大小**
知道了手势方向之后,我们就可以处理 `GridView` 应该如何滑动,这里我们需要先知道当然应该展示哪个 index 。
默认情况下我们需要展示的是最中间的 Item ,例如有 25 个 Item 的时候, index 应该在第 13 ,然后我们再根据方向来调整下一个 index 是哪个:
- dy > 0 ,就是手指下滑,也就是页面要往上,那么 index 就需要 -1反过来就是 + 1
- dx > 0 ,就是手指右滑,也就是页面要往左,那么 index 就需要 -1反过来就是 + 1
```dart
// Index starts in the middle of the grid (eg, 25 items, index will start at 13)
int _index = ((_gridSize * _gridSize) / 2).round();
/// Converts a swipe direction into a new index
void _handleSwipe(Offset dir) {
// Calculate new index, y swipes move by an entire row, x swipes move one index at a time
int newIndex = _index;
/// Offset(1.0, 0.0) 是手指右滑
/// Offset(-1.0, 0.0) 是手指左滑
/// Offset(0.0, 1.0) 是手指下滑
/// Offset(0.0, -1.0) 是手指上滑
/// dy > 0 ,就是手指下滑,也就是页面要往上,那么 index 就需要 -1反过来就是 + 1
if (dir.dy != 0) newIndex += _gridSize * (dir.dy > 0 ? -1 : 1);
/// dx > 0 ,就是手指右滑,也就是页面要往左,那么 index 就需要 -1反过来就是 + 1
if (dir.dx != 0) newIndex += (dir.dx > 0 ? -1 : 1);
///这里判断下 index 是不是超出位置
// After calculating new index, exit early if we don't like it...
if (newIndex < 0 || newIndex > _imgCount - 1)
return; // keep the index in range
if (dir.dx < 0 && newIndex % _gridSize == 0)
return; // prevent right-swipe when at right side
if (dir.dx > 0 && newIndex % _gridSize == _gridSize - 1)
return; // prevent left-swipe when at left side
/// 响应
_lastSwipeDir = dir;
HapticFeedback.lightImpact();
_setIndex(newIndex);
}
void _setIndex(int value, {bool skipAnimation = false}) {
if (value < 0 || value >= _imgCount) return;
setState(() => _index = value);
}
```
通过手势方向,我们就可以得到下一个需要展示的 Item 的 index 是什么,然后就可以使用 `Transform.translate` 来移动 `GridView`
是的,在这个 Photo Gallery 里的滑动效果是通过 `Transform.translate` 实现,**核心之一也就是根据方向计算其应该偏移的 Offset 位置**
- 首先根据水平方向的数量 / 2 得到一个 `halfCount`
- 计算出一个 Item 加上 Padding 大小的 `paddedImageSize`
- 计算出默认中心位置的 top-left 的 `originOffset`
- 计算出要移动的 index 所在的行和列位置 `indexedOffset`
- 最后两者相减(因为 `indexedOffset` 里是负数),得到一个相对的偏移 `Offset`
```dart
/// Determine the required offset to show the current selected index.
/// index=0 is top-left, and the index=max is bottom-right.
Offset _calculateCurrentOffset(double padding, Size size) {
/// 获取水平方向一半的大小,默认也就是 2.0,因为 floorToDouble
double halfCount = (_gridSize / 2).floorToDouble();
/// Item 大小加上 Padding也就是每个 Item 的实际大小
Size paddedImageSize = Size(size.width + padding, size.height + padding);
/// 计算出开始位置的 top-left
// Get the starting offset that would show the top-left image (index 0)
final originOffset = Offset(
halfCount * paddedImageSize.width, halfCount * paddedImageSize.height);
/// 得到要移动的 index 所在的行和列位置
// Add the offset for the row/col
int col = _index % _gridSize;
int row = (_index / _gridSize).floor();
/// 负数计算出要移动的 index 的 top-left 位置,比如 index 比较小,那么这个 indexedOffset 就比中心点小,相减之后 Offset 就会是正数
/// 是不是有点懵逼?为什么正数 translate 会往 index 小的 方向移动??
/// 因为你代入的不对,我们 translate 移动的是整个 GridView
/// 正数是向左向下移动,自然就把左边或者上面的 Item 显示出来
final indexedOffset =
Offset(-paddedImageSize.width * col, -paddedImageSize.height * row);
return originOffset + indexedOffset;
}
```
具体点如下图所示,比如在 5 x 5 的 `GridView` 下:
- 通过 `halfCount``paddedImageSize` 计算会得到黑色虚线的位置
- 红色是要展示的 index 位置,也就是通过 `col ``row` 计算出来的 `indexedOffset` 就是红色框的左上角,在上面代码里用过的是负数
- 当 ` originOffset + indexedOffset` ,其实就是得到两者之差的 currentOffset比如这时候得到是一个 `dx` 为正数的 `Offset` ,整个 `GridView` 要向左移动一个 currentOffset ,自然就把红色框放到中间显示。
![](http://img.cdn.guoshuyu.cn/20230317_W/image2.png)
更形象的可以看这个动画,核心就是整个 `GridView` 在发生了偏移,从把需要展示的 Item 移动到中心的位置,利用 `Transform.translate` 来实现类似滑动的效果,当然实现里还会用到 `TweenAnimationBuilder` 来实现动画过程,
![](http://img.cdn.guoshuyu.cn/20230317_W/image3.gif)
```dart
TweenAnimationBuilder<Offset>(
tween: Tween(begin: gridOffset, end: gridOffset),
duration: offsetTweenDuration,
curve: Curves.easeOut,
builder: (_, value, child) =>
Transform.translate(offset: value, child: child),
child: GridView.count(
physics: NeverScrollableScrollPhysics(),
```
解决完移动,最后就是实现蒙层和高亮动画效果,这个的核心主要是通过 `flutter_animate` 包和 `ClipPath` 实现,如下代码所示:
- 使用 `Animate` 并在上面添加一个具有透明度的黑色 `Container`
- 利用 `CustomEffect` 添加自定义动画
- 在动画里利用 `ClipPath` ,并通过自定义 `CustomClipper` 结合动画 value 实现 `PathOperation.difference` 的「挖空」效果
> 动画效果就是根据 `Animate` 的 value 得到的 `cutoutSize` ,默认是从 `1 - 0.25 * x` 开始,这里的 x 是滑动方向,最终表现就是从 0.75 到 1 的过程,所以动画会根据方向有一个从 0.75 到 1 的展开效果。
```dart
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
// 用 ClipPath 做一个动画抠图
Animate(
effects: [
CustomEffect(
builder: _buildAnimatedCutout,
curve: Curves.easeOut,
duration: duration)
],
key: animationKey,
onComplete: (c) => c.reverse(),
// 用一个黑色的蒙层,这里的 child 会变成 effects 里 builder 里的 child
// 也就是黑色 Container 会在 _buildAnimatedCutout 作为 ClipPath 的 child
child: IgnorePointer(
child: Container(color: Colors.black.withOpacity(opacity))),
),
],
);
}
/// Scales from 1 --> (1 - scaleAmt) --> 1
Widget _buildAnimatedCutout(BuildContext context, double anim, Widget child) {
// controls how much the center cutout will shrink when changing images
const scaleAmt = .25;
final size = Size(
cutoutSize.width * (1 - scaleAmt * anim * swipeDir.dx.abs()),
cutoutSize.height * (1 - scaleAmt * anim * swipeDir.dy.abs()),
);
return ClipPath(clipper: _CutoutClipper(size), child: child);
}
class _CutoutClipper extends CustomClipper<Path> {
_CutoutClipper(this.cutoutSize);
final Size cutoutSize;
@override
Path getClip(Size size) {
double padX = (size.width - cutoutSize.width) / 2;
double padY = (size.height - cutoutSize.height) / 2;
return Path.combine(
PathOperation.difference,
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
Path()
..addRRect(
RRect.fromLTRBR(
padX,
padY,
size.width - padX,
size.height - padY,
Radius.circular(6),
),
)
..close(),
);
}
@override
bool shouldReclip(_CutoutClipper oldClipper) =>
oldClipper.cutoutSize != cutoutSize;
}
```
从这里可以看到,其实高亮的效果就是在黑色的蒙层上,利用 ` PathOperation.difference` 「挖」出来一个空白的 Path 。
> **小技巧 3 ` PathOperation.difference` 可以用在需要「镂空」 的场景上**
更直观的可以参考一下例子,就是对两个路径进行 difference 操作,,利用 Rect2 把 Rect1 中间给消除掉,得到一个中间 「镂空」的绘制 Path。
```dart
class ShowPathDifference extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ShowPathDifference'),
),
body: Stack(
alignment: Alignment.center,
children: [
Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage("static/gsy_cat.png"),
),
),
),
),
Center(
child: CustomPaint(
painter: ShowPathDifferencePainter(),
),
),
],
),
);
}
}
class ShowPathDifferencePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint();
paint.color = Colors.blue.withAlpha(160);
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()
..addRRect(
RRect.fromLTRBR(-150, -150, 150, 150, Radius.circular(10))),
Path()
..addOval(Rect.fromCircle(center: Offset(0, 0), radius: 100))
..close(),
),
paint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```
![](http://img.cdn.guoshuyu.cn/20230317_W/image4.png)
最终效果如下图所依,这里是把 wonderous 里关键部分代码剥离出来后的效果,因为 wonderous 并没有把这部分代码封装为 package ,所以我把这部分代码剥离出来放在了后面,感兴趣的可以自己运行试试效果。
![](http://img.cdn.guoshuyu.cn/20230317_W/image5.gif)
## 源码
```dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
/// 来自 https://github.com/gskinnerTeam/flutter-wonderous-app 上的一个 UI 效果
class PhotoGalleryDemoPage extends StatefulWidget {
const PhotoGalleryDemoPage({Key? key}) : super(key: key);
@override
State<PhotoGalleryDemoPage> createState() => _PhotoGalleryDemoPageState();
}
class _PhotoGalleryDemoPageState extends State<PhotoGalleryDemoPage> {
@override
Widget build(BuildContext context) {
return PhotoGallery();
}
}
class PhotoGallery extends StatefulWidget {
const PhotoGallery({Key? key}) : super(key: key);
@override
State<PhotoGallery> createState() => _PhotoGalleryState();
}
class _PhotoGalleryState extends State<PhotoGallery> {
static const int _gridSize = 5;
late List<Color> colorList;
// Index starts in the middle of the grid (eg, 25 items, index will start at 13)
int _index = ((_gridSize * _gridSize) / 2).round();
Offset _lastSwipeDir = Offset.zero;
bool _skipNextOffsetTween = false;
///根据屏幕尺寸,决定 Padding 的大小,通过 scale 缩放
_getPadding(Size size) {
double scale = 1;
final shortestSide = size.shortestSide;
const tabletXl = 1000;
const tabletLg = 800;
const tabletSm = 600;
const phoneLg = 400;
if (shortestSide > tabletXl) {
scale = 1.25;
} else if (shortestSide > tabletLg) {
scale = 1.15;
} else if (shortestSide > tabletSm) {
scale = 1;
} else if (shortestSide > phoneLg) {
scale = .9; // phone
} else {
scale = .85; // small phone
}
return 24 * scale;
}
int get _imgCount => pow(_gridSize, 2).round();
Widget _buildImage(int index, Size imgSize) {
/// Bind to collectibles.statesById because we might need to rebuild if a collectible is found.
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
width: imgSize.width,
height: imgSize.height,
color: colorList[index],
),
);
}
/// Converts a swipe direction into a new index
void _handleSwipe(Offset dir) {
// Calculate new index, y swipes move by an entire row, x swipes move one index at a time
int newIndex = _index;
/// Offset(1.0, 0.0) 是手指右滑
/// Offset(-1.0, 0.0) 是手指左滑
/// Offset(0.0, 1.0) 是手指下滑
/// Offset(0.0, -1.0) 是手指上滑
/// dy > 0 ,就是手指下滑,也就是页面要往上,那么 index 就需要 -1反过来就是 + 1
if (dir.dy != 0) newIndex += _gridSize * (dir.dy > 0 ? -1 : 1);
/// dx > 0 ,就是手指右滑,也就是页面要往左,那么 index 就需要 -1反过来就是 + 1
if (dir.dx != 0) newIndex += (dir.dx > 0 ? -1 : 1);
///这里判断下 index 是不是超出位置
// After calculating new index, exit early if we don't like it...
if (newIndex < 0 || newIndex > _imgCount - 1)
return; // keep the index in range
if (dir.dx < 0 && newIndex % _gridSize == 0)
return; // prevent right-swipe when at right side
if (dir.dx > 0 && newIndex % _gridSize == _gridSize - 1)
return; // prevent left-swipe when at left side
/// 响应
_lastSwipeDir = dir;
HapticFeedback.lightImpact();
_setIndex(newIndex);
}
void _setIndex(int value, {bool skipAnimation = false}) {
print("######## $value");
if (value < 0 || value >= _imgCount) return;
_skipNextOffsetTween = skipAnimation;
setState(() => _index = value);
}
/// Determine the required offset to show the current selected index.
/// index=0 is top-left, and the index=max is bottom-right.
Offset _calculateCurrentOffset(double padding, Size size) {
/// 获取水平方向一半的大小,默认也就是 2.0,因为 floorToDouble
double halfCount = (_gridSize / 2).floorToDouble();
/// Item 大小加上 Padding也就是每个 Item 的实际大小
Size paddedImageSize = Size(size.width + padding, size.height + padding);
/// 计算出开始位置的 top-left
// Get the starting offset that would show the top-left image (index 0)
final originOffset = Offset(
halfCount * paddedImageSize.width, halfCount * paddedImageSize.height);
/// 得到要移动的 index 所在的行和列位置
// Add the offset for the row/col
int col = _index % _gridSize;
int row = (_index / _gridSize).floor();
/// 负数计算出要移动的 index 的 top-left 位置,比如 index 比较小,那么这个 indexedOffset 就比中心点小,相减之后 Offset 就会是正数
/// 是不是有点懵逼?为什么正数 translate 会往 index 小的 方向移动??
/// 因为你代入的不对,我们 translate 移动的是整个 GridView
/// 正数是向左向下移动,自然就把左边或者上面的 Item 显示出来
final indexedOffset =
Offset(-paddedImageSize.width * col, -paddedImageSize.height * row);
return originOffset + indexedOffset;
}
@override
void initState() {
colorList = List.generate(
_imgCount,
(index) => Color((Random().nextDouble() * 0xFFFFFF).toInt())
.withOpacity(1));
super.initState();
}
@override
Widget build(BuildContext context) {
var mq = MediaQuery.of(context);
var width = mq.size.width;
var height = mq.size.height;
bool isLandscape = mq.orientation == Orientation.landscape;
///根据横竖屏状态决定 Item 大小
Size imgSize = isLandscape
? Size(width * .5, height * .66)
: Size(width * .66, height * .5);
var padding = _getPadding(mq.size);
final cutoutTweenDuration =
_skipNextOffsetTween ? Duration.zero : Duration(milliseconds: 600) * .5;
final offsetTweenDuration =
_skipNextOffsetTween ? Duration.zero : Duration(milliseconds: 600) * .4;
var gridOffset = _calculateCurrentOffset(padding, imgSize);
gridOffset += Offset(0, -mq.padding.top / 2);
//动画效果
return _AnimatedCutoutOverlay(
animationKey: ValueKey(_index),
cutoutSize: imgSize,
swipeDir: _lastSwipeDir,
duration: cutoutTweenDuration,
opacity: .7,
child: SafeArea(
bottom: false,
// Place content in overflow box, to allow it to flow outside the parent
child: OverflowBox(
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
alignment: Alignment.center,
// 手势获取方向上下左右
child: EightWaySwipeDetector(
onSwipe: _handleSwipe,
threshold: 30,
// A tween animation builder moves from image to image based on current offset
child: TweenAnimationBuilder<Offset>(
tween: Tween(begin: gridOffset, end: gridOffset),
duration: offsetTweenDuration,
curve: Curves.easeOut,
builder: (_, value, child) =>
Transform.translate(offset: value, child: child),
child: GridView.count(
physics: NeverScrollableScrollPhysics(),
crossAxisCount: _gridSize,
childAspectRatio: imgSize.aspectRatio,
mainAxisSpacing: padding,
crossAxisSpacing: padding,
children:
List.generate(_imgCount, (i) => _buildImage(i, imgSize)),
)),
),
),
),
);
}
}
class EightWaySwipeDetector extends StatefulWidget {
const EightWaySwipeDetector(
{Key? key,
required this.child,
this.threshold = 50,
required this.onSwipe})
: super(key: key);
final Widget child;
final double threshold;
final void Function(Offset dir)? onSwipe;
@override
State<EightWaySwipeDetector> createState() => _EightWaySwipeDetectorState();
}
class _EightWaySwipeDetectorState extends State<EightWaySwipeDetector> {
Offset _startPos = Offset.zero;
Offset _endPos = Offset.zero;
bool _isSwiping = false;
void _resetSwipe() {
_startPos = _endPos = Offset.zero;
_isSwiping = false;
}
///这里主要是返回一个 -1 1 之间的数值,具体用于判断方向
/// Offset(1.0, 0.0) 是手指右滑
/// Offset(-1.0, 0.0) 是手指左滑
/// Offset(0.0, 1.0) 是手指下滑
/// Offset(0.0, -1.0) 是手指上滑
void _maybeTriggerSwipe() {
// Exit early if we're not currently swiping
if (_isSwiping == false) return;
/// 开始和结束位置计算出移动距离
// Get the distance of the swipe
Offset moveDelta = _endPos - _startPos;
final distance = moveDelta.distance;
/// 对比偏移量大小是否超过了 threshold ,不能小于 1
// Trigger swipe if threshold has been exceeded, if threshold is < 1, use 1 as a minimum value.
if (distance >= max(widget.threshold, 1)) {
// Normalize the dx/dy values between -1 and 1
moveDelta /= distance;
// Round the dx/dy values to snap them to -1, 0 or 1, creating an 8-way directional vector.
Offset dir = Offset(
moveDelta.dx.roundToDouble(),
moveDelta.dy.roundToDouble(),
);
widget.onSwipe?.call(dir);
_resetSwipe();
}
}
void _handleSwipeStart(d) {
_isSwiping = true;
_startPos = _endPos = d.localPosition;
}
void _handleSwipeUpdate(d) {
_endPos = d.localPosition;
_maybeTriggerSwipe();
}
void _handleSwipeEnd(d) {
_maybeTriggerSwipe();
_resetSwipe();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onPanStart: _handleSwipeStart,
onPanUpdate: _handleSwipeUpdate,
onPanCancel: _resetSwipe,
onPanEnd: _handleSwipeEnd,
child: widget.child);
}
}
class _AnimatedCutoutOverlay extends StatelessWidget {
const _AnimatedCutoutOverlay(
{Key? key,
required this.child,
required this.cutoutSize,
required this.animationKey,
this.duration,
required this.swipeDir,
required this.opacity})
: super(key: key);
final Widget child;
final Size cutoutSize;
final Key animationKey;
final Offset swipeDir;
final Duration? duration;
final double opacity;
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
// 用 ClipPath 做一个动画抠图
Animate(
effects: [
CustomEffect(
builder: _buildAnimatedCutout,
curve: Curves.easeOut,
duration: duration)
],
key: animationKey,
onComplete: (c) => c.reverse(),
// 用一个黑色的蒙层,这里的 child 会变成 effects 里 builder 里的 child
// 也就是黑色 Container 会在 _buildAnimatedCutout 作为 ClipPath 的 child
child: IgnorePointer(
child: Container(color: Colors.black.withOpacity(opacity))),
),
],
);
}
/// Scales from 1 --> (1 - scaleAmt) --> 1
Widget _buildAnimatedCutout(BuildContext context, double anim, Widget child) {
// controls how much the center cutout will shrink when changing images
const scaleAmt = .25;
final size = Size(
cutoutSize.width * (1 - scaleAmt * anim * swipeDir.dx.abs()),
cutoutSize.height * (1 - scaleAmt * anim * swipeDir.dy.abs()),
);
print("### anim ${anim} ");
return ClipPath(clipper: _CutoutClipper(size), child: child);
}
}
/// Creates an overlay with a hole in the middle of a certain size.
class _CutoutClipper extends CustomClipper<Path> {
_CutoutClipper(this.cutoutSize);
final Size cutoutSize;
@override
Path getClip(Size size) {
double padX = (size.width - cutoutSize.width) / 2;
double padY = (size.height - cutoutSize.height) / 2;
return Path.combine(
PathOperation.difference,
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
Path()
..addRRect(
RRect.fromLTRBR(
padX,
padY,
size.width - padX,
size.height - padY,
Radius.circular(6),
),
)
..close(),
);
}
@override
bool shouldReclip(_CutoutClipper oldClipper) =>
oldClipper.cutoutSize != cutoutSize;
}
class ShowPathDifference extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ShowPathDifference'),
),
body: Stack(
alignment: Alignment.center,
children: [
Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage("static/gsy_cat.png"),
),
),
),
),
Center(
child: CustomPaint(
painter: ShowPathDifferencePainter(),
),
),
],
),
);
}
}
class ShowPathDifferencePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint();
paint.color = Colors.blue.withAlpha(160);
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()
..addRRect(
RRect.fromLTRBR(-150, -150, 150, 150, Radius.circular(10))),
Path()
..addOval(Rect.fromCircle(center: Offset(0, 0), radius: 100))
..close(),
),
paint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```

111
Flutter-roadmap2023.md Normal file
View File

@ -0,0 +1,111 @@
# Flutter 2023 Roadmap 解析
随着 [Flutter Forward](https://juejin.cn/post/7192646390948823098) 大会召开, Flutter 官方在 [3.7 版本 ](https://juejin.cn/post/7192468840016511034)之余为我们展示了如 3D 渲染支持、add-to-web 等未来可能出现的 Feature但是这些都还只是处于开发中未来可能还会有其他变动而在大会结束后官方也公布了更详细 [2023 年的 Roadmap](https://github.com/flutter/flutter/wiki/Roadmap)。
> [Flutter Forward](https://juejin.cn/post/7192646390948823098) 展示未来大方面Roadmap 展示接下来更详细的计划。
# 性能
首先 **2023 年官方首要任务还是在于性能优化,也就是 Impeller** 3.7 开始 Impeller 已经可以在 iOS 上进行预览,那么下一步就是将 Impeller 提升为 iOS 的默认底层渲染器,从而解决陈年顽疾如[色器编译器卡顿](https://github.com/orgs/flutter/projects/21) 的问题。
> iOS 之后, Impeller 在 Android 上针对 Vulkan 支持和在桌面端的支持也会逐步推进,**这将是 Flutter 2023 最让人期待的目标:全员 Impeller**,相信自己的渲染器,修复器问题会比 Skia 更快?
对于 WebFlutter 一直都存在两种底层 render 支持html 和 canvaskit而随着 Dart3 将直接支持 WebAssembly (使用 WebAssembly 规范的新 WasmGC 指令),**Flutter 官方也将[更多投入 WASM 路线](https://github.com/flutter/flutter/issues/41062)** 。
![](http://img.cdn.guoshuyu.cn/20230129_roadmap/image1.png)
> 那么这是不是官方在二选一中做出了最终抉择?为此 [ Flutter Web 支持 “hot reload”不仅仅是 “hot restart” - #53041](https://github.com/flutter/flutter/issues/53041) 相关进度目前也暂时停滞。
**另外对于 Web 还有并计划实现[多线程渲染](https://github.com/flutter/flutter/issues/114243) ,减少应用的下载大小,并提高自定义着色器的性能等相关计划**。
> 看起来 Flutter Web 的实用性在 2023 会被进一步增强。
最后关于 VM 的性能优化,官方在 2023 将致力于[**改进存分配策略**](https://github.com/dart-lang/sdk/issues/47574),从而提高应用的响应速度和启动性能:
> 目前考虑是利用 v8 GC 的 RAILResponse、Animation、Idle、Loading模型在不同阶段提供通知就像它目前为 idle 所做的那样),并且 VM 可以相应地调整一些 GC 行为。
# 质量
首先 Flutter 官方很看重 Accessibility 的能力,所以 2023 年目标之一是:**提高所有平台上的 Accessibility 的支持质量**。
> 虽然国内开发团队貌似对 Accessibility 并不是特别感冒。
同时继续改进 Flutter 相关的文档质量也是目标之一,其实从我个人来看,目前 Flutter 提供的各类文档的质量和覆盖已经相当不错了。
另外2023 Flutter 还将继续完善所有平台上 UI 还原能力,尤其是 Android 和 iOS
> 例如,预计今年 Cupertino 相关控件集将取得重大进展,让 iOS 平台能够保持最新状态并增加支持的 Widget 数量。
同时在界面相关方面,未来 Flutter 官方还计划实现:
- [Android 13 的预测后退手势](https://github.com/flutter/flutter/issues/109513) `android:enableOnBackInvokedCallback` ,主要是用于大屏幕和可折叠设备
![](http://img.cdn.guoshuyu.cn/20230129_roadmap/image2.gif)
- [Android 手写输入](https://github.com/flutter/flutter/issues/115607)支持
- [相机插件](https://github.com/flutter/plugins/tree/main/packages/camera)移植到 Android 最新的 CameraX API
> 貌似 Android 14 也要来了,一波未平一波又起。
# Features
2023 还会有一些实用的新功能,这些功能对于开发者来说应该是很迫切的需求,衡量它们的标准主要有:
- 受欢迎程度(一个问题收到了多少“点赞”)
- 平价性和可移植性(一旦一个平台支持后,它能不能给其他平台同时带来价值)
- 能够达到一些更好的结果(例如可以进一步提高性能的新功能)。
所以 2023 预计要实现的功能有:
- [自定义 asset 转换器](https://github.com/flutter/flutter/issues/101077),因为它们可以提高性能,例如在构建时对 icon fonts 进行转换,支持自定义 API让第三方工具可以自定义转换
- [优化可滚动控件](https://github.com/orgs/flutter/projects/32),例 [Table ](https://github.com/flutter/flutter/issues/87370)和[ Tree](https://github.com/flutter/flutter/issues/114299) ,提供类似 builder 的懒加载能力 ,以此来应用的性能
- **[多窗口支持](https://github.com/flutter/flutter/issues/30701),特别是对于桌面端,因为这是一个呼声很高的功能**,例如考虑在实现上通过三个打开的窗口共享相同的统一 `widget-tree`
- **[macOS ](https://github.com/flutter/flutter/issues/41722) 和 [Windows](https://github.com/flutter/flutter/issues/108486) 上的 `PlatformView ` 支持**,也是呼声很高的功能
- [边界拖放 ](https://github.com/flutter/flutter/issues/30719)能力的支持。
- **[iOS 上支持的无线调试](https://github.com/flutter/flutter/issues/15072)** 。
- [自定义 “flutter create” 模板](https://github.com/flutter/flutter/issues/77104),从而更好支持如 [Flame 引擎](https://flame-engine.org/)引导。
- 支持 [element embedding](https://github.com/flutter/flutter/issues/118481) ,也就是 [add-to-web - #32329](https://github.com/flutter/flutter/issues/32329) 从而开发人员可以将 Flutter 内容添加到任何 Web `<div>`
> 都是很值得期待的功能,期待下个版本时能够用上。
# 研究
由于 Impeller 的到来,**未来 Flutter 可能会支持某种形式的自适应布局,从而实现更贴近平台特性的 UI 效果**。
> 这个探索会先从 Android 与 iOS 开始,这类支持可以很好补全目前 Flutter 上,针对某些平台特性需要在业务代码上额外适配的问题。
另外 Flutter Forward 提到的 3D 能力,也在今年的实验范围之内,同时利用 Impeller 改进底层 `dart:ui` API 和新的着色器等相关能力,也是探索的目标之一。
与此相关的还有 [Display P3 宽色域支持](https://github.com/flutter/flutter/issues/55092)(可能会从 iOS 开始),这也是一项要求很高的功能
> 这个改进总觉得可能会引发其他坑。。。。
除此之外Flutter 还在研究从 ICU4C 迁移到 ICU4X新的[基于 Rust 的 ICU 后端](https://github.com/unicode-org/icu4x)),这里需要探索如何将 Rust 嵌入到所有平台的构建渠道,如何在引擎和 Dart FFI 包之间共享 Rust 代码,以及如何对此类包中使用的二进制代码执行 tree-shaking。
最后,**还有如何更新 Flutter SDK 使用 Dart 3 的新功能,例如更新我们的 API 使用 records 和 patterns**,更新我们的工具链支持 RISC-V还有使用插件的新 FFI 功能等。
# 发布
2023 年计划**发布 4 个稳定版本和 12 个 Beta 版本**,在 2023 年不一样的地方是新功能在 Beta 时 Flutter 团队就会对外公布它们,而不是和之前一样等倒它们进入 Stable 版本。
> 也就是官方鼓励开发者更多投入到 Beta 的尝试中来,我忽然想起 Android Studio Canary 版本貌似比 Release 更稳定的现状····
# 非目标的功能
目前 Web 上实现 hot reload 暂时停滞,因为 Flutter 的 Web 团队目前都在致力于 Wasm 的生产支持。
另外,对于以下功能 Flutter 团队目前依旧没有支持的计划:
- [code push](https://github.com/flutter/flutter/issues/14330#issuecomment-1279484739)
- 对可穿戴设备([Apple Watch](https://github.com/flutter/flutter/issues/28901#issuecomment-1385926218)、[Android Wear](https://github.com/flutter/flutter/issues/2057)
- [汽车集成 ](https://github.com/flutter/flutter/issues/26801#issuecomment-1013565542)的内置支持
- [对 Web SEO 的 ](https://github.com/flutter/flutter/issues/46789#issuecomment-1007835929)内置支持
- [通过 honebrew 安装](https://github.com/flutter/flutter/issues/14050#issuecomment-1012647917)
虽然以上一些功能的呼声虽然也很高,但是主要是因为一些技术可行性和成本相关等的考虑,一些不可行或者难以解决的问题会暂且被搁置。
> 对于 code push 的官方支持就不要期待了这都多少年过去了对于热门问题的修复顺序具体可见https://github.com/flutter/flutter/wiki/Popular-issues

View File

@ -92,6 +92,7 @@
- [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md)
- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md)
- [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md)
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
@ -99,6 +100,7 @@
- [Dart 2.16 发布的新特性](Dart-216.md)
- [Dart 2.17 发布的新特性](Dart-217.md)
- [Dart 2.18 发布Objective-C 和 Swift interop](Dart-218.md)
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
* [番外](FWREADME.md)
@ -170,6 +172,13 @@
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)
* [Android 开发者的跨平台 - Flutter or Compose ](SQS.md)
* [Flutter 小技巧之快速理解手势逻辑](N15.md)
* [2023 Flutter Forward 大会回顾,快来看看 Flutter 的未来会有什么](Flutter-FF2023.md)
* [Flutter 2023 Roadmap 解析](Flutter-roadmap2023.md)
* [Flutter 小技巧之 3.7 性能优化background isolate](Flutter-N16.md)
* [Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?](Flutter-N18.md)
* [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md)
* [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md)
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)
[Flutter 工程化选择](GCH.md)

View File

@ -59,6 +59,7 @@
- [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md)
- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md)
- [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md)
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
@ -66,143 +67,158 @@
- [Dart 2.16 发布的新特性](Dart-216.md)
- [Dart 2.17 发布的新特性](Dart-217.md)
- [Dart 2.18 发布Objective-C 和 Swift interop](Dart-218.md)
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
* [番外](FWREADME.md)
* [Flutter 跨平台框架应用实战-2019极光开发者大会](Flutter-jg-meet.md)
* [Flutter 面试知识点集锦](Flutter-msjj.md)
* [全网最全 Flutter 与 ReactNative深入对比分析](qwzqdb.md)
* [Flutter 开发实战与前景展望 - RTC Dev Meetup](Flutter-rtc-meetup.md)
* [Flutter Interact 的 Flutter 1.12 大进化和回顾](Flutter-Interact-2019.md)
* [Flutter 升级 1.12 适配教程](Flutter-update-1.12.md)
* [Spuernova 是如何提升 Flutter 的生产力](Flutter-Supernova.md)
* [Flutter 中的图文混排与原理解析](Flutter-TWHP.md)
* [Flutter 实现视频全屏播放逻辑及解析](Flutter-Player-Full.md)
* [Flutter 上的一个 Bug 带你了解键盘与路由的另类知识点](Flutter-keyboard-rs.md)
* [Flutter 上默认的文本和字体知识点](Flutter-Font-Other.md)
* [带你深入理解 Flutter 中的字体“冷”知识](Flutter-Font-Cool.md)
* [Flutter 1.17 中的导航解密和性能提升](Flutter-nav+1_17.md)
* [Flutter 1.17 对列表图片的优化解析](Flutter-Image+1_17.md)
* [Flutter 1.20 下的 Hybrid Composition 深度解析](flutter-hy-composition.md)
* [2020 腾讯Techo Park - Flutter与大前端的革命](Flutter-TECHO.md)
* [带你全面了解 Flutter它好在哪里它的坑在哪里 应该怎么学?](Flutter-WHAT.md)
* [Flutter 中键盘弹起时Scaffold 发生了什么变化](Flutter-KEY.md)
* [Flutter 2.0 下混合开发浅析](Flutter-Group.md)
* [Flutter 搭建 iOS 命令行服务打包发布全保姆式流程](Flutter-iOS-Build.md)
* [不一样角度带你了解 Flutter 中的滑动列表实现](Flutter-N-Scroll.md)
* [带你深入 Dart 解析一个有趣的引用和编译实验](DEMO-INTEREST.md)
* [Dart 里的类型系统](Dart-SYS.md)
* [Dart VM 的相关简介与运行模式解析](Dart-VM.md)
* [Flutter 里的语法糖解析,知其所然方能潇洒舞剑](Flutter-SU.md)
* [Flutter 实现完美的双向聊天列表效果,滑动列表的知识点](Flutter-SC.md)
* [Flutter 启动页的前世今生适配历程](Flutter-LA.md)
* [Flutter 快速解析 TextField 的内部原理](Flutter-TE.md)
* [谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter](Flutter-DevFest2021.md)
* [Flutter for Web 2022 年:简单探讨](Flutter-W2022.md)
* [2021 年的 Flutter 状态管理:如何选择?](Flutter-StateM.md)
* [Flutter 2.10 升级填坑指南](Flutter-210-FIX.md)
* [Flutter Riverpod 全面深入解析,为什么官方推荐它?](Flutter-Riverpod.md)
* [ Flutter 2022 战略和路线解读与想法](Flutter-2022-roadmap.md)
* [原生开发如何学习 Flutter | 谷歌社区说](Flutter-SQS.md)
* [Fluttter 混合开发下 HybridComposition 和 VirtualDisplay 的实现与未来演进](Flutter-HV.md)
* [Flutter 双向聊天列表效果进阶优化](Flutter-Chat2.md)
* [Flutter 上字体的另类玩法FontFeature ](Flutter-FontFeature.md)
* [移动端系统生物认证技术详解](Flutter-BIO.md)
* [完整解析使用 Github Action 构建和发布 Flutter 应用](Flutter-GB.md)
* [Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结](Flutter-120HZ.md)
* [Flutter Festival | 2022 年 Flutter 适合我吗Flutter VS Other 量化对比](Flutter-FF.md)
* [Flutter 从 TextField 安全泄漏问题深入探索文本输入流程](Flutter-TL.md)
* [Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑](Flutter-BIOS.md)
* [Flutter Web 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md)
* [大前端时代的乱流:带你了解最全面的 Flutter Web](Flutter-Web-T.md)
* [Flutter 深入探索混合开发的技术演进](Flutter-DWW.md)
* [Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer](Flutter-P3.md)
* [Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状](Flutter-Extended.md)
* [掘金x得物公开课 - Flutter 3.0下的混合开发演进](Flutter-DWN.md)
* [Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty ](Flutter-N1.md)
* [Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3 ](Flutter-N2.md)
* [Flutter 小技巧之玩转字体渲染和问题修复 ](Flutter-N3.md)
* [Flutter 小技巧之有趣的动画技巧](Flutter-N4.md)
* [Flutter 小技巧之 Dart 里的 List 和 Iterable 你真的搞懂了吗?](Flutter-N6.md)
* [Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密](Flutter-N7.md)
* [Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套](Flutter-N5.md)
* [Flutter 小技巧之优化你使用的 BuildContext](Flutter-N8.md)
* [如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果](Flutter-N9.md)
* [给掘金 Logo 快速添加动画效果,并支持全平台开发框架](Flutter-N10.md)
* [Flutter 实现 “真” 3D 动画效果,用纯代码实现立体 Dash 和 3D 掘金 Logo](Flutter-N11.md)
* [Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它](Flutter-N12.md)
* [Flutter 小技巧之优化你的代码性能](Fluttter-N13.md)
* [Flutter 之快速理解混合开发里的手势事件传递](Flutter-N17.md)
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)
* [Android 开发者的跨平台 - Flutter or Compose ](SQS.md)
* [Flutter 小技巧之快速理解手势逻辑](N15.md)
* [2023 Flutter Forward 大会回顾,快来看看 Flutter 的未来会有什么](Flutter-FF2023.md)
* [Flutter 2023 Roadmap 解析](Flutter-roadmap2023.md)
* [Flutter 小技巧之 3.7 性能优化background isolate](Flutter-N16.md)
* [Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?](Flutter-N18.md)
* [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md)
* [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md)
* [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md)
* [Flutter 工程化选择](GCH.md)
* [Flutter 工程化框架选择——搞定 Flutter 动画](Z1.md)
* [Flutter 工程化框架选择 — 搞定 UI 生产力](Z3.md)

View File

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