From 6ea34c1df81e952ca42b17ee8f8d266989f41551 Mon Sep 17 00:00:00 2001 From: guoshuyu <359369982@qq.com> Date: Mon, 20 Mar 2023 17:10:51 +0800 Subject: [PATCH] update --- Dart-300a.md | 574 ++++++++++++++++++++++++++++ FWREADME.md | 7 + Flutter-370.md | 361 ++++++++++++++++++ Flutter-FF2023.md | 198 ++++++++++ Flutter-GPT.md | 449 ++++++++++++++++++++++ Flutter-N16.md | 192 ++++++++++ Flutter-N18.md | 264 +++++++++++++ Flutter-N19.md | 118 ++++++ Flutter-N20.md | 823 +++++++++++++++++++++++++++++++++++++++++ Flutter-roadmap2023.md | 111 ++++++ README.md | 9 + SUMMARY.md | 148 ++++---- UPDATE.md | 2 + 13 files changed, 3190 insertions(+), 66 deletions(-) create mode 100644 Dart-300a.md create mode 100644 Flutter-370.md create mode 100644 Flutter-FF2023.md create mode 100644 Flutter-GPT.md create mode 100644 Flutter-N16.md create mode 100644 Flutter-N18.md create mode 100644 Flutter-N19.md create mode 100644 Flutter-N20.md create mode 100644 Flutter-roadmap2023.md diff --git a/Dart-300a.md b/Dart-300a.md new file mode 100644 index 0000000..c5a8e34 --- /dev/null +++ b/Dart-300a.md @@ -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` 之类的代码来承载数据多样性。 + +当然,可能你会觉得,这和我定义一个 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(x, y) a; +@metadata (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 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 尝尝鲜了~ \ No newline at end of file diff --git a/FWREADME.md b/FWREADME.md index f4b7771..dd2f115 100644 --- a/FWREADME.md +++ b/FWREADME.md @@ -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) diff --git a/Flutter-370.md b/Flutter-370.md new file mode 100644 index 0000000..3c05bf1 --- /dev/null +++ b/Flutter-370.md @@ -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 应用不再需要 bitcode,App 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 部分,所以按照惯例,等三个小版本会更稳。 \ No newline at end of file diff --git a/Flutter-FF2023.md b/Flutter-FF2023.md new file mode 100644 index 0000000..4344518 --- /dev/null +++ b/Flutter-FF2023.md @@ -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 Ufondu,Google 课堂软件工程师) + +另外从 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 `
`中** ,当以这种方式嵌入时,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 都能用上吧,在没有坑的情况下~ \ No newline at end of file diff --git a/Flutter-GPT.md b/Flutter-GPT.md new file mode 100644 index 0000000..d1237a4 --- /dev/null +++ b/Flutter-GPT.md @@ -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 可以如何提升我的开(mo)发(yu)效率,只是没想到随手一试,我会被 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` 循环时我就觉得不对了,但是秉承着「一切以实物为准」的理念,在运行后不出意外的发生了意外,确实是五个角一样大,不过是一个等边五边形。 + +> 算一个发胖的 ⭐️ 能解(jiao)释(bian)过去不? + +| ![](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 with TickerProviderStateMixin { + bool _isExploded = false; + late AnimationController _animationController; + late Animation _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 + 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 的魅力在于,它终有一天能得到准确的结果** 。 \ No newline at end of file diff --git a/Flutter-N16.md b/Flutter-N16.md new file mode 100644 index 0000000..e903a8e --- /dev/null +++ b/Flutter-N16.md @@ -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 sharedPreferencesSet = SharedPreferences.getInstance() + .then((sharedPreferences) => sharedPreferences.setBool('isDebug', true)); + /// 获取本地目录 + final Future 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 buffer = List.filled(256, 0); + while (reader.readIntoSync(buffer) == 256) { + List 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 sharedPreferencesSet = SharedPreferences.getInstance() + .then((sharedPreferences) => sharedPreferences.setBool('isDebug', true)); + + /// 获取本地目录 + final Future 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 buffer = List.filled(256, 0); + while (reader.readIntoSync(buffer) == 256) { + List 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 在后续的版本中还能给我们带来更多的惊喜。 \ No newline at end of file diff --git a/Flutter-N18.md b/Flutter-N18.md new file mode 100644 index 0000000..1818766 --- /dev/null +++ b/Flutter-N18.md @@ -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 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` 就是你必不可少的菜刀。 \ No newline at end of file diff --git a/Flutter-N19.md b/Flutter-N19.md new file mode 100644 index 0000000..5c70621 --- /dev/null +++ b/Flutter-N19.md @@ -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 能给我们带来更好的体验。 \ No newline at end of file diff --git a/Flutter-N20.md b/Flutter-N20.md new file mode 100644 index 0000000..63e1f21 --- /dev/null +++ b/Flutter-N20.md @@ -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 { + 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); + } +} +``` + +> **小技巧 2:Offset.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( + 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 { + _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 createState() => _PhotoGalleryDemoPageState(); +} + +class _PhotoGalleryDemoPageState extends State { + @override + Widget build(BuildContext context) { + return PhotoGallery(); + } +} + +class PhotoGallery extends StatefulWidget { + const PhotoGallery({Key? key}) : super(key: key); + + @override + State createState() => _PhotoGalleryState(); +} + +class _PhotoGalleryState extends State { + static const int _gridSize = 5; + + late List 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( + 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 createState() => _EightWaySwipeDetectorState(); +} + +class _EightWaySwipeDetectorState extends State { + 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 { + _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; +} +``` \ No newline at end of file diff --git a/Flutter-roadmap2023.md b/Flutter-roadmap2023.md new file mode 100644 index 0000000..baf1ee6 --- /dev/null +++ b/Flutter-roadmap2023.md @@ -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 更快? + +对于 Web,Flutter 一直都存在两种底层 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 的 RAIL(Response、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 `
` + +> 都是很值得期待的功能,期待下个版本时能够用上。 + +# 研究 + +由于 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 \ No newline at end of file diff --git a/README.md b/README.md index 20c5eb1..8cb42aa 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/SUMMARY.md b/SUMMARY.md index c21516c..d55e800 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -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) diff --git a/UPDATE.md b/UPDATE.md index dcff103..f6d5e22 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -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)