Compare commits
No commits in common. "master" and "add-license-1" have entirely different histories.
master
...
add-licens
Binary file not shown.
|
@ -1,3 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
custom: http://img.cdn.guoshuyu.cn/thanks.jpg
|
|
@ -1,2 +1 @@
|
|||
_book/
|
||||
.DS_Store
|
||||
_book/
|
348
DEMO-INTEREST.md
348
DEMO-INTEREST.md
|
@ -1,348 +0,0 @@
|
|||
本篇主要通过一个简单例子,讨论一下 Dart 代码里一个有趣的现象。
|
||||
|
||||
我们都知道 Dart 里一切都是对象,就连基础类型 `int` 、`double` 、`bool` 也都是 `class` 。
|
||||
|
||||
当我们对于 `int` 、 `double` 这些 `class` 进行的 `+` 、`-` 、`*` 、 `\` 等操作时,其实是执行了这个 `class` 的 `operator` 操作符的操作, 然后返回了新的 `num` 对象。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_DEMO-INTEREST/image1)
|
||||
|
||||
对于这些 `operator` 操作最终会通过 `VM` 去进行实现返回,而本质上 dart 代码也只是文本,需要最终编译成二进制去运行。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_DEMO-INTEREST/image2)
|
||||
|
||||
> **以下例子基于 dart 2.12.3 测试**
|
||||
|
||||
那这里想要讨论什么呢?
|
||||
|
||||
首先我们看一段代码,如下代码所示,可以看到:
|
||||
|
||||
- 首先我们定义了一个叫 `idx` 的 `int` 型参数;
|
||||
- 然后在 `for` 循环里添加了三个 `InkWell` 可点击控件;
|
||||
- 最后在 `onTap` 里面将 `idx` 打印出来;
|
||||
|
||||
|
||||
```dart
|
||||
class MyHomePage extends StatelessWidget {
|
||||
var images = ["RRR", "RRR", "RRR",];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> contents = [];
|
||||
int idx = 0;
|
||||
for (var imgUrl in images) {
|
||||
contents.add(InkWell(
|
||||
onTap: () {
|
||||
print("######## $idx");
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
child: Text(imgUrl),
|
||||
)));
|
||||
idx++;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
...contents,
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 问题来了,**你觉得点击这三个 `InkWell` 打印出来的会是什么结果?**
|
||||
|
||||
- **答案是打印出来的都是 3。**
|
||||
|
||||
为什么呢?让我们看这段代码编译后的逻辑,如下所示代码,可以看到上述代码编译后, **`print` 函数里指向的永远是 `idx` 这个 `int*` 指针,当我们点击时,最终打印出来的都是最后的 `idx` 的值**。
|
||||
|
||||
```c++
|
||||
@#C475
|
||||
method build(fra2::BuildContext* context) → fra2::Widget* {
|
||||
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
|
||||
core::int* idx = 0;
|
||||
{
|
||||
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
|
||||
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
|
||||
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
|
||||
{
|
||||
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
|
||||
core::print("######## ${idx}");
|
||||
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
|
||||
idx = idx.{core::num::+}(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**那如果我们需要打印出来的是每个 `InkWell` 自己的 `index` 呢?**
|
||||
|
||||
如下代码所示,我们在 for 循环里增加了一个 `index` 参数,把每次 `idx` 都赋值给 `index` ,这样点击打印出来的结果,就会是点击对应的 `index` 。
|
||||
|
||||
|
||||
```dart
|
||||
class MyHomePage extends StatelessWidget {
|
||||
var images = ["RRR", "RRR", "RRR",];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> contents = [];
|
||||
int idx = 0;
|
||||
for (var imgUrl in images) {
|
||||
int index = idx;
|
||||
contents.add(InkWell(
|
||||
onTap: () {
|
||||
print("######## $index");
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
child: Text(imgUrl),
|
||||
)));
|
||||
idx++;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
...contents,
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
为什么呢?
|
||||
|
||||
让我们看新编译出来的代码,如下所示,可以看到对了 `core::int* index = idx;` 这段代码,然后回忆下前面所说的,**Dart 里基本类型都是对象,而 `operator` 操作符运算后返回新的对象。**
|
||||
|
||||
|
||||
这样就等于用 `index` 把每次的操作到保存下来,而 `print` 打印的自然就是每次被保存下来的 `idx` 。
|
||||
|
||||
|
||||
```c++
|
||||
@#C475
|
||||
method build(fra2::BuildContext* context) → fra2::Widget* {
|
||||
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
|
||||
core::int* idx = 0;
|
||||
{
|
||||
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
|
||||
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
|
||||
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
|
||||
{
|
||||
core::int* index = idx;
|
||||
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
|
||||
core::print("######## ${index}");
|
||||
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
|
||||
idx = idx.{core::num::+}(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**那再来个不一样的写法**。
|
||||
|
||||
如下代码所示,把 `InkWell` 放到一个 `getItem` 函数里返回,然后 `index` 通过函数参数传递进来,可以看到运行后的结果,也是点击对应 `InkWell` 打印对应的 `index` 。
|
||||
|
||||
|
||||
```dart
|
||||
class MyHomePage extends StatelessWidget {
|
||||
var images = ["RRR", "RRR", "RRR",];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> contents = [];
|
||||
int idx = 0;
|
||||
getItem(int index, String imgUrl) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
print("######## $index");
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
child: Text(imgUrl)));
|
||||
}
|
||||
for (var imgUrl in images) {
|
||||
contents.add(getItem(idx, imgUrl));
|
||||
idx++;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
...contents,
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
为什么呢?
|
||||
|
||||
我们继续看编译后的代码,如下代码所示,其实就是每次的 `idx` 都通过 ` getItem.call(idx)` 被 `getItem` 的 `index` 引用,然后下次又再次传递一个对应的 `idx` 进去,原理其实和上面的情况一样,所以每次点击也会打印对应的 `index` 。
|
||||
|
||||
|
||||
```c++
|
||||
@#C475
|
||||
method build(fra2::BuildContext* context) → fra2::Widget* {
|
||||
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
|
||||
core::int* idx = 0;
|
||||
function getItem(core::int* index) → ink5::InkWell* {
|
||||
return new ink5::InkWell::•(onTap: () → Null {
|
||||
core::print("######## ${index}");
|
||||
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614);
|
||||
}
|
||||
{
|
||||
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
|
||||
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
|
||||
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
|
||||
{
|
||||
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}([@vm.call-site-attributes.metadata=receiverType:library package:flutter/src/material/ink_well.dart::InkWell* Function(dart.core::int*)*] getItem.call(idx));
|
||||
idx = idx.{core::num::+}(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**最后我们再换种写法。**
|
||||
|
||||
如下代码所示,直接用最基本的 `for` 循环添加 `InkWell` 并打印 `idx` ,结果会怎么样呢?
|
||||
|
||||
|
||||
```dart
|
||||
class MyHomePage extends StatelessWidget {
|
||||
var images = [ "RRR","RRR", "RRR"];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> contents = [];
|
||||
for (int idx = 0; idx < images.length; idx++) {
|
||||
contents.add(InkWell(
|
||||
onTap: () {
|
||||
print("######## $idx");
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
child: Text(images[idx]),
|
||||
)));
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
...contents,
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
答案就是:**点击对应 `InkWell` 打印对应的 `index`**。
|
||||
|
||||
为什么呢?
|
||||
|
||||
我们继续看编译后的代码,可以看到都是打印的 `idx` ,为什么这样就可以正常呢?
|
||||
|
||||
**这里最大的不同就是`idx` 被声明的位置不同**。
|
||||
|
||||
|
||||
```c++
|
||||
@#C475
|
||||
method build(fra2::BuildContext* context) → fra2::Widget* {
|
||||
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
|
||||
for (core::int* idx = 0; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
|
||||
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
|
||||
core::print("######## ${idx}");
|
||||
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
|
||||
}
|
||||
```
|
||||
|
||||
那这时候我们重新调整下,**把 `idx` 放到 for 外面,点击测试会发现,打印的结果又都是 3**。
|
||||
|
||||
|
||||
```
|
||||
class MyHomePage extends StatelessWidget {
|
||||
var images = [ "RRR", "RRR","RRR"];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> contents = [];
|
||||
int idx = 0;
|
||||
for (; idx < images.length; idx++) {
|
||||
contents.add(InkWell(
|
||||
onTap: () {
|
||||
print("######## $idx");
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
child: Text(images[idx]),
|
||||
)));
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
...contents,
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这是为什么呢?
|
||||
|
||||
看编译后的代码,唯一不同的就是 `core::int* idx` 的声明位置,那原因究竟是什么呢?
|
||||
|
||||
```c++
|
||||
@#C475
|
||||
method build(fra2::BuildContext* context) → fra2::Widget* {
|
||||
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
|
||||
core::int* idx = 0;
|
||||
for (; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
|
||||
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
|
||||
core::print("######## ${idx}");
|
||||
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
因为 `onTap` 是在点击后才输出参数的,而对于 `for (core::int* idx = 0;` 来说,`idx` 的作用域是在 `for` 循环之内,所以编译后在 `onTap` 内要有对应持有一个值,来保存需要输出的结果。
|
||||
|
||||
而对于 `for` 循环外定义的 `core::int* idx` , 循环内的所有 `onTap` 都可以指向它这个地址,所以导致点击时都输出了同一个 `idx` 的值。
|
||||
|
||||
至于为什么会有这样的逻辑,在深入的运行时逻辑就没有去探索了(懒),**推测应该是编译后的二进制文件在运行时,针对循环外的参数和循环内的参数优化有关系**。
|
||||
|
||||
理论上,应该是属于变量捕获:
|
||||
|
||||
* 对于全局变量,不会捕获,通过全局变量访问。
|
||||
* 对于局部变量,自动变量将会捕获,且是值传递。
|
||||
|
||||
|
||||
|
||||
最后,如果你也想查看 `dill` 内容,可以通过 mac 下的 xxd 命令:
|
||||
|
||||
```
|
||||
xxd /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill
|
||||
```
|
||||
|
||||
也可以通过 `dump_kernel.dart` (在完整版 `dart-sdk` 的`/Users/guoshuyu/workspace/dart-sdk/pkg/vm` 目录下)执行如下命令,生成 `app.dill.txt` 查看,比如你可以查看 `final` 和 `const` 编译后的区别。
|
||||
|
||||
```
|
||||
dart dump_kernel.dart /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill.txt
|
||||
```
|
267
Dart-212.md
267
Dart-212.md
|
@ -1,267 +0,0 @@
|
|||
今天 Dart 2.12 发布了,该版本具有稳定的空安全声明和Dart FFI版本。
|
||||
|
||||
空安全是最新的关键生产力功能,旨在帮助开发者避免空错误,这是一项通常很难被发现的错误。
|
||||
|
||||
FFI是一种互操作性机制,可以让开发者调用 C 语言编写的现有代码,例如调用 Windows Win32 API。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-212/image1)
|
||||
|
||||
### Dart平台的独特功能
|
||||
|
||||
在详细解释空安全声明和 FFI 之前,让我们先讨论一下 Dart 平台如何将它适合我们的目标平台上。
|
||||
|
||||
编程语言通常倾向于共享许多功能,例如许多语言支持面向对象的编程或在在 Web 上运行,真正使语言与众不同的是它们独特的功能组合。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-212/image2)
|
||||
|
||||
Dart的独特功能涵盖了三个方面:
|
||||
|
||||
- 可移植性:高效的编译器为设备生成 x86 和 ARM 机器代码,并为 Web 生成优化后的JavaScript代码。所以 Dart 的持续目的就是支持:移动设备、台式机、应用后端等等。大量的库和软件包提供了可在所有平台上使用的一致性 API,从而进一步降低了创建真正的多平台应用程序的成本。
|
||||
|
||||
- 高效:Dart平台支持热重装,从而可以对本机设备和Web进行快速地迭代和开发。Dart提供了丰富的结构,如` isolates` 和 `async/await`,用于处理常见的并发和事件驱动的模式。
|
||||
|
||||
- 健壮:Dart 是健全的,null 安全系统会在开发过程中捕获错误。整个平台具有高度的可扩展性和可靠性,Dart 已经有十多年的历史被运用于生产开发,其中包括 Google Ads和Google Assistant等业务关键型应用程序。
|
||||
|
||||
> PS:事实上被大规模应用还是因为近几年的 Flutter
|
||||
|
||||
可靠的空安全声明使类型系统更加强大,并具更好的性能,而 Dart FFI 可以让开发者用现有的 C 库来得到了更便捷的可移植性,可以选择对性能要求很高的任务使用经过C代码来调度实现。
|
||||
|
||||
|
||||
### 空安全声明
|
||||
|
||||
|
||||
自 Dart 2.0 引入空安全声明以来,空安全声明是 Dart 语言的最大补充。空安全性进一步增强了类型系统,使得开发者能够在开发阶段就捕获到空错误,这是过去应用程序崩溃的常见原因。
|
||||
|
||||
合理的空安全声明是围绕一些[核心原则设计](https://dart.dev/null-safety#null-safety-principles)的,接下来让我们了解下这对开发人员会有声明影响。
|
||||
|
||||
### 默认情况下不可为空
|
||||
|
||||
空安全声明之前的核心挑战是,开发者无法分辨**传递空值的代码与不能使用空值的代码之间的区别**。
|
||||
|
||||
几个月前,我们在 Flutter master channel 中发现了一个错误,该错误会在某些机器配置上会使得各种 flutter 工具命令崩溃,并出现 null 错误:`The method '>=' was called on null` , 而根本的问题是这样的代码:
|
||||
|
||||
```
|
||||
final int major = version?.major;
|
||||
final int minor = version?.minor;
|
||||
if (globals.platform.isMacOS) {
|
||||
// plugin path of Android Studio changed after version 4.1.
|
||||
if (major >= 4 && minor >= 1) {
|
||||
...
|
||||
```
|
||||
|
||||
发现错误的地方了吗?因为 `version` 可以为 `null` ,所以 `major` 和 `minor` 也可以为 null。
|
||||
|
||||
这样的独立错误看起来很容易被发现,但实际上即使是经过严格的代码审查过程(如Flutter Code Review),这样的代码也始终无处不在。所以出于安全考虑,静态分析会立即捕获此问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-212/image3)
|
||||
|
||||
那是一个非常简单的错误,而在 Google 的内部,早期使用 null 安全性的过程中,我们发现了很多复杂的错误,其中一些是已经存在多年的 bug,但是如果没有 null 安全性的额外静态检查,团队就无法找到原因。
|
||||
|
||||
这里有一些例子:
|
||||
|
||||
- 一个内部团队发现,他们经常检查到永远不能为 null 的表达式得到了 null 值。使用protobuf 的代码中最经常出现此问题,其中可选字段在未设置时返回默认值,并且永远不会为null。如此一来,通过混淆默认值和空值,代码错误地检查了默认条件。
|
||||
|
||||
- Google Pay 小组在 Flutter 代码中发现了一些错误,这些错误会在 `State` 在上下文之外尝试访问 Flutter 对象的 `Widget` 。在实现 null 安全之前,这些对象将返回 null并掩盖错误;出于安全考虑,声明分析确定这些属性永远不会为空,并引发了分析错误。
|
||||
|
||||
- Flutter 小组发现了一个错误,如果将 `null` 的 `scene` 参数传递给 `Window.render()`,Flutter 引擎可能会崩溃。在进行 null 安全迁移期间,他们添加了一个提示,将 `Scene` 标记为 `non-nullable`,然后能够轻松地防止可能触发 null 的潜在应用崩溃。
|
||||
|
||||
### 默认情况下使用非空
|
||||
|
||||
一旦启用空安全,变量声明的基本行为会被改变,因为默认的类型是不可为空:
|
||||
|
||||
```
|
||||
// In null-safe Dart, none of these can ever be null.
|
||||
var i = 42; // Inferred to be an int.
|
||||
String name = getFileName();
|
||||
final b = Foo();
|
||||
```
|
||||
|
||||
如果要创建一个可以包含值或 null 的变量,则需要通过 ? 在类型声明中添加后缀来使该变量在变量声明中显式显示:
|
||||
|
||||
```
|
||||
// aNullableInt can hold either an integer or null.
|
||||
int? aNullableInt = null;
|
||||
```
|
||||
|
||||
空安全性的实现是健壮的,并且具有丰富的静态流分析功能,使用可空类型的工作变得更加容易。例如,在检查了null之后,Dart将局部变量的类型从 nullable 提升为 non-nullable :
|
||||
|
||||
```
|
||||
int definitelyInt(int? aNullableInt) {
|
||||
if (aNullableInt == null) {
|
||||
return 0;
|
||||
}
|
||||
// aNullableInt has now promoted to a non-null int.
|
||||
return aNullableInt;
|
||||
}
|
||||
```
|
||||
|
||||
我们还添加了一个新关键字 `required ` ,当命名参数被标记为 `required`(在Flutter小部件API中经常发生)并且调用者忘记提供参数时,就会发生分析错误:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-212/image4)
|
||||
|
||||
### 逐步迁移到空安全性
|
||||
|
||||
因为空安全性改变了我们的编码习惯,所以如果坚持强制采用,那将是极度破坏性的。所以我们决定让开发者在最需要的时候启用,所以空安全是一项可选功能:你可以用 Dart 2.12 而无需被迫启用空安全,你甚至可以依赖已经启用了空安全性的软件包,无论应用程序或软件包是否启用了空安全性。
|
||||
|
||||
|
||||
为了帮助开发者将现有代码迁移到安全性状态,我们提供了迁移工具和迁移指南。这些工具首先将分析所有现有代码,然后开发者可以交互地查看该工具推断的可空性属性。
|
||||
|
||||
如果开发者不同意该工具的结论,则可以通过添加可空性提示以更改推断,添加一些迁移提示可能会对迁移的质量带来较大的影响。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-212/image5)
|
||||
|
||||
目前使用 `dart create` 和 `flutter create` 不启用空安全性声明创建的新程序包和应用程序。使用 `dart migrate` 可以简单地启用空安全的功能。
|
||||
|
||||
### Dart 生态系统的空安全迁移状况
|
||||
|
||||
在过去的一年中,我们提供了几种空安全声明的预览版和 Beta 版,目的是为生态系统植入支持空安全的软件包。
|
||||
|
||||
这项准备工作很重要,因为我们建议按顺序迁移,以确保空安全声明不会影响开发者现有的应用,开发者在所以依赖完成迁移之前最好不要启动空安全配置。
|
||||
|
||||
Dart,Flutter,Firebase 和 Material 团队已经发布提供的数百个具备 null 安全的软件包的版本,而且我们已经从惊人的 Dart 和 Flutter 生态系统中获得了巨大的支持,因此pub.dev 现在有超过一千个支持 null 安全的软件包。
|
||||
|
||||
重要的是,最受欢迎的那些软件包已首先完成迁移,因此对于今天的发布而言,最流行的前100个软件包中有98%是支持 null safety,前250个顶级软件包中的 78% 和前500个顶级软件包中的 57% 也已经支持零安全性。
|
||||
|
||||
我们期待在未来几周内在 pub.dev 上看到更多具有空安全性的软件包。分析表明,pub.dev 上的绝大多数软件包已被解除阻止,可以开始迁移。
|
||||
|
||||
|
||||
### 完全空安全性的好处
|
||||
|
||||
完全迁移后,Dart 的空安全性就可以启用了,这意味着 Dart 100% 确保具有不可为 null 的类型的表达式不能为 null 。
|
||||
|
||||
当 Dart 分析开发者的代码并确定某个变量不可为空时,该变量将始终为不可为空,而 Dart 与 Swift 共享空安全声明,这在其他编程语言上很少见。
|
||||
|
||||
> PS :Kotlin 有话要说
|
||||
|
||||
|
||||
Dart 空安全声明的健壮性性还具有另一个意义:**这意味着您的程序可以更小,更快**。
|
||||
|
||||
由于 Dart 确保不可为空的变量永远不会为 null ,因此 Dart 可以进行优化。例如 Dart 提前(AOT)编译器可以生成更小,更快的本机代码,因为当知道变量不为 null 时,它不需要添加对 null 的检查。
|
||||
|
||||
|
||||
### Dart FFI,用于将Dart与C集成
|
||||
|
||||
Dart FFI 让开发者能够利用 C 语言中的现有代码,以实现更好的可移植性,并且利用调整的 C 代码集成以执行对性能要求较高的任务。
|
||||
|
||||
从Dart 2.12 开始,Dart FFI 已脱离Beta阶段,现已被认为稳定并且可以投入生产,我们还添加了一些新功能,包括嵌套结构和按值传递结构。
|
||||
|
||||
|
||||
#### 通过值传递结构
|
||||
|
||||
可以在C代码中按引用和按值传递结构,FFI 以前仅支持按引用传递,但从 Dart 2.12 开始开发者可以按值传递结构,例如:
|
||||
|
||||
```
|
||||
struct Link {
|
||||
double value;
|
||||
Link* next;
|
||||
};
|
||||
void MoveByReference(Link* link) {
|
||||
link->value = link->value + 10.0;
|
||||
}
|
||||
Coord MoveByValue(Link link) {
|
||||
link.value = link.value + 10.0;
|
||||
return link;
|
||||
}
|
||||
```
|
||||
|
||||
#### 嵌套结构
|
||||
|
||||
C API 通常使用嵌套结构-本身包含结构体的结构,例如以下示例:
|
||||
|
||||
```
|
||||
struct Wheel {
|
||||
int spokes;
|
||||
};
|
||||
struct Bike {
|
||||
struct Wheel front;
|
||||
struct Wheel rear;
|
||||
int buildYear;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
从Dart 2.12开始,FFI支持嵌套结构。
|
||||
|
||||
|
||||
### API变更
|
||||
|
||||
为了完善 FFI 稳定并支持上述功能,我们进行了一些较小的API更改。
|
||||
|
||||
现在禁止创建空结构 ([#44622](https://github.com/dart-lang/sdk/issues/44622)) ,并产生弃用警告,开发者可以使用新的类型 `Opaque` 来表示空结构。
|
||||
|
||||
`dart:ffi`的 `sizeOf` 、 `elementAt` 以及 `ref` 现在需要编译时类型参数([#44621](https://github.com/dart-lang/sdk/issues/44621))因为 `package:ffi` 已添加了新的便利功能,所以在常见情况下,不需要分配和释放内存。
|
||||
```
|
||||
// Allocate a pointer to an Utf8 array, fill it from a Dart string,
|
||||
// pass it to a C function, convert the result, and free the arg.
|
||||
//
|
||||
// Before API change:
|
||||
final pointer = allocate<Int8>(count: 10);
|
||||
free(pointer);
|
||||
final arg = Utf8.toUtf8('Michael');
|
||||
var result = helloWorldInC(arg);
|
||||
print(Utf8.fromUtf8(result);
|
||||
free(arg);
|
||||
// After API change:
|
||||
final pointer = calloc<Int8>(10);
|
||||
calloc.free(pointer);
|
||||
final arg = 'Michael'.toNativeUtf8();
|
||||
var result = helloWorldInC(arg);
|
||||
print(result.toDartString);
|
||||
calloc.free(arg);
|
||||
```
|
||||
|
||||
### 自动生成FFI绑定
|
||||
|
||||
对于较大的 API 暴露,在编写与 C 代码集成的 Dart 绑定可能会非常耗时,为了减轻这种负担,我们构建了一个绑定生成器,用于根据 C 头文件自动创建 FFI 包装器 `package:ffigen`。
|
||||
|
||||
### FFI路线图
|
||||
|
||||
随着核心 FFI 平台的完成,我们将重点转移到扩展FFI功能上,使其具有在核心平台之上分层的功能,我们正在调查的一些功能包括:
|
||||
|
||||
- 特定于ABI的数据类型,例如int,long,size_t [#36140](https://github.com/dart-lang/sdk/issues/36140)
|
||||
- 内联结构中的数组 [#35763](https://github.com/dart-lang/sdk/issues/35763)
|
||||
- 打包的结构 [#38158](https://github.com/dart-lang/sdk/issues/38158)
|
||||
- 联合类型 [#38491](https://github.com/dart-lang/sdk/issues/38491)
|
||||
- 将 finalizers 暴露给 Dart[#35770](https://github.com/dart-lang/sdk/issues/35770)
|
||||
|
||||
### FFI的示例用法
|
||||
|
||||
前面我们已经讲了 Dart FFI 的许多创造性用法,以与各种基于 C 的API集成,这里有一些例子:
|
||||
|
||||
- [open_file](https://pub.dev/packages/open_file) 是用于跨多个平台打开文件的单个API,它使用 FFI 来调用 Windows,macOS 和Linux上的本机操作系统API。https://pub.dev/packages/open_file
|
||||
|
||||
- [win32](https://pub.dev/packages/win32) 封装了最常见的Win32 API,从而可以直接从Dart调用各种Windows API。https://pub.dev/packages/win32
|
||||
|
||||
- [objectbox](https://pub.dev/packages/objectbox) 是由基于C的实现支持的快速数据库。https://pub.dev/packages/objectbox
|
||||
|
||||
- [tflite_flutter](https://pub.dev/packages/tflite_flutter) 使用FFI包装TensorFlow Lite API。
|
||||
|
||||
### Dart语言的下一步是什么?
|
||||
|
||||
空安全声明是我们几年来对 Dart 语言所做的最大改变,接下来我们将考虑在我们强大的基础上对语言和平台进行更多的增量更改。
|
||||
|
||||
我们在语言设计渠道中正在尝试的一些事情:
|
||||
|
||||
- Type aliases [#65](https://github.com/dart-lang/language/issues/65):可以为非函数类型创建类型别名,例如可以创建一个 `typedef` 并将其用作变量类型:
|
||||
|
||||
```
|
||||
typedef IntList = List <int>;
|
||||
IntList il = [1,2,3];
|
||||
```
|
||||
|
||||
- Type aliases [#120](https://github.com/dart-lang/language/issues/120):添加了一个新的,完全可重写的 `>>>` 运算符,用于对整数进行无符号移位。
|
||||
|
||||
- Generic metadata annotations [#1297](https://github.com/dart-lang/language/issues/1297):扩展元数据注释以也支持包含类型参数的注释。
|
||||
|
||||
- Static meta-programming [#1482](https://github.com/dart-lang/language/issues/1482):支持静态元编程 — Dart程序在编译过程中会生成新的Dart 源代码,类似于 Rust 宏和 Swift 函数生成器(该功能仍处于早期探索阶段,但是我们认为它可以启用当今依赖于代码生成的用例。)
|
||||
|
||||
|
||||
### Dart 2.12 is available now
|
||||
|
||||
|
||||
Dart 2.12 和 Flutter 2.0 SDK 现已提供具有可靠的空安全性和稳定FFI的,所以请花点时间查看 Dart 和 Flutter 的已知的无效安全问题,如果你发现任何其他问题,请在 Dart tracker 中报告这些问题。
|
||||
|
||||
> https://github.com/dart-lang/sdk/issues
|
||||
|
||||
如果你已经在pub.dev上发布了软件包,请立即查看迁移指南,并了解如何迁移以达到安全性。迁移软件包可能会帮助解除阻止其他依赖于该软件包的软件包和应用程序,并且我们还要感谢已经迁移的人!
|
140
Dart-214.md
140
Dart-214.md
|
@ -1,140 +0,0 @@
|
|||
> 原文链接: https://medium.com/dartlang/announcing-dart-2-14-b48b9bb2fb67
|
||||
|
||||
Dart 2.14 的发布对 Apple Silicon 处理器提供了更好的支持,并新增了更多提升生产力的功能,例如通过代码样式分析捕获 lint 错误、更快的发布工具、更好的级联代码格式以及一些细小的语言特性更新。
|
||||
|
||||
|
||||
## Dart SDK 对 Apple Silicon 支持
|
||||
|
||||
自从在 2020 年末 Apple 发布了新的 [Apple Silicon](https://support.apple.com/en-us/HT211814) 处理器以来, Dart SDK 一直致力于增加对该处理器上的 Native 执行支持。
|
||||
|
||||
现在从 Dart 2.14.1 正式增加了对 Apple Silicon 的支持,当 [下载](https://dart.dev/get-dart) MacOS 的 Dart SDK时,一定要选择 ARM64 选项,**这里需要额外注意, Flutter SDK 中的 Dart SDK 还没有绑定这一项改进**。
|
||||
|
||||
本次更新支持在 Apple Silicon 上运行 SDK/Dart VM 本身,以及对 `dart compile` 编译后的可执行文件在 Apple Silicon 上运行的支持,**由于 Dart 命令行工具使用原生 Apple Silicon ,因此它们的启动速度会快得多** 。
|
||||
|
||||
## Dart 和 Flutter 共享的标准 lint
|
||||
|
||||
开发人员通常会需要他们的代码遵循某种风格,其中许多规则不仅仅是风格偏好(如众所周知的制表符与空格的问题),还涵盖了可能导致错误或引入错误的编码风格。
|
||||
|
||||
|
||||
比如 **Dart 风格指南要求对所有控制流结构使用花括号**,例如 `if-else` 语句,这可以防止经典的 [dangling else](https://en.wikipedia.org/wiki/Dangling_else) 问题,也就是在多个嵌套的 `if-else` 语句上会存在解释歧义。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-214/image1)
|
||||
|
||||
另一个例子是类型推断,虽然在声明具有初始值的变量时使用类型推断没有问题,但**在[声明未初始化的变量](https://dart-lang.github.io/linter/lints/prefer_typing_uninitialized_variables.html) 时指定类型很重要,因为这可以确保类型安全**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-214/image2)
|
||||
|
||||
良好代码风格的通常是通过代码审查来维持,但是通过在编写代码时,运行静态分析来强制执行规则通常会更有效得多。
|
||||
|
||||
在 Dart 中,这种静态分析规则是高度[可配置的](https://dart.dev/guides/language/analysis-options),Dart 提供了有[数百条样式规则](https://dart.dev/tools/linter-rules)(也称为*lints*),有了如此丰富的选项,选择启用这些的规则时,一开始可能会有些不知所措。
|
||||
|
||||
> 配置支持: https://dart.dev/guides/language/analysis-options
|
||||
>
|
||||
> lint 规则: https://dart.dev/tools/linter-rules
|
||||
|
||||
Dart 团队维护了一个 [Dart 风格指南](https://dart.dev/guides/language/effective-dart/style),它描述了 Dart 团队认为编写和设计 Dart 代码的最佳方式。
|
||||
|
||||
> 风格指南: https://dart.dev/guides/language/effective-dart/style
|
||||
|
||||
许多开发人员以及 pub.dev 站点[评分](https://pub.dev/help/scoring)引擎都使用了一套叫 [Pedantic](https://github.com/google/pedantic) 的 lint 规则, Pedantic 起源于 Google 内部的 Dart 风格指南,由于历史原因它不同于一般的 Dart 风格指南,此外 Flutter 框架也从未使用过 Pedantic 的规则集,而是拥有自己的一套规范规则。
|
||||
|
||||
这听起来可能有点混乱,但是在本次的 2.14 发布中,Dart 团队很高兴地宣布**现在拥有一套全新的 lint 集合来实现代码样式指南**,并且 Dart 和 Flutter SDK 默认情况下将这些规则集用于新项目:
|
||||
|
||||
- [`package:lints/core.yaml`](https://github.com/dart-lang/lints/blob/main/lib/core.yaml): **所有 Dart 代码都应遵循的 Dart 风格指南中的主要规则,pub.dev 评分引擎已更新为 lints/core 而不是 Pedantic。**
|
||||
|
||||
- `package:lints/recommended.yaml` :核心规则之外加上推荐规则,建议将它用于所有通用 Dart 代码。
|
||||
|
||||
- `package:flutter_lints/flutter.yaml`:核心和推荐之外的 Flutter 特定推荐规则,这个集合推荐用于所有 Flutter 代码。
|
||||
|
||||
如果你已经存在现有的 Dart 或者 Flutter项目,强烈建议升级到这些新规则集,从 pedantic 升级只需几步:https://github.com/dart-lang/lints#migrating-from-packagepedantic 。
|
||||
|
||||
## Dart 格式化程序和级联
|
||||
|
||||
Dart 2.14 对 Dart 格式化程序如何使用[级联](https://dart.dev/guides/language/language-tour#cascade-notation) 格式化代码进行了一些优化。
|
||||
|
||||
以前格式化程序在某些情况下出现一些令人困惑的格式,例如 `doIt()` 在这个例子中调用了什么?
|
||||
|
||||
```dart
|
||||
var result = errorState ? foo : bad..doIt();
|
||||
```
|
||||
|
||||
它看起来像是被 `bad` 调用 ,**但实际上级联适是用于整个 `?` 表达式上的**,因此级联是在该表达式的结果上调用的,而不仅仅是在 false 子句上,新的格式化程序清晰地描述了这一点:
|
||||
|
||||
```dart
|
||||
var result = errorState ? foo : bad\
|
||||
..doIt();
|
||||
```
|
||||
|
||||
|
||||
Dart 团队还大大提高了格式化包含级联的代码的速度;在[协议缓冲区](https://developers.google.com/protocol-buffers/docs/reference/dart-generated)生成的 Dart 代码中,可以看到格式化速度提高了 10 倍。
|
||||
|
||||
## Pub 支持忽略文件
|
||||
|
||||
目前当开发者将包[发布](https://dart.dev/tools/pub/publishing)到[pub.dev](https://pub.dev/)社区时,pub 会抓取该文件夹中的所有文件,但是会跳过隐藏文件(以 . 开头的文件)和`.gitignore` 文件。
|
||||
|
||||
Dart 2.14 中更新的 pub 命令支持新 `.pubignore` 文件,开发者可以在其中列出不想上传到 pub.dev 的文件,此文件使用与 `.gitignore` 文件相同的格式。
|
||||
|
||||
> 有关详细信息,请参阅包发布文档 https://dart.dev/tools/pub/publishing#what-files-are-published
|
||||
|
||||
## Pub and "dart test" 性能
|
||||
|
||||
虽然 pub 最常用于管理代码依赖项,但它还有第二个重要的用途:驱动工具。
|
||||
|
||||
比如 Dart 测试工具通过 `dart test` 命令运行,而它实际上只是 `command pub run test:test` 命令的包装, `package:test` 在调用该 `test` 入口点之前,pub 首先将其编译为可以更快运行的本机代码。
|
||||
|
||||
在 Dart 2.14 之前对 pubspec 的任何更改(包括与 `package:test` 无关的更改)都会使此测试构建无效,并且还会看到一堆这样的输出,其中包含“预编译可执行文件”:
|
||||
|
||||
```
|
||||
$ dart test\
|
||||
Precompiling executable... (11.6s)\
|
||||
Precompiled test:test.\
|
||||
00:01 +1: All tests passed!
|
||||
```
|
||||
|
||||
在 Dart 2.14 中,pub 在构建步骤方面更加智能,让构建仅在版本更改时发生,此外还使用并行化改进了执行构建步骤的方式,因此可以完成得更快。
|
||||
|
||||
|
||||
## 新的语言功能
|
||||
|
||||
Dart 2.14 还包含一些语言特性变化。
|
||||
|
||||
首先添加了一个新的 [三重移位](https://github.com/dart-lang/language/issues/120) 运算符 ( `>>>`),这类似于现有的移位运算符 ( `>>`),但 `>>` 执行算术移位,`>>>` 执行逻辑或无符号移位,其中零位移入最高有效位,而不管被移位的数字是正数还是负数。
|
||||
|
||||
此次还删除了对类型参数的旧限制,该限制不允许使用泛型函数类型作为类型参数,以下所有内容在 2.14 之前都是无效的,但现在是允许的:
|
||||
|
||||
```dart
|
||||
late List<T Function<T>(T)> idFunctions;
|
||||
var callback = [<T>(T value) => value];
|
||||
late S Function<S extends T Function<T>(T)>(S) f;
|
||||
```
|
||||
|
||||
最后对注释类型进行了小幅调整,(诸如 `@Deprecated` 在 Dart 代码中常用来捕获元数据的注解)以前注解不能传递类型参数,因此 `@TypeHelper<int>(42, "The meaning")` 不允许使用诸如此类的代码,而现在此限制现已取消。
|
||||
|
||||
## 包和核心库更改
|
||||
|
||||
对核心 Dart 包和库进行了许多增强修改,包括:
|
||||
|
||||
- `dart:core`: 添加了静态方法 `hash`、`hashAll` 和 `hashAllUnordered`。
|
||||
|
||||
- `dart:core`: `DateTime` 类现在可以更好地处理本地时间。
|
||||
|
||||
- `package:ffi`:添加了对使用 [arena](https://pub.dev/documentation/ffi/latest/ffi/Arena-class.html) 分配器管理内存的支持([示例](https://github.com/dart-lang/sdk/blob/master/samples/ffi/resource_management/arena_sample.dart))。Arenas 是一种[基于区域的内存管理](https://en.wikipedia.org/wiki/Region-based_memory_management)形式,一旦退出 arena/region 就会自动释放资源。
|
||||
|
||||
- `package:ffigen`:现在支持从 C 类型定义生成 Dart 类型定义。
|
||||
|
||||
|
||||
## 重大变化
|
||||
|
||||
Dart 2.14 还包含一些重大更改,预计这些变化只会影响一些特定的用例。
|
||||
|
||||
### [#46545](https://github.com/dart-lang/sdk/issues/46545):取消对 ECMAScript5 的支持
|
||||
|
||||
[所有浏览器都](https://caniuse.com/es6)支持最新的 ECMAScript 版本,因此两年前 Dart 就[宣布](https://groups.google.com/a/dartlang.org/g/announce/c/x7eDinVT6fM/m/ZSFl2a9tEAAJ?pli=1) 计划弃用对 ECMAScript 5 (ES5) 的支持,这使 Dart 能够利用最新 ECMAScript 中的改进并生成更小的输出,**在 Dart 2.14 中,这项工作已经完成,Dart Web 编译器不再支持 ES5。因此不再支持较旧的浏览器(例如 IE11)**。
|
||||
|
||||
### [#46100](https://github.com/dart-lang/sdk/issues/46100):弃用 stagehand、dartfmt 和 dart2native
|
||||
|
||||
在 2020 年 10 月的 [Dart 2.10 博客文章中](https://medium.com/dartlang/announcing-dart-2-10-350823952bd5) 宣布了将所有 Dart CLI 开发人员工具组合成一个单一的组合`dart`工具(类似于该`flutter`工具),而现在 Dart 2.14 弃用了 `dartfmt` 和 `dart2native` 命令,并停止使用 `stagehand` ,这些工具在统一在 `dart-tool` 中都有等价的替代品。
|
||||
|
||||
### [#45451](https://github.com/dart-lang/sdk/issues/45451):弃用 VM Native 扩展
|
||||
|
||||
Dart SDK 已弃用 Dart VM 的 Native 扩展,这是从 Dart 代码调用 Native 代码的旧机制,Dart [FFI](https://dart.dev/guides/libraries/c-interop)(外来函数接口)是当前用于此用例的新机制,正在积极[发展](https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067) 以使其功能更加强大且易于使用。
|
257
Dart-215.md
257
Dart-215.md
|
@ -1,257 +0,0 @@
|
|||
|
||||
> 原文链接 https://medium.com/dartlang/dart-2-15-7e7a598e508a
|
||||
|
||||
Dart SDK 的 2.15 版本新增了**具备有更快并发能力的 isolates 、支持 tear-off 的构造函数 、关于 dart:core 库枚举支持的改进、包发布者相关的新功能**等等。
|
||||
|
||||
|
||||
|
||||
|
||||
## isolates 的并发
|
||||
|
||||
如今的物理设备几乎都有多核的 CPU ,能够并行地执行多个任务,而对于大多数 Dart 程序而言,这些内核的使用过程对开发人员来说是透明的:
|
||||
|
||||
> 默认情况下 Dart 运行时所有 Dart 代码只会在单个内核上运行,但会使用其他内核来执行系统级任务,例如:异步的输入/输出、读写文件或者网络调用等。
|
||||
|
||||
但有时候 Dart 代码本身可能需要用到并发执行的场景,例如可能同时有“连续的动画和一个需要长时间运行的任务”,或者解析一个大型 JSON 文件等等场景。
|
||||
|
||||
如果附加需要执行的任务花费时间太长,可能就会导致 UI 卡顿或运行滞后,所以通过将这些额外的任务移动到另外一个单独的核心运行,保证动画可以继续在主执行线程上运行而不受干扰是必要的支持。
|
||||
|
||||
**Dart 的并发模型是基于[isolates](https://dart.dev/guides/language/concurrency)设计的——一种相互隔离的独立执行单元。这是为了防止在共享内存时,出现相关的并发编程错误问题**,例如[data races.](https://en.wikipedia.org/wiki/Race_condition#In_software)等。
|
||||
|
||||
Dart 通过不允许在 isolates 之间共享任何可变对象来防止这些错误,而是使用 [*消息传递*](https://dart.dev/guides/language/concurrency#sending-multiple-messages-between-isolates) 在 isolates 之间交换状态,**而如今在 Dart 2.15 中对 isolates 进行了许多实质性的改进。**
|
||||
|
||||
**Dart 2.15 重新设计和实现了 isolates 的工作方式,引入了一个新概念:*isolate groups*, isolate groups 中的 isolate 共享正在运行的程序中的各种内部数据结构,这使得 groups 中的个体 isolates 变得更加轻便。**
|
||||
|
||||
> 如今在 isolate groups 中启动额外的 isolate 可以快近 100 倍,因为现在不需要初始化程序结构,并且产生新的 isolate 所需要的内存减少了 10-100 倍。
|
||||
|
||||
**虽然 isolate groups 还是不允许 isolate 之间共享可变对象,但该 group 可以通过共享堆来实现的,所以能够解锁更多功能**,比如可以将对象从一个 isolate 传递到另一 isolate,这样就可以用于执行需要返回大量内存数据的任务。
|
||||
|
||||
> 例如通过网络调用获取数据,将该数据解析为一个大型 JSON 对象,然后将该 JSON 对象返回到主isolates。 在 Dart 2.15 之前执行该操作需要“深度复制”,如果复制花费的时间超过帧预算时间,就可能会导致 UI 卡顿。
|
||||
|
||||
**在 2.15 中工作的 isolates 可以调用 `Isolate.exit()` 将其结果作为参数传递**。将运行的 isolates 结果的内存传递给主 isolates ,而不是进行复制,主 isolates 可以在指定时间内接收结果。
|
||||
|
||||
这个行为在更新的[Flutter 2.8 中](https://medium.com/flutter/whats-new-in-flutter-2-8-d085b763d181) 的 [`compute()`](https://api.flutter.dev/flutter/foundation/compute-constant.html) 函数里,同样已经改变成这种实现, 如果你已经在使用 `Isolate.exit()` 和 `compute()` 函数,那么在升级到 Flutter 2.8 后将自动获得这些性能提升。
|
||||
|
||||
**最后 Dart 2.15 还重新设计了 isolates 消息传递机制的实现方式,使得中小型的消息传递速度提高了大约 8 倍**。另外扩展了 isolates 可以相互发送的对象种类,增加了对函数类型、闭包和堆栈跟踪对象的支持,有关详细信息,请参阅 API 文档 `SendPort.send()`:
|
||||
|
||||
> 要了解有关如何使用隔离的更多信息,请参阅 2.15 添加的 [*Dart*](https://dart.dev/guides/language/concurrency) 文档中的[*并发介绍*](https://dart.dev/guides/language/concurrency), 另外还有许多[代码示例](https://github.com/dart-lang/samples/tree/master/isolates)可以查看。
|
||||
|
||||
## 新的语言特性:构造函数 tear-offs
|
||||
|
||||
在 Dart 中可以通过使用函数的名称创建一个函数对象,该对象指向另一个对象上的函数,如下代码所示,`main()` 方法的第二行声明了“将 `g` 设置为 `m.greet` ”的语法:
|
||||
|
||||
|
||||
|
||||
```dart
|
||||
class Greeter {
|
||||
final String name;
|
||||
Greeter(this.name);
|
||||
|
||||
void greet(String who) {
|
||||
print('$name says: Hello $who!');
|
||||
}
|
||||
}void main() {
|
||||
final m = Greeter('Michael');
|
||||
final g = m.greet; // g holds a function pointer to m.greet.
|
||||
g('Leaf'); // Invokes and prints "Michael says: Hello Leaf!"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
在使用 Dart core libraries 时,这种类函数指针(也称为函数 tear-offs)经常出现,如下是`foreach()` 通过传递函数指针来调用可迭代对象的示例:
|
||||
|
||||
```dart
|
||||
final m = Greeter('Michael');['Lasse', 'Bob', 'Erik'].forEach(m.greet);// Prints "Michael says: Hello Lasse!", "Michael says: Hello Bob!",
|
||||
// "Michael says: Hello Erik!"
|
||||
```
|
||||
|
||||
在之前的版本中 Dart SDK 不支持从构造函数创建 tear-offs(语言问题[#216](https://github.com/dart-lang/language/issues/216)),这就显得很烦人,因为在许多情况下,构建 Flutter UI 时构造函数 tear-offs 会是开发所需要的,所以从 Dart 2.15 开始支持这种语法。
|
||||
|
||||
如下是构建 `Column` 包含三个 `Text` Widget 的示例,通过调用 `.map()` 它并将其传递给`Text` :
|
||||
|
||||
```dart
|
||||
class FruitWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: ['Apple', 'Orange'].map(Text.new).toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`Text.new` 指 `Text` 类的默认构造函数,还可以引用命名构造函数,例如: `.map(Text.rich)`**。
|
||||
|
||||
## Related language changes
|
||||
|
||||
当实现构造函数的 tear-offs 时,Dart 2.15 借此机会修复了现有的函数指针支持中的一些不一致问题,现在可以专门化一个泛型方法来创建一个非泛型方法:
|
||||
|
||||
```dart
|
||||
T id<T>(T value) => value;\
|
||||
var intId = id<int>; // New in 2.15.\
|
||||
int Function(int) intId = id; // Pre-2.15 workaround.
|
||||
```
|
||||
|
||||
你甚至可以特化一个泛型函数对象来创建一个非泛型函数对象:
|
||||
|
||||
|
||||
```dart
|
||||
const fo = id; // Tear off `id`, creating a function object.\
|
||||
const c1 = fo<int>; // New in 2.15; error before.
|
||||
```
|
||||
|
||||
最后还清理了涉及泛型的类型文字:
|
||||
|
||||
```dart
|
||||
var y = List; // Already supported.\
|
||||
var z = List<int>; // New in 2.15.\
|
||||
var z = typeOf<List<int>>(); // Pre-2.15 workaround.
|
||||
```
|
||||
|
||||
## Improved enums in the dart:core library
|
||||
|
||||
Dart 2.15 为 `dart:core` 库([#1511](https://github.com/dart-lang/language/issues/1511))中的枚举 API 添加了更多的优化,现在开发者可以通过 `.name` 来获取每个枚举值的 `String` 值:
|
||||
|
||||
```dart
|
||||
enum MyEnum {
|
||||
one, two, three
|
||||
}
|
||||
void main() {
|
||||
print(MyEnum.one.name); // Prints "one".
|
||||
}
|
||||
```
|
||||
|
||||
还可以按名称查找枚举值:
|
||||
|
||||
```dart
|
||||
print(MyEnum.values.byName('two') == MyEnum.two); // Prints "true".
|
||||
```
|
||||
|
||||
最后还可以获得所有名称-值对的映射:
|
||||
|
||||
```dart
|
||||
final map = MyEnum.values.asNameMap();\
|
||||
print(map['three'] == MyEnum.three); // Prints "true".
|
||||
```
|
||||
|
||||
有关使用这些新 API 的示例可以参阅 [Flutter PR #94496](https://github.com/flutter/flutter/pull/94496/files)。
|
||||
|
||||
## Compressed pointers
|
||||
|
||||
Dart 2.15 添加了对 Compressed pointers 的支持,**如果只需要支持 32 位的地址空间(最多 4 GB 内存),则 64 位 SDK 可以使用更节省空间的指针表示形式**。
|
||||
|
||||
> 压缩指针导致显着的内存减少,在对 GPay 应用程序的内部测试中,我们看到 Dart 堆大小减少了大约 10%。
|
||||
|
||||
由于压缩指针意味着无法寻址 4 GB 以上的 RAM,因此该功能位于 Dart SDK 中的配置选项之后,只能在构建 SDK 时由 Dart SDK 的嵌入器切换。
|
||||
|
||||
Flutter SDK 2.8 版已为 Android 构建中启用此配置,Flutter 团队正在考虑在未来版本中[为 iOS](https://github.com/flutter/flutter/issues/94753) 构建启用此配置。
|
||||
|
||||
|
||||
## Dart SDK 中包含 Dart DevTools
|
||||
|
||||
以前的 [DevTools](https://dart.dev/tools/dart-devtools#) 调试和性能工具[套件](https://dart.dev/tools/dart-devtools#) 不在 Dart SDK 中,所以开发者需要单独下载。
|
||||
|
||||
从 Dart 2.15 开始,现在可以在下载的 Dart SDK 里直接获取 DevTools,而无需进一步的安装步骤。
|
||||
|
||||
有关将 DevTools 与 Dart 命令行应用结合使用的更多信息,请参阅 [DevTools 文档](https://dart.dev/tools/dart-devtools#using-devtools-with-a-command-line-app)。
|
||||
|
||||
## 包发布者的新 pub 功能
|
||||
|
||||
Dart 2.15 SDK 在 `dart pub` 开发者命令和[pub.dev](https://pub.dev/)包存储库中还有两个新的功能。
|
||||
|
||||
首先包发布者有了一个新的安全功能,目的是用于检测发布者在发布包中意外发布的机密(例如 Cloud 或 CI 凭据)。
|
||||
|
||||
在了解到 GitHub 存储库中[每天有数以千计的秘密被泄露](https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf)后,Dart SDK 受到启发添加了此泄漏检测。
|
||||
|
||||
泄漏检测作为 `dart pub publish` 命令中预发布验证运行的一部分运行,如果它在即将发布的文件中检测到潜在的秘密,该 `publish` 命令将退出而不发布,并打印如下输出:
|
||||
|
||||
|
||||
```
|
||||
Publishing my_package 1.0.0 to [https://pub.dartlang.org](https://pub.dartlang.org/):\
|
||||
Package validation found the following errors:\
|
||||
* line 1, column 1 of lib/key.pem: Potential leak of Private Key detected.\
|
||||
╷\
|
||||
1 │ ┌ - - -BEGIN PRIVATE KEY - - -\
|
||||
2 │ │ H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ\
|
||||
3 │ └ - - -END PRIVATE KEY - - -\
|
||||
╵\
|
||||
* line 2, column 23 of lib/my_package.dart: Potential leak of Google OAuth Refresh Token detected.\
|
||||
╷\
|
||||
2 │ final refreshToken = "1//042ys8uoFwZrkCgYIARAAGAQSNwF-L9IrXmFYE-sfKefSpoCnyqEcsHX97Y90KY-p8TPYPPnY2IPgRXdy0QeVw7URuF5u9oUeIF0";
|
||||
```
|
||||
|
||||
在极少数情况下此检测可能会出现误报,在这些情况下可以将文件添加到许可白名单。
|
||||
|
||||
> 白名单: https://dart.dev/go/false-secrets
|
||||
|
||||
其次还为发布者添加了另一个功能:**支持收回已发布的软件包版本。**
|
||||
|
||||
当发布了有问题的包版本时,通常建议是发布一个小增量的新版本,以修复意外问题。
|
||||
|
||||
在极少数情况下,例如当开发者还没有这样的修复能力时,或者是不小心发布了一个新的主要版本,就可以使用新的包收回功能作为最后的手段,此功能在 pub.dev 上的管理 UI 中可用:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-215/image1)
|
||||
|
||||
当一个包版本被收回时,pub 客户端不再为 `pub get` 或者 `pub upgrade` 解析那个版本,如果开发者使用了成功撤回的版本(因此在他们的`pubspec.lock`文件中),他们将在下次运行时看到警告`pub`:
|
||||
|
||||
|
||||
```
|
||||
$ dart pub get\
|
||||
Resolving dependencies…\
|
||||
mypkg 0.0.181-buggy (retracted, 0.0.182-fixed available)\
|
||||
Got dependencies!
|
||||
```
|
||||
|
||||
## Security analysis for detecting bidirectional Unicode characters (CVE-2021–22567)
|
||||
|
||||
最近发现了一个涉及双向 Unicode 字符的通用编程语言漏洞 ( [CVE-2021–42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574) ),此漏洞影响大多数支持 Unicode 的现代编程语言,下面的 Dart 源代码演示了这个问题:
|
||||
|
||||
```dart
|
||||
main() {
|
||||
final accessLevel = 'user';
|
||||
if (accessLevel == 'user .// Check if admin ') {
|
||||
print('You are a regular user.');
|
||||
} else {
|
||||
print('You are an admin.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你可能会认为该程序打印出 *`You are a regular user.`* ,但实际上它可能会打印 *`You are an admin`。* !
|
||||
|
||||
通过使用包含双向 Unicode 字符的字符串,就可以利用此漏洞,例如上述都在一行中的这些字符,将文本的方向从左到右更改为从右到左和回退。
|
||||
|
||||
对于双向字符,文本在屏幕上的呈现与实际文本内容截然不同,开发者可以在[GitHub code gist](https://gist.github.com/mit-mit/7dda00ca6278ce7d2555f78d59d9e67b?h=1) 中看到这样的示例。
|
||||
|
||||
针对此漏洞的缓解措施包括使用检测双向 Unicode 字符的工具(编辑器、代码审查工具等)以便开发人员了解它们,并在知情的情况下接受它们的使用,上面链接的 GitHub gist 文件查看器是显示这些字符的工具的一个示例。
|
||||
|
||||
Dart 2.15 引入了进一步的缓解措施(Dart 安全[公告 CVE-2021–22567](https://github.com/dart-lang/sdk/security/advisories/GHSA-8pcp-6qc9-rqmv)):Dart 分析器现在扫描双向 Unicode 字符,并标记它们的任何使用:
|
||||
|
||||
```
|
||||
$ dart analyze
|
||||
Analyzing cvetest... 2.6sinfo • bin/cvetest.dart:4:27 • The Unicode code point 'U+202E'
|
||||
changes the appearance of text from how it's interpreted
|
||||
by the compiler. Try removing the code point or using the
|
||||
Unicode escape sequence '\u202E'. •
|
||||
text_direction_code_point_in_literal
|
||||
```
|
||||
|
||||
我们建议用 Unicode 转义序列替换这些字符,让它们在任何文本编辑器或查看器中可见,或者如果开发者觉得确实合法使用了这些字符,则可以通过在使用前的行中添加覆盖来禁用警告:
|
||||
|
||||
```
|
||||
// 忽略:text_direction_code_point_in_literal
|
||||
```
|
||||
|
||||
|
||||
## Pub.dev credentials vulnerability when using third-party pub servers (CVE-2021–22568)
|
||||
|
||||
|
||||
Dart 2.15 还发布了第二个与 pub.dev 相关的 Dart 安全[公告 CVE-2021–22568](https://github.com/dart-lang/sdk/security/advisories/GHSA-r32f-vhjp-qhj7)。
|
||||
|
||||
> 此公告面向可能涉及已将包发布到第三方发布包服务器(例如私人或公司内部包服务器)的包发布者,仅发布到公共 pub.dev 存储库(标准配置)的开发人员不受此漏洞的影响。
|
||||
|
||||
如果开发者已发布到第三方存储库,则该漏洞是在该第三方存储库中提供用于身份验证的 OAuth2 临时(一小时)访问令牌,可能会被滥用来针对公共 pub.dev 存储库进行身份验证。
|
||||
|
||||
因此恶意的第三方 pub 服务器可能会使用访问令牌在 pub.dev 上冒充开发者并在那里发布包。
|
||||
|
||||
如果开发者已将软件包发布到不受信任的第三方软件包存储库,请考虑对 pub.dev 公共软件包存储库中的所有帐户活动进行审核,[推荐可以使用 pub.dev 的活动日志](https://pub.dev/my-activity-log)。
|
64
Dart-216.md
64
Dart-216.md
|
@ -1,64 +0,0 @@
|
|||
|
||||
> 原文链接:https://medium.com/dartlang/dart-2-16-improved-tooling-and-platform-handling-dd87abd6bad1
|
||||
|
||||
|
||||
今天 Dart 2.16 跟随 Flutter 2.10 正式发布,它不包含新的语言特性,但有**一堆错误修复(包括对安全漏洞的修复),改进了 Dart 包在特定平台的支持,以及 [pub.dev](https://pub.dev/) 的全新搜索体验**。
|
||||
|
||||
## Dart 2.16
|
||||
|
||||
今天与 Flutter 2.10 一起发布的 Dart 2.16 SDK 继续从传统的 Dart CLI 工具(`dartfmt`、`dartdoc` 等)过渡到新的组合 `dart` 开发工具,新的弃用工具是 `dartdoc`( use `dart doc`) 和 `dartanalyzer` (use `dart analyze`)。
|
||||
|
||||
> 在 Dart 2.17 中我们计划完全删除 `dartdoc`、`dartanalyzer` 和 `pub` 命令(在 Dart 2.15 中已弃用;使用 `dart pub` 或者 `flutter pub`)。有关详细信息请参阅[#46100](https://github.com/dart-lang/sdk/issues/46100)。
|
||||
|
||||
2.16 版本还包括了一个安全漏洞的修复和两个小的重大更改:
|
||||
|
||||
- `dart:io` 中的 `HttpClient` API 允许为 `authorization`、`www-authenticate`、`cookie` 和 `cookie2` 设置可选标头,Dart 2.16 之前的 SDK 中重定向逻辑的实现存在一个漏洞,当跨域重定向发生时,这些 headers(可能包含敏感信息)会被传递,在 Dart 2.16 中这些 headers 被删除了。
|
||||
|
||||
- `dart:io` 中的 `Directory.rename` API 已更改了在 Windows 上的行为:它不再删除与目标名称匹配的现有目录([#47653](https://github.com/dart-lang/sdk/issues/47653))。
|
||||
|
||||
- `Platform.packageRoot` 和 `Isolate.packageRoot` API—— 从 Dart 1.x 中遗留下来并且在 Dart 2.x 中不起作用,所以已被删除(issue # [47769](https://github.com/dart-lang/sdk/issues/47769))。
|
||||
|
||||
> 要查找有关 Dart 2.16 更改的更多详细信息,请参阅更改日志: https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#2160。
|
||||
|
||||
## pub.dev 包新的平台声明支持
|
||||
|
||||
Dart 本身是为了可移植而设计的,我们努力使代码能够在更多的平台上运行,但是有时你可能会在 pub.dev 上创建和共享专为一个或几个平台设计的包,你可能有一个依赖于仅在特定操作系统上可用的 API 的包,或者一个使用 `dart:ffi` 仅在 Native 平台而非 Web 上受支持的库的包。
|
||||
|
||||
使用 Dart 2.16,你现在可以在包的 pubspec 中手动声明支持的平台集,例如如果你的包仅支持 Windows 和 macOS,则其 `pubspec.yaml` 文件可能如下所示:
|
||||
|
||||
```
|
||||
name: mypackage
|
||||
version: 1.0.0platforms:
|
||||
windows:
|
||||
macos:dependencies:
|
||||
```
|
||||
|
||||
新 `platforms` 标签适用于正在开发 Dart 包的情况,如果你正在开发和共享的包含特定于主机的代码(例如 Kotlin 或 Swift)的 Flutter 插件,则 [Flutter 插件标签](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms) 通常会指定支持的平台。
|
||||
|
||||
## 新的 pub.dev 搜索 UI
|
||||
|
||||
响应开发人员的请求,我们为在 pub.dev 上搜索包提供了更好的支持,今天发布的更改的主要目标是帮助开发者更好地识别和搜索受支持的平台集,以下是新搜索体验的视图:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Dart-216/image1)
|
||||
|
||||
新的搜索 UI 在左侧有一个搜索过滤器侧边栏,你可以使用它来限制你的包搜索:
|
||||
|
||||
- **Platforms**:选择一个或多个平台以,将搜索结果缩小到仅支持所有所选平台的软件包。
|
||||
- **SDKs**:选择 Dart 或 Flutter 以将结果限制为分别支持 Dart SDK 或 Flutter SDK 的包。
|
||||
- **Advanced**:附加搜索选项,例如过滤到 Flutter favorite包。
|
||||
|
||||
## 空安全更新
|
||||
|
||||
自从我们上次讨论 null 安全以来已经发布了几个版本,这是一年前在 [Dart 2.12](https://medium.com/dartlang/announcing-dart-2-12-499a6e689c87) 中推出的主要语言添加。
|
||||
|
||||
我们对 Dart 生态系统迁移包以支持 null 安全的速度感到惊讶:
|
||||
|
||||
> 截至今天,前 250 个包中的 100% 支持以及前 1000 个包中的 96% 支持 !感谢所有为这一伟大成就做出贡献的包作者。
|
||||
|
||||
我们还看到应用程序迁移到健全的空安全已经方面取得了良好进展,根据我们的分析,Flutter 工具中 71% 的所有运行会话现在都具有完全可靠的 null 安全性,如果你是应用开发人员,但仍未迁移到 null 安全,那么现在是个好时机。
|
||||
|
||||
## 结束评论
|
||||
|
||||
我们希望新的 pub.dev 搜索 UI 会对你有用,也欢迎你提供[任何反馈](https://github.com/dart-lang/pub-dev/issues/),请继续关注计划于 2022 年第二季度发布的下一个 Dart SDK 版本,我们正在开发一些[令人兴奋的语言功能](https://github.com/dart-lang/language/projects/1),希望在今年晚些时候发布。
|
||||
|
||||
> https://github.com/dart-lang/language/projects/1
|
105
Dart-217.md
105
Dart-217.md
|
@ -1,105 +0,0 @@
|
|||
# Dart 2.17 正式发布
|
||||
|
||||
随着 [Flutter 3](https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FUZJX0HIi7ERW_ZNcz1hflg) 在本次 I/O 大会的发布,我们也同时正式发布了 Dart 2.17 稳定版 SDK。这个版本的发布是围绕着我们的核心主题构建的,即:领先的生产力和平台可移植性。
|
||||
|
||||
Dart 2.17 提供了新的语言特性:**枚举支持成员变量、改进的超类参数继承,以及更为灵活的命名参数**。我们同时为 `package:lints` 开启了 2.x 版本,这是一套官方的 lint 规则,是根据我们总结的 Dart 最佳实践整合而成的一个 lint 规则集。与此同时,我们也更新了核心库的 API 文档,为其带来了丰富的示例代码。并且,为了改善平台集成特性,我们在 Flutter 插件中提供了一个新的模版,使用 `dart:ffi` 与原生平台进行 C 语言的互操作、对 RISC-V 指令集提供实验性支持,以及对 macOS 和 Windows 可执行文件的签名支持。
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image1)
|
||||
|
||||
## 编程语言新特性助力生产力提升
|
||||
|
||||
我们一直在持续地改进 Dart 编程语言,不断添加新特性以及改进现有的特性,以助力开发者们工作效率的提升。Dart 2.17 增加了对枚举成员变量的支持,优化了在构造函数中使用命名参数的方式,并且开始使用继承超类的参数以减少冗长和重复的代码。
|
||||
|
||||
### 增强的支持成员变量的枚举
|
||||
|
||||
枚举非常适合表示一组离散的状态。例如,我们可以将水描述为 `enum Water { frozen, lukewarm, boiling }`。但如果我们想在 `enum` 上添加一些方法,例如,将每个状态转换为温度,并支持将 `enum` 转换为 `String`,该怎么办?或许我们可以使用扩展方法来添加一个 `waterToTemp()` 方法,但我们必须时刻注意它与 `enum` 的同步。对于 `String` 我们希望覆写 `toString()` 方法,但它不支持这么做。
|
||||
|
||||
在 Dart 2.17 中现已支持枚举类型的成员变量。这意味着我们可以添加保存状态的字段、设置状态的构造函数、具有功能的方法,甚至覆写现有的方法。社区中许多开发者一直有这样的需求,这是我们在 Dart 编程语言仓库的问题追踪中 [投票排名第三的问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%3Fq%3Dis%3Aissue%2Bsort%3Areactions-%2B1-desc%2B)。
|
||||
|
||||
继续拿 `Water` 举例,我们可以添加一个保存温度的 `int` 字段,并添加接收 `int` 的默认构造函数:
|
||||
|
||||
```dart
|
||||
enum Water
|
||||
const Water(this.tempInFahrenheit);
|
||||
|
||||
final int tempInFahrenheit;
|
||||
}
|
||||
```
|
||||
|
||||
为了确保在创建枚举时构造函数被正常调用,我们需要为每一个枚举值附以显式的调用:
|
||||
|
||||
```dart
|
||||
enum Water {
|
||||
frozen(32),
|
||||
lukewarm(100),
|
||||
boiling(212);
|
||||
}
|
||||
```
|
||||
|
||||
想要支持从枚举转换为 `String`,我们可以很简单地覆写 `toString` 方法,因为 `enums` 也继承自 `Object`:
|
||||
|
||||
```dart
|
||||
@override
|
||||
String toString() => "The $name water is $tempInFahrenheit F.";
|
||||
```
|
||||
|
||||
如此一来,你就有了一个可以轻松实例化完整功能的枚举类,并且可以在任意位置调用方法:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
print(Water.frozen); // 打印内容为 “The frozen water is 32 F.”
|
||||
}
|
||||
```
|
||||
|
||||
这两种方法的完整示例如下所示,有了这些改动,新版本的代码更易于阅读和维护。
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image2)
|
||||
|
||||
### 超类的初始化构造
|
||||
|
||||
当你的代码存在类型继承关系时,一个常见的做法是将一些构造函数参数传递给超类的构造函数。为此子类需要 1) 在其构造函数中列出每个参数 2) 使用这些参数调用超类的构造函数。这导致了大量的代码重复,使代码难以阅读和维护。
|
||||
|
||||
几位 Dart 社区成员帮助 Dart 实现了这项语言目标。半年前,GitHub 用户 [@roy-sianez](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Froy-sianez) 提交了一个 [语言问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%2F1855)。他的建议类似于 GitHub 用户 [@apps-transround](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fapps-transround) 先前的 [建议](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%2F493%23issuecomment-879624528):也许我们可以通过引入一个新的方式来表示在超类中指定了一个参数,来解决这个问题。我们认为这是一个好主意,因此已将其实现并添加到了 Dart 2.17 版本中。从以下示例中可以看出,这与 Flutter widget 的代码有很强的相关性。实际上当我们将这项特性应用到 Flutter 框架时,我们看到框架总共减少了 [近两千行代码](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F100905%2Ffiles)!
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image3)
|
||||
|
||||
### 可在任意参数位置使用命名参数
|
||||
|
||||
最后,我们改进了方法调用时命名参数的方式。在此次更新之前,命名参数的调用必须出现在普通参数列表的后面。当你想要提升代码可读性,希望将命名参数写在靠前的位置但它无法工作时,会觉得非常惆怅。例如下方 `List<T>.generate` 构造函数的调用。此次更新之前 `growable` 参数必须放在最后,这会导致这个参数很容易被可能有很多内容的构造参数所影响而错过。现在你可以根据自己的喜好对它们进行排序,你可以先使用命名参数,最后使用生成器参数。
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image4)
|
||||
|
||||
更多有关这三项改进的示例,请参阅我们更新的 [枚举](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fenhanced_enums%2Flib%2Fmembers.dart)、[超类的初始化构造](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fparameters%2Flib%2Fsuper_initalizer.dart) 和 [命名参数](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fparameters%2Flib%2Fnamed_parameters.dart) 示例代码。
|
||||
|
||||
## 生产力工具改进
|
||||
|
||||
回到生产力的主题,我们围绕生产力对核心工具进行了一些改进。
|
||||
|
||||
在 Dart 2.14 中,我们引入了 `package:lints`,它与 Dart 分析器一起工作以防止你编写错误的代码,并使用更规范的规则审查你的 Dart 代码。之后分析器中又新增了许多代码提示规则,我们对其进行了仔细分类,并从中选择了 [10 条新的用于所有 Dart 代码的代码提示规则](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flints%2Fblob%2Fmain%2FCHANGELOG.md%23200) ,以及 [2 条新的专门用于 Flutter 代码的代码提示规则](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fpackages%2Fblob%2Fmaster%2Fpackages%2Fflutter_lints%2FCHANGELOG.md%23200)。它们包括确保你导入的 package 中有正确地在你 pubspec 文件中声明、防止滥用对类型参数的空检查以及确保子属性格式一致的代码提示规则。你可以简单地使用命令升级到新的 `lints` package:
|
||||
|
||||
- 对 Dart package 可以使用: `dart pub upgrade —-major-versions lints`
|
||||
- 对 Flutter package 可以使用: `flutter pub upgrade —-major-versions flutter_lints`
|
||||
|
||||
`SecureSockets` 通常用于启用使用 TLS 和 SSL 保护的 TCP 套接字连接。在 Dart 2.17 之前,因为没有办法检查安全数据流量,在开发过程中调试这些加密连接变得十分棘手。现在我们添加了对指定 `keyLog` 文件的支持,指定后,当与服务器交换新的 TLS 密钥时,[NSS 密钥日志格式](https://link.juejin.cn/?target=https%3A%2F%2Ffirefox-source-docs.mozilla.org%2Fsecurity%2Fnss%2Flegacy%2Fkey_log_format%2Findex.html) 中的一行文本将附加到文件中。这将使网络流量分析工具 (例如 [Wireshark](https://link.juejin.cn/?target=https%3A%2F%2Fgitlab.com%2Fwireshark%2Fwireshark%2F-%2Fwikis%2FTLS%23tls-decryption)) 能够解密通过套接字发送的内容。更多详细信息,请参阅`SecureSocket.connect()` 的 [API 文档](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-io%2FSecureSocket%2Fconnect.html)。
|
||||
|
||||
`dart doc` 生成的 API 文档是大多数 Dart 开发者学习新 API 的重要内容之一。虽然我们的核心库 API长期以来都有丰富的文本描述,但许多开发者告诉我们,他们更喜欢通过阅读示例代码来学习 API。在 Dart 2.17 中,我们检查了所有主要的核心库,为浏览量排名的前 200 个页面添加了详实的示例代码。你可以对比 `dart:convert` 在 [Dart 2.16](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.16.2%2Fdart-convert%2Fdart-convert-library.html) 和 [2.17](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-convert%2Fdart-convert-library.html) 的文档页面查看这些改变,希望这些改变可以帮助你更好地使用 API 文档。
|
||||
|
||||
助力生产力的提高不仅是做加法,做减法也同样重要,我们清理了一些堆积的内容,并删除了 SDK 里已弃用的的 API,这将帮助我们保持更小的代码体积,这对新上手的开发者们尤为重要。为此,我们从 `dart:io` 库中删除了 [231 行已弃用的代码](https://link.juejin.cn/?target=https%3A%2F%2Fdart-review.googlesource.com%2Fc%2Fsdk%2F%2B%2F236840)。如果你仍在使用这些已弃用的 API,你可以使用 `dart fix` 进行修复和替换。我们还在继续努力删除 [已弃用的 Dart CLI 工具](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsdk%2Fissues%2F46100),本次更新删除了 `dartdoc` 工具 (使用`dart doc` 代替) 和 `pub` 工具 (使用 `dart pub` 或 `flutter pub` 代替)。
|
||||
|
||||
## 扩大平台集成和支持
|
||||
|
||||
第二个核心主题是平台集成和支持。Dart 是一种真正的多平台语言。虽然我们已经支持 [大量的平台](https://link.juejin.cn/?target=https%3A%2F%2Fdart.cn%2Foverview%23platform),但我们仍在不断拓展新平台,以确保你可以与每个受支持的平台深度集成,同时也关注更新兴的平台。
|
||||
|
||||
我们 [与 C 语言或原生代码互操作](https://link.juejin.cn/?target=https%3A%2F%2Fdart.cn%2Fguides%2Flibraries%2Fc-interop) 的核心机制——Dart FFI,是一种将 Dart 代码与现有原生平台代码集成的流行方式。在 Flutter 上,FFI 是构建使用宿主平台原生 API (例如 Windows win32 API) 插件的好方法。在 Dart 2.17 和 Flutter 3 中,我们向 `flutter` 工具添加了 FFI 的模板,现在你可以轻松地创建 FFI 插件,这些插件具有通过 `dart:ffi` 调用原生代码支持的 Dart API。详细信息请参阅开发者文档 [开发 package 和插件](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.cn%2Fdocs%2Fdevelopment%2Fpackages-and-plugins%2Fdeveloping-packages%23dart-only-platform-implementations) 页面。
|
||||
|
||||
FFI 现在支持特定于 ABI 的类型,可以在具有特定 [ABI (应用程序二进制接口)](https://link.juejin.cn/?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2FABI%2F10912305) 类型的平台上使用 FFI。例如,现在你可以使用 `Long` (C 语言中的 `long`) 正确表示具有特定于 ABI 大小的长整数,由于 CPU 架构的区别,结果可能是 32 位或 64 位。有关支持类型的完整列表,请参阅 [AbiSpecificInteger API 页面](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-ffi%2FAbiSpecificInteger-class.html) 中的 "Implementers" 列表。
|
||||
|
||||
在使用 Dart FFI 与原生平台深度集成时,有时需要将 Dart 分配的内存或其他资源 (端口、文件等) 的清理行为与原生代码对齐。长期以来,这个问题都十分棘手,因为 Dart 是一种会自动处理垃圾回收清理行为的语言。在 Dart 2.17 中,我们通过引入 Finalizer 的概念解决了这个问题,它包括一个 `Finalizable` 标记接口,用于「标记」不应过早终结或丢弃的对象,以及一个 `NativeFinalizer` 可以附加到 Dart 对象上,当对象即将被垃圾回收时提供回调运行。Finalizer 让原生代码和 Dart 代码中同时运行清理。更多详细信息请参阅 [NativeFinalizer API 文档](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-ffi%2FNativeFinalizer-class.html) 中的描述和示例,或 [WeakReferences](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-core%2FWeakReference-class.html) 以及 [Finalizer](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-core%2FFinalizer-class.html) 在 Dart 代码中的类似支持。
|
||||
|
||||
将 Dart 编译为本机代码的支持,也是使 Flutter 应用具有出色的启动性能和快速渲染的核心。除此之外,你还可以使用 `dart compile` 编译 Dart 文件为可执行文件。这些可执行文件可以在任何机器上独立运行,无需安装 Dart SDK。Dart 2.17 中的另一个新功能是支持对可执行文件进行签名,生成的产物可以在经常需要签名的 Windows 和 macOS 上进行部署。
|
||||
|
||||
我们还保持在新兴的平台前沿,继续扩大我们所支持的平台集。[RISC-V](https://link.juejin.cn/?target=https%3A%2F%2Friscv.org%2Fabout%2F) 是一个全新的指令集体系。RISC-V International 是一家全球性的非盈利组织,拥有 RISC-V 规范,使得指令集自由开放。这仍然是一个新兴的平台,但我们对其潜力感到兴奋,因此我们的 `2.17.0–266.1.beta` Linux 版本包含了对它的实验性支持。我们希望能够听到你的反馈,你可以 [提出问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsdk%2Fissues) 或 [分享](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fa%2Fdartlang.org%2Fg%2Fmisc) 你的体验!
|
||||
|
||||
## 开始使用 Dart 2.17!
|
||||
|
||||
我们希望 Dart 的 2.17 正式版能打动你并能助力你提高工作效率,也同时能够把你的应用带去更多的平台。即刻下载 Dart 2.17 并开始使用,也安装使用 Flutter 3,使用内置的 Dart SDK。
|
278
Dart-218.md
278
Dart-218.md
|
@ -1,278 +0,0 @@
|
|||
# Dart 2.18 发布,Objective-C 和 Swift interop
|
||||
|
||||
> 原文链接: https://medium.com/dartlang/dart-2-18-f4b3101f146c
|
||||
|
||||
**Dart 2.18 版本开始提供与 Objective-C 和 Swift 交互的能力预览**,以及在这基础上构建的新 iOS / macOS 包支持。
|
||||
|
||||
**Dart 2.18 还包含对通用函数的类型推断改进、异步代码的性能改进、新的pub.dev 功能支持以及对工具和核心库的整理**。
|
||||
|
||||
最后,**还有最新的 *null safety* 迁移状态解析**,以及通往完全 *null safety* 的重要路线图更新。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_# Dart/image1.png)
|
||||
|
||||
|
||||
|
||||
# Dart 支持与 Objective-C 和 Swift 交互的能力
|
||||
|
||||
在 2020 年的时候我们预览了用于调用原生 C API 的 Dart 外函数接口(FFI),并于 2021 年 3 月在 Dart 2.12 中发布了它。
|
||||
|
||||
自该版本发布以来,大量软件包利用此功能与现有的原生C API集成,例如: `file_picker`、`printing`、`win32`、`objectbox`、`realm`、`isar`、`tflite_flutter `和 `dbus ` 等。
|
||||
|
||||
**Dart 团队希望支持所运行平台上所有主要语言的交互能力,而 Dart 2.18达到了实现这一目标的下一个里程碑**。
|
||||
|
||||
在 2.18, Dart 代码可以调用 Objective-C 和 Swift 代码,这通常用于调用 macOS 和 iOS 平台上的API,Dart在任何应用中都支持这种互操作机制,从CLI 应用到后端代码和 Flutter UI。
|
||||
|
||||
这种新机制其实是利用了 Objective-C 和 Swift 代码可以基于 API 绑定 C 代码公开,Dart API 包装了生成工具 `ffigen` ,可以从 API 标头创建这些绑定。
|
||||
|
||||
|
||||
|
||||
# 使用Objective-C的时区示例
|
||||
|
||||
macOS 有一个 API 可用于查询 `NSTimeZone` 上公开的时区信息,开发者可以查询该 API 以了解用户为其设备配置的时区和 UTC [时区偏移量](https://www.w3.org/International/core/2005/09/timezone.html#:~:text=What is a "zone offset,or "-" from UTC.)。
|
||||
|
||||
以下示例中 Objective-C 使用此时区 API 获取系统时区和GMT偏移量:
|
||||
|
||||
```objective-c
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
NSTimeZone *timezone = [NSTimeZone systemTimeZone]; // Get current time zone.
|
||||
NSLog(@"Timezone name: %@", timezone.name);
|
||||
NSLog(@"Timezone offset GMT: %ld hours", timezone.secondsFromGMT/60/60);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
这里导入了 `Foundation.h`,其中包含 Apple Foundation 库的 API headers。
|
||||
|
||||
接下来,在 `main` 方法中,它从 `NSTimeZone` 类调用了 `systemTimeZone` 方法,此方法返回设备上选定时区的 `NSTimeZone` 实例。
|
||||
|
||||
最后,应用向控制台输出两行结果,其中包含时区名称和UTC偏移量(以小时为单位)。
|
||||
|
||||
如果运行此程序,它应该会返回类似于以下内容的东西,具体取决于开发者的位置:
|
||||
|
||||
```
|
||||
Timezone name: Europe/Copenhagen
|
||||
Timezone offset GMT: 2 hours
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 使用 Dart 的时区示例
|
||||
|
||||
让我们使用新的 Dart 与 Objective-C 一起重新实现上面的结果。
|
||||
|
||||
首先创建一个新的 Dart CLI :
|
||||
|
||||
```
|
||||
$ dart create timezones
|
||||
```
|
||||
|
||||
然后编辑 `pubspec `文件以包含 `ffigen` 配置,配置指向头文件,并列出了哪些 Objective-C 接口应该生成包装器:
|
||||
|
||||
```yaml
|
||||
|
||||
ffigen:
|
||||
name: TimeZoneLibrary
|
||||
language: objc
|
||||
output: "foundation_bindings.dart"
|
||||
exclude-all-by-default: true
|
||||
objc-interfaces:
|
||||
include:
|
||||
- "NSTimeZone"
|
||||
headers:
|
||||
entry-points:
|
||||
- "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/
|
||||
Headers/NSTimeZone.h"
|
||||
```
|
||||
|
||||
这就为 `NSTimeZone.h` 中的 headers 选择 Objective-C 绑定,并仅包括`NSTimeZone `接口中的API,要生成 wrappers, 可以允行 `ffigen`:
|
||||
|
||||
```
|
||||
$ dart run ffigen
|
||||
```
|
||||
|
||||
该命令会创建一个新文件 `foundation_bindings.dart`,其中包含一堆生成的API绑定,使用该绑定文件,就可以编写 Dart `main` 方法,此方法镜像Objective-C 代码:
|
||||
|
||||
```dart
|
||||
void main(List<String> args) async {
|
||||
const dylibPath =
|
||||
'/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
|
||||
final lib = TimeZoneLibrary(DynamicLibrary.open(dylibPath));
|
||||
|
||||
final timeZone = NSTimeZone.getLocalTimeZone(lib);
|
||||
if (timeZone != null) {
|
||||
print('Timezone name: ${timeZone.name}');
|
||||
print('Offset from GMT: ${timeZone.secondsFromGMT / 60 / 60} hours');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
就这样,从 Dart 2.18 开始,这种新的支持在实验状态下可用,该能力增强了Dart 的交互支持,以直接调用 macOS 和 iOS API 支持。
|
||||
|
||||
并且这也反向补充了 Flutter 的插件,提供了允许开发者直接从 Dart 代码调用macOS 和 iOS API 的能力。
|
||||
|
||||
> 要了解有关这种互操作性的更多信息,请参阅 [Objective-C 和 Swift 交互指南](https://dart.dev/guides/libraries/objective-c-interop)。
|
||||
|
||||
|
||||
|
||||
# 特定于平台的http库
|
||||
|
||||
Dart 里包括一个通用的多平台`http`库,该库允许开着编写代码而无需考虑平台细节,但是有时候开发者可能希望编写特定于特定 native 平台的 网络 API的代码,例如:苹果的网络 库`NSURLSession `允许指定仅限 WiFi 或 VPN的网络。
|
||||
|
||||
为了支持这些用例,我们为 macOS 和 iOS 平台创建了一个新的网络包 `cupertino_http`,该能力建立在上一节中提到的 Objective-C 直接交互的基础上。
|
||||
|
||||
# Cupertino http library 示例
|
||||
|
||||
以下示例将 Flutter 的 http 客户端设置为在其他平台上使用 `cupertino_http`库,以及 `dart:io` 下的 http 库:
|
||||
|
||||
```dart
|
||||
|
||||
late Client client;
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
final config = URLSessionConfiguration.ephemeralSessionConfiguration()
|
||||
..allowsCellularAccess = false
|
||||
..allowsExpensiveNetworkAccess = false;
|
||||
client = CupertinoClient.fromSessionConfiguration(config);
|
||||
} else {
|
||||
client = Client(); // Uses an HTTP client based on dart:io
|
||||
}
|
||||
```
|
||||
|
||||
初始配置后,应用会对特定客户端进行后续网络调用,例如 http `get()` 请求现在类似于以下内容:
|
||||
|
||||
```dart
|
||||
final response = await get(
|
||||
Uri.https(
|
||||
'www.googleapis.com',
|
||||
'/books/v1/volumes',
|
||||
{'q': 'HTTP', 'maxResults': '40', 'printType': 'books'},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
当开发者无法使用通用客户端接口时,就可以直接使用 `cupertino_http` 库调用苹果的网络API:
|
||||
|
||||
```dart
|
||||
final session = URLSession.sessionWithConfiguration(
|
||||
URLSessionConfiguration.backgroundSession('com.example.bgdownload'),
|
||||
onFinishedDownloading: (s, t, fileUri) {
|
||||
actualContent = File.fromUri(fileUri).readAsStringSync();
|
||||
});
|
||||
|
||||
final task = session.downloadTaskWithRequest(
|
||||
URLRequest.fromUrl(Uri.https(...))
|
||||
..resume();
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 多平台应用程序中特定于平台的网络
|
||||
|
||||
在设计该功能时,目标仍然是使应用尽支持更多的平台,为了实现这个目标,我们为基本的 http 操作保留了通用的多平台 `http` API 集,并允许为每个平台配置要使用的网络库。
|
||||
|
||||
[`package:http`](https://pub.dev/documentation/http/latest/http/Client-class.html) 将需要编写的特定于平台的代码量降至最低,此 API 可以按平台配置,但以独立于平台的方式使用。
|
||||
|
||||
Dart 2.18 提供了对两个对于 `package:http` 特定于平台的 http 库的实验性支持:
|
||||
|
||||
- `cupertino_http` 基于 `NSURLSession` 的 macOS/iOS 支持。
|
||||
- `cronet_http `基于 [Cronet](https://developer.android.com/guide/topics/connectivity/cronet),Android 上流行的网络库支持。
|
||||
|
||||
将一个通用客户端 API 与多个 HTTP 实现相结合,以获得特定于平台的行为,同时仍然从所有平台的一组共享源中维护应用。
|
||||
|
||||
|
||||
|
||||
# 改进的类型推断
|
||||
|
||||
Dart 使用了许多通用函数,例如 `fold`方法,它将元素集合减少为单个值,如计算整数列表的总和:
|
||||
|
||||
```dart
|
||||
List<int> numbers = [1, 2, 3];
|
||||
final sum = numbers.fold(0, (x, y) => x + y);
|
||||
print(‘The sum of $numbers is $sum’);
|
||||
```
|
||||
|
||||
对于 Dart 2.17 或更早版本,这个方法返回类型错误:
|
||||
|
||||
```
|
||||
line 2 • The operator ‘+’ can’t be unconditionally invoked because the receiver can be ‘null’.
|
||||
```
|
||||
|
||||
**Dart 2.18 改进了类型推断,前面的示例通过了静态分析,可以推断出 x 和 y 都是不可为空的整数**,此更改允许开发者编写更简洁的 Dart 代码,同时保留强推断类型的完整可靠性属性。
|
||||
|
||||
|
||||
|
||||
# 异步性能改进
|
||||
|
||||
此版本的 Dart 改进了 Dart VM 应用 `async` 方法和 `async*`/`sync* `生成器功能的方式。
|
||||
|
||||
这减少了代码大小,在两个大型内部 Google 应用程序中,我们看到 AOT 快照大小减少了约 10%,还可以看到微基准测试的性能有所提高。
|
||||
|
||||
> 这些变化包括额外的小行为变化;要了解更多信息,请参阅[更改日志](https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#dart-vm)。
|
||||
|
||||
|
||||
|
||||
# pub.dev 改进
|
||||
|
||||
结合 2.18 版本,我们对 `pub.dev`包 存储库进行了两项更改。
|
||||
|
||||
个人业余时间通过 `pub.dev` 维护和发布的可能会产生一些时间上的投入,为了促进赞助,我们现在在 中支持一个新 `funding` 标签,`pubspec`包发布者可以使用该标签列出指向一种或多种赞助包的方式的链接。然后这些链接显示`pub.dev`在侧边栏中:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_# Dart/image2.png)
|
||||
|
||||
> 要了解更多信息,请参阅`pubspec`[文档](https://dart.dev/tools/pub/pubspec#funding)。
|
||||
|
||||
此外,我们希望鼓励丰富的开源软件包生态系统,为了突出这一点,自动包评分对使用 [OSI 批准的许可证](https://opensource.org/licenses) 的 `pub.dev`包额外奖励 10 分。
|
||||
|
||||
|
||||
|
||||
# 一些重大变化
|
||||
|
||||
Dart 非常注重简单和易学的能力,在添加新功能时,我们一直在努力保持谨慎的平衡。
|
||||
|
||||
保持简单的一种方法是删除历史功能和 API,Dart 2.18 清理了此类别中的项目,包括一些较小的重大更改:
|
||||
|
||||
- 我们早在 2020 年 10 月就添加了统一的 `dart` CLI 开发人员工具,在 2.18 中我们完成了过渡。此版本删除了最后两个已弃用的工具 `dart2js` (use `dart compile js`) 和 `dartanalyzer` (use `dart analyze`)。
|
||||
- 随着语言版本控制的引入,`pub `生成了一个新的解析文件:`.dart_tool/package_config.json` 。 之前的 `.packages` 文件使用了一种不能包含版本的格式,而现在我们停止使用 `.packages`文件,如果你有任何`.packages`文件,现在可以删除它们了。
|
||||
- 不能使用未扩展的类的混合 `Object`(重大更改[#48167](https://github.com/dart-lang/sdk/issues/48167))。
|
||||
- `dart:io` 的 `RedirectException`的 `uri` 属性已更改为可为空(重大更改[#49045](https://github.com/dart-lang/sdk/issues/49045))。
|
||||
- `dart:io `遵循 SCREAMING_SNAKE 约定的网络 API 中的常量已被删除(重大更改# [34218](https://github.com/dart-lang/sdk/issues/34218);以前已弃用),请改用相应的 lowerCamelCase 常量。
|
||||
- Dart VM 在退出时不再恢复初始终端设置,更改 `Stdin` 设置 `lineMode` 的 `echoMode` 现在负责在程序退出时恢复设置(重大更改[#45630](https://github.com/dart-lang/sdk/issues/45630))。
|
||||
|
||||
|
||||
|
||||
# 空安全更新
|
||||
|
||||
自 2020 年 11 月发布测试版和 2021 年 3 月发布 [Dart 2.12](https://medium.com/dartlang/announcing-dart-2-12-499a6e689c87) 以来,我们很高兴看到 null 安全性的广泛使用。
|
||||
|
||||
首先,大多数流行包的开发人员都在 `pub.dev` 迁移到了零安全性,分析表明,100% 的前 250 个和 98% 的前 1000 个最常用的包支持零安全。
|
||||
|
||||
其次,大多数应用开发人员在具有完全空安全迁移的代码库中工作,这是至关重要的条件,在迁移所有代码和所有依赖项(包括传递性)之前, Dart [健全的 null safety](https://dart.dev/null-safety/understanding-null-safety) 不会发挥作用。
|
||||
|
||||
下图显示了 `flutter run` 在引入零安全和没有引起之间的对比,随着应用开始迁移到零安全,开发人员进行了部分迁移,但仍存在部分内容未迁移到 null safety。
|
||||
|
||||
随着时间的推移可以看到, null safety 使用在健康地增长。到上月底,与不使用 null safety 相比, null safety 多出四倍,所以我们希望,在接下来的几个季度中,我们将看到 100% 的可靠零安全方法。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_# Dart/image3.png)
|
||||
|
||||
|
||||
|
||||
# 重要的零安全路线图更新
|
||||
|
||||
同时支持空安全和非空安全会增加开销和复杂性。
|
||||
|
||||
首先,Dart 开发者需要学习和理解这两种模式,每当阅读一段 Dart 代码时,检查[语言版本](https://dart.dev/guides/language/evolution#language-versioning)以查看类型是否默认为非空(Dart 2.12 及更高版本)或默认可空(Dart 2.11 及更早版本)。
|
||||
|
||||
其次,在我们的编译器和运行时同时支持这两种模式会减慢 Dart SDK 的发展以支持新功能。
|
||||
|
||||
**基于非空安全的开销和上一节中提到的非常积极的采用数字,我们的目标是过渡到仅支持可靠的空值安全,并停止非空值安全和不健全的空值安全模式,我们暂时将其定于 2023 年年中发布**。
|
||||
|
||||
**这将意味着停止对 Dart 2.11 及更早版本的支持**,具有低于 2.12 的 SDK 约束的 Pubspec 文件将不再在 Dart 3 及更高版本中解析。
|
||||
|
||||
> 在包含语言标记的源代码中,如果设置为小于 2.12(例如`// @dart=2.9`)也会失败。
|
||||
|
||||
如果已迁移到可靠的 null 安全性,那么你的代码将在 Dart 3 中以完全的 null 安全性工作,如果还没有,请立即迁移!
|
||||
|
||||
> 要了解有关这些更改的更多信息,请参阅[此 GitHub 问题](https://github.com/dart-lang/sdk/issues/49530)。
|
266
Dart-300.md
266
Dart-300.md
|
@ -1,266 +0,0 @@
|
|||
# Google I/O 2023 - Dart 3 发布,快来看看有什么更新吧
|
||||
|
||||
> 核心原文链接: https://medium.com/dartlang/announcing-dart-3-53f065a10635
|
||||
|
||||
自从 Flutter Forword 发布了 [Dart 3α 预览](https://juejin.cn/post/7194741144482218045) 之后,大家对 Dart 3 的正式发布就一直翘首以待,这不仅仅是 Dart 版本号追上了 Flutter 版本号,更是 Dart 在 2.0 之后迎来的最大一次更新,主要包括了:
|
||||
|
||||
- 100% 空安全
|
||||
- records
|
||||
- patterns
|
||||
- class modifiers
|
||||
- Wasm 对 Web 的增加支持,可以预览 dart wasm native 了
|
||||
|
||||
|
||||
|
||||
# 100% 空安全支持
|
||||
|
||||
如下图所示,Dart 的 null safety 历经三年的时间,如今 Dart 终于有用了完善的类型系统,现在的 Dart 3 下,如果一个类型说一个值不是 `null`,那么它永远不可能是 `null` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_D3/image1.png)
|
||||
|
||||
> 说起来,还真有不少用户的项目没升级到 null safety ,这次就不能再等了。
|
||||
|
||||
另外,目前 pub.dev 上排名前 1000 的包中有 99% 支持空安全,所以官方预计升级到 Dart 3 的兼容问题并不大,少数情况下,Dart 3 中的对一些历史代码的相关清理可能会影响某些代码的运行,例如
|
||||
|
||||
- 一些旧的核心库 API 已被删除([#34233](https://github.com/dart-lang/sdk/issues/34233)、[#49529](https://github.com/dart-lang/sdk/issues/49529))
|
||||
- 一些工具已被调整([#50707](https://github.com/dart-lang/sdk/issues/50707))。
|
||||
|
||||
> 如果你在迁移到到 Dart 3 时遇到问题,可以查阅 https://dart.dev/resources/dart-3-migration
|
||||
|
||||
# Record, patterns 和 class modifiers
|
||||
|
||||
关于万众期待的 record 和 patterns 其实在之前的 [Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](https://juejin.cn/post/7194741144482218045)上已经有个详细解释,这里主要重新根据官方内容简诉一些这些变化。
|
||||
|
||||
## 使用 record 构建结构化数据
|
||||
|
||||
在此之前 Dart 函数只能返回一个值,如果需要返回多个值,必须将这些值打包成其他数据类型,例如 Map 或 List,或者定义可以保存这些值的新类。
|
||||
|
||||
使用非类型化数据结构削弱了类型安全性,而定义新类来传输数据会增加编码过程中的工作量,但是现在,通过 record 就可以简洁明地构建结构化数据:
|
||||
|
||||
```dart
|
||||
(String, int) userInfo(Map<String, dynamic> json) {
|
||||
return (json['name'] as String, json['height'] as int);
|
||||
}
|
||||
```
|
||||
|
||||
在 Dart 中,record 是一个通用功能,它们不仅可以用于函数返回值,还可以将它们存储在变量中,例如将它们放入 List 中或者它们用作 Map 中的键,或创建包含其他 record 的 record。
|
||||
|
||||
另外还可以添加未命名字段,就像我们在前面的示例中所做的那样,也可以添加命名字段,例如 `(42, description: ‘Meaning of life’)` 。
|
||||
|
||||
record 是值类型,没有标识,这让编译器能够在某些情况下完全擦除记录对象,记录还带有自动定义的 `==` 运算符和 `hashCode` 函数。
|
||||
|
||||
> 详细可以参考官方文档:https://dart.dev/language/records 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045
|
||||
|
||||
## 使用具有 pattern 和 pattern 匹配的结构化数据
|
||||
|
||||
record 简化了构建结构化数据的方式,这不会取代使用类来构建正式的类型层次结构的方式,它只是提供了另一种选择。
|
||||
|
||||
在任何一种情况下,你可能希望将结构化数据分解为单独的元素,这就是 pattern 匹配发挥作用的地方。
|
||||
|
||||
考虑 pattern 的基本形式,以下记录 pattern 将 record 解构为两个新变量 `name` 和 `height` ,然后可以像任何其他变量一样使用这些变量:
|
||||
|
||||
```dart
|
||||
var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
|
||||
print('User $name is $height cm tall.');
|
||||
```
|
||||
|
||||
List 和 Map 存在类似的 pattern ,都可以使用下划线模式跳过单个元素:
|
||||
|
||||
```dart
|
||||
var (String name, _) = userInfo(…);
|
||||
```
|
||||
|
||||
在 switch 语法中, Dart 3 扩展了语句 switch 的支持,现在支持在这些情况下进行 pattern 匹配:
|
||||
|
||||
```dart
|
||||
switch (charCode) {
|
||||
case slash when nextCharCode == slash:
|
||||
skipComment();
|
||||
|
||||
case slash || star || plus || minus:
|
||||
operator(charCode);
|
||||
|
||||
case >= digit0 && <= digit9:
|
||||
number();
|
||||
|
||||
default:
|
||||
invalid();
|
||||
}
|
||||
```
|
||||
|
||||
还可以通过新的表达式进行微调,以下示例函数返回 switch 表达式的值以计算今天工作日的描述:
|
||||
|
||||
```dart
|
||||
String describeDate(DateTime dt) =>
|
||||
switch (dt.weekday) {
|
||||
1 => 'Feeling the Monday blues?',
|
||||
6 || 7 => 'Enjoy the weekend!',
|
||||
_ => 'Hang in there.'
|
||||
};
|
||||
```
|
||||
|
||||
**模式的一个强大功能是检查 “exhaustiveness” 的能力,此功能可确保 switch 处理所有可能的情况**。
|
||||
|
||||
在前面的示例中,我们正在处理工作日的所有可能值,这是一个`int` ,所以我们通过针对特定值 `1` 或 `6 `/`7` 的匹配语句的组合来穷尽所有可能的值,然后通过 `_` 对其余情况使用默认情况。
|
||||
|
||||
要对用户定义的数据层次结构(例如类层次结构)启用该能力,请在类层次结构的顶部使用 `sealed` 修饰符,如下例所示:
|
||||
|
||||
```dart
|
||||
sealed class Animal { … }
|
||||
class Cow extends Animal { … }
|
||||
class Sheep extends Animal { … }
|
||||
class Pig extends Animal { … }
|
||||
|
||||
String whatDoesItSay(Animal a) =>
|
||||
switch (a) { Cow c => ' $c says moo' , Sheep s => ' $s says baa' };
|
||||
```
|
||||
|
||||
这将返回以下错误,提醒我们错过了最后一个可能的子类型 Pig 的处理:
|
||||
|
||||
```
|
||||
line 6 • The type 'Animal' is not exhaustively matched by the switch cases
|
||||
since it doesn't match 'Pig()'.
|
||||
```
|
||||
|
||||
最后,`if ` 语句也可以使用 pattern ,在下面的例子里,我们使用 *if-case* 匹配映射模式来解构 JSON 映射,这里匹配常量值(字符串如 `'name'` and `'Michael' `)和类型测试模式 `int h` 以读出 JSON 值,如果模式匹配失败,Dart 将执行该 `else` 语句。
|
||||
|
||||
```dart
|
||||
final json = {'name': 'Michael', 'height': 180};
|
||||
|
||||
// Find Michael's height.
|
||||
if (json case {'name': 'Michael', 'height': int h}) {
|
||||
print('Michael is $h cm tall.');
|
||||
} else {
|
||||
print('Error: json contains no height info for Michael!');
|
||||
}
|
||||
```
|
||||
|
||||
> 详细可以参考官方文档:http://dart.dev/language/patterns 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045
|
||||
|
||||
|
||||
|
||||
# classes with class modifiers
|
||||
|
||||
Dart 3 的第三个语言特性是类修饰符,与前两个支持不同的是,这更像是一个高级用户功能,它主要是为了满足了 Dart 开发人员制作大型 API 或构建企业级应用时的需求。
|
||||
|
||||
> 目前是基于 *constructed*、 *extended* 和 *implemented* 来实现处理,关键词有
|
||||
|
||||
类修饰符使 API 作者能够仅支持一些特定的功能,而默认值保持不变,例如:`abstract`、`base` 、`final`、`interface`、`sealed`、`mixin` 。
|
||||
|
||||
> 只有`base `修饰符可以出现在 mixin 声明之前,修饰符不适用于其他声明如 `enum`、`typedef`或 `extension`。
|
||||
|
||||
```dart
|
||||
class Vehicle {
|
||||
String make; String model;
|
||||
void moveForward(int meters) { … }
|
||||
}
|
||||
|
||||
// Construct.
|
||||
var myCar = Vehicle(make: 'Ford', model: 'T',);
|
||||
|
||||
// Extend.
|
||||
class Car extends Vehicle {
|
||||
int passengers;
|
||||
}
|
||||
|
||||
// Implement.
|
||||
class MockVehicle implements Vehicle {
|
||||
@override void moveForward …
|
||||
}
|
||||
```
|
||||
|
||||
例如要强制继承类或 mixin 的实现,就可以使用 `base` 修饰符。 `base` 不允许在其自己的库之外实现,这保证:
|
||||
|
||||
- 每当创建类的子类型的实例时,都会调用基类构造函数
|
||||
- 所有实现的私有成员都存在于子类型中
|
||||
- 类中新实现的成 员`base `不会破坏子类型,因为所有子类型都继承了新成员
|
||||
|
||||
```dart
|
||||
// Library a.dart
|
||||
base class Vehicle {
|
||||
void moveForward(int meters) { ... }
|
||||
}
|
||||
|
||||
|
||||
// Library b.dart
|
||||
import 'a.dart';
|
||||
|
||||
var myCar = Vehicle(); // Can be constructed
|
||||
|
||||
base class Car extends Vehicle { // Can be extended
|
||||
int passengers;
|
||||
// ...
|
||||
}
|
||||
|
||||
base class MockVehicle implements Vehicle { // ERROR: Cannot be implemented
|
||||
@override
|
||||
void moveForward { ... }
|
||||
}
|
||||
```
|
||||
|
||||
如果要创建一组已知的、可枚举的子类型,就可以使用修饰符 `sealed` ,[sealed 允许在那些静态](https://dart.dev/language/branches#exhaustiveness-checking)子类型上创建一个 switch 。
|
||||
|
||||
```dart
|
||||
sealed class Vehicle { ... }
|
||||
|
||||
class Car extends Vehicle { }
|
||||
class Truck implements Vehicle { }
|
||||
class Bicycle extends Vehicle { }
|
||||
|
||||
// ...
|
||||
|
||||
var vehicle = Vehicle(); // ERROR: Cannot be instantiated
|
||||
var vehicle = Car(); // Subclasses can be instantiated
|
||||
|
||||
// ...
|
||||
|
||||
// ERROR: The switch is missing the Bicycle subtype or a default case.
|
||||
return switch (vehicle) {
|
||||
Car() => 'vroom',
|
||||
Truck() => 'VROOOOMM'
|
||||
};
|
||||
```
|
||||
|
||||
类修饰符存在一些添加限制,例如:
|
||||
|
||||
- 使用 `interface class` ,可以定义 contract 给其他人去实现,但不能扩展接口类。
|
||||
- 使用 `base class`,可以确保类的所有子类型都继承自它,而不是实现它的接口,这确保私有方法在所有实例上都可用。
|
||||
- 使用 `final class`,可以关闭类型层次结构,以防止自己的库之外的任何子类。这样的好处是允许 API 所有者添加新成员,而不会出现破坏 API 使用者更改的风险。
|
||||
|
||||
> 是不是没看明白?有关详细信息,可以参考 https://dart.dev/language/class-modifiers
|
||||
|
||||
# 展望未来
|
||||
|
||||
Dart 3 不仅仅是是在这些新功能上向前迈出了重要的一步,还为大家提供了下一步的预览。
|
||||
|
||||
## Dart language
|
||||
|
||||
Records, patterns 和 class modifiers 是非常庞大的新功能,因此它们的某些设计可能还需要改进,所以接下来还会有一些更小、更增量的功能更新,这些功能完全不会中断,并且专注于在没有迁移成本的情况下提高开发人员的工作效率。
|
||||
|
||||
目前正在探索的还有 [primary constructors](https://github.com/dart-lang/language/issues/2364) 和 [inline classes](https://github.com/dart-lang/language/issues/2727) 包装,另外之前讨论过的宏(也称为[元编程](https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md))也在进行探索,因为元编程的规模和固有风险,目前正在采取一种更有效和彻底的方法进行探索,因此没有具体的时间表可以分享,即使是最终确定的设计决策。
|
||||
|
||||
## native interop
|
||||
|
||||
移动和桌面上的应用通常依赖于 native 平台提供的大量 API,无论是通知、支付还是获取手机位置等。
|
||||
|
||||
在之前 Flutter 中,这些是通过构建插件来访问的,这需要为 API 编写 Dart 代码和一堆特定于平台的代码来提供实现。
|
||||
|
||||
目前已经支持与使用 `dart:ffi` 直接和原生语言进行交互,我们目前正在努力扩展它在Android 上的支持,再次之前可以看 [Java 和 Kotlin interop ](https://dart.dev/guides/libraries/java-interop) 以及 [Objective-C 和 Swift interop](https://juejin.cn/post/7137874832988831751) 。
|
||||
|
||||
> 请查看新的 Google I/O 23 的 [Android interop 视频](https://io.google/2023/program/2f02692d-9a41-49c0-8786-1a22b7155628/)。
|
||||
|
||||
## 编译为 WebAssembly——使用 native 代码定位 web
|
||||
|
||||
[WebAssembly (缩写为 Wasm)作为跨](https://webassembly.org/)[所有浏览器的](https://caniuse.com/wasm)平台的二进制指令格式,其可用性度一直在增长,Flutter 框架使用 Wasm 有一段时间了,这就是我们如何通过 Wasm 编译模块将用 C++ 编写的 SKIA 图形渲染引擎交付给浏览器的实现。
|
||||
|
||||
Flutter 也一直对使用 Wasm 来部署 Dart 代码很感兴趣,但是在此之前该实现被阻止了,与许多其他面向对象的语言一样,因为 Dart 需要使用垃圾回收。
|
||||
|
||||
在过去的一年里,Flutter 和 Wasm 生态系统中的多个团队合作,将新的 WasmGC 功能添加到 WebAssembly 标准中,目前在 Chromium 和 Firefox 浏览器中已经接近稳定。
|
||||
|
||||
将 Dart 编译为 Wasm 模块的工作有两个针对 Web 的高级目标:
|
||||
|
||||
- **加载时间:**我们希望我们可以使用 Wasm 交付部署有效负载,使浏览器可以更快地加载,从而缩短到达用户可以与 Web 交互的时间。
|
||||
- **性能:**由 JavaScript 提供支持的 Web 应用需要即时编译才能获得良好的性能,Wasm 模块更底层,更接近机器代码,因此我们认为它们可以提供更高的性能、更少的卡顿和更一致的帧率。
|
||||
- **语义一致性**:Dart 在我们支持的平台之间保持高度一致而自豪。但是,在 web 上有一些例外情况,例如 Dart web 目前在[数字表示](https://dart.dev/guides/language/numbers)方式上有所不同,而使用 Wasm 模块,我们可以将 web 视为具有与其他原生目标相似语义的“原生”平台。
|
||||
|
||||
**跟随 Dart3 的发布, Dart 到 Wasm 编译的第一个预览也一起发布**,这是最初的 Flutter Web 重点支持。虽然现在还早,后续还有很多工作要完成,但已经可以通过 https://flutter.dev/wasm 开始测试。
|
574
Dart-300a.md
574
Dart-300a.md
|
@ -1,574 +0,0 @@
|
|||
# Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解
|
||||
|
||||
> 由于 Dart 3 还处于 alpha ,某些细节可能还会有所变化,但是总体设定和大部分细节应该不会变太多,大家可以提前尝鲜。
|
||||
>
|
||||
> 更多更新也可以关注官方的 [records-feature-specification](https://github.com/dart-lang/language/blob/master/accepted/future-releases/records/records-feature-specification.md) 和 [feature-specification.md](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#patterns) 相关进展。
|
||||
|
||||
Record 和 Patterns 作为 Dart 3 的 Big Things ,无疑是 Flutter 和 Dart 开发者都十分关注的新特性。
|
||||
|
||||
简单来说,**Records 支持高效简洁地创建匿名复合值,不需要再声明一个类来保存,而在 Records 组合数据的地方,Patterns 可以将复合数据分解为其组成部分**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image14.png)
|
||||
|
||||
众所周知 Dart 语言本身一直都 “相对保守”,而这次针对 Records 和 Patterns 的支持却很“彻底”,属于全能力的模式匹配,能递归匹配,有 condition guards ,对于 Flutter 开发者来说无疑是生产力的大幅提升。
|
||||
|
||||
> 当然,也可能是 Bug 的大幅度提升。
|
||||
|
||||
# Records
|
||||
|
||||
如下方代码所示,**Records 属于是一种匿名的不可变聚合类型** ,类似于 Map 和 List ,但是 Records 固定大小,组合更灵活,并且支持不同类型存储。
|
||||
|
||||
```dart
|
||||
var record = (1, a: 2, 3, b: 4);
|
||||
```
|
||||
|
||||
> 除了大小固定之外,Records 和 Map 和 List 最大不同就是它支持不同类型聚合存储,也就是你不用再写 `List<Object>` 之类的代码来承载数据多样性。
|
||||
|
||||
当然,可能你会觉得,这和我定义一个 Class 来承载不同数据对象有什么区别?其实还是有很大区别的:
|
||||
|
||||
- 定义了类,也就是说你的数据集合需要和特定类耦合
|
||||
- 使用 Records 就不必声明对应类型,**只要具有相同字段集的记录, Dart 就会认为它们是相同类型**(这个后面会介绍)
|
||||
|
||||
> 所以从上面可以看到, Records 的出现对于Dart 来说是很重要的能力拓展,尽管对于其他语言这也许并不是什么新鲜特性。
|
||||
|
||||
## 简单介绍
|
||||
|
||||
对于 Records ,我们拓展前面的代码,通过打印对应的数值,可以清晰看到 Records 内数值的获取方式:**通过 `$` 位置字段或者命名字段的方式获取数据**。
|
||||
|
||||
```dart
|
||||
var record = (1, a: 2, 3, b: 4);
|
||||
print(record.$1); // Print "1"
|
||||
print(record.a); // Print "2"
|
||||
print(record.$2); // Print "3"
|
||||
print(record.b); // Print "4"
|
||||
```
|
||||
|
||||
> 在 Records 的变更记录里:**现在 Records 开始位置记录是从 `$1` 开始,而不是 `$0`** ,但是 DartPad 上你可能还会遇到需要从 `$0` 开始。
|
||||
|
||||
而定义 Records 是通过 `()` 和 "`,`" 实现,为什么要有 "`,`" ,如下代码所示:
|
||||
|
||||
```dart
|
||||
var num = (123); // num
|
||||
var records = (123,); // record
|
||||
```
|
||||
|
||||
- 如果没有 "`,`" ,那么 `(123)` 就是一个 num 类型的对象
|
||||
- 有 "`,`" 之后 `(123,)` 才会被识别为是一个 Records 类型
|
||||
|
||||
所以,作为一个集合类型,Records 也是可以用来声明变量,比如:
|
||||
|
||||
```dart
|
||||
(bool, num, {int n, String s}) records;
|
||||
records = (false, 1, n: 12, s : "xxx");
|
||||
print(records);
|
||||
```
|
||||
|
||||
当然,如果你如下代码一样赋值就会收获一个 ` can't be assigned to a variable of type` 的错误,因为它们类型不相同,Records 是固定大小的:
|
||||
|
||||
```dart
|
||||
records = (false, 1, s : "xxx2");
|
||||
records = (false, 1, n : 12);
|
||||
```
|
||||
|
||||
而 Records 上的命名字段主要在于可以如下这样赋值:
|
||||
|
||||
```dart
|
||||
records = (false, 1, s : "xxx2", n : 12);
|
||||
records = (s : "xxx2", n : 12, false, 1, );
|
||||
print(records);
|
||||
```
|
||||
|
||||
最后,在 Records 的定义里需要遵循以下规则:
|
||||
|
||||
- 同一命名字段名称只能出现一次,这个不难理解,比如上面代码你不可能定义两个 `s` 。
|
||||
- `(,)` 这样的表达式是不允许的,但是 `()` 可以是没有任何字段的常量空 Records
|
||||
- 有参数但是只有 `()` 没有 "`,`" 也不是 Records ,如 `(6)`
|
||||
- 命令为 `hashCode`、 `runtimeType`、 `noSuchMethod`, 、`toString` 的字段是不允许的
|
||||
- 以下划线开头的命令字段是不允许的
|
||||
- 与位置字段名称冲突的命令字段,比如 *`('pos', $1: 'named')`* 这样是不行的,但是 `($1: 'records')` 这样可以
|
||||
|
||||
知道了 Records 的大概逻辑之后,这里面有个有趣的设定,比如:
|
||||
|
||||
```dart
|
||||
var t = (int, String);
|
||||
print(t);
|
||||
print(t.$0.runtimeType);
|
||||
print(t.$1.runtimeType);
|
||||
```
|
||||
|
||||
通过打印你会发现 `t` 里面的 `$0` 和 `$1` 是 `_Type` 类型,也就是如果后面再写 ` t = (1, "fff");` ,就会收获这样的错误
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image1.png)
|
||||
|
||||
> 其实这个例子没什么实际意义,注意强调一下 `var t = (int, String);` 和 `(int, String) t` 的区别。
|
||||
|
||||
最后简单介绍下 Records 的类型关系:
|
||||
|
||||
- **`Record` 是 `Object` 、 `dynamic` 的子类和 `Never` 的父类**
|
||||
- **所有的 Records 都是 `Record` 的子类和 `Never` 的父类**
|
||||
|
||||
如果拓展到 Records 之间进行比较,假设有 A、B 两个都是 Records 对象,而 **B 在和 A 具有相同 shape 的前提下,所有的字段都是 A 里字段的子类**,那么 Records B 可以认为是 Records A 的子类。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image2.png)
|
||||
|
||||
|
||||
|
||||
## 进阶探索
|
||||
|
||||
前面我们介绍过,**在 Records 里,只要具有相同字段集的记录, Dart 就会认为它们是相同类型**,这怎么理解呢?
|
||||
|
||||
首先需要确定的是,**Records 类型里命名字段的顺序并不重要**,就是 `{int a, int b}` 与`{int b, int a} ` 的类型系统和 runtime 会完全相同。
|
||||
|
||||
> 另外位置字段不仅仅是名为 `$1` 、`$2` 这样的字段语法糖,`('a', 'b')` 和 `($1: 'a', $2: 'b') ` 从外部看是具有相同的 *members* ,只是具有不同的 *shapes*。
|
||||
|
||||
例如 `(1.2, name: 's', true, count: 3) ` 的签名大概会是这样:
|
||||
|
||||
```dart
|
||||
class extends Record {
|
||||
double get $1;
|
||||
String get name;
|
||||
bool get $2;
|
||||
int get count;
|
||||
}
|
||||
```
|
||||
|
||||
> **Records 里每个字段都有 getter ,并且字段是不可变的,所以不会又 Setter**。
|
||||
|
||||
所以由于 Records 本身数据复杂性等原因,所以设定上 Records 的标识就是它的内容,**也就是具有相同 shape 和字段的两条 Records 是相等的值**。
|
||||
|
||||
```dart
|
||||
print((a: 1, b: 2) == (b: 2, a: 1)); // true
|
||||
```
|
||||
|
||||
当然,如果是以下这种情况,因为位置参数顺序不一样,所以它们并不相等,因为 shape 不同,会输出 `false`。
|
||||
|
||||
```dart
|
||||
print((true, 2, a: 1, b: 2,) == (2, true, b: 2, a: 1)); // false
|
||||
```
|
||||
|
||||
同时,**Records 运行时的类型由其字段的运行时的类型确定**,例如:
|
||||
|
||||
```dart
|
||||
(num, Object) pair = (1, 2.3);
|
||||
print(pair is (int, double)); // "true".
|
||||
```
|
||||
|
||||
这里**运行时 `pair`是 `(int, double)`,不是`(num, Object)`** ,虽然官方文档是这么提供的,但是 Dartpad 上验证目前却很有趣,大家可以自行体会:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image3.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image4.png)
|
||||
|
||||
我们再看个例子,如下代码所示, Records 是可以作为用作 Map 里的 key 值,因为它们的 shape 和 value 相等,所以可以提取出 Map 里的值。
|
||||
|
||||
```dart
|
||||
var map = {};
|
||||
map[(1, "aa")] = "value";
|
||||
print(map[(1, "aa")]); //输出 "value"
|
||||
```
|
||||
|
||||
如果我们定义一个 `newClass` , 如下代码所示,可以预料到输出结果会是 `null` ,因为两个 `newClass` 并不相等。
|
||||
|
||||
```dart
|
||||
|
||||
class newClass {
|
||||
|
||||
}
|
||||
|
||||
var map = {};
|
||||
map[(1, new newClass())] = "value";
|
||||
print(map[(1, new newClass())]); //输出 "null"
|
||||
|
||||
```
|
||||
|
||||
但是如果给 `newClass` 的 `==` 和 `hashCode `进行` override `,就可以又看到输出 `"value"` 的结果。
|
||||
|
||||
```dart
|
||||
class newClass {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => 1111111;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
所以到这里,你应该就理解了“**只要具有相同字段集的记录, Dart 就会认为它们是相同类型**”这句话的含义。
|
||||
|
||||
最后再介绍一个 Runtime 时的特性, **Records 中的字段是从左到右计算的**,即使后续实现选择了重新排序命名字段也是如此,例如:
|
||||
|
||||
```dart
|
||||
int say(int i) {
|
||||
print(i);
|
||||
return i;
|
||||
}
|
||||
|
||||
var x = (a: say(1), b: say(2));
|
||||
var y = (b: say(3), a: say(4));
|
||||
|
||||
```
|
||||
|
||||
上门结果一定是打印 *“1”、“2” / “3”、“4”* , 就算是下面代码的排列,也是输出 *“0”、“1”、“2” / “3”、“4”、“5”* 。
|
||||
|
||||
```dart
|
||||
var x = (say(0), a: say(1), b: say(2));
|
||||
var y = (b: say(3), a: say(4), say(5));
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Records 带来的语法歧义
|
||||
|
||||
因为 Dart 3 的 Records 是在以前版本的基础上升级的,那么一些语法兼容就是必不可少的,这里整理一下目前官方罗列出来的常见调整。
|
||||
|
||||
### try/on
|
||||
|
||||
首先是 `try/on` 相关语法, 如果按照以前的设定,第二行的 `on` 应该是被识别为一个局部函数,但是在增加了 Records 之后,现在它是可以匹配的 `on` Records 类型。
|
||||
|
||||
```dart
|
||||
void recordTryOn() {
|
||||
try {
|
||||
} on String {
|
||||
}
|
||||
|
||||
on(int, String) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 这里声明的类型其实没什么意义,只是为了形象展示对比
|
||||
|
||||
鉴于消除歧义的目的,如果在早于 Records 支持版本里,`on `关键字后带 `()` 这样的类型,将直接被语法解析为 Records 类型,提示为语法错误,因为该 Dart 版本不支持 Records 类型。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image5.png)
|
||||
|
||||
|
||||
|
||||
### metadata 注解
|
||||
|
||||
如下代码所示,因为多了 Records 之后,注解的理解上可能就会多了一些语法歧义:
|
||||
|
||||
```dart
|
||||
@metadata (a, b) function() {}
|
||||
```
|
||||
|
||||
如果不约定好理解,这可能是:
|
||||
|
||||
- `@metadata(a, b)` 与没有返回类型的函数声明关联的metadata 注解
|
||||
- `@metadata`与返回类型为 Records 类型的函数关联的metadata 注解 `(a, b)`
|
||||
|
||||
所以这里主要通过空格来约定,尽管这样很容易出现纰漏:
|
||||
|
||||
```dart
|
||||
@metadata(a, b) function() {}
|
||||
|
||||
@metadata (a, b) function() {}
|
||||
```
|
||||
|
||||
- 前者由于 `@metadata` 之后没有空格,所以表示为 `(a, b)` 的 metadata 注解
|
||||
- 前者由于有空格,所以表示为 Records 返回类型
|
||||
|
||||
它们的不同之处可以参考下面的两种类型:
|
||||
|
||||
```dart
|
||||
// Records 和 metadata 是一起作用在 a
|
||||
@metadata(x, y) a;
|
||||
@metadata<T>(x, y) a;
|
||||
@metadata <T>(x, y) a;
|
||||
|
||||
// Records 是直接作用在 a ,和 metadata 无关
|
||||
@metadata (x, y) a;
|
||||
|
||||
@metadata
|
||||
(x, y) a;
|
||||
|
||||
@metadata/* comment */(x, y) a;
|
||||
|
||||
@metadata // Comment.
|
||||
(x,) a;
|
||||
```
|
||||
|
||||
举个例子,比如下面这种情况 `@TestMeta(1, "2")` 没有空格,所以不会有语法错误
|
||||
|
||||
```dart
|
||||
@TestMeta(1, "2")
|
||||
class C {}
|
||||
|
||||
|
||||
class TestMeta {
|
||||
final String message;
|
||||
final num code;
|
||||
|
||||
const TestMeta(this.code, this.message);
|
||||
|
||||
@override
|
||||
String toString() => "feature: $code, $message";
|
||||
}
|
||||
```
|
||||
|
||||
但是如果是 `@TestMeta (1, "2")` ,就会有 `Annotations can't have spaces or comments before the parenthesis.` 这样的错误提示。
|
||||
|
||||
```dart
|
||||
@TestMeta (1, "2") //Error
|
||||
class C {}
|
||||
```
|
||||
|
||||
> 所以有无空格对于 metadata 注解来说将会变得完全不一样,可能这对一些第三方插件的适配使用上会有一定 breaking change。
|
||||
|
||||
### toString
|
||||
|
||||
在 Debug 版本中,Records 的 `toString()` 方法会通过调用每个字段的 `toString()`值,并在其前面加上字段名称,后续是否添加 `: ` 字符取决于字段是否为命名字段,最终会将每个字段转换为字符串。
|
||||
|
||||
> 看下面例子可能会更形象。
|
||||
|
||||
每个字段会利用 `, ` 作为分隔符连接起来,并返回用括号括起来的结果,例如:
|
||||
|
||||
```
|
||||
print((1, 2, 3).toString()); // "(1, 2, 3)".
|
||||
print((a: 'str', 'int').toString()); // "(a: str, int)".
|
||||
```
|
||||
|
||||
在 **Debug 版本中,命名字段出现的顺序以及它们如何与位置字段进行排列是不确定的,只有位置字段必须按位置顺序出现**。
|
||||
|
||||
> 所以 toString 内部实现可以自由地为命名字段选择规范顺序,而与创建记录的顺序无关。
|
||||
|
||||
而在发布或优化构建中,`toString()` 行为是更不确定的, 所以可能会有选择地丢弃命名字段的全名以减少代码大小等操作。
|
||||
|
||||
> **所以用户最好只将 Records 的 `toString()` 用于调试**,强烈建议不要解析调用结果 `toString()` 或依赖它来获得某些逻辑判断,避免产生歧义。
|
||||
|
||||
# Patterns
|
||||
|
||||
如果只是单纯 Records 可能还看不到巨大的价值,但是如果配合上 Patterns ,那开发效率就可以得到进一步提升,**其中最值得关注的就是多个返回值的支持**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image6.png)
|
||||
|
||||
## 简单介绍
|
||||
|
||||
**关于 Patterns 这里不会有太长的篇幅**,首先目前 Patterns 在 DartPad 上还是 disabled 的状态,其次 Patterns 的复杂度和带来的语法歧义问题实在太多,它目前还具有太多未确定性。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image7.png)
|
||||
|
||||
> 从[提案](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md#summary)上看,未来感觉也不会一次性所有能力全部发布。
|
||||
|
||||
### 多返回值
|
||||
|
||||
回到主题,我们知道,使用 Records 可以让我们的方法实现多个返回值,例如下面代码的实现
|
||||
|
||||
```dart
|
||||
(double, double) geoCode(String city) {
|
||||
var lat = // Calculate...
|
||||
var long = // Calculate...
|
||||
|
||||
return (lat, long); // Wrap in record and return.
|
||||
}
|
||||
```
|
||||
|
||||
但是当我们需要获取这些值的时候,就需要 **Patterns 的解构赋值**,例如:
|
||||
|
||||
```dart
|
||||
var (lat, long) = geoCode('Aarhus');
|
||||
print('Location lat:$lat, long:$long');
|
||||
```
|
||||
|
||||
**当然 Patterns 下的解构赋值不只是针对 Records** ,例如对 `List` 或者 `Map` 也可以:
|
||||
|
||||
```dart
|
||||
var list = [1, 2, 3];
|
||||
var [a, b, c] = list;
|
||||
print(a + b + c); // 6.
|
||||
|
||||
var map = {'first': 1, 'second': 2};
|
||||
var {'first': a, 'second': b} = map;
|
||||
print(a + b); // 3.
|
||||
```
|
||||
|
||||
更近一步还可以解构并分配给现有变量:
|
||||
|
||||
```dart
|
||||
var (a, b) = ('left', 'right');
|
||||
(b, a) = (a, b); // Swap!
|
||||
print('$a $b'); // Prints "right left".
|
||||
```
|
||||
|
||||
> 有没有觉得代码变得难阅读了?哈哈哈哈
|
||||
|
||||
### 代数数据类型
|
||||
|
||||
就如 Flutter Forward 介绍那样,现在类层次结构基本上已经可以对代数数据类型进行建模,Patterns 下提供了新的模式匹配结构,例如代码可以变成这样:
|
||||
|
||||
```dart
|
||||
///before
|
||||
double calculateArea(Shape shape) {
|
||||
if (shape is Square) {
|
||||
return shape.length + shape.length;
|
||||
} else if (shape is Circle) {
|
||||
return math.pi * shape.radius * shape.radius;
|
||||
} else {
|
||||
throw ArgumentError("Unexpected shape.");
|
||||
}
|
||||
}
|
||||
|
||||
//after
|
||||
double calculateArea(Shape shape) =>
|
||||
switch (shape) {
|
||||
Square(length: var l) => l * l,
|
||||
Circle(radius: var r) => math.pi * r * r
|
||||
};
|
||||
```
|
||||
|
||||
> 甚至 `switch `都不需要添加 `case` 关键字,并且用上了后面会简单介绍的可变模式。
|
||||
|
||||
### Patterns
|
||||
|
||||
目前 Dart 上 Patterns 的设定还挺复杂,简单来说是:
|
||||
|
||||
> **通过一些简洁、可组合的符号,排列后确定一个对象是否符合条件,并从中解构出数据,然后仅当所有这些都为 true 时才执行代码**。
|
||||
|
||||
也就是你会看到一系列充满操作符的简短代码,如 `"||"`、 `" && "`、 `"=="`、 `"<"`、 `"as"`、 `"?"`、 `"_"`、`"[]"`、`"()"`、`"{}"`等的排列组合,并尝试逐个去理解它们,例如:
|
||||
|
||||
```dart
|
||||
var isPrimary = switch (color) {
|
||||
Color.red || Color.yellow || Color.blue => true,
|
||||
_ => false
|
||||
};
|
||||
```
|
||||
|
||||
用 `"||" `可以在 switch 中让多个 case 共享一个主体,`"_"` 表示默认,甚至如下代码所示,你还可以在绑定 `s` 之后,多个共享一个 `when` 条件:
|
||||
|
||||
```dart
|
||||
switch (shape) {
|
||||
case Square(size: var s) || Circle(size: var s) when s > 0:
|
||||
print('Non-empty symmetric shape');
|
||||
case Square() || Circle():
|
||||
print('Empty symmetric shape');
|
||||
default:
|
||||
print('Asymmetric shape');
|
||||
}
|
||||
```
|
||||
|
||||
这种写法可以大大优化 `switch` 的结构 ,如下所示可以看到,类似写法代码得到了很大程度的精简:
|
||||
|
||||
```dart
|
||||
String asciiCharType(int char) {
|
||||
const space = 32;
|
||||
const zero = 48;
|
||||
const nine = 57;
|
||||
|
||||
return switch (char) {
|
||||
< space => 'control',
|
||||
== space => 'space',
|
||||
> space && < zero => 'punctuation',
|
||||
>= zero && <= nine => 'digit'
|
||||
// Etc...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当然,还有一些很奇葩的设定,比如利用 `? `匹配非空值,很明显这样的写法很反直觉,最终是否这样落地还是要看社区讨论的结果:
|
||||
|
||||
```dart
|
||||
String? maybeString = ...
|
||||
switch (maybeString) {
|
||||
case var s?:
|
||||
// s has type non-nullable String here.
|
||||
}
|
||||
```
|
||||
|
||||
更进一步还有在解构的 position 赋值时通过 `!` 强制转为非空,还有在 switch 匹配时第一个列为 `'user'` 时 `name` 不为空。
|
||||
|
||||
```dart
|
||||
(int?, int?) position = ...
|
||||
|
||||
// We know if we get here that the coordinates should be present:
|
||||
var (x!, y!) = position;
|
||||
|
||||
|
||||
List<String?> row = ...
|
||||
|
||||
// If the first column is 'user', we expect to have a name after it.
|
||||
switch (row) {
|
||||
case ['user', var name!]:
|
||||
// name is a non-nullable string here.
|
||||
}
|
||||
```
|
||||
|
||||
如果搭配上 Records 就更难理解了,比如下代码,可变 pattern 将匹配值绑定到新变量,这里的 `var a `和 `var b ` 是可变模式,最终分别绑定到 `1` 和 `2` 上。
|
||||
|
||||
```dart
|
||||
switch ((1, 2)) {
|
||||
case (var a, var b): ...
|
||||
}
|
||||
|
||||
|
||||
switch (record) {
|
||||
case (int x, String s):
|
||||
print('First field is int $x and second is String $s.');
|
||||
}
|
||||
```
|
||||
|
||||
其实就类似于 Flutter Forword 介绍的能力,`case` 下可以做对应的绑定,如上 `switch (record)` 也是类似这种绑定。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230131_D3/image6.png)
|
||||
|
||||
> 如果使用变量的名称是 `_`,那么它不绑定任何变量
|
||||
|
||||
更多的可能还有如 List、 Map 、 Records、 Object 等相关的 pattern 匹配等,**可以看到 Patterns 将很大程度改变 Dart 代码的编写和逻辑组织风格**:
|
||||
|
||||
```dart
|
||||
var list = [1, 2, 3];
|
||||
var [_, two, _] = list;
|
||||
|
||||
|
||||
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
|
||||
print('$a $b $rest $c $d'); // Prints "1 2 [3, 4, 5] 6 7".
|
||||
|
||||
|
||||
// Variable:
|
||||
var (untyped: untyped, typed: int typed) = ...
|
||||
var (:untyped, :int typed) = ...
|
||||
|
||||
switch (obj) {
|
||||
case (untyped: var untyped, typed: int typed): ...
|
||||
case (:var untyped, :int typed): ...
|
||||
}
|
||||
|
||||
// Null-check and null-assert:
|
||||
switch (obj) {
|
||||
case (checked: var checked?, asserted: var asserted!): ...
|
||||
case (:var checked?, :var asserted!): ...
|
||||
}
|
||||
|
||||
// Cast:
|
||||
var (field: field as int) = ...
|
||||
var (:field as int) = ...
|
||||
|
||||
|
||||
class Rect {
|
||||
final double width, height;
|
||||
|
||||
Rect(this.width, this.height);
|
||||
}
|
||||
|
||||
display(Object obj) {
|
||||
switch (obj) {
|
||||
case Rect(width: var w, height: var h): print('Rect $w x $h');
|
||||
default: print(obj);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 从目前看来,**这会是一种自己写起来很爽,别人看起来可能很累的特性**,同时也可能会带来不少的 breaking change ,更多详细可见:[patterns-feature-specification](https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md)
|
||||
|
||||
好了,关于 Patterns 的这里就不再继续展开,它落地会如何最终还不完全确定,但是从我的角度来看,它绝对会是一把双刃剑,希望 Patterns 到来的同时不会引入太多的 Bug。
|
||||
|
||||
# 最后
|
||||
|
||||
其实我相信大多数人可能都只关心 Records 和解构赋值,从而实现函数的多返回值能力,这对我们来说是最直观和最实用的。
|
||||
|
||||
至于 switch 如何匹配和 Patterns 如何精简代码结构,这都是后话了。
|
||||
|
||||
现在,或者你可以选择 Dart 3 尝尝鲜了~
|
174
Dart-303.md
174
Dart-303.md
|
@ -1,174 +0,0 @@
|
|||
# Dart 3.3 发布:扩展类型、JavaScript Interop 等
|
||||
|
||||
> 参考链接:https://medium.com/dartlang/dart-3-3-325bf2bf6c13
|
||||
|
||||
跟随 [Flutter 3.19 发布](https://juejin.cn/post/7334503381200781363)的还有 Dart 3.3 ,Dart 3.3 主要包含扩展类型增强,性能优化和 native 代码交互推进,例如本次改进的**JavaScript Interop** 模型就引入了类型安全,[所以这一切都为 WebAssembly 支持铺平了道路](https://juejin.cn/post/7232164444985622588)。
|
||||
|
||||
>在[《Flutter 2024 路线规划里》](https://juejin.cn/post/7335067315452428297) ,**Web 平台上未来 CanvasKit 将成为默认渲染**,所以未来 Dart 在 Web 上肯定是 Wasm Native 的路线。
|
||||
|
||||
|
||||
|
||||
# 扩展类型
|
||||
|
||||
扩展类型是一种编译时抽象,它用不同的纯静态接口来 “Wrapper” 现有类型,同时它们也是 Dart 和[静态 JS 互操作](https://dart.dev/go/next-gen-js-interop)的主要实现基础,因为它们可以轻松修改现有类型的接口(对于任何类型的相互调用都至关重要),而不会产生实际 Wrapper 的成本。
|
||||
|
||||
```dart
|
||||
extension type E(int i) {
|
||||
// Define set of operations.
|
||||
}
|
||||
```
|
||||
|
||||
扩展类型引入了类型的零成本 wrappers,使用它们来优化性能敏感的代码,尤其是在与 native 平台交互时,扩展类型提供了具有特定成员自定义类型的便利性,同时消除了典型的 wrappers 分配开销。
|
||||
|
||||
```dart
|
||||
extension type Wrapper(int i) {
|
||||
void showValue() {
|
||||
print('my value is $i');
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final wrapper = Wrapper(42);
|
||||
wrapper.showValue(); // Prints 'my value is 42'
|
||||
}
|
||||
```
|
||||
|
||||
上面的例子实现了一个 **`Wrapper`** 扩展类型,但将其用作普通的 Dart 类型,在实际使用里,开发者可以实例化它并调用函数。
|
||||
|
||||
这里的主要区别在于 Dart 将其编译为普通 Dart **`int`** 类型,扩展类型允许创建具有唯一的成员类型,而无需分配典型 wrappers 类型的间接成本,例如以下例子包装了对应的 `int` 类型以创建仅允许对 ID 号有意义的操作的扩展类型。
|
||||
|
||||
```dart
|
||||
extension type IdNumber(int id) {
|
||||
// Wraps the 'int' type's '<' operator:
|
||||
operator <(IdNumber other) => id < other.id;
|
||||
// Doesn't declare the '+' operator, for example,
|
||||
// because addition does not make sense for ID numbers.
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Without the discipline of an extension type,
|
||||
// 'int' exposes ID numbers to unsafe operations:
|
||||
int myUnsafeId = 42424242;
|
||||
myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.
|
||||
|
||||
var safeId = IdNumber(42424242);
|
||||
safeId + 10; // Compile-time error: No '+' operator.
|
||||
myUnsafeId = safeId; // Compile-time error: Wrong type.
|
||||
myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
|
||||
safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
|
||||
}
|
||||
```
|
||||
|
||||
因此,虽然 [extension members](https://dart.dev/language/extension-methods) 功能(Dart 2.7 开始)允许向现有类型添加函数和属性,但扩展类型功能也可以执行相同的操作,**并且还允许定义隐藏底层表示的新 API**。
|
||||
|
||||
这对于与 native code 的相互调用特别有用。可以直接使用原生类型,无需创建 Wrapper 和相关间接的成本,同时仍然提供干净的生产 Dart API。
|
||||
|
||||
> 扩展类型与 Wrapper 具有相同的用途,但不需要创建额外的运行时对象,当开发者需要包装大量对象时,Wrapper 这个行为可能会变得昂贵,由于扩展类型仅是静态的并且在运行时编译,因此它们本质上是零成本。
|
||||
>
|
||||
> [**扩展方法**](https://dart.dev/language/extension-methods)(也称为“扩展”)是类似于扩展类型的静态抽象。但是扩展方法是**直接**向其基础类型的每个实例添加功能;扩展类型不同,**扩展类型的接口仅适用于静态类型为该扩展类型的表达式**。
|
||||
>
|
||||
> 默认情况下它们与其基础类型的接口不同。
|
||||
|
||||
扩展类型有两个同样有效但本质上不同的核心用例:
|
||||
|
||||
- **为现有类型提供扩展接口**,当扩展类型实现其表示类型时,一般可以认为它是“透明的”,因为它允许扩展类型“看到”底层类型。
|
||||
|
||||
透明扩展类型可以调用表示类型的所有成员([未重新声明的](https://dart.dev/language/extension-types#redeclare)),以及它定义的任何辅助成员,这将为现有类型创建一个新的扩展接口,新接口可用于静态类型为扩展类型的表达式,例如如下代码里,`v1.i` 可以正常调用,但是 int 类似的` v2` 不可以调用 `v2.i`:
|
||||
|
||||
```dart
|
||||
extension type NumberT(int value)
|
||||
implements int {
|
||||
// Doesn't explicitly declare any members of 'int'.
|
||||
NumberT get i => this;
|
||||
}
|
||||
void main () {
|
||||
// All OK: Transparency allows invoking `int` members on the extension type:
|
||||
var v1 = NumberT(1); // v1 type: NumberT
|
||||
int v2 = NumberT(2); // v2 type: int
|
||||
var v3 = v1.i - v1; // v3 type: int
|
||||
var v4 = v2 + v1; // v4 type: int
|
||||
var v5 = 2 + v1; // v5 type: int
|
||||
// Error: Extension type interface is not available to representation type
|
||||
v2.i;
|
||||
}
|
||||
```
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Dart-303/image1.png)
|
||||
|
||||
- **为现有类型提供不同的接口**,[不透明](https://dart.dev/language/extension-types#transparency)的扩展类型(不是 [`implement `](https://dart.dev/language/extension-types#implements)其表示类型)被静态地视为全新类型,与其表示类型不同,所以无法将其分配给其表示类型,并且它不会公开其表示类型的成员,例如 `NumberE` 不能为 int ,并且 :
|
||||
|
||||
```dart
|
||||
extension type NumberE(int value) {
|
||||
NumberE operator +(NumberE other) =>
|
||||
NumberE(value + other.value);
|
||||
|
||||
NumberE get next => NumberE(value + 1);
|
||||
bool isValid() => !value.isNegative;
|
||||
}
|
||||
|
||||
void testE() {
|
||||
var num1 = NumberE(1);
|
||||
int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
|
||||
|
||||
num1.isValid(); // OK: Extension member invocation.
|
||||
num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
|
||||
|
||||
var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
|
||||
var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
|
||||
var diff2 = num1.value - 2; // OK: Can access representation object with reference.
|
||||
var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'.
|
||||
|
||||
List<NumberE> numbers = [
|
||||
NumberE(1),
|
||||
num1.next, // OK: 'i' getter returns type 'NumberE'.
|
||||
1, // Error: Can't assign 'int' element to list type 'NumberE'.
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Dart-303/image2.png)
|
||||
|
||||
另外需要注意,**扩展类型是编译时包装构造,在运行时绝对没有扩展类型的踪迹**,任何类型查询或类似的运行时操作都适用于表示类型,在任何情况下,扩展类型的表示类型都不是其子类型,因此在需要扩展类型的情况下表示类型不能互换使用。
|
||||
|
||||
# JavaScript Interop
|
||||
|
||||
Dart 3.3 引入了一种与 JavaScript 和 Web 相互调用的新模型,它从一组用于与 JavaScript 交互的新 API 开始:[dart:js_interop](https://api.dart.dev/dart-js_interop/dart-js_interop-library.html) 。
|
||||
|
||||
现在,**Dart 开发人员可以访问类型化 API 来与 JavaScript 交互**,该 API 通过静态强制明确定义了两种语言之间的边界,在编译之前消除了许多问题。
|
||||
|
||||
除了用于访问 JavaScript 代码的新 API 之外,Dart 现在还包含一个新模型,用于使用扩展类型在 Dart 中表示 JavaScript 类型,如下代码就是前面拓展类型的实际使用实例:
|
||||
|
||||
```dart
|
||||
import 'dart:js_interop';
|
||||
|
||||
/// Represents the `console` browser API.
|
||||
extension type MyConsole(JSObject _) implements JSObject {
|
||||
external void log(JSAny? value);
|
||||
external void debug(JSAny? value);
|
||||
external void info(JSAny? value);
|
||||
external void warn(JSAny? value);
|
||||
}
|
||||
```
|
||||
|
||||
基于扩展类型的语法比扩展成员允许更多的表达和健全性。这简化了 Dart 中 JavaScript API 的利用,更多详细信息可以查看:https://dart.dev/interop/js-interop 。
|
||||
|
||||
# 改进 browser libraries
|
||||
|
||||
从 1.0 版本开始,Dart SDK 就包含了一套全面的 browser libraries,其中包括核心 [dart:html](https://api.dart.dev/dart-html/dart-html-library.html) 库以及 SVG、WebGL 等库。
|
||||
|
||||
改进后的 JavaScript 调用模型提供了重新构想这些库的机会,未来 browser libraries 支持将集中在 [package:web](https://pub.dev/packages/web)上,这简化了版本控制、加速了更新并与[MDN](https://developer.mozilla.org/)资源保持一致,这一系列的改进推动了[将 Dart 编译为 WebAssembly](https://juejin.cn/post/7232164444985622588)。
|
||||
|
||||
# 从今天开始,开启 WebAssembly 的未来
|
||||
|
||||
Dart 3.3 为WebAssembly 的 Web 应用奠定基础,虽然 Flutter Web 中的 WebAssembly 支持仍处于试验阶段,但是这对于 Dart 和 Flutter 是明显的方向。
|
||||
|
||||
要使用 WebAssembly 在 Web 上运行 Flutter 应用,需要使用新的 JavaScript Interop 机制和 `package:web` ,旧版 JavaScript 和 browser libraries 保持不变,并支持编译为 JavaScript 代码。但是,如果编译为 WebAssembly 需要迁移,例如:
|
||||
|
||||
```dart
|
||||
import 'dart:html' as html; // Remove
|
||||
import 'package:web/web.dart' as web; // Add
|
||||
|
||||
dependencies:
|
||||
web: ^0.5.0
|
||||
```
|
||||
|
||||
> 更多可见:https://dart.dev/interop/js-interop/package-web
|
101
Dart-320.md
101
Dart-320.md
|
@ -1,101 +0,0 @@
|
|||
# Dart 3.2 更新,Flutter Web 的未来越来越明朗
|
||||
|
||||
> 参考原文:https://medium.com/dartlang/dart-3-2-c8de8fe1b91f
|
||||
|
||||
本次跟随 [Flutter 3.16 发布](https://juejin.cn/post/7301574930869321779) 的 Dart 3.2 ,包含有:私有 final 字段的非空改进、新的 interop 改进、对 DevTools 中的扩展支持、以及对 Web 路线图的更新,包括对 Wasm 的Web 组件支持。
|
||||
|
||||
> 最重要的就是 Wasm 的Web 组件支持。
|
||||
|
||||
# private final 的非空类型提升
|
||||
|
||||
自 Dart 2.12 发布 sound null safety 以来,类型提升一直是空安全的核心部分之一,但仅限于局部变量里,字段和顶级变量无法处理,例如在这样的情况下会报错:
|
||||
|
||||
```dart
|
||||
class Container {
|
||||
final int? _fillLevel;
|
||||
Container(this._fillLevel);
|
||||
check() {
|
||||
if (_fillLevel != null) {
|
||||
int i = _fillLevel; // Prior to Dart 3.2, causes an error.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种限制是由于几个复杂的情况造成的,在这些情况下,flow analysis 无法确定字段何时会发生什么变化,例如,在字段提升的情况下,如果子类使用 getter 覆盖字段(有时会返回 null),这就可能会出现问题。
|
||||
|
||||
> 在 Dart 3.2 开始,Dart 改进了 flow analysis ,现在能够归类出 **private Final fields**。
|
||||
|
||||
现在 3.2 里上面的代码片段可以顺利通过检测:对于 private & final 的字段,它的值在初始分配后永远不会改变,因此仅检查一次就被认为是安全的。
|
||||
|
||||
# 包中的新代码分析选项:lints 3.0
|
||||
|
||||
3.2 还对 [package:lints](https://pub.dev/packages/lints) 中的标准代码分析规则进行了一些改进,package 包含了默认和推荐的静态分析规则集,这些规则随 `dart create` 或 `flutter create` 创建的新项目一起提供 (通过 [package:flutter_lints](https://pub.dev/packages/flutter_lints) )。
|
||||
|
||||
该 lint 集的主要版本(3.0)目前已经发布,其中向核心集添加了 6 个 lint,向推荐集添加了 2 个 lint,它具有用于验证 pubspec URL、验证是否使用正确参数调用集合方法等相关的 lints 。
|
||||
|
||||
> 有关更改的完整列表,请查看 https://github.com/dart-lang/lints/blob/main/CHANGELOG.md#300, 3.0 版本将成为即将发布的新项目的默认版本。
|
||||
|
||||
# Dart interop 更新
|
||||
|
||||
目前正在努力扩展 Dart interop 以全面支持与 [Java 和 Kotlin](https://dart.dev/guides/libraries/java-interop) 和 [Objective C 和 Swift](https://dart.dev/guides/libraries/objective-c-interop) 的直接调用支持,从 Dart 3.2 开始进行了许多改进:
|
||||
|
||||
- 引入了 C FFI 的构造函数 `NativeCallable.isolateLocal` ,它可以从任意 Dart 函数创建一个 C 函数指针,这是 `Pointer.fromFunction` 提供的功能的扩展,它只能从顶级函数创建函数指针。
|
||||
|
||||
- 更新了 Objective-C 利用`NativeCallable.listener` 绑定生成器,生成器现在可以自动处理包含异步回调的 API,例如 [Core Motion](https://developer.apple.com/documentation/coremotion) 这种以前需要手动绑定的代码。
|
||||
|
||||
- 改进 [package:jnigen](https://dart.dev/guides/libraries/java-interop) 实现 Java 和 Kotlin 的直接调用支持,现在我们能够将 [package:cronet_http](https://pub.dev/packages/cronet_http)(Android Cronet HTTP 客户端的包装器)从手写绑定代码迁移到[自动生成的](https://github.com/dart-lang/http/blob/master/pkgs/cronet_http/jnigen.yaml)包装器。
|
||||
|
||||
- 在 [Native Assets](https://github.com/dart-lang/sdk/issues/50565) 功能上取得了重大进展,该功能旨在解决与依赖于 Native 代码的 Dart 包分发相关的许多问题,它通过提供统一的钩子来与构建 Flutter 和独立 Dart 应用所涉及的各种构建需要,详细可见 :http://dart.dev/guides/libraries/c-interop#native-assets
|
||||
|
||||
> Native Assets 目前是一个**实验性的**功能,它可以让 Dart 包更无缝依赖和使用 Native 代码,通过 `flutter run`/`flutter build ` 和 `dart run`/`dart build` 构建并捆绑 Native 代码 。
|
||||
>
|
||||
> 备注:可通过 `flutter config --enable-native-assets` 和 `flutter create --template=package_ffi [package name]` 启用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Dart32/image1.png)
|
||||
|
||||
Demo [`native_add_library`](https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/native_add_library) 展示了相关使用,当 Flutter 项目依赖 `package:native_add_library` 时, 脚本会自动在 `build.dart` 命令上调用:
|
||||
|
||||
```dart
|
||||
import 'package:native_add_library/native_add_library.dart';
|
||||
|
||||
void main() {
|
||||
print('Invoking a native function to calculate 1 + 2.');
|
||||
final result = add(1, 2);
|
||||
print('Invocation success: 1 + 2 = $result.');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Dart 包的 DevTools 扩展
|
||||
|
||||
在 Dart 3.2 和 Flutter 3.16 中发布了一个新的[扩展框架](https://pub.dev/packages/devtools_extensions),该框架让包作者能够为它的 package 构建自定义工具,并直接在 DevTools 中显示。
|
||||
|
||||
它允许包含框架的 pub.dev 包提供特定用例的自定义工具,例如 [Serverpod](https://pub.dev/packages/serverpod) 的作者一直在努力为它的 package 构建开发人员工具,并且很高兴在即将发布的 [1.2 版本](https://github.com/orgs/serverpod/projects/4) 中提供 DevTools 扩展。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Dart32/image2.png)
|
||||
|
||||
# Dart Web 和 Wasm 更新
|
||||
|
||||
从 Chrome 119 开始,Chrome 会默认启用 [Wasm 垃圾收集支持(称为 Wasm-GC)](https://developer.chrome.com/blog/wasmgc/) ,Wasm-GC 支持也出现在 Firefox 120(他们的下一个稳定版本)中被支持,那么 Dart、Flutter 和 Wasm-GC 的现状如何?
|
||||
|
||||
Dart-to-Wasm 编译器的功能几乎已经完全实现,团队对性能和兼容性非常满意,现在的重点是边缘情况,以确保在广泛的场景中能同样完美运行。
|
||||
|
||||
对于 Flutter Web,这里类似于完成了一个全新的 “Skwasm” 渲染引擎。为了最大限度地提高性能,Skwasm 通过 wasm-to-wasm 绑定将编译后的应用代码,直接连接到自定义 [CanvasKit Wasm 模块](https://skia.org/docs/user/modules/canvaskit/) ,这也是 Flutter Web 多线程渲染支持的第一次迭代,进一步提高了帧时间。
|
||||
|
||||
在 Wasm 的 Flutter web 准备脱离当前的实验状态之前,还有一些事情要做:
|
||||
|
||||
- **双编译**:生成 Wasm 和 JavaScript 输出,并在运行时启用功能检测,以支持支持和不支持 Wasm-GC 的浏览器。
|
||||
- **JavaScript interop**:一种基于[扩展类型](https://github.com/dart-lang/language/issues/2727)的新 JS 互操作机制,当针对 JavaScript 和 Wasm 时,可以在 Dart 代码、浏览器 API 和 JS 库之间进行简洁、类型安全的调用。
|
||||
- **支持 Wasm 的浏览器 API**:一个新的 `package:web`,基于现代 JS 互操作机制,取代了 dart:html (和相关库),这将提供对浏览器 API 的更轻松访问,并支持 JS 和 Wasm 目标。
|
||||
|
||||
目前已经开始将一些内部项目迁移到 `package:web` 和新的 JS 互操作机制,并期望在下一个稳定版本中有更多更新。
|
||||
|
||||
> 可以在 https://flutter.dev/wasm 了解更多。
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
本次更新最重要有两个点,第一就是 Dart interop 越来越成熟,相信以后直接通过 flutter run 就可以完成所有 interop 的绑定和编译,第二就是 Web 路线随着 Dart Wasm 支持的进展,越来越值得期待了。
|
282
Dart-SYS.md
282
Dart-SYS.md
|
@ -1,282 +0,0 @@
|
|||
|
||||
> 原文链接: https://recipes.tst.sh/docs/faq/type-system.html
|
||||
|
||||
## 什么是类型?
|
||||
|
||||
类型是用于描述实例接口的节点,如下示例和注释所示:
|
||||
|
||||
```dart
|
||||
|
||||
// Foo is now an interface type.
|
||||
class Foo {}
|
||||
|
||||
// FooFn is now an alias of the `Foo Function()` type.
|
||||
typedef FooFn = Foo Function();
|
||||
|
||||
// You can now create interface types of Bar with any subtype of Foo as the type argument.
|
||||
class Bar<T extends Foo> {
|
||||
// T is a subtype of Foo in this context.
|
||||
}
|
||||
```
|
||||
|
||||
在最顶层的级别里,只有少数几种类型:
|
||||
|
||||
- `dynamic`
|
||||
- `void`
|
||||
- `interface` 类型
|
||||
- `function` 类型
|
||||
- `parameter` 类型
|
||||
|
||||
最常见的是 `interface` 类型,它描述了类和决定了类型参数。
|
||||
|
||||
`dart:core` 包含了一堆具有特殊类型属性的类,下面将介绍这些类。
|
||||
|
||||
> https://github.com/dart-lang/sdk/blob/master/pkg/kernel/binary.md
|
||||
|
||||
## 实例
|
||||
|
||||
在对象的整个生命周期中,它只有一个类型,该类型在构造时确定并且永远不能更改:
|
||||
|
||||
```dart
|
||||
int x = 2;
|
||||
num y = x;
|
||||
print(x is int); // true
|
||||
print(y is int); // true
|
||||
int z = y as int; // works
|
||||
```
|
||||
|
||||
用于声明变量类型的只是 `interface` ,它可以存储任何实现了该 `interface` 的子类型。
|
||||
|
||||
## 方法
|
||||
|
||||
当在实例上调用方法时,创建实例的类型始终决定了该方法的实现,例如:
|
||||
|
||||
|
||||
```dart
|
||||
class Foo {
|
||||
void hi() => print("i am foo");
|
||||
}
|
||||
|
||||
class Bar implements Foo {
|
||||
void hi() => print("i am bar");
|
||||
}
|
||||
|
||||
void callHi(Foo foo) => foo.hi();
|
||||
|
||||
void main() {
|
||||
callHi(Bar()); // prints "i am bar"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如上述例子所示,`Bar` 实现的 `hi` 将始终覆盖来自其实例的调用结果,而不管它在什么上下文中。
|
||||
|
||||
**在 Dart 代码所有可见的类型,都是 `Object` 的子类型,并继承其默认实现的 `interface`** 。
|
||||
|
||||
**Dart 是强类型的语言**,这意味着编译器可以在运行时,对值的类型做出强有力的保证。
|
||||
|
||||
当然,**强类型并不意味着方法一定存在**,如果调用时缺少方法,Dart 会调用默认情况下调用 `noSuchMethod` 会抛出 `NoSuchMethodError`。
|
||||
|
||||
|
||||
```dart
|
||||
(42 as dynamic).foo(); // throws NoSuchMethodError
|
||||
```
|
||||
|
||||
实例上在 Dart 里的所有字段访问,都是通过对 `setter` 和 `getter` 方法的调用来完成,当在类中声明一个字段时,它隐式声明了读取和写入内部变量的 `setter` 和 `getter `方法。
|
||||
|
||||
```dart
|
||||
class Foo {
|
||||
// This declares both set:a and get:a
|
||||
int a;
|
||||
}
|
||||
|
||||
class Bar extends Foo {
|
||||
// This overrides get:a without touching set:a
|
||||
int get a => super.a * 2;
|
||||
}
|
||||
|
||||
main() {
|
||||
var foo = Bar();
|
||||
foo.a = 2;
|
||||
print(foo.a); // prints 4
|
||||
}
|
||||
```
|
||||
|
||||
## 子类型
|
||||
|
||||
变量可以包含不是其声明类型的实际子类型的值,除了 `null` :
|
||||
|
||||
```dart
|
||||
int x;
|
||||
print(x is int); // false
|
||||
```
|
||||
|
||||
这段代码会打印 `false` ,**因为 `is` 运算符是子类型检查,而不是可分配性检查**。
|
||||
|
||||
而另一方面,**`as` 操作会进行可分配性检查**:
|
||||
|
||||
```dart
|
||||
int x;
|
||||
print(x as int); // null, works
|
||||
```
|
||||
|
||||
这是因为,在以下情况下 `x` 可以是 `T` 的子类型:
|
||||
|
||||
- `x` 的运行时类型是 `T `的子类型。
|
||||
- `x` 为空并且 `T` 可以为空。
|
||||
|
||||
|
||||
## Null vs void vs dynamic vs Object
|
||||
|
||||
`Null` 对象是特殊的,当不是 `get:hashCode`,`get:runtimeType` 和 `operator==` 的方法被调用,它抛出一个格式为 `NoSuchMethodError` 的异常。
|
||||
|
||||
`dynamic` 和 `void` 类型都是 `Object` 的有效别名,但它们改变了一些可见的方法:
|
||||
|
||||
- 使用 `Object`,只能方法 `Object` 的接口(如普通类),例如 `hashCode`。
|
||||
- 使用 `void`,可以存储和转换,但不能访问任何方法。
|
||||
- 使用 `dynamic`,可以访问任何方法,并使用任何参数调用它,这些返回值也被视为 `dynamic` 。
|
||||
|
||||
## 闭包
|
||||
|
||||
提取是将实例方法转换为闭包的过程,这通常称为 `tear-off`。
|
||||
|
||||
> 如果在一个对象上调用函数并省略了括号, Dart 称之为 `”tear-off”` :一个和函数使用同样参数的闭包,当调用闭包的时候会执行其中的函数,比如:`names.forEach(print);` 等同于 `names.forEach((name){print(name);});`
|
||||
|
||||
|
||||
可以通过调用名称为 `getter` 的方法来提取方法:
|
||||
|
||||
```dart
|
||||
typedef ToStringFn = String Function();
|
||||
ToStringFn getToString(Object x) => x.toString;
|
||||
```
|
||||
|
||||
在这个例子中,我们从一个任意对象中 `x `中提取了 `toString`方法,通过闭包,就可以像调用上的常规实例一样调用 `x`。
|
||||
|
||||
```dart
|
||||
typedef ToStringFn = String Function();
|
||||
ToStringFn getToString(Object x) => x.toString;
|
||||
main() {
|
||||
var foo = 111;
|
||||
var a = getToString(foo);
|
||||
print(a());
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
实际上,上面的代码与以下代码相同,除了前者效率更高一些。
|
||||
|
||||
```dart
|
||||
typedef ToStringFn = String Function();
|
||||
ToStringFn getToString(Object x) => () => x.toString();
|
||||
```
|
||||
|
||||
`Functions` 非常特殊,它们实际上可以指两个不同的东西:
|
||||
|
||||
- 用参数和返回类型声明的函数类型,即 `void Function() foo;`。
|
||||
- `Function` 类作为接口类型,任何方法的父类。
|
||||
|
||||
`Function` 类型类似于泛型接口类型,但可以描述参数名称和类型。
|
||||
|
||||
所有函数类型都是 `Function` 的子类型,无论它们的返回类型和参数如何:
|
||||
|
||||
```dart
|
||||
print(print is Function); // true
|
||||
```
|
||||
|
||||
这里做一个有趣的实验,如下代码所示:
|
||||
|
||||
|
||||
```dart
|
||||
|
||||
void main() {
|
||||
void foo() {}
|
||||
int bar([int aaa]) {}
|
||||
Null biz({int aaa}) {}
|
||||
int baz(int aa, {int aaa}) {}
|
||||
|
||||
print(foo is void Function());
|
||||
print(bar is void Function());
|
||||
print(biz is void Function());
|
||||
print(baz is void Function());
|
||||
}
|
||||
```
|
||||
|
||||
打印结果是
|
||||
|
||||
```
|
||||
true
|
||||
true
|
||||
true
|
||||
false
|
||||
```
|
||||
|
||||
这是因为 Dart 类型系统比较灵活,**只要函数采用相同位置的参数,并具有兼容的返回类型,它就是有效的函数子类型**,所以除了 `baz` 打印 `false` 之外所有的结果都是 `true`。
|
||||
|
||||
换个方式,如下代码所示:
|
||||
|
||||
|
||||
```
|
||||
void main() {
|
||||
int foo({int a}) {}
|
||||
int bar({int a, int b}) {}
|
||||
|
||||
print(foo is int Function());
|
||||
print(foo is int Function({int a}));
|
||||
print(bar is int Function({int a}));
|
||||
print(bar is int Function({int b}));
|
||||
print(bar is int Function({int b, int c}));
|
||||
}
|
||||
```
|
||||
|
||||
输出的结果会是:
|
||||
|
||||
|
||||
```
|
||||
true
|
||||
true
|
||||
true
|
||||
true
|
||||
false
|
||||
```
|
||||
|
||||
**因为当函数具有命名参数的子集时,代码检查函数是否具有有效的子类型**,所以除最后一个之外的所有函数都打印 `true`。
|
||||
|
||||
如果最后一个修改为 `print(bar is int Function({int b, int a}));` ,也会打印出 `true` 。
|
||||
|
||||
|
||||
|
||||
## 可调用对象
|
||||
|
||||
类是可以被调用的,例如:
|
||||
|
||||
```
|
||||
class Foo {
|
||||
void call() => print('hi');
|
||||
}
|
||||
|
||||
void main() {
|
||||
Foo()(); // prints "hi"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这实际上是一种欺骗,`Foo` 实例本身实际上是不可调用的,出现这样的结果是因为 `call` 隐式提取了该方法。
|
||||
|
||||
例如:
|
||||
|
||||
```dart
|
||||
void callFoo(void Function() x) {
|
||||
print(x is Foo); // false
|
||||
print(x is Function); // true
|
||||
x();
|
||||
}
|
||||
|
||||
void main() {
|
||||
var x = Foo();
|
||||
print(x is Foo); // true
|
||||
print(x is Function); // false
|
||||
callFoo(x);
|
||||
}
|
||||
```
|
||||
|
||||
在这里 `x` 似乎是在 `Foo` 和 `Function` 之间转变,这是因为 `x` 被传递到 `callFoo` 之前,被隐式转换成一个 `Closure`。
|
309
Dart-VM.md
309
Dart-VM.md
|
@ -1,309 +0,0 @@
|
|||
|
||||
> 原文链接:https://mrale.ph/dartvm/
|
||||
>
|
||||
> PS:内容比较繁杂,请酌情观看
|
||||
|
||||
Dart VM 是用于本地执行 Dart 代码的组件集合,它主要包括以下内容:
|
||||
|
||||
- 运行时系统
|
||||
- 对象模型
|
||||
- 垃圾收集
|
||||
- 快照
|
||||
- 核心库的 native 方法
|
||||
- 可以通过 service protocol 访问的组件: 调试 * 分析 * 热重载
|
||||
- 即时 (JIT) 和提前 (AOT) 编译管道
|
||||
- Interpreter
|
||||
- ARM模拟器
|
||||
|
||||
Dart VM 从某种意义上说是一个虚拟机,它为高级编程语言提供了一个执行环境,**但这并不意味着 Dart 在 Dart VM 上执行时总是需要被解释或 JIT 编译的**。
|
||||
|
||||
例如可以使用 Dart VM AOT 将 Dart 代码编译成机器代码,然后在 Dart VM 的裁剪版本中执行,这被称为预编译运行时,它不包含任何编译器组件,无法动态加载 Dart 源代码。
|
||||
|
||||
## Dart VM 如何运行你的代码?
|
||||
|
||||
Dart VM 有多种执行代码的方式,例如:
|
||||
|
||||
- 使用源代码或内核二进制文件的 JIT 模式;
|
||||
- 使用快照:
|
||||
- 来自 AOT 快照;
|
||||
- 来自 AppJIT 快照;
|
||||
|
||||
然而它们之间的主要区别在于: **VM “何时”以及“如何”将 Dart 源代码转换为可执行代码**,然后保证执行的运行时环境保持不变。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image1)
|
||||
|
||||
|
||||
VM 中的任何 Dart 代码都在某个 `isolate` 中运行,可以将其描述为:**具有自己的内存(堆)并且通常具有自己的控制线程(mutator 线程)的 Dart 隔离宇宙**。
|
||||
|
||||
VM 可以有许多 `isolate` 同时执行 Dart 代码,但它们不能直接共享任何状态,只能通过端口传递消息进行通信(不要与网络端口混淆!)。
|
||||
|
||||
这里的 OS 线程和 `isolate` 之间的关系有点模糊,并且高度依赖于虚拟机嵌入到应用程序的方式,但是主要需要保证以下内容:
|
||||
|
||||
- 一个 OS 线程一次只能进入一个 `isolate` ,如果它想进入另一个 `isolate`,它必须离开当前 `isolate`;
|
||||
- 一次只能有一个与 `isolate` 相关联的 Mutator 线程,Mutator 线程是执行 Dart 代码并使用 VM 的公共 C API 的线程。
|
||||
|
||||
然而同一个 OS 线程可以先进入一个 `isolate` 执行 Dart 代码,然后离开这个 `isolate` 并进入另一个 `isolate` 继续执行;或者有许多不同的 OS 线程进入一个 `isolate` 并在其中执行 Dart 代码,只是不会同时发生。
|
||||
|
||||
当然,除了单个 Mutator 线程之外,`isolate` 还可以关联多个辅助线程,例如:
|
||||
|
||||
- 一个后台 JIT 编译器线程;
|
||||
- GC sweeper 现场;
|
||||
- 并发 GC marker 线程;
|
||||
|
||||
VM 在内部使用线程池 (`dart::ThreadPool`) 来管理 OS 线程,并且代码是围绕 `dart::ThreadPool::Task` 概念而不是围绕 OS 线程的概念构建的。
|
||||
|
||||
例如在 GC VM 中将 `dart::ConcurrentSweeperTask` 发布到全局 VM 的线程池,而不是生成专用线程来执行后台清除,**并且线程池实现要么选择空闲线程,要么在没有可用线程时生成新线程**;类似地,**用于 `isolate` 来消息处理事件循环的默认实现实际上,并没有产生专用的事件循环线程,而是在新消息到达时将`dart::MessageHandlerTask` 发布到线程池**。
|
||||
|
||||
|
||||
> `dart::Isolate` 类相当于一个 `isolate` ,`dart::Heap` 类相当于 `isolate` 的堆,`dart::Thread` 类描述了线程连接到 `isolate` 相关的状态。
|
||||
>
|
||||
> 请注意,该名称 `Thread `可能会让人有些困惑,因为所有 OS 线程都附加到与 Mutator 相同的 `isolate`,将重用相同的 Thread 实例。有关 `isolate` 消息处理的默认实现,请参阅 `Dart_RunLoop` 和 `dart::MessageHandler`。
|
||||
|
||||
|
||||
## 通过 JIT 运行源代码
|
||||
|
||||
本节将介绍当从命令行执行 Dart 时会发生什么:
|
||||
|
||||
```dart
|
||||
// hello.dart
|
||||
main() => print('Hello, World!');
|
||||
|
||||
$ dart hello.dart
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Dart 2 VM 开始不再具有从原始代码直接执行 Dart 的能力,相反 VM 希望获得包含序列化内核 AST 的内核二进制文件(也称为 dill 文件)**。将 Dart 源代码翻译成 Kernel AST 的任务是由通用前端 (CFE)处理的,CFE 是用 Dart 编写并在不同 Dart 工具上共享(例如 VM、dart2js、Dart Dev Compiler)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image2)
|
||||
|
||||
为了保持直接从源代码执行 Dart ,这里托管一个名为 kernel service 的辅助 `isolate`,它处理将 Dart 源代码编译到内核中,然后 VM 运行生成的内核二进制文件。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image3)
|
||||
|
||||
然而这种设置并不是 CFE 和 VM 运行 Dart 代码的唯一方法,例如 **Flutter 是将编译到 Kernel 的过程和从 Kernel 执行的过程完全分离**,并将它们放在不同的设备上实现:编译发生在开发者机器(主机)上,执行在目标移动设备上处理,目标移动设备接收由 flutter 工具发送给它的内核二进制文件。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image4)
|
||||
|
||||
这里需要注意,该 Flutter 工具不处理 Dart 本身的解析, 相反它会生成另一个持久进程 `frontend_server`,它本质上是围绕 CFE 和一些 Flutter 特定的 Kernel-to-Kernel 转换的封装。
|
||||
|
||||
`frontend_server` 将 Dart 源代码编译为内核文件, 然后 flutter 将其发送到设备, 当开发人员请求热重载时 `frontend_server` 开始发挥作用:在这种情况下 `frontend_server` 可以重用先前编译中的 CFE 状态,并重新编译实际更改的库。
|
||||
|
||||
**一旦内核二进制文件加载到 VM 中,它就会被解析以创建代表各种程序实体的对象,然而这个过程是惰性完成的**:首先只加载关于库和类的基本信息,源自内核二进制文件的每个实体都保留一个指向二进制文件的指针,以便以后可以根据需要加载更多信息。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image5)
|
||||
|
||||
|
||||
> 每当我们引用 VM 内部分配的对象时,我们都会使用 Untagged 前缀,因为这遵循了 VM 自己的命名约定:内部 VM 对象的布局由 C++ 类定义,名称以 Untagged头文件 `runtime/vm/raw_object.h` 开头。例如 `dart::UntaggedClass` 是描述一个 Dart 类 VM 对象, `dart::UntaggedField` 是一个 VM 对象
|
||||
|
||||
|
||||
**只有在运行时需要它时(例如查找类成员、分配实例等),有关类的信息才会完全反序列化**,在这个阶段,类成员会从内核二进制文件中读取,然而在此阶段不会反序列化完整的函数体,只会反序列化它们的签名。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image6)
|
||||
|
||||
此时 methods 在运行时可以被成功解析和调用,因为已经从内核二进制文件加载了足够的信息,例如它可以解析和调用 `main` 库中的函数。
|
||||
|
||||
> `package:kernel/ast.dart` 定义了描述内核 AST 的类; `package:front_end` 处理解析 Dart 源代码并从中构建内核 AST。`dart::kernel::KernelLoader::LoadEntireProgram是` 将内核 AST 反序列化为相应 VM 对象的入口点;`pkg/vm/bin/kernel_service.dart` 实现了内核服务隔离,`runtime/vm/kernel_isolate.cc` 将 Dart 实现粘合到 VM 的其余部分; `package:vm` 承载大多数基于内核的 VM 特定功能,例如各种内核到内核的转换;由于历史原因一些特定于 VM 的转换仍然存在于 `package:kernel` 中。
|
||||
|
||||
最初所有的函数都会有一个占位符,而不是它们的主体的实际可执行代码:它们指向 `LazyCompileStub`,它只是要求运行时系统为当前函数生成可执行代码,然后 `tail-calls ` 这个新生成的代码。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image7)
|
||||
|
||||
第一次编译函数时,是通过未优化编译器完成的。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image8)
|
||||
|
||||
未优化编译器分两遍生成机器代码:
|
||||
|
||||
- 1、**遍历函数体的序列化 AST 以生成函数体的控制流图( CFG ),CFG 由填充有中间语言( IL ) 指令的基本块组成**。在此阶段使用的 IL 指令类似于基于堆栈的虚拟机的指令:它们从堆栈中获取操作数,执行操作,然后将结果推送到同一堆栈。
|
||||
|
||||
> 实际上并非所有函数都具有实际的 Dart / Kernel AST 主体,例如在 C++ 中定义的本地函数或由 Dart VM 生成的人工 `tear-off` 函数,在这些情况下,IL 只是凭空创建,而不是从内核 AST 生成。
|
||||
|
||||
- 2、生成的 CFG 使用一对多的底层 IL 指令直接编译为机器代码:每个 IL 指令扩展为多个机器语言指令。
|
||||
|
||||
在此阶段没有执行任何优化,未优化编译器的主要目标是快速生成可执行代码。
|
||||
|
||||
这也意味着:**未优化的编译器不会尝试静态解析内核二进制文件中未解析的任何调用**,VM 当前不使用基于虚拟表或接口表的调度,而是使用**内联缓存**实现动态调用。
|
||||
|
||||
> 内联缓存的原始实现,实际上是修补函数的 native 代码,因此得名**内联缓存**,内联缓存的想法可以追溯到 Smalltalk-80,请参阅 Smalltalk-80 系统的高效实现。
|
||||
|
||||
**内联缓存背后的核心思想,是在特定的调用点中缓存方法解析的结果**,VM 使用的内联缓存机制包括:
|
||||
|
||||
- 一个调用特定的缓存( `dart::UntaggedICData`),它将接收者的类映射到一个方法,如果接收者是匹配的类,则应该调用该方法,缓存还存储一些辅助信息,例如调用频率计数器,用于跟踪给定类在此调用点上出现的频率;
|
||||
|
||||
- 一个共享查找 stub ,它实现了方法调用的快速路径。这个 stub 搜索给定的缓存,以查看它是否包含与接收者的类匹配的条目。如果找到该条目,则 stub 将增加频率计数器和 `tail-calls` 用缓存方法。否则 stub 将调用一个运行时系统助手来实现方法解析逻辑。如果方法解析成功,则缓存将被更新,后续调用将不需要进入运行时系统。
|
||||
|
||||
如下图所示,展示了与 `animal.toFace()` 调用关联的内联缓存的结构和状态,该缓存使用 `Dog` 的实例执行了两次,使用 `Cat` 的实例执行了一次C。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image9)
|
||||
|
||||
未优化的编译器本身足以执行任何 Dart 代码,然而它产生的代码相当慢,这就是为什么 VM 还实现了自适应优化编译管道的原因,自适应优化背后的想法是:**使用运行程序的执行配置文件来驱动优化决策**。
|
||||
|
||||
当未优化的代码运行时,它会收集以下信息:
|
||||
|
||||
- 如上所述,内联缓存收集有关在调用点观察到的接收器类型的信息;
|
||||
- 函数和函数内的基本块相关联的执行计数器跟踪代码的热点区域;
|
||||
|
||||
当与函数关联的执行计数器达到一定阈值时,该函数被提交给后台优化编译器进行优化。
|
||||
|
||||
优化编译的启动方式与非优化编译的启动方式相同:**通过遍历序列化内核 AST ,为正在优化的函数构建未优化的 IL**。
|
||||
|
||||
然而不是直接将 IL 处理为机器代码,而是基于表单的优化 IL, 优化编译器继续将未优化的 IL 转换为静态单赋值(SSA) ,然后基于 SSA 的 IL 根据收集的类型反馈进行专业化的推测,并通过一系列Dart 的特定优化,例如:
|
||||
|
||||
- 内联(inlining);
|
||||
- 范围分析(range analysis);
|
||||
- 类型传播( type propagation);
|
||||
- 代理选择(representation selection);
|
||||
- 存储加载和加载转发(store-to-load and load-to-load forwarding);
|
||||
- 全局值编号(global value numbering);
|
||||
- 分配下沉(,allocation sinking)等,;
|
||||
|
||||
最后使用线性扫描寄存器和简单的一对多降低 IL 指令,将优化的 IL 转化为机器代码。
|
||||
|
||||
|
||||
编译完成后,后台编译器会请求 mutator 线程进入安全点并将优化的代码附加到函数中。
|
||||
|
||||
> 广义上讲,当与线程相关联的状态(例如堆栈帧、堆等)一致,并且可以在不受线程本身中断的情况下访问或修改时,托管环境(虚拟机)中的线程被认为处于安全点。通常这意味着线程要么暂停,要么正在执行托管环境之外一些代码,例如运行非托管 native 代码。
|
||||
|
||||
下次调用此函数时, 它将使用优化的代码。 某些函数包含非常长的运行循环,对于那些函数,在函数仍在运行时,将执行从未优化代码切换到优化代码是有意义的。
|
||||
|
||||
**这个过程被称为堆栈替换( OSR )**,它的名字是因为:一个函数版本的堆栈帧被透明地替换为同一函数的另一个版本的堆栈帧。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image10)
|
||||
|
||||
> 编译器源代码位于 `runtime/vm/compiler` 目录中;编译管道入口点是 `dart::CompileParsedFunctionHelper::Compile`;IL 在 `runtime/vm/compiler/backend/il.h` 中定义;内核到 IL 的转换从 `dart::kernel::StreamingFlowGraphBuilder::BuildGraph` 开始,该函数还处理各种人工函数的 IL 构建;当 `InlineCacheMissHandler` 处理 IC 的未命中,`dart::compiler::StubCodeCompiler::GenerateNArgsCheckInlineCacheStub` 为内联缓存存根生成机器代码; `runtime/vm/compiler/compiler_pass.cc` 定义了优化编译器传递及其顺序; `dart::JitCallSpecializer` 大多数基于类型反馈的专业化。
|
||||
|
||||
需要强调的是,优化编译器生成的代码,是在基于应用程序执行配置文件的专业推测下假设的。
|
||||
|
||||
例如,一个动态调用点只观察到一个 `C` 类的实例作为一个接收方,它将被转换成一个可以直接调用的对象,并通过检查来验证接收方是否有一个预期的 `C` 类。然而这些假设可能会在程序执行期间被违反:
|
||||
|
||||
```C
|
||||
void printAnimal(obj) {
|
||||
print('Animal {');
|
||||
print(' ${obj.toString()}');
|
||||
print('}');
|
||||
}
|
||||
|
||||
// Call printAnimal(...) a lot of times with an intance of Cat.
|
||||
// As a result printAnimal(...) will be optimized under the
|
||||
// assumption that obj is always a Cat.
|
||||
for (var i = 0; i < 50000; i++)
|
||||
printAnimal(Cat());
|
||||
|
||||
// Now call printAnimal(...) with a Dog - optimized version
|
||||
// can not handle such an object, because it was
|
||||
// compiled under assumption that obj is always a Cat.
|
||||
// This leads to deoptimization.
|
||||
printAnimal(Dog());
|
||||
```
|
||||
|
||||
|
||||
每当代码正在做一些假设性优化时,它可能会在执行过程中被违反,所以它需要保证当出现违反假设的情况下,可以恢复原本的执行。
|
||||
|
||||
这个恢复过程又被称为去优化:当优化版本遇到它无法处理的情况时,它只是将执行转移到未优化函数的匹配点,并在那里继续执行,函数的未优化版本不做任何假设,可以处理所有可能的输入。
|
||||
|
||||
VM 通常在去优化后丢弃函数的优化版本,而之后再次重新优化它时,会 使用更新的类型反馈。
|
||||
|
||||
VM 有两种方式保护编译器做出的推测性假设:
|
||||
|
||||
|
||||
- 内联检查(例如CheckSmi,CheckClassIL 指令)验证假设在编译器做出此假设的使用站点是否成立。例如将动态调用转换为直接调用时,编译器会在直接调用之前添加这些检查。
|
||||
- Global guards 会运行时丢弃优化代码,当依赖的内容变化时。例如优化编译器可能会观察到某个 `C` 类从未被扩展,并在类型传播过程中使用此信息。然而随后的动态代码加载或类终结可能会引入一个子类 C。此时运行时需要查找并丢弃在 `C` 没有子类的假设下编译的所有优化代码。运行时可能会在执行堆栈上找到一些现在无效的优化代码,在这种情况下受影响的帧将被标记为“去优化”,并在执行返回时取消优化。**这种去优化被称为惰性去优化: 因为它被延迟执行,直到控制返回到优化的代码**。
|
||||
|
||||
> 去优化器机制在 `runtime/vm/deopt_instructions.cc` 中,它本质上是一个解优化指令的微型解释器,它描述了如何从优化代码的状态,重建未优化代码的所需状态。去优化指令由 `dart::CompilerDeoptInfo::CreateDeoptInfo` 在编译期间针对优化代码中的每个潜在"去优化"位置生成。
|
||||
|
||||
|
||||
## 从快照运行
|
||||
|
||||
VM 能够将 `isolate` 的堆,或位于堆中的更精确地序列化对象的图称为二进制快照,然后可以使用快照在启动 VM isolates 时重新创建相同的状态。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image11)
|
||||
|
||||
快照的格式是底层的,并且针对快速启动进行了优化:**它本质上是一个要创建的对象列表以及有关如何将它们连接在一起的说明**。
|
||||
|
||||
快照背后的最初想法:VM 无需解析 Dart 源和逐步创建内部 VM 数据结构,而是可以将所有必要的数据结构从快照中快速解包出来,然后进行 `isolate ` up。
|
||||
|
||||
> 快照的想法源于 Smalltalk 图像,而后者又受到 Alan Kay 的硕士论文的启发。Dart VM 使用集群序列化格式,这类似于 《Parcels: a Fast and Feature-Rich Binary Deployment Technology》和《Clustered serialization with Fuel》论文中描述的技术。
|
||||
|
||||
最初快照不包括机器代码,但是后来在开发 AOT 编译器时添加了此功能。开发 AOT 编译器和带有代码的快照的动机:**是为了允许在由于平台级别限制而无法进行 JIT 的平台上使用 VM**。
|
||||
|
||||
带有代码的快照的工作方式几乎与普通快照相同,但有细微差别:它们包含一个代码部分,这部分与快照的其余部分不同,它不需要反序列化,此代码部分的放置方式允许它在映射到内存后直接成为堆的一部分。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image12)
|
||||
|
||||
|
||||
> `runtime/vm/clustered_snapshot.cc` 处理快照的序列化和反序列化; API 函数 `Dart_CreateXyzSnapshot[AsAssembly]` 负责写出堆的快照(例如`Dart_CreateAppJITSnapshotAsBlobs` 和 `Dart_CreateAppAOTSnapshotAssembly` ); `Dart_CreateIsolateGroup` 可选择获取快照数据以启动 `isolate `。
|
||||
|
||||
|
||||
## 从 AppJIT 快照运行
|
||||
|
||||
**引入 AppJIT 快照是为了减少大型 Dart 应用程序的 JIT 预热时间**,例如 `dartanalyzer` 或 `dart2js`。当这些工具用于小型项目时,它们花在实际工作上的时间与 VM 花在 JIT 编译这些应用程序上的时间一样多。
|
||||
|
||||
AppJIT 快照可以解决这个问题:可以使用一些模拟训练数据在 VM 上运行应用程序,然后将所有生成的代码和 VM 内部数据结构序列化为 AppJIT 快照,然后分发此快照,而不是以源(或内核二进制)形式分发应用程序。
|
||||
|
||||
从这个快照开始的 VM 仍然可以 JIT。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image13)
|
||||
|
||||
|
||||
## 从 AppAOT 快照运行
|
||||
|
||||
|
||||
AOT 快照最初是为无法进行 JIT 编译的平台引入的,但它们也可用于快速启动和更低性能损失的情况。
|
||||
|
||||
关于 JIT 和 AOT 的性能特征比较通常存在很多混淆的概念:
|
||||
|
||||
- JIT 可以访问正在运行的应用程序的本地类型信息和执行配置文件,但是它必须为预热付出代价;
|
||||
- AOT 可以在全局范围内推断和证明各种属性(为此它必须支付编译时间),没有关于程序实际执行方式的信息, 但 AOT 编译代码几乎立即达到其峰值性能,几乎没有任何预热.
|
||||
|
||||
> 目前 Dart VM JIT 的峰值性能最好,而 Dart VM AOT 的启动时间最好。
|
||||
|
||||
无法进行 JIT 意味着:
|
||||
|
||||
- 1、AOT 快照必须包含可以在应用程序执行期间调用的每个函数的可执行代码;
|
||||
- 2、可执行代码不得依赖任何可能在执行过程中会被违反的推测性假设;
|
||||
|
||||
为了满足这些要求,AOT 编译过程会进行全局静态分析(类型流分析或TFA),以确定应用程序的哪些部分可以从已知的入口点集合、分配哪些类的实例,以及类型如何在程序运转。
|
||||
|
||||
所有这些分析都是保守的:意味着它们在没办法和 JIT 一样执行更多的优化执行,因为它总是可以反优化为未优化的代码以实现正确的行为。
|
||||
|
||||
所有可能用到的函数都会被编译为本机代码,无需任何推测优化,而类型流信息仍然用专门代码处理(例如去虚拟化调用)。
|
||||
|
||||
编译完所有函数后,就可以拍摄堆的快照,然后就可以使用预编译运行时运行生成的快照,这是 Dart VM 的一种特殊变体,它不包括 JIT 和动态代码加载工具等组件。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image14)
|
||||
|
||||
> `package:vm/transformations/type_flow/transformer.dart` 是基于 TFA 结果的类型流分析和转换的入口点;`dart::Precompiler::DoCompileAll` 是 VM 中 AOT 编译循环的入口点。
|
||||
|
||||
## 可切换调用
|
||||
|
||||
|
||||
即使进行了全局和局部分析,AOT 编译代码仍可能包含无法去虚拟化的调用(意味着它们无法静态解析)。为了补偿这种 AOT 编译代码,运行时使用 JIT 中的内联缓存技术扩展,此扩展版本称为 `switchable calls`。
|
||||
|
||||
JIT 部分已经描述了与调用点关联的每个内联缓存由两部分组成:
|
||||
|
||||
- 缓存对象(由 `dart::UntaggedICData` 实例表示);
|
||||
- 要调用的本地代码块(例如 `InlineCacheStub`);
|
||||
|
||||
在 JIT 模式下,运行时只会更新缓存本身,但是在 AOT 运行时可以根据内联缓存的状态选择替换缓存和要调用的本机代码。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image15)
|
||||
|
||||
最初所有动态调用都以未链接状态开始,当达到第一次调用点 `SwitchableCallMissStub` 被调用时,它只是调用到运行帮手 `DRT_SwitchableCallMiss` 链接该调用位置。
|
||||
|
||||
之后 `DRT_SwitchableCallMiss` 会尝试将呼叫点转换为单态状态,在这种状态下调用点变成了直接调用,它通过一个特殊的入口点进入方法,该入口点验证接收者是否具有预期的类。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image16)
|
||||
|
||||
在上面的示例中,我们假设 `obj.method()` 第一次执行的实例是 `C`, 并 `obj.method` 解析为 `C.method`。
|
||||
|
||||
下次我们执行相同的调用点时,它将 `C.method` 直接调用,绕过任何类型的方法查找过程。
|
||||
|
||||
但是它会将 `C.method` 通过一个特殊的入口点进入,这将验证它 `obj` 仍然是 `C`, 如果不是这种情况,将调用 `DRT_SwitchableCallMiss` 并尝试选择下一个呼叫点状态。
|
||||
|
||||
`C.method` 可能仍然是调用的有效目标,例如 `obj` 是 D `extends` C , 但不覆盖的类的实例 `C.method`,在这种情况下,我们会检查调用点是否可以转换为单个目标状态,由 `SingleTargetCallStub` 实现(另见 `dart::UntaggedSingleTargetCache`)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Dart-VM/image17)
|
102
FLutter-X15.md
102
FLutter-X15.md
|
@ -1,102 +0,0 @@
|
|||
# Flutter 小技巧之升级适配 Xcode15
|
||||
|
||||
美好的 2024 从「适配」开始,按照苹果的尿性,2024 春季开始大家将不得使用 Xcode15 来构建 App ,另外根据[《2024 的 iOS 的隐私清单》](https://juejin.cn/post/7311876701909549065) 要求,使用 Flutter 的开发者是无法逃避适配 Xcode15 更新的命运。
|
||||
|
||||
另外,众所周知,**安装 Xcode15 需要系统升级到 macOS Sonoma ,而 Sonoma 版本无法直接使用 Xcode14** ,所以升级到 Sonoma 系统后你会看到 Xcode 无法打开,不要急,因为升级 Xcode15 现在只要 3G+ ,模拟器(7G+)部分可以下载完成后再手动二次下载,老板再也不用当心我更新 Xcode 时「摸鱼」了。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image1.png)
|
||||
|
||||
> PS,如果因为特殊情况,你想先升级 Sonoma 又暂时想使用 Xcode14,但是不想降级系统 ,可以在命令行通过 `/Applications/Xcode.app/Contents/MacOS/Xcode` 执行激活 14 安装,然后通过命令行编译。
|
||||
|
||||
那么,接下来开始适配 Xcode15 吧~
|
||||
|
||||
# Crash 问题
|
||||
|
||||
**使用 Xcode 15 构建 Flutter 的时候,你可能会有低于 iOS 17 的真机设备上发现 App 运行崩溃**,这个问题提交在 [#136060](https://github.com/flutter/flutter/issues/136060) ,直接引发问题的点大部分来自引入的 Plugin,例如 `connectivity_plus` ,而核心问题其实算是 Xcode 本身的一个 bug。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image2.png)
|
||||
|
||||
解决问题的点很简单,**就是将 IPHONEOS_DEPLOYMENT_TARGET 设置为 12.0** , 另外有时候 Xcode 可能会删除不受支持的`IPHONEOS_DEPLOYMENT_TARGET` 值,而导致使用了最新的 (17.0),这将导致二进制文件只能在 iOS 17+ 上启动。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image3.png)
|
||||
|
||||
类似问题也体现在如 `connectivity_plus 4.xx` 的 IPHONEOS_DEPLOYMENT_TARGET 为11.0,而现在connectivity_plus 5.0.0 中也调整到 12 从而规避问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image4.png)
|
||||
|
||||
另外,如果 Plugin 的 IPHONEOS_DEPLOYMENT_TARGET 影响编译,你也可以在 Profile 下添加 `config.build_settings.delete` 来强制移除。
|
||||
|
||||
```
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' <--- add this
|
||||
```
|
||||
|
||||
目前这个问题在模拟器上运行一般不会出现,主要是 Xcode 15 带有 IPHONEOS_DEPLOYMENT_TARGET 的 iOS 11(以前 Flutter 默认最低版本)在使用该 `Networking` 框架时会崩溃 ,具体表现在:
|
||||
|
||||
- 16.x -> 崩溃
|
||||
- 17.0.x -> 正常
|
||||
|
||||
**所以在升级到 Xcode 15 的时候,最好将 App 运行到 16.x 的真机上测试一下是否存在同样问题**,目前看来主要是 iOS 的 `Network` 在存在 target iOS 11 导致,能够用 `NWProtocolIP.Metadata ` ,`NWEndpoint.hostPort` 去复现,其实编译时也会有一些警告,只是一般都被大家忽略:
|
||||
|
||||
```
|
||||
…/Test737672.xcodeproj The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 11.0, but the range of supported deployment target versions is 12.0 to 17.0.99.
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `Flutter/Flutter.h` file not found
|
||||
|
||||
Flutter 在 Xcode 15 上的这个问题提交于 [#135099](https://github.com/flutter/flutter/issues/135099) ,其实算是 Cocoapods 的问题,这个问题可能涉及`DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image5.png)
|
||||
|
||||
根据反馈,**基本上就是你升级 Cocoapods 升级到 v1.13.0 之后的版本就可以解决**,注意升级之后记得手动重新运行 `pod install` 来确保正确执行,当然,如果你因为某些原因不想升级 Cocoapods ,那么可以临时通过CocoaPods 的 [#12012#issuecomment-1659803356](https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1659803356) ,在 Profile 下添加相关路径:
|
||||
|
||||
```
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
xcconfig = File.read(xcconfig_path)
|
||||
xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR")
|
||||
File.open(xcconfig_path, "w") { |file| file << xcconfig_mod }
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
> PS ,如果更新 pod 的时候,不知道卡在那里,可以通过 `gem install cocoapods -v 1.13.0 -V` 后面的 -V 看详细操作日志,如果是网络问题,可以通过 gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ 来解决,可以通过 gem sources -l 查看镜像地址。
|
||||
|
||||
|
||||
|
||||
# Library 'iconv.2.4.0' not found
|
||||
|
||||
如果你在 Xcode 15 上运行发现如下所示错误,不要相信什么 `other link flags add "-ld64"` ,而是应该在 `Build Phases > Link Binary With Libraries` 下找到 iconv.2.4.0 ,然后删除它,然后添加 iconv.2,因为在 Xcode15 里,现在只有 iconv.2 。
|
||||
|
||||
```
|
||||
Error (Xcode): Library 'iconv.2.4.0' not found
|
||||
|
||||
Error (Xcode): Linker command failed with exit code 1 (use -v to see invocation)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image6.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image7.png)
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image8.png)
|
||||
|
||||
> **如果还有问题,可以全局搜索 'iconv.2.4.0',在出来的文件里将 iconv.2.4.0 替换为 iconv.2 即可**。
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
好了,Xcode 15 的适配上主要就这些问题,更多的还是[《2024 的 iOS 的隐私清单》](https://juejin.cn/post/7311876701909549065) 部分的适配,属于审核要求,相比起代码上能解决的,平台要求就需要精神领会了,因为这些的要求内容其实很主观理解,总的来说, Flutter & Xcode 15 ,跑得通,可以上。
|
103
FWREADME.md
103
FWREADME.md
|
@ -1,103 +0,0 @@
|
|||
# 番外系列
|
||||
|
||||
### 作为对 Flutter 系列额外的补充,不定时给你带来有趣的文章。
|
||||
|
||||
* [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 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md)
|
||||
* [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md)
|
||||
* [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md)
|
||||
* [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md)
|
||||
* [Flutter 小技巧之 3.10 全新的 MediaQuery 优化与 InheritedModel ](Flutter-N25.md)
|
||||
* [Flutter 小技巧之 InkWell & Ink 你了解多少](Flutter-N26.md)
|
||||
* [Flutter 小技巧之滑动控件即将“抛弃” shrinkWrap 属性](Flutter-N28.md)
|
||||
* [Flutter III 之你不知道的 PlatformView 的混乱之治](Flutter-N30.md)
|
||||
* [掘力计划|Flutter 混合开发的混乱之治【直播回顾】](Flutter-JL2023.md)
|
||||
* [社区说|Flutter 一知半解,带你拨云见月](Flutter-SQS2023.md)
|
||||
* [Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener ](Flutter-N31.md)
|
||||
* [Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?](Flutter-Rive.md)
|
||||
* [Harmony 开始支持 Flutter ,聊聊 Harmony 和 Flutter 之间的因果](Flutter-HF.md)
|
||||
* [Flutter 与 Dart 的市场应用](Flutter-WH.md)
|
||||
* [Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画](Flutter-GLSL.md)
|
||||
* [Flutter 小技巧之 3.16 升级最坑 M3 默认适配技巧](Flutter-M3D.md)
|
||||
* [2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救](Flutter-macros.md)
|
||||
* [Flutter 小技巧之升级适配 Xcode15 ](Flutter-X15.md)
|
||||
* [Flutter 2024 路线规划,更多可期待的功能正在路上](Flutter-2024.md)
|
||||
* [2024 Impeller:快速了解 Flutter 的渲染引擎的优势](Flutter-Impeller.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
85
Flutter-0.md
85
Flutter-0.md
|
@ -1,85 +0,0 @@
|
|||
|
||||
|
||||
**让 GSY 成为你 Flutter 学习路上的“保姆”吧。**
|
||||
|
||||
> - [Flutter 完整开发实战详解系列文章](https://juejin.im/user/582aca2ba22b9d006b59ae68/posts)
|
||||
> - [开源 Flutter 多案例学习型项目](https://github.com/CarGuo/GSYFlutterDemo)
|
||||
>- [开源 Flutter 完整实战项目](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
> - [开源 Flutter 电子书项目](https://github.com/CarGuo/GSYFlutterBook)
|
||||
>
|
||||
|
||||
|
||||
|
||||
自 2018 年 06 月以来,`Flutter` 开始在 **GSY** 系列中初绽锋芒,在经历一年的发展之后,目前 **GSY `Flutter`** 系列已包含有 **《[Flutter完整开发实战详解》](https://juejin.im/user/582aca2ba22b9d006b59ae68/posts) 系列文章** 、 **多案例学习型项目 [GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo)** 、**完整实战项目 [GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)** 、 **Flutter 电子书项目 [GSYFlutterBook](https://github.com/CarGuo/GSYFlutterBook)** 等,目前改系列项目的 **star** 情况如下所示:
|
||||
|
||||
|
||||
|
||||
项目 | Star
|
||||
----| --
|
||||
**[GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)** |[![](https://img.shields.io/github/stars/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/stargazers)
|
||||
**[GSYFlutterBook](https://github.com/CarGuo/GSYFlutterBook)** | [![](https://img.shields.io/github/stars/CarGuo/GSYFlutterBook.svg)](https://github.com/CarGuo/GSYFlutterBook/stargazers)
|
||||
**[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo)** |[![](https://img.shields.io/github/stars/CarGuo/GSYFlutterDemo.svg?logo=GSYGithubAppFlutter)](https://github.com/CarGuo/GSYFlutterDemo/stargazers)
|
||||
|
||||
|
||||
|
||||
## 一、Flutter完整开发实战详解
|
||||
|
||||
**[《Flutter完整开发实战详解》](https://juejin.im/user/582aca2ba22b9d006b59ae68/posts)** 系列文章,更新至今已有 **主系列文章 15 篇,番外系列文章 3 篇** ,内容主要覆盖 **开发实战、源码分析、填坑技巧、面试集锦** 等等,并且该系列目前仍处于更新阶段。
|
||||
|
||||
通过本系列文章,你将快速了解到 `Flutter` 中的各种特性和实战技巧,掌握 `Flutter Framework` 的工作原理,从入门到出家应有尽有。
|
||||
|
||||
同时为了更方便学习 ,**《[Flutter完整开发实战详解》](https://juejin.im/user/582aca2ba22b9d006b59ae68/posts)** 系列文章会同步整合到 **[GSYFlutterBook](https://github.com/CarGuo/GSYFlutterBook)** 项目中,项目将通过**在线 Gitbook 和离线 PDF 方式, 进一步满足你的学习要求。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190628_gsy/image4)
|
||||
|
||||
|
||||
## 二、GSYGithubAppFlutter
|
||||
|
||||
**[GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)** 项目属于 Flutter 完整实战项目,项目从 **状态管理、控件展示、数据请求保存、平台交互、动画效果等,完整展示了如何实现一个 Flutter 的应用项目,同时针对一些特殊场景进行填坑,并混入了多种开发和设计模式** ,项目最终的目的,是希望可以成为你实战过程中的引路者。
|
||||
|
||||
> **GSYGithubApp** 系列项目起源于 `React Native` ,目前共有四个版本。
|
||||
>
|
||||
> 时间 | 项目
|
||||
> -------- | ---
|
||||
> **2017-11-07**|**[GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp) 版开源**
|
||||
> **2018-04-22**|**[GSYGithubApp Weex](https://github.com/CarGuo/GSYGithubAppWeex) 版开源**
|
||||
> **2018-06-26**|**[GSYGithubApp Flutter](https://github.com/CarGuo/GSYGithubAppFlutter) 版开源**
|
||||
> **2018-11-08**|**[GSYGithubApp Kotlin ](https://github.com/CarGuo/GSYGithubAppKotlin) 版开源**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190628_gsy/image5)
|
||||
|
||||
## 三、GSYFlutterDemo
|
||||
|
||||
**[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo) 是本月刚创建的学习型项目** ,因为 **[GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)** 属于完整型项目,不适合频繁调整和 Demo 示例,所以在接受到用户反馈后, **更轻便的 [GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo) 诞生了。**
|
||||
|
||||
**[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo)** 作为简单示例和解决方案 Demo ,它可以给你学习和工作中提供一些便捷的帮助,比如 **如何自定义布局,如何滚动控件到指定 `child position` ,如何调整 `Text` 控件的 `Line Space` ,如何监听键盘的弹出和收起** 等等,所以例子方案都独立实现,方便阅读 CV 。
|
||||
|
||||
> 其中一些需求因为 `Flutter` 特性限制,需要特殊处理才能实现。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190628_gsy/image6)
|
||||
|
||||
## 最后
|
||||
|
||||
**GSY `Flutter`** 系列断断续续一路走来,有着太多的机缘巧合在推动前进 ,个人是希望 **GSY** 能成为你 `Flutter` 学习路上的“保姆” ,最终能产生交流互动,共同成长。
|
||||
|
||||
未来 **《[Flutter完整开发实战详解》](https://juejin.im/user/582aca2ba22b9d006b59ae68/posts)** 系列文件将继续更新,同时逐步完善 **[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo)** 中的各种案例,并同步优化 **[GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)** 中的各种问题,**你的认可就是我坚持的动力!**
|
||||
|
||||
> 学习并非一朝一夕,我相信在分享过程中的“碰撞”,能让我们更快的进步,因为码农并不孤单!
|
||||
|
||||
### 其他推荐
|
||||
|
||||
- [Flutter 状态管理示例](https://github.com/CarGuo/state_manager_demo)
|
||||
|
||||
- [Flutter 混合开发示例](https://github.com/CarGuo/flutter_app_lib)
|
||||
|
||||
- [GSYGithubAPP React Native](https://github.com/CarGuo/GSYGithubAPP)
|
||||
|
||||
- [GSYGithubApp Kotlin](https://github.com/CarGuo/GSYGithubAppKotlin)
|
||||
|
||||
|
||||
- [GSYVideoPlayer Android 播放器](https://github.com/CarGuo/GSYVideoPlayer)
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190628_gsy/image7)
|
||||
|
||||
|
90
Flutter-1.md
90
Flutter-1.md
|
@ -1,17 +1,14 @@
|
|||
## 前言
|
||||
|
||||
在如今的 Flutter 大潮下,本系列是让你看完会安心的文章。
|
||||
在如今的 Fultter 大潮下,本系列是让你看完会安心的文章。
|
||||
|
||||
本系列将完整讲述:如何入门 Flutter 开发,如何快速从 0 开发一个完整的 Flutter APP,配套高完成度 Flutter 开源项目 [GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter),提供 Flutter 的开发技巧和问题处理,之后深入源码和实战为你全面解析 Flutter。
|
||||
本系列将完整讲述:如何快速从0开发一个完整的 Flutter APP,配套高完成度 Flutter 开源项目 [GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter),同时也会提供一些Flutter的开发细节技巧,并针对开发过程中可能遇到的问题进行填坑,之后深入源码和实战为你全面解析 Flutter。
|
||||
|
||||
> 笔者相继开发过 Flutter、React Native 、Weex 等主流跨平台框架项目,其中 Flutter 的跨平台兼容性无疑最好。前期开发调试完全在 Android 端进行的情况下,第一次在 iOS 平台运行居然没有任何错误,并且还没出现 UI 兼容问题,相信对于经历过跨平台开发的猿们而言,是多么的不可思议画面,并且 Fluuter 的 HotLoad 相比较其他两个平台,也是丝滑的让人无法相信,吹爆了!
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>笔者相继开发过 *Flutter、React Native 、Weex* 等主流跨平台框架项目,其中 *Flutter* 的跨平台兼容性无疑最好。前期开发调试完全在 Android 端进行的情况下,第一次在 IOS 平台运行居然没有任何错误,并且还没出现UI兼容问题,相信对于经历过跨平台开发的猿们而言,这是多么的不可思议画面。并且 Fluuter 的 HotLoad 相比较其他两个平台,也是丝滑的让人无法相信。吹爆了!
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
>这些特点其实这得益于Flutter Engine 和 Skia ,如果有兴趣的可以看看笔者之前的[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)。
|
||||
>
|
||||
>好了,感慨那么多,让我们进入正题吧。
|
||||
|
||||
## 一、基础篇
|
||||
|
||||
|
@ -21,21 +18,22 @@
|
|||
### 1、环境搭建
|
||||
|
||||
Flutter 的环境搭建十分省心,特别对应 Android 开发者而言,只是在 Android Stuido
|
||||
上安装插件,并到 GitHub Clone Flutter 项目到本地之后执行 flutter doctor 命令就可以完成配置,其实中文网的[搭建Futter开发环境](https://flutterchina.club/get-started/install/) 已经很贴心详细,从平台指引开始安装基本都不会遇到问题。
|
||||
上安装插件,并下载flutter Sdk到本地,配置在环境变量即可,其实中文网的[搭建Futter开发环境](https://flutterchina.club/get-started/install/) 已经很贴心详细,从平台指引开始安装基本都不会遇到问题。
|
||||
|
||||
这里主要是需要注意,因为某些不可抗力的原因,国内的用户有时候需要配置 Flutter 的代理,并且国内用户在搜索 Flutter 第三方包时,也是在 https://pub.flutter-io.cn 内查找,下方是需要配置到环境变量的地址。*(ps Android Studio下运行 IOS 也是蛮有意思的感觉)*
|
||||
这里主要是需要注意,因为某些不可抗力的原因,国内的用户有时候需要配置 Flutter 的代理,并且国内用户在搜索 Flutter 第三方包时,也是在 https://pub.flutter-io.cn 内查找,下方是需要配置到环境变量的地址。*(ps Android Studio下运行 IOS 也是蛮有意思的(◐‿◑))*
|
||||
|
||||
```
|
||||
///win直接配置到环境编辑即可,mac配置到bash_profile或者zsh
|
||||
///win直接配置到环境编辑即可,mac配置到bash_profile
|
||||
export PUB_HOSTED_URL=https://pub.flutter-io.cn //国内用户需要设置
|
||||
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //国内用户需要设置
|
||||
```
|
||||
|
||||
### 2、Dart语言下的Flutter
|
||||
|
||||
在跨平台开领域被 JS 一统天下的今天,Dart 语言的出现无疑是一股清流。作为后来者,Dart语言有着不少 Java、Kotlin 和 JS 的影子,所以对于 Android 原生开发者、前端开发者而言无疑是非常友好。
|
||||
在跨平台开领域被 JS 一统天下的今天,Dart 语言的出现无疑是一股清流。作为后来者,Dart语言有着不少Java、kotlin 和 JS 的影子,所以对于 Android 原生开发者、前端开发者而言无疑是非常友好的。
|
||||
|
||||
官方也提供了包括 IOS 、React Native 等开发者迁移到 Flutter 上的文档,所以请不要担心,Dart语言不会是你掌握 Flutter 的门槛,甚至作为开发者,就算你不懂 Dart 也可以看着代码摸索。
|
||||
|
||||
官方也提供了包括 iOS 、React Native 等开发者迁移到 Flutter 上的文档,所以请不要担心,Dart 语言不会是你掌握 Flutter 的门槛,甚至作为开发者,就算你不懂 Dart 也可以看着代码摸索。
|
||||
|
||||
Come on,下面主要通过对比,简单讲述下 Dart 的一些特性,主要涉及的是 Flutter 下使用。
|
||||
|
||||
|
@ -43,23 +41,23 @@ Come on,下面主要通过对比,简单讲述下 Dart 的一些特性,主
|
|||
|
||||
- var 可以定义变量,如 `var tag = "666"` ,这和 JS 、 Kotlin 等语言类似,同时 Dart 也算半个动态类型语言,同时支持闭包。
|
||||
|
||||
- `Dart` 属于是**强类型语言** ,但可以用 `var` 来声明变量,`Dart` 会**自推导出数据类型**,所以 `var` 实际上是编译期的“语法糖”。**`dynamic` 表示动态类型**, 被编译后,实际是一个 `object` 类型,在编译期间不进行任何的类型检查,而是在运行期进行类型检查。
|
||||
- `Dart` 属于是**强类型语言** ,但可以用 `var` 来声明变量,`Dart` 会**自推导出数据类型**,`var` 实际上是编译期的“语法糖”。**`dynamic` 表示动态类型**, 被编译后,实际是一个 `object` 类型,在编译期间不进行任何的类型检查,而是在运行期进行类型检查。
|
||||
|
||||
- Dart 中 number 类型分为 `int` 和 `double` ,其中 java 中的 long 对应的也是 Dart 中的 int 类型,Dart 中没有 float 类型。
|
||||
- Dart 中 number 类型分为 `int` 和 `double` ,其中 java 中的 long 对应的也是 Dart 中的 int 类型。Dart 中没有 float 类型。
|
||||
|
||||
- Dart 下只有 bool 型可以用于 if 等判断,不同于 JS 这种使用方式是不合法的 `var g = "null"; if(g){}` 。
|
||||
|
||||
- Dart 中,switch 支持 String 类型。
|
||||
- DART中,switch 支持 String 类型。
|
||||
|
||||
#### 2.2、变量
|
||||
|
||||
- Dart 不需要给变量设置 `setter getter` 方法, 这和 kotlin 等语言类似。Dart 中所有的基础类型、类等都继承 Object ,默认值是 NULL, 自带 getter 和 setter ,而如果是 final 或者 const 的话,那么它只有一个 getter 方法。
|
||||
- Dart 不需要给变量设置 `setter getter ` 方法, 这和 kotlin 等类似。Dart 中所有的基础类型、类等都继承 Object ,默认值是 NULL, 自带 getter 和 setter ,而如果是 final 或者 const 的话,那么它只有一个 getter 方法。
|
||||
|
||||
- Dart 中 final 和 const 表示常量,比如 `final name = 'GSY'; const value= 1000000; ` 同时 `static const` 组合代表了静态常量,其中 const 的值在编译期确定,final 的值要到运行时才确定。
|
||||
- Dart 中 final 和 const 表示常量,比如 `final name = 'GSY'; const value= 1000000; ` 同时 `static const` 组合代表了静态常量。其中 const 的值在编译期确定,final 的值要到运行时才确定。*(ps Flutter 在 Release 下是 AOT 模式。)*
|
||||
|
||||
- Dart 下的数值,在作为字符串使用时,是需要显式指定的。比如:`int i = 0; print("aaaa" + i);` 这样并不支持,需要 `print("aaaa" + i.toString());` 这样使用,这和 Java 与 JS 存在差异,**所以在使用动态类型时,需要注意不要把 number 类型当做 String 使用。**
|
||||
- Dart 下的数值,在作为字符串使用时,是需要显式指定的。比如:`int i = 0; print("aaaa" + i);` 这样并不支持,需要 `print("aaaa" + i.toString());` 这样使用。这和 Java 与 JS 存在差异。**所以在使用动态类型时,需要注意不要把 number 类型当做 String 使用。**
|
||||
|
||||
- Dart 中数组等于列表,所以 `var list = [];` 和 `List list = new List()` 可以简单看做一样。
|
||||
- DART 中数组等于列表,所以 `var list = [];` 和 `List list = new List()` 可以简单看做一样。
|
||||
|
||||
#### 2.3、方法
|
||||
|
||||
|
@ -69,7 +67,7 @@ Come on,下面主要通过对比,简单讲述下 Dart 的一些特性,主
|
|||
|
||||
- Dart 不像 Java ,没有关键词 public 、private 等修饰符,` _ `下横向直接代表 private ,但是有 `@protected` 注解。
|
||||
|
||||
- Dart 中多构造函数,可以通过如下代码实现的。默认构造方法只能有一个,而通过`Model.empty()` 方法可以创建一个空参数的类,其实方法名称随你喜欢,而变量初始化值时,只需要通过 `this.name` 在构造方法中指定即可:
|
||||
- Dart 中多构造函数,可以通过如下代码实现的。默认构造方法只能有一个,而通过`Model.empty()` 方法可以创建一个空参数的类,其实方法名称随你喜欢。而变量初始化值时,只需要通过 `this.name` 在构造方法中指定即可:
|
||||
|
||||
```
|
||||
class ModelA {
|
||||
|
@ -90,7 +88,7 @@ class ModelA {
|
|||
|
||||
#### 2.4、Flutter
|
||||
|
||||
Flutter 中支持 `async`/`await` ,**如下代码所示**, `async`/`await` 其实只是语法糖,最终会编译为 Flutter 中返回 `Future` 对象,之后通过 `then` 可以执行下一步。如果返回的还是 `Future` 便可以 `then().then.()` 的流式操作了 。
|
||||
Flutter 中支持 `async`/`await` 。这一点和 ES7 很像,**如下代码所示**,只是定义的位置不同。同时异步操作也和 ES6 中的`Promise ` 很像,只是 Flutter 中返回的是 `Future` 对象,通过 `then` 可以执行下一步。如果返回的还是 `Future` 便可以 `then().then.()` 的流式操作了 。
|
||||
|
||||
```
|
||||
///模拟等待两秒,返回OK
|
||||
|
@ -115,7 +113,7 @@ Flutter 中支持 `async`/`await` ,**如下代码所示**, `async`/`await`
|
|||
}
|
||||
```
|
||||
|
||||
- Flutter 中 `setState` 很有 React Native 的既视感,Flutter 中也是通过 State 跨帧实现管理数据状态的,这个后面会详细讲到。
|
||||
- Flutter 中 `setState` 很有 React Native 的既视感,Flutter 中也是通过 state 跨帧实现管理数据状态的,这个后面会详细讲到。
|
||||
|
||||
- Flutter 中一切皆 Widget 呈现,通过 `build`方法返回 Widget,这也是和 React Native 中,通过 `render` 函数返回需要渲染的 component 一样的模式。
|
||||
- Stream 对应的 async* / yield 也可以用于异步,这个后面会说到。
|
||||
|
@ -123,11 +121,11 @@ Flutter 中支持 `async`/`await` ,**如下代码所示**, `async`/`await`
|
|||
|
||||
### 3、Flutter Widget
|
||||
|
||||
在 Flutter 中一切的显示都是 Widget ,Widget 是一切的基础,利用响应式模式进行渲染。
|
||||
在 Flutter 中,一切的显示都是 Widget 。Widget 是一切的基础,作为响应式的渲染,类似 MVVM 的实现机制。
|
||||
|
||||
我们可以通过修改数据,再用`setState` 设置数据,Flutter 会自动通过绑定的数据更新 Widget , **所以你需要做的就是实现 Widget 界面,并且和数据绑定起来**。
|
||||
我们可以通过修改数据,再用`setState` 设置数据,Flutter 会自动通过绑定的数据更新 Widget 。 **所以你需要做的就是实现 Widget 界面,并且和数据绑定起来**。
|
||||
|
||||
Widget 分为 *有状态* 和 *无状态* 两种,在 Flutter 中每个页面都是一帧,无状态就是保持在那一帧,而有状态的 Widget 当数据更新时,其实是创建了新的 Widget,只是 State 实现了跨帧的数据同步保存。
|
||||
Widget 分为 *有状态* 和 *无状态* 两种,在 Flutter 中每个页面都是一帧,无状态就是保持在那一帧,而有状态的 Widget 当数据更新时,其实是绘制了新的 Widget,只是 State 实现了跨帧的数据同步保存。
|
||||
|
||||
 
|
||||
|
||||
|
@ -141,10 +139,12 @@ Widget 分为 *有状态* 和 *无状态* 两种,在 Flutter 中每个页面
|
|||
|
||||
#### 3.1、无状态StatelessWidget
|
||||
|
||||
直接进入主题,如下下代码所示是无状态 Widget 的简单实现。**继承 StatelessWidget,通过 `build ` 方法返回一个布局好的控件**。可能现在你还对 Flutter 的内置控件不熟悉,but **Don't worry , take it easy** ,后面我们就会详细介绍这里你只需要知道,一个无状态的 Widget 就是这么简单。
|
||||
直接进入主题,下方代码是无状态 Widget 的简单实现。
|
||||
|
||||
**继承 StatelessWidget,通过 `build ` 方法返回一个布局好的控件**。可能现在你还对 Flutter 的内置控件不熟悉,but **Don't worry , take is easy** ,后面我们就会详细介绍。这里你只需要知道,一个无状态的 Widget 就是这么简单。
|
||||
|
||||
|
||||
Widget 和 Widget 之间通过 ` child: ` 进行嵌套。其中有的 Widget 只能有一个 child,比如下方的 `Container` ;有的 Widget 可以多个 child ,也就是`children`,比如` Column 布局,下方代码便是 Container Widget 嵌套了 Text Widget。
|
||||
Widget 和 Widget 之间通过 ` child: ` 进行嵌套。其中有的 Widget 只能有一个 child,比如下方的 `Container` ;有的 Widget 可以多个 child ,也就是`children:`,比如` Column 布局。下方代码便是 Container Widget 嵌套了 Text Widget。
|
||||
|
||||
```
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -172,7 +172,9 @@ class DEMOWidget extends StatelessWidget {
|
|||
|
||||
#### 3.2、有状态StatefulWidget
|
||||
|
||||
继续直插主题,如下代码,是有状态的widget的简单实现,你需要创建管理的是主要是 `State` , 通过 State 的 ` build` 方法去构建控件。在 State 中,你可以动态改变数据,在 `setState ` 之后,改变的数据会触发 Widget 重新构建刷新,而下方代码中,是通过延两秒之后,让文本显示为 *"这就变了数值"*。
|
||||
继续直插主题,如下代码,是有状态的widget的简单实现。
|
||||
|
||||
你需要创建管理的是主要是 `State` , 通过 State 的 ` build` 方法去构建控件。在 State 中,你可以动态改变数据,这类似 MVVM 实现,在 `setState ` 之后,改变的数据会触发 Widget 重新构建刷新。而下方代码中,是通过延两秒之后,让文本显示为 *"这就变了数值"*。
|
||||
|
||||
|
||||
如下代码还可以看出,State 中主要的声明周期有 :
|
||||
|
@ -210,7 +212,7 @@ class _DemoStateWidgetState extends State<DemoStateWidget> {
|
|||
void initState() {
|
||||
///初始化,这个函数在生命周期中只调用一次
|
||||
super.initState();
|
||||
///定时1秒
|
||||
///定时2秒
|
||||
new Future.delayed(const Duration(seconds: 1), () {
|
||||
setState(() {
|
||||
text = "这就变了数值";
|
||||
|
@ -256,7 +258,7 @@ Flutter 中拥有需要将近30种内置的 [布局Widget](https://flutterchina.
|
|||
| Expanded | 只有一个子 Widget。在 Column 和 Row 中充满。 |
|
||||
| ListView | 可以有多个子 Widget。自己意会吧。 |
|
||||
|
||||
* Container :最常用的默认控件,但是实际上它是由多个内置控件组成的模版,只能包含一个`child`,支持 *padding,margin,color,宽高,decoration(一般配置边框和阴影)等配置*,在 Flutter 中,不是所有的控件都有 *宽高、padding、margin、color* 等属性,所以才会有 Padding、Center 等 Widget 的存在。
|
||||
* Container :最常用的默认布局!只能包含一个`child:`,支持配置 *padding,margin,color,宽高,decoration(一般配置边框和阴影)等配置*,在 Flutter 中,不是所有的控件都有 *宽高、padding、margin、color* 等属性,所以才会有 Padding、Center 等 Widget 的存在。
|
||||
```
|
||||
new Container(
|
||||
///四周10大小的maring
|
||||
|
@ -285,7 +287,7 @@ mainAxisSize: MainAxisSize.max,
|
|||
crossAxisAlignment :CrossAxisAlignment.center,
|
||||
```
|
||||
|
||||
* Expanded 在 Column 和 Row 中代表着平均充满的作用,当有两个存在的时候默认均分充满。同时页可以设置 `flex` 属性决定比例。
|
||||
* Expanded 在 Column 和 Row 中代表着平均充满,当有两个存在的时候默认均分充满。同时页可以设置 `flex` 属性决定比例。
|
||||
|
||||
```
|
||||
new Column(
|
||||
|
@ -302,7 +304,7 @@ crossAxisAlignment :CrossAxisAlignment.center,
|
|||
],
|
||||
);
|
||||
```
|
||||
接下来我们来写一个复杂一些的控件,首先我们创建一个私有方法`_getBottomItem `,返回一个 `Expanded Widget`,因为后面我们需要将这个方法返回的 Widget 在 Row 下平均充满。
|
||||
接下来我们来写一个复杂一些的控件。首先我们创建一个私有方法`_getBottomItem `,返回一个 `Expanded Widget`,因为后面我们需要将这个方法返回的 Widget 在 Row 下平均充满。
|
||||
|
||||
如代码中注释,布局内主要是现实一个居中的Icon图标和文本,中间间隔5.0的 padding:
|
||||
|
||||
|
@ -352,7 +354,7 @@ crossAxisAlignment :CrossAxisAlignment.center,
|
|||
![item效果](http://img.cdn.guoshuyu.cn/20190604_Flutter-1/image1)
|
||||
|
||||
|
||||
接着我们把上方的方法,放到新的布局里,如下流程和代码:
|
||||
接着我们把上方的方法,放到新的布局里。如下流程和代码:
|
||||
|
||||
* 首先是 `Container `包含了` Card`,用于快速简单的实现圆角和阴影。
|
||||
* 然后接下来包含了`FlatButton`实现了点击,通过Padding实现了边距。
|
||||
|
@ -414,7 +416,7 @@ Flutter 中,你的布局很多时候就是这么一层一层嵌套出来的,
|
|||
|
||||
### 5、Flutter 页面
|
||||
|
||||
Flutter 中除了布局的 Widget,还有交互显示的 Widget 和完整页面呈现的Widget,其中常见的有 *MaterialApp、Scaffold、Appbar、Text、Image、FlatButton*等,下面简单介绍这些 Wdiget,并完成一个页面。
|
||||
Flutter 中除了布局的 Widget,还有交互显示的 Widget 和完整页面呈现的Widget。其中常见的有 *MaterialApp、Scaffold、Appbar、Text、Image、FlatButton*等。下面简单介绍这些 Wdiget,并完成一个页面。
|
||||
|
||||
|
||||
| 类型 | 作用特点 |
|
||||
|
@ -431,7 +433,7 @@ Flutter 中除了布局的 Widget,还有交互显示的 Widget 和完整页面
|
|||
那么再次直插主题实现一个简单完整的页面试试。如下方代码:
|
||||
|
||||
* 首先我们创建一个StatefulWidget:`DemoPage `。
|
||||
* 然后在` _DemoPageState` 中,通过`build`创建了一个`Scaffold `。
|
||||
* 然后在 _DemoPageState中,通过`build`创建了一个`Scaffold `。
|
||||
* Scaffold内包含了一个`AppBar`和一个`ListView`。
|
||||
* AppBar类似标题了区域,其中设置了 `title `为 Text Widget。
|
||||
* body是`ListView`,返回了20个之前我们创建过的 DemoItem Widget。
|
||||
|
@ -496,20 +498,22 @@ class DemoApp extends StatelessWidget {
|
|||
![最终显示](http://img.cdn.guoshuyu.cn/20190604_Flutter-1/image3)
|
||||
|
||||
|
||||
好吧,第一部分终于完了,这里主要讲解都是一些简单基础的东西,适合安利入坑,后续更多实战等你开启
|
||||
好吧,第一部分终于完了,这里主要讲解都是一些简单基础的东西,适合安利入坑,后续还有两篇主要实战,敬请期待哟!( ̄^ ̄)ゞ
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
Github : [https://github.com/CarGuo/](https://github.com/CarGuo/)
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [本文相关 :GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
##### 文章
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190604_Flutter-1/image4)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
作为系列文章的第十篇,本篇主要深入了解 Flutter 中图片加载的流程,剥析图片流程中有意思的片段,结尾再实现 Flutter 实现本地图片缓存的支持。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
> * [九、 深入绘制原理](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
在 Flutter 中,图片的加载主要是通过 **`Image`** 控件实现的,而 **`Image`** 控件本身是一个 **StatefulWidget** ,通过前文我们可以快速想到, **`Image`** 肯定对应有它的 **RenderObject** 负责 *layout* 和 *paint* ,那么这个过程中,图片是如何变成画面显示出来的?
|
||||
|
||||
|
@ -155,10 +160,8 @@ const int _kDefaultSizeBytes = 100 << 20; // 100
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -166,6 +169,34 @@ const int _kDefaultSizeBytes = 100 << 20; // 100
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-10/image7)
|
|
@ -1,10 +1,16 @@
|
|||
作为系列文章的第十一篇,本篇将非常全面带你了解 Flutter 中最关键的设计之一,深入原理帮助你理解 Stream 全家桶,这也许是目前 Flutter 中最全面的 Stream 分析了。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、 Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
> * [九、 深入绘制原理](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
> * [十、 深入图片加载流程](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
## 一、Stream 由浅入深
|
||||
|
||||
|
@ -289,10 +295,8 @@ subject.close();
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型习项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -300,8 +304,35 @@ subject.close();
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-11/image10)
|
208
Flutter-112.md
208
Flutter-112.md
|
@ -1,208 +0,0 @@
|
|||
昨天谷歌为在 `Flutter Interact` 上为我们带来了 `Flutter 1.12` ,这是 1.9.x 的版本在经历 6 次 `hotfix` 之后,才带来的 stable 大版本更新。**该版本解决了 4,571 个报错,合并了 1,905 份 pr,同时本次发布也是 Flutter 一年内的第五个稳定版本。**
|
||||
|
||||
结合本次 `Flutter Interact` ,可以总结出几个关键词是: **`Platform` 、 `DartPad` 、`Spuernova` 、`AdobeXD`、`Hot UI` 和 `Layout Explorer` 。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image1)
|
||||
|
||||
## 一、更多的平台
|
||||
|
||||
本次 `Flutter Interact` 提出了让开发者更聚焦于精美的应用开发,从**以设备为中心转变为以应用为中心的开发理念**,Flutter 将帮助开发者忽略 Android、iOS、Web、PC 等不同平台差异,如下图所示是现场一套代码同时调试 7 台设备的演示。
|
||||
|
||||
本次 Flutter 也开始兑现当初的承诺,目前 **Web 的支持已经发布到 Beta 分支,而 MacOS 的支持已经发布到 Master** 分支。虽然进度不算快,但是作为“白嫖党”表示还是很开心能看到有所推进。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image2)
|
||||
|
||||
使用 `Flutter Web` 和 `Flutter MacOS` 需要通过如下命令行打开配置,并且执行 `flutter create xxxx` 就可以创建带有 Web 和 MacOS 的项目(如果已有项目也可以执行 `flutter create` 补全),并且需要注意**调试 MacOS 平台应用需要本地 Flutter SDK 要处于 `master` 分支,如果仅测试 Web 可以使用 `beta` 分支。**
|
||||
|
||||
```
|
||||
flutter config --enable-macos-desktop
|
||||
flutter config --enable-web
|
||||
|
||||
///其他平台的支持
|
||||
flutter config --enable-linux-desktop
|
||||
flutter config --enable-windows-desktop
|
||||
```
|
||||
|
||||
最后可以通过 `run` 或者 `build` 命令运行和打包程序,同时需要注意这里提到的 `linux` 和 `window` 平台目前还未合并到主项目中,如果想测试可在 [Desktop-shells](https://github.com/flutter/flutter/wiki/Desktop-shells) 查看对应配置项目:[flutter-desktop-embedding](https://github.com/google/flutter-desktop-embedding)。
|
||||
|
||||
```
|
||||
///调试运行
|
||||
flutter run -d chrome
|
||||
flutter run -d macOS
|
||||
|
||||
///打包
|
||||
flutter build web
|
||||
flutter build macos
|
||||
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image3)
|
||||
|
||||
## 二、更多开发工具
|
||||
|
||||
### 1、DartPad
|
||||
|
||||
`DartPad` 是用于在线体验 Dart 功能的平台,而本次更新后 `DartPad` 也支持 Flutter 的在线编写预览,这代表着开发者可以在没有 `idea` 的情况下也能实时测试自己的 Flutter 代码,算是补全了 Flutter 的在线用例测试。
|
||||
|
||||
> DartPad 的官方地址:[dartpad.dev](https://dartpad.dev) 和国内镜像地址 [ dartpad.cn](https://dartpad.cn)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image4)
|
||||
|
||||
### 2、Spuernova
|
||||
|
||||
[`Spuernova`](https://supernova.io) 可以说是本次 `Flutter Interact` 的亮点之一,通过导入设计师的 `Sketch` 文件就可以生成 Flutter 代码,这无疑提升了 Flutter 的生产力和可想象空间,**虽然这种生成代码的方法并不罕见,完整实用程度有待考验,但是这也让开发者可以更聚焦于业务逻辑和操作逻辑。**
|
||||
|
||||
> 放心,这个坑不是谷歌 Flutter 团队开的,它属于另外一家商业公司。
|
||||
|
||||
使用 `Spuernova` 可以从 [https://supernova.io](https://supernova.io) 下载 `Supernova Studio` ,之后需要注册用户信息(可能需要科学S网),最后就可以看到如下图所示的界面。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image5)
|
||||
|
||||
在导入 `Sketch` 文件后可以看到设计师完成的界面效果,同时选中 `"</>"` 按键,可以在右侧看到对应的 Flutter 代码,左侧可以看到对应的层级设计,但是这时候的代码看起来还比较简单和笨重,并且不具备交互能力。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image6)
|
||||
|
||||
如果进一步配置,用户需要在对应的控件上,使用右键的弹出框配置控件的功能,比如 `List`、`Button`、`TextField` 等组件去 Convert 原有的控件,让控件更新具备交互能力,同时还可以为控件配置布局属性和动画效果等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image7)
|
||||
|
||||
当然, `Spuernova` 并不是什么完全的公益项目,目前只有对于 Flutter 的简单支持上是免费的,其他项目支持还是处于收费状态。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image8)
|
||||
|
||||
另外类似的还有 `AdobeXD`, Adobe 的 Creative Cloud 添加了 Flutter 支持,只需一个插件,用户就可以将 `AdobeXD` 导出到 Flutter,目前处于[注册参加优先体验计划](https://xd.adobelanding.com/xd-to-flutter) 的进度。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image9)
|
||||
|
||||
### 3、Hot UI
|
||||
|
||||
Hot UI 就是大家盼星盼月的预览功能,如下图所示,在 Android Studio 的 Flutter 插件中在开发 widget 开发的过程中,直接在 IDE 的镜像里进行预览并与之进行交互。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image10)
|
||||
|
||||
在官方的 [HotUI-Getting-Started-instructions](https://github.com/flutter/flutter-intellij/wiki/HotUI-Getting-Started-instructions) 中可以看到相关的描述:**This feature is currently experimental. To enable, go to Preferences > Languages & Frameworks > Flutter Then check "Enable Hot UI" under "Experiments".** 目前该功能还处于实验阶段,在 Android Studio 的设置中,如图所示底部勾选启动这个功能。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image11)
|
||||
|
||||
但是如下图所示,开启后会发现和官方宣传的不一样?因为目前预览的 `Screen mirror` 处于 `coming soon` 的状态。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image12)
|
||||
|
||||
现阶段的 Hot UI 如下 GIF 所示,暂时只支持用户动态调试和配置控件的属性等逻辑,让我们期待官方填坑吧。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image13)
|
||||
|
||||
|
||||
### 4、Layout Explorer
|
||||
|
||||
[`Layout Explorer`](https://flutter.dev/docs/development/tools/devtools/inspector#flutter-layout-explorer) 是另外实验性的布局调试模式,`Layout Explorer` 主要是用于帮助开发者更直观地适配屏幕和调试如 `overflowed` 等场景的问题。
|
||||
|
||||
在最新的 `Dart DevTools` 工具添加了一个名为 `Layout Explorer` 的功能,它能够以可视化的方式呈现应用的布局信息,从而让检查器可以更好地发挥功,同时 `Layout Explorer` 不仅能以可视化的方式展现正在运行的应用中的 widget 布局,而且还允许以交互的方式更改布局选项。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image14)
|
||||
|
||||
启动 `Layout Explorer` 同样需要 Flutter SDK 处于 `master` 分支,然后在程序运行之后,点击 `DevTools` 在 chrome 打开,之后点击最右侧的按键进入 Flutter 调试模式。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image15)
|
||||
|
||||
|
||||
如下 GIF 所示,当选中的控件是具备 `Flex` 的支持时,可以看到有 `Layout Explorer` 的面板,在面板中可以动态调整控件的显示逻辑和控件的布局情况。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image16)
|
||||
|
||||
比如当控件出现了 `overflowed` ,我们可以很直观的看到问题的根源并且进行调整。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image17)
|
||||
|
||||
另外可以在 `Layout Explorer` 中动态调整控件的 flex 等相关信息,实时预览修改情况。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image18)
|
||||
|
||||
|
||||
## 三、Flutter SDK 改进
|
||||
|
||||
Flutter SDK 相关的更新本次解决了 4,571 个报错,合并了 1,905 份 pr,同时包含了许多的新功能支持。
|
||||
|
||||
- 首先 Flutter 1.12 建议开发者将 Android 项目迁移到 AndroidX,SDK 的瘦身,增加了 [google_fonts](https://pub.flutter-io.cn/packages/google_fonts) 字体的支持等。
|
||||
|
||||
- Android 插件的改进 [Android plugins APIs](https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration),相比起以前更为简单明了,分割了 `FlutterPlugin` and `MethodCallHandler` ,同时提供 `ActivityAware` 、 `ServiceAware` 作为独立支持。
|
||||
|
||||
- iOS 13 深色模式,支持使用 darkTheme 设置,同时还增加了如 `CupertinoContextMenu`、`CupertinoSlidingSegmentedControl`、`CupertinoAlertDialog`、`CupertinoDatePicker` 等 iOS 风格的控件支持。
|
||||
|
||||
```
|
||||
new MaterialApp(
|
||||
title: '',
|
||||
navigatorKey: navigatorKey,
|
||||
theme: model.themeData,
|
||||
darkTheme: model.darkthemeData,
|
||||
locale: model.locale,
|
||||
```
|
||||
|
||||
- [Add-to-App](https://flutter.dev/docs/development/add-to-app) 混合集成模式的进一步的更新。
|
||||
|
||||
- 新增加了不兼容的 `breaking change`,比如: [PageView 启用 RenderSliverFillViewport](https://github.com/flutter/flutter/pull/37024) 、 [WidgetsBinding 中的 attachRootWidget 被替换为 scheduleAttachRootWidget](https://github.com/flutter/flutter/pull/39079/files) 、[Allow gaps in the initial route](https://github.com/flutter/flutter/pull/39440/files)、[TextField's minimum height from 40 to 48 ](https://github.com/flutter/flutter/pull/42449) 等需要开发者注意重新适配的修改,更多可查阅 [release-notes-1.12.13](https://flutter.dev/docs/development/tools/sdk/release-notes/release-notes-1.12.13)。
|
||||
|
||||
- 增加了 [MediaQuery.systemGestureInsets 支持 Android Q 的手势导航](https://github.com/flutter/flutter/pull/37416);增加了 SliverIgnorePointer 、SliverOpacity、SliverAnimatedList 等控件支持;PageRouteBuilder 支持 fullscreenDialog。
|
||||
|
||||
|
||||
- [Dart 2.7 的发布,支持扩展方法](https://medium.com/dartlang/dart-2-7-a3710ec54e97)。
|
||||
|
||||
```
|
||||
extension ExtendsFun on String {
|
||||
int parseInt() {
|
||||
return int.parse(this);
|
||||
} double parseDouble() {
|
||||
return double.parse(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
main() {
|
||||
int i = '42'.parseInt();
|
||||
print(i);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
更多完整的 release-notes 可见 [release-notes-1.12.13](https://flutter.dev/docs/development/tools/sdk/release-notes/release-notes-1.12.13)
|
||||
|
||||
|
||||
### 四、其他
|
||||
|
||||
本次 `Flutter Interact` 还推荐了 [flutter-d-art](https://github.com/Solido/flutter-d-art) 和
|
||||
[gskinner](https://flutter.gskinner.com) 等精美的开源项目,同时
|
||||
**Flutter 本次也表示了将在未来优化代码的开发模式,而 Flutter 在不断开新坑的同时,也需要面对目前层出的问题。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image19)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image20)
|
||||
|
||||
Flutter 过去的一年无疑是火热的,所以暴露的问题也指数级出现,比如最近开发中就遇到了**在断网时加载图后之后,再打开网络无法继续显示图片的问题。**
|
||||
|
||||
不过既然是开源项目,“白嫖”之余也得多靠自己,上述问题经过查找后,在自定义的 `ImageProvider` 里图片加载失败时,可以通过清除了 `ImageCache` 中的 `PendingImage` 来解决问题,同时因为 `Image` 的封装与 `DecorationImage` 的差异化,还需要对 `Image` 的 `didUpdateWidget` 做二次处理才解决了问题。
|
||||
|
||||
说这个问题其实就是想表达开源的意义,用一个框架不能够只是坐享其成的心态,开源的目的更是交流,不管什么框架都不可能尽善尽美,我们可以用更开放的心态去尝试和“批判”,而我们的岗位不就是解决这些问题的么?
|
||||
|
||||
|
||||
### Flutter 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/6845243771605499912)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/6845244058865139726)
|
||||
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-112/image21)
|
213
Flutter-117.md
213
Flutter-117.md
|
@ -1,213 +0,0 @@
|
|||
2020年5月6日,Flutter终于迎来的本年的第一个稳定版本:`1.17.0`,距离上一个稳定版本的发布已经过去了将近5个月(146天)。受到世界范围的~~你我都知道的情况~~影响,今年的Google I/O也被取消,一定程度影响了本次版本的发布。同时在临近发布前出现了具有严重影响的BUG,也导致发布时间再次被推迟。
|
||||
|
||||
那么`1.17.0`到底更新了什么?是不是能再一次颠覆使用体验?能否让开发者再次真香?以下通过**对Medium原文进行部分总结翻译以及补充**,为各位进行解答~(如有错漏欢迎指出)(有英文原文没有的内容噢😉)
|
||||
|
||||
## 写在前面
|
||||
|
||||
本次Flutter版本的发布,开发团队将更多的时间用于[构建新的发布流程架构](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fp%2Ff723d898d7af)。自上一次发布稳定版本以来,已解决了**6339个问题(issue)**,分别从**231位开源贡献者**合并了[**3164个提交请求**](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Ftools%2Fsdk%2Frelease-notes%2Fchangelogs%2Fchangelog-1.17.0)。得益[*NeverCode*与团队的合作](https://link.juejin.cn/?target=https%3A%2F%2Fblog.codemagic.io%2Fflutter-and-codemagic-join-forces-on-github%2F),今年团队在Flutter的仓库上关闭的issue比新增的issue更多,总数减少了**约800个**。与Flutter一同发布的还有Dart 2.8、iOS新增Metal渲染支持、新的Meterial组件和新的网络追踪调试工具等。
|
||||
|
||||
## 移动端性能及应用大小优化
|
||||
|
||||
性能及内存优化的工作是这次新版本的重点。升级到新版本后,用户将立刻感受到更流畅快速的动画、更小的应用大小以及更低的内存占用。现在一般的路由变换场景(非透明的路由过渡)将[提速20%-37%](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F48900)。根据不同设备硬件性能的不同,在简单的iOS动画上能减少最多40%的CPU/GPU使用率([engine#14104](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fengine%2Fpull%2F14104) / [engine#13976](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fengine%2Fpull%2F13976))。
|
||||
|
||||
该版本还包含了大量的应用大小优化。例如官方的展示应用*Flutter Gallery*的Android应用包大小从9.6MB减小到了8.1MB(18.5%)。
|
||||
|
||||
另外在内存使用率方面,带有图片的列表在快速滚动时造成的内存占用及波动一直是Flutter的痛点之一。该版本将快速滚动带图片(大图)列表场景的[内存使用率降低了70%](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fengine%2Fpull%2F14265)。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image1)
|
||||
|
||||
|
||||
|
||||
但,最值得说明的性能强力提升是对iOS Metal的支持。
|
||||
|
||||
## Metal的支持为iOS应用带来了50%的性能提升
|
||||
|
||||
[Metal](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fmetal%2F)是与iOS 8一同发布的API,具有兼顾图形与计算功能、面向底层、低开销的硬件加速[4]等优势。Flutter现已默认使用Metal,让用户应用的平均渲染速度加快了50%*(由实际业务决定)*。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image2)
|
||||
|
||||
|
||||
|
||||
而在不支持Metal的设备上(A7处理器及iOS 10以下),Flutter将继续使用OpenGL进行渲染。
|
||||
|
||||
## Material部件:`NavigationRail`、`DatePicker`、`VisualDensity`及更多
|
||||
|
||||
Flutter团队正在持续地根据客户的反馈推进Flutter中Material Design的实现。本次由Material Design团队设计并实现了一个用于响应式App布局的路由组件`NavigationRail`,该组件能与`BottomNavigator`快速转换,在设备尺寸变大时随之改变,能很好地适应移动端与桌面版的布局。
|
||||
|
||||
*快速体验 `NavigationRail` 可以访问 [demo](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fsamples%2Ftree%2Fmaster%2Fexperimental%2Fweb_dashboard) 或在 [dartpad](https://link.juejin.cn/?target=https%3A%2F%2Fdartpad.dev%2Fb9c6cd345fd1cff643353c1f4902f888) 上进行尝试。*
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image3)
|
||||
|
||||
|
||||
|
||||
此外,基于Material Design的日期选择器`DatePicker`也已一同发布。新的`DatePicker`使用了符合Material指导的视觉效果,通过[详情文章](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fgo%2Fmaterial-date-picker-redesign)可以了解到更多。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image4)
|
||||
|
||||
|
||||
|
||||
`VisualDensity`也是新引入的内容,其指代的是Material Design中各类组件的视觉密度。通过调整它可以使得标准Material组件之间更加紧凑或疏远。新版本在`ThemeData`中引入了设置(`ThemeData.visualDensity`)。详细介绍请移步[文档](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fmaterial%2FVisualDensity-class.html)。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image5)
|
||||
|
||||
|
||||
|
||||
文字选择菜单也已针对平台做了相应的改进。现在菜单内的选项超出屏幕宽度时将会自动收起并可以通过与原生一致的操作切换,解决了某些语言中操作项的文字过长时布局溢出的问题。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image6)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*Android平台的文本选择*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image7)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*iOS平台的文本选择*
|
||||
|
||||
|
||||
|
||||
与1.17一起,Flutter团队一同发布了基于[Material 运动系统](https://link.juejin.cn/?target=https%3A%2F%2Fmaterial.io%2Fdesign%2Fmotion%2Fthe-motion-system.html)的预设[动画组件包](https://link.juejin.cn/?target=https%3A%2F%2Fpub.dev%2Fpackages%2Fanimations)。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image8)
|
||||
|
||||
|
||||
|
||||
在Material的[运动系统介绍文章](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fgoogle-design%2Fimplementing-motion-9f2839002016)中,Material Design团队定义了从组件到全屏视图的四种动画变化类型:容器转换、共轴转换、交叉渐变、渐变。尽管Flutter原本就可以实现对应效果,但该动画组件包能让开发者更轻松地实现它们。
|
||||
|
||||
## Material文本:更现代化的Flutter文字主题
|
||||
|
||||
在该版本的发布中,Flutter团队在兼容以前版本的App的前提下完整整合了2018标准的Material Design文本大小定义。原有的`TextTheme` API使用未受影响,但现在被标记为**废弃**,提醒开发者尽快迁移到新的标准。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image9)
|
||||
|
||||
|
||||
|
||||
在Flutter的`TextTheme`中,`bodyText1`和`bodyText2`对应着Material Design的`body1`和`body2`。类似的还有`H1-H6`,对应`headline1-headline6`。
|
||||
|
||||
## 用于Flutter的谷歌字体
|
||||
|
||||
如果你对新的Material Design文字缩放感兴趣,那么相信你对在Flutter中使用[`GoogleFonts`](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fflutter%2Fintroducing-google-fonts-for-flutter-v-1-0-0-c0e993617118)也抱有非常大的期待。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image10)
|
||||
|
||||
|
||||
|
||||
`GoogleFonts`让开发者能轻易地在开发的App中体验和使用`fonts.google.com`上的所有字体,开发者可以选择让用户直接通过API下载字体,或在应用中内置提供这些字体。
|
||||
|
||||
## 辅助功能和国际化
|
||||
|
||||
Flutter团队持续关注的另一个方向便是辅助功能,它将让Flutter应用的应用范围更加广泛并且为特定场景增加了可用性。在该版本中针对[滑动](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%2F43883)、[文字](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%2F52487)、[输入框](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%2F53065)及[其他输入组件](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%2F49259)修复了[大量问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%3Fq%3Dis%3Aclosed%2Bis%3Aissue%2Blabel%3A%22a%3A%2Baccessibility%22%2Bclosed%3A2019-11-25..2020-04-02)。官方推荐开发者根据文档中更新的[**最佳实践**](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Faccessibility-and-localization%2Faccessibility)去测试自己的应用。
|
||||
|
||||
在国际化方面,团队针对三星输入法对许多东亚语言的影响完成了修复。思密达开发者们应该会庆祝这些改动🤣
|
||||
|
||||
## 工具:整合Flutter的Dart DevTools、Android快速启动应用调试及更多
|
||||
|
||||
该版本的发布伴随着即将发布的与Flutter进行整合的Dart DevTools,如果你想立刻尝试它,请启动DevTools并点击右上角的"beaker"图标。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image11)
|
||||
|
||||
|
||||
|
||||
在预发布的DevTools中,你会看到多项优化点,但最重要的一项便是`Network` (**网络**)选项卡。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image12)
|
||||
|
||||
|
||||
|
||||
网络选项卡现在可以查看来自应用的网路请求,方便快速地排查请求问题。如果开发者在DevTools中没有找到该选项卡,可以通过以下命令来进行升级或启用:
|
||||
|
||||
```
|
||||
$ pub global actiat devtools
|
||||
复制代码
|
||||
```
|
||||
|
||||
默认情况下,网络选项卡会在你点击"Record"(开始记录)后显示你的网络活动,但如果开发者希望从App启动开始时就记录,可以在`main()`入口中使用以下方法:
|
||||
|
||||
```
|
||||
void main() {
|
||||
// 启用网络请求输出
|
||||
HttpClient.enableTimelineLogging = true;
|
||||
runApp(MyApp());
|
||||
}
|
||||
复制代码
|
||||
```
|
||||
|
||||
新的DevTools带来了另一项实验性功能:[安卓“快速启动”](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F46140),让开发者开启调试的速度加快70%。这项功能可以通过`flutter run --fast-start -d <your Android Device>`开启。使用功能时,一个仅基于平台代码、不含dart代码和资源变动的“壳”APK包将被安装到设备上。由于修改dart代码或资源并不需要APK重新构建,这会让`flutter run`更快地启动。与传统的启动模式不同,这个功能会将你的代码包装在一个壳中运行。在某些情况下这项功能将无法生效,例如在应用使用了原生插件调用了方法。
|
||||
|
||||
从该版本起,创建新的Flutter应用时,将仅允许创建使用AndroidX的应用。团队已将所有的support API标记为废弃,在创建新项目时`--android`参数是唯一有用的参数。虽然support版本的应用仍然能正常打包,但此时不[迁移](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.dev%2Fdocs%2Fdevelopment%2Fandroidx-migration)更待何时?
|
||||
|
||||
如果开发者是Android Studio或者IntelliJ用户,会发现热重载变得更为灵活了。在以前当分析器认为代码中存在错误(error)时,将阻止开发者热重载。当这些错误并不对你当前正在开发或调试的功能造成影响时,会让你花费更多的时间去处理这些错误,让人恼火。自此分析器不再有权利阻止热重载的进程,而交给VM编译过程来进行判断。
|
||||
|
||||
这些改动都发布在了对应的dev分支,如果开发者想尽快参与到其中,可以通过[此处](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fforum%2Fm%2F%23!topic%2Fflutter-announce%2FtTgQcTgqrKg)报名。通过参与新版本的测试更频繁的更新,与Flutter团队反馈使用感想,将让编译器插件更加健壮。
|
||||
|
||||
对于VS Code开发者,团队推荐一项新的功能:**Dart: List Outdated Packages(`pub outdated`)**。它可以帮助开发者排查依赖版本不匹配导致的问题。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image13)
|
||||
|
||||
|
||||
|
||||
最后,如果开发者碰到了Flutter crash,工具将引导开发者以正确的方式上报异常,Flutter团队会密切关注这类错误的严重程度和频率。
|
||||
|
||||
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20211223_Flutter-117/image14)
|
||||
|
||||
|
||||
|
||||
## 不兼容的改动
|
||||
|
||||
该版本中包含以下重大不兼容API改动:
|
||||
|
||||
- [#42100](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F42100) [使用pushReplacement时调用secondaryAnimation](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fg%2Fflutter-announce%2Fc%2Fy0SvesRHlcE%2Fm%2F39TuR5FVDQAJ)
|
||||
- [#44930](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F44930) Navigator 2.0 命令式API改动
|
||||
- [#45940](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F45940) 废弃UpdateLiveRegionEvent
|
||||
- [#49389](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F49389) 延迟快速滚动时的图像解码
|
||||
- [#49391](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F49391) Android文本选择溢出
|
||||
- [#49771](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F49771) [不对空Painter设置cache断言](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fg%2Fflutter-announce%2Fc%2FgDfazJIBdDo%2Fm%2Fd5AC8gR3FQAJ)
|
||||
- [#50318](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F50318) [实时的图片缓存](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fg%2Fflutter-announce%2Fc%2FIdfjYvRBR4c%2Fm%2F1_JxffXTGAAJ)
|
||||
- [#50354](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F50354) [使用strut box高度计算矩形,以确保它们在可见范围内](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fg%2Fflutter-announce%2Fc%2FhVP699NQ7PQ%2Fm%2FBgVgmsAdFwAJ)
|
||||
- [#50733](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F50733) 为gen_I10n生成信息查询
|
||||
- [#51435](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F51435) 从`RouteSettings`中移除`isInitialRoute`
|
||||
- [#52781](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F52781) 将`mouse_tracking.dart`移动至rendering
|
||||
|
||||
##
|
||||
|
||||
|
||||
作者:AlexV525
|
||||
链接:https://juejin.cn/post/6844904150182920199
|
||||
来源:稀土掘金
|
||||
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
|
|
@ -1,10 +1,18 @@
|
|||
作为系列文章的第十二篇,本篇将通过 scope_model 、 BloC 设计模式、flutter_redux 、 fish_redux 来全面深入分析, Flutter 中大家最为关心的状态管理机制,理解各大框架中如何设计实现状态管理,从而选出你最为合适的 state “大管家”。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、 Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
> * [九、 深入绘制原理](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
> * [十、 深入图片加载流程](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
> * [十一、全面深入理解Stream](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
|
||||
在所有 **响应式编程** 中,状态管理一直老生常谈的话题,而在 Flutter 中,目前主流的有 `scope_model` 、`BloC 设计模式` 、`flutter_redux` 、`fish_redux` 等四种设计,它们的 *复杂度* 和 *上手难度* 是逐步递增的,但同时 **可拓展性** 、**解耦度** 和 **复用能力** 也逐步提升。
|
||||
|
@ -378,11 +386,9 @@ Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文Demo :https://github.com/CarGuo/state_manager_demo
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -390,4 +396,34 @@ Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《Flutter完整开发实战详解(十二、全面深入理解状态管理设计)》](https://juejin.im/post/5cc816866fb9a03231209c7c)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-12/image7)
|
200
Flutter-120.md
200
Flutter-120.md
|
@ -1,200 +0,0 @@
|
|||
> 原文链接:https://medium.com/flutter/announcing-flutter-1-20-2aaf68c89c75
|
||||
|
||||
谷歌对 Flutter 的定位是提供一个便捷的工具包,从而在任何设备上得到出色的绘制体验,所以对于每个 release 版本,将会努力确保 Flutter 能**快速,美观,高效和开放**地支持每个平台,而在今天发布到 release 分支的 1.20 版本中,主要也是关于以上这四个方面的改进。
|
||||
|
||||
|
||||
在**快速**这个类别中,从底层级别的渲染引擎到 Dart 语言本身,本次我们都实现了多项性能改进。
|
||||
|
||||
为了使开发者能够构建更加**精美**的 Flutter 应用程序,1.20 版本提供了多项 UI 增强功能,包括期待已久的:
|
||||
|
||||
- `autofill` 支持;
|
||||
- 对 `Widget` 进行分层以支持平移和缩放的新方式;
|
||||
- 新的鼠标光标支持;
|
||||
- 对旧版本的 `Material Widget`(例如时间和日期选择器),以及 desktop 和 mobile 上 Flutter 应用中 About box 的全新响应式 license 页面的更新。
|
||||
|
||||
为了继续提高 Flutter 的工作**效率**,我们对 `Visual Studio Code` 的 Flutter 扩展进行了更新,该扩展将 `Dart DevTools` 直接带入的 IDE 中,在移动文件时会自动更新了导入语句,并提供了一组新的元数据用于构建自己的工具。
|
||||
|
||||
由于 Flutter 的**开放**性和出色的社区贡献者,本 stable 版包含来自全球 `359` 个贡献者的 `3,029` 个合并 **PR** 和 `5,485` 个 **closed issues**,其中包括来自 Flutter 社区的 `270` 个贡献者。
|
||||
|
||||
实际上,这是 Flutter release 版本中包含的最多社区贡献,特别是向这些社区贡献者表示感谢:
|
||||
|
||||
- CareF 的 28个 PR;
|
||||
- AyushBherwani1998 的 26个PR(包括 10 个 Flutter samples 作为他的 Google Summer of Code 项目的一部分);
|
||||
- a14n 的 13个PR(其中许多用于为 Flutter 的 landing null safety)
|
||||
|
||||
如果没有广泛的社区贡献者团队,我们将无法持续发布 Flutter,所以非常感谢大家的支持!
|
||||
|
||||
Flutter 的每个新版本都会带来了更多使用的动力,实际上在 4月就有报道过 Google Play 商店中的 Flutter 应用程序数量已达到 `50,000`,每月峰值新应用程序数量为 `10,000`。
|
||||
|
||||
现在,仅三个月后,Google Play 中就已经有超过 `90,000` 个Flutter应用,我们在印度看到了很多这种增长,现在印度是 Flutter 开发人员的第一大区域,在过去六个月中翻了一番,这与Google 在该地区增加的投资相吻合。最后 Flutter 不能没有 Dart ,因此很高兴看到 IEEE 报告说 Dart 自去年以来已经上升了 4 个排位,在他们跟踪的前 50 种语言中排名第 12。
|
||||
|
||||
## Flutter 和 Dart 的性能改进
|
||||
|
||||
在 Flutter 团队中,我们一直在寻找减少应用程序大小和延迟的新方法。以上一个版本为示例,此版本**修复了 [icon font tree shaking 时的工具性能问题](https://github.com/flutter/flutter/pull/55417),[并在构建非 Web 应用程序时font tree shaking 为默认行为](https://github.com/flutter/flutter/pull/56633)**。
|
||||
|
||||
icon font tree shaking 会删除未在应用程序中使用的图标,从而减小尺寸。将其用于Flutter Gallery 应用程序时,我们发现它使应用程序大小减少了100kb。现在,在进行 release 版本构建时,默认情况下在移动应用程序中会出现这个行为,目前仅限 `TrueType` 字体,但在将来的版本中将取消该限制。
|
||||
|
||||
我们在此版本中进行的另一项性能改进是**使用预热阶段来减少动画初始显示中的锯齿**,可以在此动画中看到一个改进的示例(降低到一半速度)。
|
||||
|
||||
![不用和有SkSL预热的动画](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image1)
|
||||
|
||||
如果 Flutter 应用程序在首次运行期间出现了不稳定的动画,则 Skia Shading Language 着色器将在应用程序构建过程中提供预编译功能,从而可以使其速度提高 2 倍以上。如果想利用此高级功能,请参见 flutter.dev 上的 [SkSL 预热页面](https://flutter.dev/docs/perf/rendering/shader)。
|
||||
|
||||
最后,当我们针对 desktop 进行优化时,我们将继续完善对鼠标的支持。在此版本中,我们重构了鼠标点击测试系统,以提供由于性能问题而被阻止的许多体系结构优势,重构使我们能够在基于 Web 的微基准测试中将性能提高多达 `15` 倍!这意味着开发者将获得更好,更一致,更准确的命中测试,而无需放弃性能:双赢!
|
||||
|
||||
通过这种更好,更快,更强大的鼠标命中测试,我们增加了对鼠标光标的支持,这是 desktop 最受欢迎的功能之一。默认情况下,几个常用的小部件将显示开发者期望的光标,或者开发者可以从受支持的光标列表中指定另一个。
|
||||
![Android上现有小部件上的新鼠标光标](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image2)
|
||||
|
||||
此版本的 Flutter 基于 2.9 版本的 Dart 构建的,它具有一个新的基于状态的 `two-pas UTF-8 `解码器,该解码器具有在 Dart VM 中优化的解码原语,部分利用了 `SIMD` 指令。UTF-8是迄今为止互联网上使用最广泛的字符编码方法,当收到较大的网络响应时,能够快速对其进行解码至关重要。在我们的UTF-8解码基准测试中,我们发现,在低端ARM设备上,英语文本的全面改进从近200%提高到中文文本的400%。
|
||||
|
||||
## 自动填充移动文本字段
|
||||
|
||||
一段时间以来,最受用户欢迎的功能之一是为 Flutter 程序中对文本自动填充在 Android 和 iOS提供支持。使用 [PR 52126](https://github.com/flutter/flutter/pull/52126),我们很高兴地说等待已经结束:不再要求用户重新输入,操作系统已为他们收集的数据。
|
||||
|
||||
![自动填充](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image3)
|
||||
|
||||
|
||||
另外你会很高兴听到我们也已经开始为 Web 添加此功能。
|
||||
|
||||
|
||||
## 一个用于常见交互模式的新控件
|
||||
|
||||
此版本引入了一个新的小部件 `InteractiveViewer`。该 `InteractiveViewer` 设计用于建设普通类型的交互性到应用程序,如: 平移,缩放和拖动“N”下降甚至大小调整,其中类似这种[简单的棋盘](https://github.com/justinmc/flutter-go)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image4)
|
||||
|
||||
|
||||
要查看如何将集成 `InteractiveViewer` 到自己的应用程序中,请查看[API文档](https://api.flutter.dev/flutter/widgets/InteractiveViewer-class.html),你可以在 DartPad 中使用它。另外,如果你想了解的 `InteractiveViewer` 设计和开发方法,则可以在YouTube 上看到 [Chicago Flutter on YouTube.](https://www.youtube.com/watch?v=ChFa0A72Uto)的演讲。
|
||||
|
||||
如果你有兴趣向 `InteractiveViewer` 启用的 Flutter 应用程序中添加新的交互,那么你可能也会很高兴听到我们在此版本中添加了更多功能来拖动“n”。具体来说,如果你想准确知道目标控件上的放置发生在哪里(Draggable对象本身始终可以使用它),现在可以使用 `DragTarget onAcceptDetails` 方法获取该信息。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image5)
|
||||
|
||||
请查看此样本以获取详细信息,并期待将来的发行版,该发行版还将在拖动期间提供此信息,以便`DragTarget` 可以在拖动操作期间更轻松地提供视觉更新。
|
||||
|
||||
## 更新了 Material Slider,RangeSlider,TimePicker 和 DatePicker
|
||||
|
||||
除了新的控件之外,此版本还包含许多更新的控件,包括 `Slider` 和 `RangeSlider`。有关更多信息,请参见 `Slider` 控件的新增功能。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image6)
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image7)
|
||||
|
||||
`DatePicker` 已更新,包括新的紧凑型设计以及对日期范围的支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image8)
|
||||
|
||||
最后,TimePicker它具有全新的风格。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image9)
|
||||
|
||||
如果您想使用它,这是一个使用 [Flutter构建的有趣的 Web 演示](https://flutter-time-picker.firebaseapp.com/#/)。
|
||||
|
||||
|
||||
### Responsive Licenses page
|
||||
|
||||
此版本的另一个更新是可以从中获得新的 esponsive licenses page: `AboutDialog`。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image10)
|
||||
|
||||
来自社区贡献者 TonicArtos 的 [PR 57588](https://github.com/flutter/flutter/pull/57588) 不仅进行了更新,以符合 Material 准则,使其看起来非常美观,而且更易于浏览,并设计为可在平板电脑和台式机上以及在手机上正常使用。谢谢 TonicArtos!由于每个 Flutter 应用程序都应显示其使用的软件包的许可证,因此使每个 Flutter 应用程序都变得更好了。
|
||||
|
||||
|
||||
## 发布插件需要新的 pubspec.yaml 格式
|
||||
|
||||
当然,Flutter不仅是控件,它也是工具,此版本附带太多更新,但是,这里有一些亮点。
|
||||
|
||||
|
||||
首先,是一项公共服务公告:如果您是 Flutter 插件的作者,那么 `pubspec.yaml` 发布插件将不再支持旧格式。如果尝试执行 `pub publish` 时会收到以下错误消息:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image11)
|
||||
|
||||
旧格式不支持指定插件支持的平台,并且自 Flutter 1.12 起已弃用。现在,发布新的或更新的插件需要新的 `pubspec.yaml` 格式。
|
||||
|
||||
对于插件客户而言,这些工具仍然可以理解旧的 `pubspec` 格式,在未来一段时间内 `pub.dev`上所有使用旧格式的现有插件将继续与Flutter应用程序配合使用。
|
||||
|
||||
|
||||
## 在Visual Studio Code 中预览嵌入式 Dart DevTools
|
||||
|
||||
此版本中最大的工具更新是 Visual Studio Code 扩展,它提供了一项新功能的预览,使得开发者能够将 Dart DevTools 屏幕直接带入编码工作区。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image12)
|
||||
|
||||
使用新的 `dart.previewEmbeddedDevTools` 设置启用此功能,上面的屏幕截图显示了直接嵌入到 Visual Studio Code 中的 Flutter Widget Inspector ,启用了此新设置,你可以使用状态栏上 的Dart DevTools 菜单选择嵌入的收藏页面。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image13)
|
||||
|
||||
此菜单允许您选择要显示的页面。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image14)
|
||||
该功能仍处于预览状态,因此,如果您有任何问题,请告诉我们。(https://github.com/Dart-Code/Dart-Code/issues)
|
||||
|
||||
## 网络跟踪更新
|
||||
|
||||
Dart DevTools 的最新版本随附“网络”页面的更新版本,可启用 Web 套接字分析。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image15)
|
||||
|
||||
现在,“Network” 页面会将计时信息以及你的状态和内容类型等其他信息添加到应用中的 network calls 中。对详细信息UI进行了其他改进,以提供 websocket 或 http 请求中数据的概述。我们还为该页面提供了更多计划,包括 HTTP请求/响应主体和监视 gRPC 流量。
|
||||
|
||||
## Updating import statements on file rename
|
||||
|
||||
Visual Studio Code 的另一个新功能是在重命名时更新导入,当文件被移动或重命名时,它会自动更新导入语句。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image16)
|
||||
|
||||
该功能目前仅适用于单个文件,不适用于多个文件或文件夹,但即将推出该功能。
|
||||
|
||||
## Tooling metadata for every tool builder
|
||||
|
||||
还要提到的另一项更新是针对构建 Flutter 工具的人员,我们在 GitHub 上创建了一个新项目,以捕获和发布有关 Flutter 框架本身的元数据,它提供以下内容的机器可读数据文件:
|
||||
- 当前所有Flutter小部件的[目录](https://github.com/flutter/tools_metadata/blob/master/resources/catalog/widgets.json)(395个小部件);
|
||||
- Material 和 Cupertino 颜色集的 Flutter 框架[颜色名称到颜色值的映射];(https://github.com/flutter/tools_metadata/tree/master/resources/colors)
|
||||
- Material和Cupertino图标的[图标元数据](https://github.com/flutter/tools_metadata/tree/master/resources/icons),包括图标名称和预览图标;
|
||||
|
||||
这与我们自己用于 Android Studio / IntelliJ 和 VS Code 扩展的元数据相同;我们认为这在构建自己的工具时可能会觉得有用。实际上,此元数据使 IntelliJ IDE 系列的功能可以显示Flutter代码中使用的颜色:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image17)
|
||||
|
||||
与此相关的是IntelliJ和Android Studio中的一项新功能,该功能显示 `Color.fromARGB()`和`Color.fromRGBO()`的色块:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image18)
|
||||
|
||||
特别感谢 GitHub 上的 dratushnyy 为 IntelliJ 中的颜色预览做出了贡献!
|
||||
|
||||
## Typesafe platform channels for platform interop
|
||||
|
||||
为了响应用户调查中插件作者的普遍需求,最近我们一直在尝试如何使 Flutter 与主机平台之间的通信对于插件和 Add-to-App 更安全更轻松。为了满足这一需求,我们创建了 `Pigeon` 这个命令行工具,该工具使用 Dart 语法在平台通道顶部生成类型安全的消息传递代码,而无需添加其他运行时依赖项。
|
||||
|
||||
使用Pigeon,你可以在直接调用 Dart 方法的情况下调用 Java / Objective-C / Kotlin / Swift 类方法并传递非基本数据对象,而无需在平台通道上手动匹配方法字符串和序列化参数。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-120/image19)
|
||||
|
||||
虽然仍然处于预发行阶段,但是 Pigeon 已经变得足够成熟,因此我们可以在 video_player 插件中使用它。如果您想对 Pigeon 进行测试以供自己使用,请参阅更新的[平台渠道文档](https://flutter.dev/docs/development/platform-integration/platform-channels#pigeon)以及[该示例项目](https://github.com/flutter/samples/tree/master/add_to_app/flutter_module_books)。
|
||||
|
||||
## 无法列出太多工具更新
|
||||
|
||||
Flutter 1.20 时间表中的工具发生了太多重大变化,因此我们无法在此处列出所有内容。但是,您可能希望自己查看更新公告:
|
||||
|
||||
- [VS Code扩展v3.13](https://groups.google.com/g/flutter-announce/c/TlN12RemsYw)
|
||||
- [VS Code扩展v3.12](https://groups.google.com/g/flutter-announce/c/8tSufvaRJUg)
|
||||
- [VS Code扩展v3.11](https://groups.google.com/g/flutter-announce/c/gM0bqO7NFA0)
|
||||
- [Flutter IntelliJ插件M46发布](https://groups.google.com/g/flutter-announce/c/8C2v2ueXjts)
|
||||
- [Flutter IntelliJ插件M47发布](https://groups.google.com/g/flutter-announce/c/6SF3PG_XB8g/m/6mAY7eC_AAAJ)
|
||||
- [Flutter IntelliJ插件M48发布](https://groups.google.com/g/flutter-announce/c/i9NTk5o9rZQ)
|
||||
- [Flutter内置的面向Flutter开发人员的新工具](https://medium.com/flutter/new-tools-for-flutter-developers-built-in-flutter-a122cb4eec86)
|
||||
|
||||
## 重大变化
|
||||
|
||||
与以往一样,我们试图将重大更改的数量保持在较低水平。以下是Flutter 1.20版本中的列表。
|
||||
|
||||
- [#55336](https://github.com/flutter/flutter/pull/55336) Adding `tabSemanticsLabel` to `CupertinoLocalizations` - [迁移指南PR](https://github.com/flutter/website/pull/3996)
|
||||
- [#55977](https://flutter.dev/go/clip-behavior) Add `clipBehavior` to widgets with `clipRect`
|
||||
- [#55998](https://groups.google.com/forum/#!searchin/flutter-announce/55998%7Csort:date/flutter-announce/yoq2VGi94q8/8pTsRL28AQAJ) Fixes the navigator pages update crashes when there is still route.
|
||||
- [#56582](https://flutter.dev/docs/release/breaking-changes/cupertino-tab-bar-localizations#migration-guide) Update Tab semantics in Cupertino to be the same as Material
|
||||
- [#57065](https://github.com/flutter/flutter/pull/57065) Remove deprecated child parameter for NestedScrollView’s overlap managing slivers
|
||||
- [#58392](https://github.com/flutter/flutter/pull/58392) iOS mid-drag activity indicator
|
||||
|
||||
## Summary
|
||||
|
||||
希望你和我们一样对这个版本感到兴奋,从许多角度来看,这是 Flutter 迄今为止最大的发行版。随着性能的提高,新的和更新的小部件以及工具的改进,我们只能做到更突出。我们要感谢社区贡献者的数量不断增长,而且不断壮大,使每个 Flutter 版本都可以比以前的版本更大,更快,更强大。还有更多的功能,包括对空安全性的支持,新版本的 `Ads`,`Maps` 和 `WebView` 插件,以及正在进行的更多工具支持。
|
166
Flutter-120HZ.md
166
Flutter-120HZ.md
|
@ -1,166 +0,0 @@
|
|||
# Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结
|
||||
|
||||
|
||||
## 一、无用的知识
|
||||
|
||||
首先科普无用的知识,说起高刷新率,就不得不提两个词汇: **ProMotion** 和 **LTPO** 。 ProMotion 是 iOS 在支持 120hz 之后出现的动态刷新率支持,也就是不同场景使用不同的屏幕刷新率,从而实现体验上提升的同时降低了电池的消耗。
|
||||
|
||||
![c64c73ef829cb88f10f35ae24e5a6c59](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image1)
|
||||
|
||||
|
||||
|
||||
LTPO(low-temperature Polycrystalline oxide) 允许显示器动态改变屏幕刷新率 ,而早在三星S20 Ultra、OPPO Find X3系列、一加 9 Pro 等系列产品上都率先采用了这种显示技术,但是实际上大家在 LTPO 又有不同的技术调教,从而出现了我们后续要聊的问题。
|
||||
|
||||
![image-20220331153929592](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image2)
|
||||
|
||||
例如 LTPO 1.0 时代可能大部分实现都只是强硬的根据场景锁死 60Hz/120Hz 的刷新率,而 LTPO 2.0 开始各大厂家则是升级了自适应策略,例如最常见的就是升级了滑动变频:
|
||||
|
||||
![0ecaee4af2444b87a73db171bd36ba3f](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image3)
|
||||
|
||||
当然,除了最常见的滑动, LTPO 2.0 上厂家可能还会有对动画、视频、文字输入、应用切换等场景进行不同的升频和降频策略,而其实介绍上面这些的原因是:
|
||||
|
||||
- **苹果 ProMotion 是基于官方实现的统一方案**;
|
||||
- **Android 的 LTPO 是基于供应商硬件后Android OEM 厂家自主调教的实现**;
|
||||
|
||||
> 以上部分资料来自[《LTPO到底是不是真的省电?-一加LTPO 2.0上手体验》](https://mobile.it168.com/a2022/0121/6612/000006612347.shtml)
|
||||
|
||||
所以这也造就了 Flutter 需要在 Android 和 iOS 上进行单独适配的主要原因。
|
||||
|
||||
## 二、Android
|
||||
|
||||
前面介绍里引用了一加的 LTPO 2.0 实现是有原因的,首先知道**自适应屏幕刷新率是 OEM 厂商自主调教,也就是理论上作为 App 是不需要做任何适配,因为跟随 Android 就行,Android 本身也是使用 Skia 渲染。**
|
||||
|
||||
但是往往事与愿违,在 Flutter 关于 [高刷问题 ](https://github.com/flutter/flutter/issues/35162) 最先被提及的就是一加,那时候基本都引用了 [《The OnePlus 7 Pro’s 90Hz Refresh Rate Doesn’t Support Every App 》](https://www.xda-developers.com/oneplus-7-pro-true-90hz-display-mode/) 这篇文章:
|
||||
|
||||
> 一加 7 Pro 的 90 fps 模式对于某些 App 而言只有 60 fps,要在所有 App 上都强制 90 fps,需要执行 `adb shell settings put global oneplus_screen_refresh_rate 0 ` 命令, 相比之下 Pixel 4 无需任何更改就直接可以支持渲染 90 fps 的 Flutter App。
|
||||
|
||||
也就是问题最开始是在一加的 90 fps 上不支持,而社区通过和一加的沟通得到的回复是:
|
||||
|
||||
- 一加7 Pro 为了平衡性能和功耗,采用的是基于 Android 定制自己的帧率控制逻辑,一般屏幕会以高帧率工作,但在某些场景下系统会切回到低帧率,而由于引入了这种机制,可能会出现当 App 希望屏幕以高帧率运行时却被系统强制设置为低帧率的问题。
|
||||
|
||||
- 那如何通过 App 设置 fps ? **如果应用程序需要设置帧速率,那首先需要通过 `getSupportedModes()` 获取目前屏幕支持的模式列表,然后遍历列表,根据找到想要使用的分辨率和刷新率的 `modeId`,赋值给窗口的`preferredDisplayModeId`**。
|
||||
|
||||
所以基于这个问题修复的方案,社区内提出了 [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) 插件,插件主要提供了获取 `Display.Mode` 和设置 `preferredDisplayModeId` 的支持,用于临时解决类似 一加7 Pro 上的这种刷新率问题。
|
||||
|
||||
```dart
|
||||
/// On OnePlus 7 Pro:
|
||||
/// #1 1080x2340 @ 60Hz
|
||||
/// #2 1080x2340 @ 90Hz
|
||||
/// #3 1440x3120 @ 90Hz
|
||||
/// #4 1440x3120 @ 60Hz
|
||||
/// On OnePlus 8 Pro:
|
||||
/// #1 1080x2376 @ 60Hz
|
||||
/// #2 1440x3168 @ 120Hz
|
||||
/// #3 1440x3168 @ 60Hz
|
||||
/// #4 1080x2376 @ 120Hz
|
||||
```
|
||||
|
||||
那什么是 `PreferredDisplayModeId` ?通过官方的 [《setframerate-vs-preferreddisplaymodeid》](https://developer.android.com/guide/topics/media/frame-rate#setframerate-vs-preferreddisplaymodeid) 可以了解:
|
||||
|
||||
> `WindowManager.LayoutParams.preferredDisplayModeId` 是 App 向平台设置所需帧率的一种方式,因为有时候 App 只想改变刷新率,但是不需要更改其他显示模式如分辨率等。类似设置还有 `setFrameRate() ` ,使用 `setFrameRate()` 代替 `preferredDisplayModeId`会更简单, 因为`setFrameRate()` 可以自动匹配显示模式列表里具有特定帧速率的模式。
|
||||
|
||||
**那为什么不直接用 `setFrameRate` ?其中之一因为这是一个 Target 很高的 API**。
|
||||
|
||||
![image-20220331170424637](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image4)
|
||||
|
||||
> PS:**这里和大家介绍一位 Flutter 大佬, 事实上这个 [问题](https://github.com/flutter/flutter/issues/93688) 作为 GDE 的 [AlexV525](https://github.com/AlexV525) 大佬跟进了很久,上面的插件也是他在参与维护,同时也恭喜🎉 大佬获得 [Google Open Source Peer Bonus Winners in 2022](https://opensource.googleblog.com/2022/03/Announcing-First-Group-of-Google-Open-Source-Peer-Bonus-Winners-in-2022.html) 的🏆**。
|
||||
|
||||
但是在安稳一段时间之后,[一加 9 pro 上了 LTPO 和 ColorOS](https://github.com/ajinasokan/flutter_displaymode/issues/10),之前的 adb 命令在新来的 ColorOS 上也随之失效,不过不要担心,后续发现这个其实是官方的一个bug,在 ColorOS `11_A.06` 版本后修复了该问题,也就是插件还可以继续生效。
|
||||
|
||||
而如今两年快过去了,对于此问题还是只能通过插件去临时解决,因为从官方的态度上好像并不是特别支持嵌入这种方式:
|
||||
|
||||
- Flutter 应该将刷新率控制交给 OS 处理, Flutter 不应该对单个刷新率去进行 hardcode;
|
||||
- 处理类似 OEM 厂商问题最好通过插件解决而不是 Flutter Engine ;
|
||||
|
||||
> 在这方面的处理思路和决策感觉和 iOS 差异较大,大概也有平台限制的因素吧。
|
||||
|
||||
事实上不同厂商对于 LTPO 的实现逻辑确实差异性很大,比如下图是一加10pro 在 LTPO 渲染是会选择性压缩或者丢弃一些冗余的指令。
|
||||
|
||||
![8888](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image5)
|
||||
|
||||
我们知道 Flutter 是把 ` Widget` 渲染到 `Surface` 上,在这点上和使用 ` SurfaceView` 和 `OpenGL` 实现的 Google Map 很类似,而经过测试 Google Map 在这些设备上,不特殊设置和 Flutter 一样也只能以 60hz 渲染运行。
|
||||
|
||||
> 对于 OEM 厂商,在调教的 LTPO 上有权决定是否允许 App 使用更高的刷新率,即使 App 要求更高的刷新率,这难道又是一个“白名单模式”?
|
||||
|
||||
所以如果需要让 `Surface` 在某些特殊设备支持 90/120 hz 运行,就需要使用 `preferredDisplayModeId` 或者 `setFrameRate` , **同时前提是厂商没有强行锁死帧率**。
|
||||
|
||||
> **一些手机厂商,会因为 “驯龙” 和控温的需要,都有自己的“稳帧”策略,甚至强制锁死帧率并且显示假帧率**。
|
||||
|
||||
![22222](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image6)
|
||||
|
||||
而在 [#78117](https://github.com/flutter/flutter/issues/78117) 讨论的最终讨论结果就是:**Flutter 并不会特别针对这部分厂商去特意做适配,如果需要,你可以通过第三方插件来解决,当然在我的测试中,目前大部分设备的刷新率支持上还是正常**。
|
||||
|
||||
同时在早期 Flutter 的 IntelliJ 插件也存在 bug ,即使应用程序以 90 fps 运行,Android Studio / IntelliJ 中的 Flutter 插件也会给出 60 fps ,当然这个问题在后续的 [#4289](https://github.com/flutter/flutter-intellij/pull/4289) 上得到了解决。
|
||||
|
||||
> 额外补充一种情况,厂家通常还会检测 `SurfaceView`/`TextureView ` 是否超过屏幕的一半,因为这时候可能代表着你正在看视频或者玩游戏,而这时候可能也会降低帧率。
|
||||
|
||||
|
||||
|
||||
最后,如果对 Flutter 在 Android 上关于刷新率部分的代码感性起,可以查阅:[vsync_waiter.cc](https://github.com/flutter/engine/blob/ebcd86f681b9421318b3b4a8abd75839e70000a5/shell/common/vsync_waiter.cc) 、[vsync_waiter_android.cc](https://github.com/flutter/engine/blob/266d3360a7babfb5f20d5e9f8ea84772b2a247dc/shell/platform/android/vsync_waiter_android.cc) 、[android_display.cc](https://github.com/flutter/engine/blob/266d3360a7babfb5f20d5e9f8ea84772b2a247dc/shell/platform/android/android_display.cc)
|
||||
|
||||
|
||||
|
||||
## 三、iOS
|
||||
|
||||
回到 iOS 上,ProMotion 的支持思路就和原生不大一样,因为在刚推出 ProMotion 时官方就在 [《刷新率优化上》](https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro) 对 ProMotion 的适配提及过:
|
||||
|
||||
如果使用的是以下这些默认框架的话,对于这些刷新率的变化 App 而无需进行任何更改:
|
||||
|
||||
- [UIKit](https://developer.apple.com/documentation/uikit)
|
||||
- [SwiftUI](https://developer.apple.com/documentation/swiftui)
|
||||
- [SpriteKit](https://developer.apple.com/documentation/spritekit)
|
||||
- [CAAnimation](https://developer.apple.com/documentation/quartzcore/caanimation)
|
||||
|
||||
但是对于 Flutter 而言并没用使用系统所提供的原生控件,所以目前需要在 `Info.plist` 文件中配置以下参数,从而启用关于 `CADisplayLink` 和 `CAAnimation` 上高于 120Hz 的相关支持:
|
||||
|
||||
```
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key><true/>
|
||||
```
|
||||
|
||||
而在 Flutter 官方的讨论记录 [flutter.dev/go/variable-refresh-rate](https://flutter.dev/go/variable-refresh-rate) 和 issue [#90675](https://github.com/flutter/flutter/issues/90675) 相关回复里可以看到,官方目前的决策是先使用 [#29797](https://github.com/flutter/engine/pull/29797) 的实现解决,通过调整 [vsync_waiter_ios.mm](https://github.com/flutter/engine/blob/4a3e7a5b72363c1f363d3000d04719c6938d963f/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm) 相关的内容来实现高刷支持:
|
||||
|
||||
```objective-c
|
||||
- (void)setMaxRefreshRateIfEnabled {
|
||||
NSNumber* minimumFrameRateDisabled =
|
||||
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"];
|
||||
if (!minimumFrameRateDisabled) {
|
||||
return;
|
||||
}
|
||||
double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60);
|
||||
double minFrameRate = fmax(maxFrameRate / 2, 60);
|
||||
|
||||
if (@available(iOS 15.0, *)) {
|
||||
display_link_.get().preferredFrameRateRange =
|
||||
CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate);
|
||||
} else if (@available(iOS 10.0, *)) {
|
||||
display_link_.get().preferredFramesPerSecond = maxFrameRate;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 默认情况下帧率会是设置为 60;
|
||||
- 在支持 ProMotion 的设备上会设置为显示器支持的最大刷新率;
|
||||
- **在 iOS 15 及更高版本上,还增加了设置帧率范围**,其中 preferred 和 max 均为屏幕支持的最大值,min 为最大值的 1/2;
|
||||
|
||||
其实在之前的讨论中还有如 [#29692](https://github.com/flutter/engine/pull/29692) 这种更灵活的实现,**也就是探索让 Flutter Engine 根据渲染和使用场景去自己选择当前的帧率**,因为社区认为:*对于普通用户来说,在不知道平台、性能等的情况下让开发者自己选择正确的刷新并不靠谱,所以通过 Engine 完成适配才是未来的方向*。
|
||||
|
||||
**当然,基于社区里目前迫切地想让 Flutter 得到 120Hz 的能力,所以会暂时优先采用上述的 `CADisableMinimumFrameDurationOnPhone` 来解决目前的困境,这也是 iOS 官方提倡的方式**。
|
||||
|
||||
另外值得一提的是,iOS 15.4 上的苹果修复了导致 ProMotion 相关的 bug ,因为在这之前会出现 ProMontion 并不是完全开放第三方支持的诡异情况,**而在 iOS 15.4 后, iOS 会自动为 App 中所有自定义动画内容启用120Hz刷新率**,所以会出现一个神奇的情况:
|
||||
|
||||
- 在 iOS 15.4 上, App 可以兼容得到 120Hz 动画;
|
||||
- 在 iOS 15.4 之前,部分动画支持 ProMotion;
|
||||
|
||||
![image-20220331182557253](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image7)
|
||||
|
||||
|
||||
|
||||
## 四、最后
|
||||
|
||||
可以看到就目前来说,高刷对于 Flutter 仍旧是一个挑战,作为独立渲染引擎,这也是 Flutter 无法逃避的问题,就目前情况来看:
|
||||
|
||||
- 在 Android 上你不需要做任何调整,如果遇到特殊设备或者系统,建议通过 [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) 来解决;
|
||||
- 在 iOS 上你可以添加 `CADisableMinimumFrameDurationOnPhone` 来粗暴解决,然后等待 [#29797](https://github.com/flutter/engine/pull/29797) 相关内容的合并发布;
|
||||
|
||||
最后,如果关于高刷方面你还有什么资料或者想法,欢迎留言评论讨论。
|
|
@ -1,10 +1,19 @@
|
|||
本篇将带你深入了解 Flutter 中的手势事件传递、事件分发、事件冲突竞争,滑动流畅等等的原理,帮你构建一个完整的 Flutter 闭环手势知识体系,这也许是目前最全面的手势事件和滑动源码的深入文章了。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、 Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
> * [九、 深入绘制原理](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
> * [十、 深入图片加载流程](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
> * [十一、全面深入理解Stream](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
> * [十二、全面深入理解状态管理设计](https://juejin.im/post/5cc816866fb9a03231209c7c)
|
||||
|
||||
|
||||
Flutter 中默认情况下,以 Android 为例,所有的事件都是起原生源于 `io.flutter.view.FlutterView` 这个 `SurfaceView` 的子类,整个触摸手势事件实质上经历了 **JAVA => C++ => Dart** 的一个流程,整个流程如下图所示,无论是 Android 还是 IOS ,原生层都只是将所有事件打包下发,比如在 Android 中,手势信息被打包成 `ByteBuffer` 进行传递,最后在 Dart 层的 `_dispatchPointerDataPacket` 方法中,通过 `_unpackPointerDataPacket` 方法解析成可用的 `PointerDataPacket` 对象使用。
|
||||
|
@ -305,11 +314,9 @@ bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() >
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文Demo :https://github.com/CarGuo/state_manager_demo
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -317,7 +324,35 @@ bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() >
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《Flutter完整开发实战详解(十二、全面深入理解状态管理设计)》](https://juejin.im/post/5cc816866fb9a03231209c7c)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-13/image6)
|
||||
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
|
||||
本篇将带你深入了解 Flutter 中打包和插件安装等原理,帮你快速完成 Flutter 集成到现有 Android 项目,实现混合开发支持。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、 Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
> * [九、 深入绘制原理](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
> * [十、 深入图片加载流程](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
> * [十一、全面深入理解Stream](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
> * [十二、全面深入理解状态管理设计](https://juejin.im/post/5cc816866fb9a03231209c7c)
|
||||
> * [十三、全面深入触摸和滑动原理](https://juejin.im/editor/posts/5cd54839f265da03b2044c32)
|
||||
|
||||
## 一、前言
|
||||
|
||||
|
@ -14,8 +23,6 @@
|
|||
但是混合开发会对 **打包、构建和启动等流程熟悉度要求较高** ,同时遇到的问题也更多,以前我在 `React Native` 也写过类似的文章 :[《从Android到React Native开发(四、打包流程解析和发布为Maven库)》](https://juejin.im/post/5b2116466fb9a01e3128359f) ,而这方面是有很多经验可以通用的,**所以适当的混开模式有利于避免一些问题,同时只有了解 `Flutter` 整体项目的构建思路,才有可能更舒适的躺坑。**
|
||||
|
||||
> 额外唠叨一句,跨平台的意义更多在于解决多端逻辑的统一 ,至少避免了逻辑重复实现,所以企业刚开始,一般会选择一些轻量级业务进行尝试。
|
||||
>
|
||||
> ### 官方未来将有 `Flutter build aar` 的方法可提供使用。
|
||||
|
||||
|
||||
## 二、打包
|
||||
|
@ -85,7 +92,7 @@
|
|||
![](http://img.cdn.guoshuyu.cn/20190604_Flutter-14/image5)
|
||||
|
||||
> 完整版可见 [flutter_app_lib](https://github.com/CarGuo/flutter_app_lib) 。
|
||||
|
||||
|
||||
## 四、堆栈
|
||||
|
||||
最后需要说的问题就是堆栈了。
|
||||
|
@ -110,11 +117,9 @@
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文Demo :https://github.com/CarGuo/flutter_app_lib
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 推荐完整代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -122,6 +127,41 @@
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《Flutter完整开发实战详解(十二、全面深入理解状态管理设计)》](https://juejin.im/post/5cc816866fb9a03231209c7c)
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)》](https://juejin.im/editor/posts/5cd54839f265da03b2044c32)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-14/image8)
|
||||
|
||||
|
||||
|
|
371
Flutter-15.md
371
Flutter-15.md
|
@ -1,371 +0,0 @@
|
|||
本篇将带你深入理解 Flutter 中 State 的工作机制,并通过对状态管理框架 **Provider** 解析加深理解,看完这一篇你将更轻松的理解你的 “State 大后宫” 。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
|
||||
```
|
||||
|
||||
⚠️第十二篇中更多讲解状态的是管理框架,本篇更多讲解 Flutter 本身的状态设计。
|
||||
|
||||
```
|
||||
|
||||
## 一、State
|
||||
|
||||
### 1、State 是什么?
|
||||
|
||||
我们知道 Flutter 宇宙中万物皆 `Widget` ,而 **`Widget` 是 `@immutable` 即不可变的,所以每个 `Widget` 状态都代表了一帧。**
|
||||
|
||||
在这个基础上, **`StatefulWidget` 的 `State` 帮我们实现了在 `Widget` 的跨帧绘制** ,也就是在每次 `Widget` 重绘的时候,通过 `State` 重新赋予 `Widget` 需要的绘制信息。
|
||||
|
||||
### 2、State 怎么实现跨帧共享?
|
||||
|
||||
这就涉及 Flutter 中 `Widget` 的实现原理,在之前的篇章我们介绍过,这里我们说两个涉及的概念:
|
||||
|
||||
- Flutter 中的 `Widget` 在一般情况下,是需要通过 `Element` 转化为 `RenderObject` 去实现绘制的。
|
||||
|
||||
- `Element` 是 `BuildContext` 的实现类,同时 `Element` 持有 `RenderObject` 和 `Widget` ,**我们代码中的 `Widget build(BuildContext context) {}` 方法,就是被 `Element` 调用的。**
|
||||
|
||||
了解这个两个概念后,我们先看下图,在 Flutter 中构建一个 `Widget` ,首先会创建出这个 `Widget` 的 `Element` ,**而事实上 `State` 实现跨帧共享,就是将 `State` 保存在`Element` 中,这样 `Element` 每次调用 `Widget build()` 时,是通过 `state.build(this);` 得到的新 `Widget` ,所以写在 `State ` 的数据就得以复用了。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image1)
|
||||
|
||||
|
||||
*那 `State ` 是在哪里被创建的?*
|
||||
|
||||
如下图所示,**`StatefulWidget` 的 `createState` 是在 `StatefulElement ` 的构建方法里创建的,** 这就保证了只要 `Element ` 不被重新创建,`State` 就一直被复用。
|
||||
|
||||
同时我们看 `update` 方法,当新的 `StatefulWidget` 被创建用于更新 UI 时,新的 `widget` 就会被重新赋予到 `_state` 中,而这的设定也导致一个常被新人忽略的问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image2)
|
||||
|
||||
我们先看问题代码,如下图所示:
|
||||
|
||||
- 1、在 `_DemoAppState` 中,我们创建了 `DemoPage` , 并且把 `data` 变量赋给了它。
|
||||
- 2、`DemoPage` 在创建 `createState` 时,又将 `data` 通过直接传入 `_DemoPageState ` 。
|
||||
- 3、在 `_DemoPageState ` 中直接将传入的 `data` 通过 `Text` 显示出来。
|
||||
|
||||
运行后我们一看也没什么问题吧? **但是当我们点击 4 中的 `setState` 时,却发现 3 中 `Text` 没有发现改变,** 这是为什么呢?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image3)
|
||||
|
||||
问题就在于前面 `StatefulElement ` 的构建方法和 `update` 方法:
|
||||
|
||||
**`State` 只在 `StatefulElement` 的构建方法中创建,当我们调用 `setState` 触发 `update` 时,只是执行了 `_state.widget = newWidget`,而我们通过 `_DemoPageState(this.data)` 传入的 *data* ,在传入后执行`setState` 时并没有改变。**
|
||||
|
||||
如果我们采用上图代码中 3 注释的 **`widget.data` 方法,因为 `_state.widget = newWidget` 时,`State` 中的 `Widget ` 已经被更新了,`Text` 自然就被更新了。**
|
||||
|
||||
### 3、setState 干了什么?
|
||||
|
||||
我们常说的 `setState` ,其实是调用了 `markNeedsBuild` ,**`markNeedsBuild` 内部会标记 `element` 为 `diry`,然后在下一帧 `WidgetsBinding.drawFrame` 才会被绘制,这可以也看出 `setState` 并不是立即生效的。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image4)
|
||||
|
||||
|
||||
### 4、状态共享
|
||||
|
||||
前面我们聊了 Flutter 中 `State` 的作用和工作原理,接下来我们看一个老生常谈的对象: **`InheritedWidget`** 。
|
||||
|
||||
状态共享是常见的需求,比如用户信息和登陆状态等等,而 Flutter 中 `InheritedWidget` 就是为此而设计的,在第十二篇我们大致讲过它:
|
||||
|
||||
> 在 `Element` 的内部有一个 `Map<Type, InheritedElement> _inheritedWidgets;` 参数,**`_inheritedWidgets` 一般情况下是空的,只有当父控件是 `InheritedWidget` 或者本身是 `InheritedWidgets` 时,它才会有被初始化,而当父控件是 `InheritedWidget` 时,这个 `Map` 会被一级一级往下传递与合并。**
|
||||
>
|
||||
> 所以当我们通过 `context` 调用 `inheritFromWidgetOfExactType` 时,就可以通过这个 `Map` 往上查找,从而找到这个上级的 `InheritedWidget ` 。
|
||||
|
||||
噢,是的,**`InheritedWidget` 共享的是 `Widget` ,只是这个 `Widget` 是一个 `ProxyWidget ` ,它自己本身并不绘制什么,但共享这个 `Widget` 内保存有的值,却达到了共享状态的目的。**
|
||||
|
||||
|
||||
如下代码所示,Flutter 内 `Theme` 的共享,共享的其实是 `_InheritedTheme ` 这个 `Widget` ,而我们通过 `Theme.of(context)` 拿到的,其实就是保存在这个 `Widget` 内的 `ThemeData` 。
|
||||
|
||||
```
|
||||
static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
|
||||
final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
|
||||
if (shadowThemeOnly) {
|
||||
/// inheritedTheme 这个 Widget 内的 theme
|
||||
/// theme 内有我们需要的 ThemeData
|
||||
return inheritedTheme.theme.data;
|
||||
}
|
||||
···
|
||||
}
|
||||
```
|
||||
|
||||
**这里有个需要注意的点,就是 `inheritFromWidgetOfExactType` 方法刚了什么?**
|
||||
|
||||
我们直接找到 `Element` 中的 `inheritFromWidgetOfExactType` 方法实现,如下关键代码所示:
|
||||
|
||||
- 首先从 `_inheritedWidgets ` 中查找是否有该类型的 `InheritedElement ` 。
|
||||
- 查找到后添加到 `_dependencies` 中,并且通过 `updateDependencies` **将当前 `Element` 添加到 `InheritedElement` 的 `_dependents` 这个Map 里。**
|
||||
- 返回 `InheritedElement ` 中的 `Widget` 。
|
||||
|
||||
```
|
||||
@override
|
||||
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
|
||||
/// 在共享 map _inheritedWidgets 中查找
|
||||
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
|
||||
if (ancestor != null) {
|
||||
/// 返回找到的 InheritedWidget ,同时添加当前 element 处理
|
||||
return inheritFromElement(ancestor, aspect: aspect);
|
||||
}
|
||||
_hadUnsatisfiedDependencies = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
|
||||
_dependencies ??= HashSet<InheritedElement>();
|
||||
_dependencies.add(ancestor);
|
||||
/// 就是将当前 element(this) 添加到 _dependents 里
|
||||
/// 也就是 InheritedElement 的 _dependents
|
||||
/// _dependents[dependent] = value;
|
||||
ancestor.updateDependencies(this, aspect);
|
||||
return ancestor.widget;
|
||||
}
|
||||
|
||||
@override
|
||||
void notifyClients(InheritedWidget oldWidget) {
|
||||
for (Element dependent in _dependents.keys) {
|
||||
notifyDependent(oldWidget, dependent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里面的关键就是 **` ancestor.updateDependencies(this, aspect);`** 这个方法:
|
||||
|
||||
我们都知道,获取 `InheritedWidget ` 一般需要 `BuildContext ` ,如`Theme.of(context)` ,而 `BuildContext ` 的实现就是 `Element` ,**所以当我们调用 `context.inheritFromWidgetOfExactType` 时,就会将这个 `context` 所代表的 `Element` 添加到 `InheritedElement` 的 `_dependents` 中。**
|
||||
|
||||
|
||||
*这代表着什么?*
|
||||
|
||||
比如当我们在 `StatefulWidget` 中调用 `Theme.of(context).primaryColor` 时,**传入的 `context` 就代表着这个 `Widget` 的 `Element`, 在 `InheritedElement` 里被“登记”到 `_dependents` 了。**
|
||||
|
||||
|
||||
**而当 `InheritedWidget ` 被更新时,如下代码所示,`_dependents` 中的 `Element` 会被逐个执行 `notifyDependent ` ,最后触发 `markNeedsBuild`** ,这也是为什么当 `InheritedWidget` 被更新时,通过如 `Theme.of(context).primaryColor` 引用的地方,也会触发更新的原因。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image5)
|
||||
|
||||
|
||||
> 下面开始实际分析 **Provider** 。
|
||||
|
||||
## 二、Provider
|
||||
|
||||
*为什么会有 **Provider** ?*
|
||||
|
||||
因为 Flutter 与 React 技术栈的相似性,所以在 Flutter 中涌现了诸如` flutter_redux` 、`flutter_dva` 、 `flutter_mobx` 、 `fish_flutter` 等前端式的状态管理,它们大多比较复杂,而且需要对框架概念有一定理解。
|
||||
|
||||
而作为 Flutter 官方推荐的状态管理 `scoped_model` ,又因为其设计较为简单,有些时候不适用于复杂的场景。
|
||||
|
||||
所以在经历了一端坎坷之后,**今年 Google I/O 大会之后, [Provider](https://github.com/rrousselGit/provider) 成了 Flutter 官方新推荐的状态管理方式之一。**
|
||||
|
||||
它的特点就是: **不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度** , 而原 Google 官方仓库的状态管理 [flutter-provide](https://github.com/google/flutter-provide) 已宣告GG , [provider](https://github.com/rrousselGit/provider) 成了它的替代品。
|
||||
|
||||
```
|
||||
⚠️注意,`provider` 比 `flutter-provide` 多了个 `r`。
|
||||
```
|
||||
|
||||
> 题外话:以前面试时,偶尔会被面试官问到“你的开源项目代码量也不多啊”这样的问题,每次我都会笑而不语,**虽然代码量能代表一些成果,但是我是十分反对用代码量来衡量贡献价值,这和你用加班时长来衡量员工价值有什么区别?**
|
||||
|
||||
### 0、演示代码
|
||||
|
||||
如下代码所示, 实现的是一个点击计数器,其中:
|
||||
|
||||
- `_ProviderPageState` 中使用`MultiProvider` 提供了多个 `providers` 的支持。
|
||||
- 在 `CountWidget` 中通过 `Consumer` 获取的 `counter ` ,同时更新 `_ProviderPageState` 中的 `AppBar` 和 `CountWidget ` 中的 `Text ` 显示。
|
||||
|
||||
```
|
||||
class _ProviderPageState extends State<ProviderPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(builder: (_) => ProviderModel()),
|
||||
],
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
var counter = Provider.of<ProviderModel>(context);
|
||||
return new Text("Provider ${counter.count.toString()}");
|
||||
},
|
||||
)
|
||||
),
|
||||
body: CountWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CountWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProviderModel>(builder: (context, counter, _) {
|
||||
return new Column(
|
||||
children: <Widget>[
|
||||
new Expanded(child: new Center(child: new Text(counter.count.toString()))),
|
||||
new Center(
|
||||
child: new FlatButton(
|
||||
onPressed: () {
|
||||
counter.add();
|
||||
},
|
||||
color: Colors.blue,
|
||||
child: new Text("+")),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderModel extends ChangeNotifier {
|
||||
int _count = 0;
|
||||
|
||||
int get count => _count;
|
||||
|
||||
void add() {
|
||||
_count++;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所以上述代码中,我们通过 `ChangeNotifierProvider ` 组合了 `ChangeNotifier` (ProviderModel) 实现共享;利用了 ` Provider.of` 和 `Consumer ` 获取共享的 `counter` 状态;通过调用 `ChangeNotifier` 的 ` notifyListeners();` 触发更新。
|
||||
|
||||
这里几个知识点是:
|
||||
|
||||
- 1、 **Provider** 的内部 `DelegateWidget` 是一个 `StatefulWidget` ,所以可以更新且具有生命周期。
|
||||
|
||||
- 2、状态共享是使用了 `InheritedProvider` 这个 `InheritedWidget` 实现的。
|
||||
|
||||
- 3、巧妙利用 `MultiProvider` 和 `Consumer` 封装,实现了组合与刷新颗粒度控制。
|
||||
|
||||
|
||||
接着我们逐个分析
|
||||
|
||||
### 1、Delegate
|
||||
|
||||
既然是状态管理,那么肯定有 `StatefulWidget` 和 `setState` 调用。
|
||||
|
||||
在 **Provider** 中,一系列关于 `StatefulWidget` 的生命周期管理和更新,都是通过各种代理完成的,如下图所示,上面代码中我们用到的 `ChangeNotifierProvider ` 大致经历了这样的流程:
|
||||
|
||||
- 设置到 `ChangeNotifierProvider ` 的 `ChangeNotifer` 会被执行 `addListener` 添加监听 `listener`。
|
||||
- `listener` 内会调用 `StateDelegate` 的 `StateSetter` 方法,从而调用到 `StatefulWidget` 的 `setState`。
|
||||
- 当我们执行 `ChangeNotifer` 的 `notifyListeners ` 时,就会最终触发 `setState` 更新。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image6)
|
||||
|
||||
而我们使用过的 `MultiProvider` 则是允许我们组合多种 `Provider` ,如下代码所示,传入的 `providers` 会倒序排列,最后组合成一个嵌套的 Widget tree ,方便我们添加多种 `Provider` :
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var tree = child;
|
||||
for (final provider in providers.reversed) {
|
||||
tree = provider.cloneWithChild(tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/// Clones the current provider with a new [child].
|
||||
/// Note for implementers: all other values, including [Key] must be
|
||||
/// preserved.
|
||||
@override
|
||||
MultiProvider cloneWithChild(Widget child) {
|
||||
return MultiProvider(
|
||||
key: key,
|
||||
providers: providers,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
通过 `Delegate` 中回调出来的各种生命周期,如 `Disposer `,也有利于我们外部二次处理,减少外部 `StatefulWidget ` 的嵌套使用。
|
||||
|
||||
### 2、InheritedProvider
|
||||
|
||||
状态共享肯定需要 `InheritedWidget` ,`InheritedProvider ` 就是`InheritedWidget ` 的子类,所有的 `Provider` 实现都在 `build` 方法中使用 `InheritedProvider ` 进行嵌套,实现 `value` 的共享。
|
||||
|
||||
### 3、Consumer
|
||||
|
||||
`Consumer ` 是 `Provider` 中比较有意思的东西,它本身是一个 `StatelessWidget` , 只是在 `build ` 中通过 ` Provider.of<T>(context)` 帮你获取到 `InheritedWidget` 共享的 `value` 。
|
||||
|
||||
```
|
||||
final Widget Function(BuildContext context, T value, Widget child) builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return builder(
|
||||
context,
|
||||
Provider.of<T>(context),
|
||||
child,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
那我们直接使用 `Provider.of<T>(context)` ,不使用 `Consumer ` 可以吗?
|
||||
|
||||
当然可以,但是你还记得前面,我们在介绍 `InheritedWidget` 时所说的:
|
||||
|
||||
> 传入的 `context` 代表着这个 `Widget` 的 `Element` 在 `InheritedElement` 里被“登记”到 `_dependents` 了。
|
||||
|
||||
`Consumer ` 做为一个单独 `StatelessWidget` ,**它的好处就是 `Provider.of<T>(context)` 传入的 `context` 就是 `Consumer ` 它自己。** 这样的话,我们在需要使用 `Provider.value` 的地方用 `Consumer` 做嵌套, `InheritedWidget` 更新的时候,就不会更新到整个页面 , 而是仅更新到 `Consumer ` 这个 `StatelessWidget` 。
|
||||
|
||||
**所以 `Consumer ` 贴心的封装了 `context` 在 `InheritedWidget` 中的“登记逻辑”,从而控制了状态改变时,需要更新的精细度。**
|
||||
|
||||
同时库内还提供了 `Consumer2` ~ `Consumer6` 的组合,感受下 :
|
||||
|
||||
```
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return builder(
|
||||
context,
|
||||
Provider.of<A>(context),
|
||||
Provider.of<B>(context),
|
||||
Provider.of<C>(context),
|
||||
Provider.of<D>(context),
|
||||
Provider.of<E>(context),
|
||||
Provider.of<F>(context),
|
||||
child,
|
||||
);
|
||||
```
|
||||
|
||||
这样的设定,相信用过 BLoC 模式的同学会感觉很贴心,以前正常用做 BLoC 时,每个 `StreamBuilder` 的 `snapShot` 只支持一种类型,多个时*要不就是多个状态合并到一个实体,要不就需要多个StreamBuilder嵌套。*
|
||||
|
||||
当然,如果你想直接利用 `LayoutBuilder` 搭配 `Provider.of<T>(context)` 也是可以的:
|
||||
|
||||
```
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
var counter = Provider.of<ProviderModel>(context);
|
||||
return new Text("Provider ${counter.count.toString()}");
|
||||
}
|
||||
```
|
||||
|
||||
其他的还有 `ValueListenableProvider` 、`FutureProvider ` 、`StreamProvider ` 等多种 `Provider` ,可见整个 **Provider** 的设计上更贴近 Flutter 的原生特性,同时设计也更好理解,并且兼顾了性能等问题。
|
||||
|
||||
**Provider** 的使用指南上,更详细的 [Vadaski](https://juejin.im/user/5b5d45f4e51d453526175c06/posts) 的
|
||||
[《Flutter | 状态管理指南篇——Provider》](https://juejin.im/post/5d00a84fe51d455a2f22023f) 已经写过,我就不重复写轮子了,感兴趣的可以过去看看。
|
||||
|
||||
>自此,第十五篇终于结束了!(///▽///)
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* 本文Demo :https://github.com/CarGuo/state_manager_demo
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [GSY Flutter 实战系列电子书](https://github.com/CarGuo/GSYFlutterBook)
|
||||
|
||||
* [GSYGithubApp Flutter](https://github.com/CarGuo/GSYGithubAppFlutter )
|
||||
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
![ ](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image7)
|
411
Flutter-16.md
411
Flutter-16.md
|
@ -1,411 +0,0 @@
|
|||
本篇将解析 Flutter 中自定义布局的原理,并带你深入实战自定义布局的流程,利用两种自定义布局的实现方式,完成如下图所示的界面效果,看完这一篇你将可以更轻松的对 Flutter 为所欲为。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190702_Flutter-16/image1)
|
||||
|
||||
## 一、前言
|
||||
|
||||
在之前的篇章我们讲过 `Widget` 、`Element` 和 `RenderObject` 之间的关系,所谓的 **自定义布局,事实上就是自定义 `RenderObject` 内 `child` 的大小和位置** ,而在这点上和其他框架不同的是,在 Flutter 中布局的核心并不是嵌套堆叠,**Flutter 布局的核心是在于 `Canvas`** ,我们所使用的 `Widget` ,仅仅是为了简化 `RenderObject` 的操作。
|
||||
|
||||
> 在[《九、 深入绘制原理》的测试绘制](https://juejin.im/post/5ca0e0aff265da309728659a#heading-1) 中我们知道, 对于 Flutter 而言,整个屏幕都是一块画布,我们通过各种 `Offset` 和 `Rect` 确定了位置,然后通过 `Canvas` 绘制 UI,而整个屏幕区域都是绘制目标,如果在 `child` 中我们 “不按照套路出牌” ,我们甚至可以不管 `parent` 的大小和位置随意绘制。
|
||||
|
||||
|
||||
## 二、MultiChildRenderObjectWidget
|
||||
|
||||
了解基本概念后,我们知道 **自定义 `Widget` 布局的核心在于自定义 `RenderObject`** ,而在官方默认提供的布局控件里,大部分的布局控件都是通过继承 `MultiChildRenderObjectWidget` 实现,那么一般情况下自定义布局时,我们需要做什么呢?
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190702_Flutter-16/image2)
|
||||
|
||||
|
||||
如上图所示,一般情况下实现自定义布局,**我们会通过继承 `MultiChildRenderObjectWidget` 和 `RenderBox` 这两个 `abstract` 类实现,而 `MultiChildRenderObjectElement` 则负责关联起它们,** 除了此之外,还有有几个关键的类
|
||||
: **`ContainerRenderObjectMixin` 、 `RenderBoxContainerDefaultsMixin` 和 `ContainerBoxParentData` 。**
|
||||
|
||||
`RenderBox` 我们知道是 `RenderObject` 的子类封装,也是我们自定义 `RenderObject` 时经常需要继承的,那么其他的类分别是什么含义呢?
|
||||
|
||||
#### 1、ContainerRenderObjectMixin
|
||||
|
||||
故名思义,这是一个 `mixin` 类,`ContainerRenderObjectMixin` 的作用,**主要是维护提供了一个双链表的 children `RenderObject` 。**
|
||||
|
||||
通过在 `RenderBox` 里混入 `ContainerRenderObjectMixin` , 我们就可以得到一个双链表的 children ,方便在我们布局时,**可以正向或者反向去获取和管理 `RenderObject` 们 。**
|
||||
|
||||
|
||||
#### 2、RenderBoxContainerDefaultsMixin
|
||||
|
||||
`RenderBoxContainerDefaultsMixin` 主要是对 `ContainerRenderObjectMixin` 的拓展,是对 `ContainerRenderObjectMixin` 内的 children 提供常用的默认行为和管理,接口如下所示:
|
||||
|
||||
```
|
||||
/// 计算返回第一个 child 的基线 ,常用于 child 的位置顺序有关
|
||||
double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline)
|
||||
|
||||
/// 计算返回所有 child 中最小的基线,常用于 child 的位置顺序无关
|
||||
double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline)
|
||||
|
||||
/// 触摸碰撞测试
|
||||
bool defaultHitTestChildren(BoxHitTestResult result, { Offset position })
|
||||
|
||||
/// 默认绘制
|
||||
void defaultPaint(PaintingContext context, Offset offset)
|
||||
|
||||
/// 以数组方式返回 child 链表
|
||||
List<ChildType> getChildrenAsList()
|
||||
|
||||
```
|
||||
|
||||
#### 3、ContainerBoxParentData
|
||||
|
||||
`ContainerBoxParentData ` 是 `BoxParentData` 的子类,主要是关联了 `ContainerDefaultsMixin` 和 `BoxParentData` ,**`BoxParentData` 是 `RenderBox` 绘制时所需的位置类。**
|
||||
|
||||
通过 `ContainerBoxParentData` ,我们可以将 `RenderBox` 需要的 `BoxParentData` 和上面的 `ContainerParentDataMixin` 组合起来,**事实上我们得到的 children 双链表就是以 `ParentData` 的形式呈现出来的。**
|
||||
|
||||
```
|
||||
abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { }
|
||||
```
|
||||
|
||||
|
||||
#### 4、MultiChildRenderObjectWidget
|
||||
|
||||
`MultiChildRenderObjectWidget` 的实现很简单 ,它仅仅只是继承了 `RenderObjectWidget`,然后提供了 `children` 数组,并创建了 `MultiChildRenderObjectElement`。
|
||||
|
||||
|
||||
> 上面的 `RenderObjectWidget` 顾名思义,它是提供 `RenderObject` 的 `Widget` ,那有不存在 `RenderObject` 的 `Widget` 吗?
|
||||
>
|
||||
> 有的,比如我们常见的 `StatefulWidget` 、 `StatelessWidget` 、 `Container` 等,它们的 `Element` 都是 `ComponentElement` , `ComponentElement` 仅仅起到容器的作用,而它的 `get renderObject ` 需要来自它的 `child` 。
|
||||
|
||||
|
||||
#### 5、MultiChildRenderObjectElement
|
||||
|
||||
前面的篇章我们说过 `Element` 是 `BuildContext` 的实现, 内部一般持有 `Widget` 、`RenderObject` 并作为二者沟通的桥梁,那么 `MultiChildRenderObjectElement` 就是我们自定义布局时的桥梁了, 如下代码所示,`MultiChildRenderObjectElement` 主要实现了如下接口,其主要功能是对内部 `children` 的 `RenderObject` ,实现了插入、移除、访问、更新等逻辑:
|
||||
|
||||
```
|
||||
/// 下面三个方法都是利用 ContainerRenderObjectMixin 的 insert/move/remove 去操作
|
||||
/// ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>
|
||||
void insertChildRenderObject(RenderObject child, Element slot)
|
||||
void moveChildRenderObject(RenderObject child, dynamic slot)
|
||||
void removeChildRenderObject(RenderObject child)
|
||||
|
||||
/// visitChildren 是通过 Element 中的 ElementVisitor 去迭代的
|
||||
/// 一般在 RenderObject get renderObject 会调用
|
||||
void visitChildren(ElementVisitor visitor)
|
||||
|
||||
/// 添加忽略child _forgottenChildren.add(child);
|
||||
void forgetChild(Element child)
|
||||
|
||||
/// 通过 inflateWidget , 把 children 中 List<Widget> 对应的 List<Element>
|
||||
void mount(Element parent, dynamic newSlot)
|
||||
|
||||
/// 通过 updateChildren 方法去更新得到 List<Element>
|
||||
void update(MultiChildRenderObjectWidget newWidget)
|
||||
|
||||
```
|
||||
|
||||
所以 `MultiChildRenderObjectElement` 利用 `ContainerRenderObjectMixin` 最终将我们自定义的 `RenderBox` 和 `Widget` 关联起来。
|
||||
|
||||
#### 6、自定义流程
|
||||
|
||||
**上述主要描述了 `MultiChildRenderObjectWidget` 、 `MultiChildRenderObjectElement` 和其他三个辅助类`ContainerRenderObjectMixin` 、 `RenderBoxContainerDefaultsMixin` 和 `ContainerBoxParentData` 之间的关系。**
|
||||
|
||||
**了解几个关键类之后,我们看一般情况下,实现自定义布局的简化流程是:**
|
||||
|
||||
- 1、自定义 `ParentData` 继承 `ContainerBoxParentData` 。
|
||||
- 2、继承 `RenderBox` ,同时混入 `ContainerRenderObjectMixin` 和 `RenderBoxContainerDefaultsMixin` 实现自定义`RenderObject` 。
|
||||
- 3、继承 `MultiChildRenderObjectWidget`,实现 `createRenderObject` 和 `updateRenderObject` 方法,关联我们自定义的 `RenderBox `。
|
||||
- 4、override `RenderBox` 的 `performLayout` 和 `setupParentData` 方法,实现自定义布局。
|
||||
|
||||
**当然我们可以利用官方的 `CustomMultiChildLayout` 实现自定义布局,这个后面也会讲到,现在让我们先从基础开始,** 而上述流程中混入的 `ContainerRenderObjectMixin` 和 `RenderBoxContainerDefaultsMixin` ,在 `RenderFlex` 、`RenderWrap` 、`RenderStack` 等官方实现的布局里,也都会混入它们。
|
||||
|
||||
## 三、自定义布局
|
||||
|
||||
**自定义布局就是在 `performLayout` 中实现的 `child.layout` 大小和 `child.ParentData.offset` 位置的赋值。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190702_Flutter-16/image3)
|
||||
|
||||
首先我们要实现类似如图效果,我们需要自定义 `RenderCloudParentData` 继承 `ContainerBoxParentData` ,用于记录宽高和内容区域 :
|
||||
|
||||
```
|
||||
class RenderCloudParentData extends ContainerBoxParentData<RenderBox> {
|
||||
double width;
|
||||
double height;
|
||||
|
||||
Rect get content => Rect.fromLTWH(
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后自定义 `RenderCloudWidget` 继承 `RenderBox` ,并混入 `ContainerRenderObjectMixin` 和 `RenderBoxContainerDefaultsMixin` 实现 `RenderBox` 自定义的简化。
|
||||
|
||||
|
||||
```
|
||||
class RenderCloudWidget extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
|
||||
RenderCloudWidget({
|
||||
List<RenderBox> children,
|
||||
Overflow overflow = Overflow.visible,
|
||||
double ratio,
|
||||
}) : _ratio = ratio,
|
||||
_overflow = overflow {
|
||||
///添加所有 child
|
||||
addAll(children);
|
||||
}
|
||||
```
|
||||
|
||||
如下代码所示,接下来主要看 `RenderCloudWidget ` 中`override performLayout` 中的实现,这里我们只放关键代码:
|
||||
|
||||
- 1、我们首先拿到 `ContainerRenderObjectMixin` 链表中的 `firstChild` ,然后从头到位读取整个链表。
|
||||
- 2、对于每个 child 首先通过 `child.layout` 设置他们的大小,然后记录下大小之后。
|
||||
- 3、以容器控件的中心为起点,从内到外设置布局,这是设置的时候,需要通过记录的 `Rect` 判断是否会重复,每次布局都需要计算位置,直到当前 child 不在重复区域内。
|
||||
- 4、得到最终布局内大小,然后设置整体居中。
|
||||
|
||||
```
|
||||
///设置为我们的数据
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! RenderCloudParentData)
|
||||
child.parentData = RenderCloudParentData();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
///默认不需要裁剪
|
||||
_needClip = false;
|
||||
|
||||
///没有 childCount 不玩
|
||||
if (childCount == 0) {
|
||||
size = constraints.smallest;
|
||||
return;
|
||||
}
|
||||
|
||||
///初始化区域
|
||||
var recordRect = Rect.zero;
|
||||
var previousChildRect = Rect.zero;
|
||||
|
||||
RenderBox child = firstChild;
|
||||
|
||||
while (child != null) {
|
||||
var curIndex = -1;
|
||||
|
||||
///提出数据
|
||||
final RenderCloudParentData childParentData = child.parentData;
|
||||
|
||||
child.layout(constraints, parentUsesSize: true);
|
||||
|
||||
var childSize = child.size;
|
||||
|
||||
///记录大小
|
||||
childParentData.width = childSize.width;
|
||||
childParentData.height = childSize.height;
|
||||
|
||||
do {
|
||||
///设置 xy 轴的比例
|
||||
var rX = ratio >= 1 ? ratio : 1.0;
|
||||
var rY = ratio <= 1 ? ratio : 1.0;
|
||||
|
||||
///调整位置
|
||||
var step = 0.02 * _mathPi;
|
||||
var rotation = 0.0;
|
||||
var angle = curIndex * step;
|
||||
var angleRadius = 5 + 5 * angle;
|
||||
var x = rX * angleRadius * math.cos(angle + rotation);
|
||||
var y = rY * angleRadius * math.sin(angle + rotation);
|
||||
var position = Offset(x, y);
|
||||
|
||||
///计算得到绝对偏移
|
||||
var childOffset = position - Alignment.center.alongSize(childSize);
|
||||
|
||||
++curIndex;
|
||||
|
||||
///设置为遏制
|
||||
childParentData.offset = childOffset;
|
||||
|
||||
///判处是否交叠
|
||||
} while (overlaps(childParentData));
|
||||
|
||||
///记录区域
|
||||
previousChildRect = childParentData.content;
|
||||
recordRect = recordRect.expandToInclude(previousChildRect);
|
||||
|
||||
///下一个
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
///调整布局大小
|
||||
size = constraints
|
||||
.tighten(
|
||||
height: recordRect.height,
|
||||
width: recordRect.width,
|
||||
)
|
||||
.smallest;
|
||||
|
||||
///居中
|
||||
var contentCenter = size.center(Offset.zero);
|
||||
var recordRectCenter = recordRect.center;
|
||||
var transCenter = contentCenter - recordRectCenter;
|
||||
child = firstChild;
|
||||
while (child != null) {
|
||||
final RenderCloudParentData childParentData = child.parentData;
|
||||
childParentData.offset += transCenter;
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
||||
///超过了嘛?
|
||||
_needClip =
|
||||
size.width < recordRect.width || size.height < recordRect.height;
|
||||
}
|
||||
```
|
||||
|
||||
**其实看完代码可以发现,关键就在于你怎么设置 `child.parentData` 的 `offset` ,来控制其位置。**
|
||||
|
||||
最后通过 `CloudWidget` 加载我们的 `RenderCloudWidget` 即可, 当然完整代码还需要结合 `FittedBox` 与 `RotatedBox` 简化完成,具体可见 :[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo/tree/master/lib/widget/cloud)
|
||||
|
||||
```
|
||||
class CloudWidget extends MultiChildRenderObjectWidget {
|
||||
final Overflow overflow;
|
||||
final double ratio;
|
||||
|
||||
CloudWidget({
|
||||
Key key,
|
||||
this.ratio = 1,
|
||||
this.overflow = Overflow.clip,
|
||||
List<Widget> children = const <Widget>[],
|
||||
}) : super(key: key, children: children);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderCloudWidget(
|
||||
ratio: ratio,
|
||||
overflow: overflow,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, RenderCloudWidget renderObject) {
|
||||
renderObject
|
||||
..ratio = ratio
|
||||
..overflow = overflow;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**最后我们总结,实现自定义布局的流程就是,实现自定义 `RenderBox` 中 `performLayout ` child 的 `offset` 。**
|
||||
|
||||
## 四、CustomMultiChildLayout
|
||||
|
||||
`CustomMultiChildLayout` 是 Flutter 为我们封装的简化自定义布局实现,**它的内部同样是通过 `MultiChildRenderObjectWidget` 实现,但是它为我们封装了 `RenderCustomMultiChildLayoutBox` 和 `MultiChildLayoutParentData` ,并通过 `MultiChildLayoutDelegate` 暴露出需要自定义的地方。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190702_Flutter-16/image4)
|
||||
|
||||
|
||||
使用 `CustomMultiChildLayout` 你只需要继承 `MultiChildLayoutDelegate` ,并实现如下方法即可:
|
||||
|
||||
```
|
||||
|
||||
void performLayout(Size size);
|
||||
|
||||
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
|
||||
|
||||
```
|
||||
|
||||
通过继承 `MultiChildLayoutDelegate`,并且实现 `performLayout` 方法,我们可以快速自定义我们需要的控件,当然便捷的封装也代表了灵活性的丧失,可以看到 `performLayout` 方法中只有布局自身的 `Size` 参数,所以完成上图需求时,**我们还需要 child 的大小和位置** ,也就是 `childSize` 和 `childId` 。
|
||||
|
||||
|
||||
`childSize` 相信大家都能故名思义,那 `childId` 是什么呢?
|
||||
|
||||
这就要从 `MultiChildLayoutDelegate` 的实现说起,**在 `MultiChildLayoutDelegate` 内部会有一个 ` Map<Object, RenderBox> _idToChild;` 对象,这个 `Map` 对象保存着 `Object id` 和 `RenderBox` 的映射关系,而在 `MultiChildLayoutDelegate` 中获取 `RenderBox` 都需要通过 `id` 获取。**
|
||||
|
||||
`_idToChild` 这个 `Map` 是在 `RenderBox performLayout` 时,在 `delegate._callPerformLayout` 方法内创建的,创建后所用的 `id` 为 `MultiChildLayoutParentData` 中的 id, **而 `MultiChildLayoutParentData` 的 id ,可以通过 `LayoutId` 嵌套时自定义指定赋值。**
|
||||
|
||||
而完成上述布局,我们需要知道每个 child 的 index ,所以我们可以把 index 作为 id 设置给每个 child 的 `LayoutId` 。
|
||||
|
||||
**所以我们可以通过 `LayoutId` 指定 id 为数字 index , 同时告知 delegate ,这样我们就知道 child 顺序和位置啦。**
|
||||
|
||||
> 这个 id 是 `Object` 类型 ,所以你懂得,你可以赋予很多属性进去。
|
||||
|
||||
如下代码所示,这样在自定义的 `CircleLayoutDelegate` 中,就知道每个控件的 `index` 位置,也就是知道了,圆形布局中每个 item 需要的位置。
|
||||
|
||||
我们只需要通过 `index` ,计算出 child 所在的角度,然后利用 `layoutChild` 和 `positionChild` 对每个item进行布局即可,完整代码:[GSYFlutterDemo](https://github.com/CarGuo/GSYFlutterDemo/blob/master/lib/widget/custom_multi_render_demo_page.dart)
|
||||
|
||||
|
||||
```
|
||||
///自定义实现圆形布局
|
||||
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
final List<String> customLayoutId;
|
||||
|
||||
final Offset center;
|
||||
|
||||
Size childSize;
|
||||
|
||||
CircleLayoutDelegate(this.customLayoutId,
|
||||
{this.center = Offset.zero, this.childSize});
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
for (var item in customLayoutId) {
|
||||
if (hasChild(item)) {
|
||||
double r = 100;
|
||||
|
||||
int index = int.parse(item);
|
||||
|
||||
double step = 360 / customLayoutId.length;
|
||||
|
||||
double hd = (2 * math.pi / 360) * step * index;
|
||||
|
||||
var x = center.dx + math.sin(hd) * r;
|
||||
|
||||
var y = center.dy - math.cos(hd) * r;
|
||||
|
||||
childSize ??= Size(size.width / customLayoutId.length,
|
||||
size.height / customLayoutId.length);
|
||||
|
||||
///设置 child 大小
|
||||
layoutChild(item, BoxConstraints.loose(childSize));
|
||||
|
||||
final double centerX = childSize.width / 2.0;
|
||||
|
||||
final double centerY = childSize.height / 2.0;
|
||||
|
||||
var result = new Offset(x - centerX, y - centerY);
|
||||
|
||||
///设置 child 位置
|
||||
positionChild(item, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
}
|
||||
```
|
||||
|
||||
总的来说,第二种实现方式相对简单,但是也丧失了一定的灵活性,可自定义控制程度更低,但是也更加规范与间接,同时我们自己实现 `RenderBox` 时,也可以用类似的 delegate 的方式做二次封装,这样的自定义布局会更行规范可控。
|
||||
|
||||
|
||||
> 自此,第十六篇终于结束了!(///▽///)
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190702_Flutter-16/image5)
|
||||
|
||||
|
399
Flutter-17.md
399
Flutter-17.md
|
@ -1,399 +0,0 @@
|
|||
作为系列文章的第十七篇,本篇再一次带来 Flutter 开发过程中的实用技巧,让你继续弯道超车,全篇均为个人的日常干货总结,以实用填坑为主,让你少走弯路狂飙车。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
[![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image1)
|
||||
](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
|
||||
|
||||
## 1、Package get git 失败
|
||||
|
||||
Flutter 项目在引用第三库时,一般都是直接引用 `pub` 上的第三方插件,但是有时候我们为了安全和私密,会选择使用 git 引用,如:
|
||||
|
||||
```
|
||||
photo_view:
|
||||
git:
|
||||
url: https://github.com/CarSmallGuo/photo_view.git
|
||||
ref: master
|
||||
```
|
||||
|
||||
这时候在执行 `flutter packages get` 过程中,如果出现失败后,再次执行 `flutter packages get` 可能会遇到如下图所示的问题:
|
||||
|
||||
[![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image2))
|
||||
](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
|
||||
而 `flutter packages get` 提示 `git` 失败的原因,主要是:
|
||||
|
||||
在下载包的过程中出现问题,下次再拉包的时候,**在 `.pub_cache` 内的 `git` 目录下会检测到已经存在目录,但是可能是空目录等等,导致 `flutter packages get` 的时候异常。**
|
||||
|
||||
**所以你需要清除掉 `.pub_cache` 内的 `git` 的异常目录,然后最好清除掉项目下的 `pubspec.lock` ,之后重新执行 `flutter packages get` 。**
|
||||
|
||||
> `win` 一般是在 `C:\Users\xxxxx\AppData\Roaming\Pub\Cache` 路径下有 `git` 目录。
|
||||
>
|
||||
> `mac` 目录在 `~/.pub-cache` 。
|
||||
|
||||
## 2、TextEditingController
|
||||
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image3)
|
||||
|
||||
如上代码所示,红线部分表示,如果 `controller` 为空,就赋值一个 `TextEditingController` ,这样的写法会导致如下图所示问题:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image4)
|
||||
|
||||
**弹出键盘时输入成功后,收起键盘时输入的内容消失了!** 这是因为键盘的弹出和收起都会触发页面 `build` ,而在 `controller` 为 `null` 时,每次赋值的 `TextEditingController` 会导致 `TextField` 的 `TextEditingValue` 重置。
|
||||
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image5)
|
||||
|
||||
如上图所示,因为当 `TextField` 的 `controller` 不为空时,update 时是不会执行 `value` 的拷贝,所以为了避免这类问题,如下图所示, **需要先在全局构建 `TextEditingController` 再赋值,如果 `controller` 为空直接给 null 即可,避免 `build` 时每次重构 `TextEditingController` 。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image6)
|
||||
|
||||
## 3、Scrollable
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image7)
|
||||
|
||||
如上图所示,在之前第七篇的时候分析过,**滑动列表内一般都会有 `Scrollable` 的存在,而 `Scrollable` 恰好是一个 `InheritedWidget`** ,这就给我们在 `children` 中调用 `Scrollable` 相关方法提供了便利。
|
||||
|
||||
如下代码所依,通过 `Scrollable.of(context)` 我们可以更解耦的在 `ListView/GridView` 的 `children` 对其进行控制。
|
||||
|
||||
```
|
||||
ScrollableState state = Scrollable.of(context)
|
||||
|
||||
///获取 _scrollable 内 viewport 的 renderObject
|
||||
RenderObject renderObject = state.context.findRenderObject();
|
||||
///监听位置更新
|
||||
state.position.addListener((){});
|
||||
///通知位置更新
|
||||
state.position.notifyListeners();
|
||||
///滚动到指定位置
|
||||
state.position.jumpTo(1000);
|
||||
····
|
||||
|
||||
```
|
||||
|
||||
## 4、图片高斯模糊
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image8)
|
||||
|
||||
在 Flutter 中,提供了 `BackdropFilter` 和 `ImageFilter` 实现了高斯模糊的支持,如下代码所示,可以快速实现上图的高斯模糊效果。
|
||||
|
||||
```
|
||||
class BlurDemoPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: new Container(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: new Image.asset(
|
||||
"static/gsy_cat.png",
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
)),
|
||||
new Center(
|
||||
child: new Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: new Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
new Icon(Icons.ac_unit),
|
||||
new Text("哇!!")
|
||||
],
|
||||
)))))
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5、滚动到指定位置
|
||||
|
||||
因为目前 Flutter 并没有直接提供滚动到指定 `Item` 的方法,在每个 `Item` 大小不一的情况下,折中利用如图下所示代码,可以快速实现滚动到指定 `Item` 的效果:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image9)
|
||||
|
||||
上图为部分代码,完整代码可见 [scroll_to_index_demo_page2.dart](https://github.com/CarGuo/GSYFlutterDemo/blob/master/lib/widget/scroll_to_index_demo_page2.dart) ,这里主要是给每个 `item` 都赋予了一个 `GlobalKey` , 利用 `findRenderObject` 找到所需 `item` 的 `RenderBox` ,然后使用 `localToGlobal` 获取 `item` 在 `ViewPort` 这个 `ancestor` 中的偏移量进行滚动:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image10)
|
||||
|
||||
|
||||
当然还有另外一种实现方式,具体可见 [scroll_to_index_demo_page.dart](https://github.com/CarGuo/GSYFlutterDemo/blob/master/lib/widget/scroll_to_index_demo_page.dart)
|
||||
|
||||
|
||||
## 6、findRenderObject
|
||||
|
||||
在 Flutter 中是存在 **容器 Widget** 和 **渲染Widget** 的区别的,一般情况下:
|
||||
|
||||
- `Text`、`Sliver` 、`ListTile` 等都是属于渲染 Widget ,其内部主要是 `RenderObjectElement` 。
|
||||
- `StatelessWidget` / `StatefulWidget` 等属于容器 Widget ,其内部使用的是 `ComponentElement` , **`ComponentElement` 本身是不存在 `RenderObject` 的。**
|
||||
|
||||
结合前面篇章我们说过 `BuildContext` 的实现就是 `Element`,所以 `context.findRenderObject()` 这个操作其实就是 `Element` 的 `findRenderObject()` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image11)
|
||||
|
||||
那么如上图所示,`findRenderObject` 的实现最终就是获取 `renderObject`,在 `Element` 中 `renderObject` 的获取逻辑就很清晰了,**在遇到 `ComponentElement` 时,执行的是 `element.visitChildren(visit);`** , 递归直到找到 `RenderObjectElement` 。
|
||||
|
||||
所以如下代码所示,`print("${globalKey.currentContext.findRenderObject()}");` 最终输出了 `SizedBox` 的 `RenderObject` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image12)
|
||||
|
||||
|
||||
## 7、行间距
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image13)
|
||||
|
||||
|
||||
在 Flutter 中,是没有直接设置 `Text` 行间距的方法的, `Text` 显示的效果是如下图所示的逻辑组成:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image14)
|
||||
|
||||
那么我们应该如何处理行间距呢?如下图所示,**通过设置 `StrutStyle` 的 `leading` , 然后利用 `Transform` 做计算翻方向位置偏移,因为 `leading` 是上下均衡的,所以计算后就可以得到我们所需要的行间距大小。** (虽然无法保证一定 100%像素准确,你是否还知道其他方法?)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image15)
|
||||
|
||||
|
||||
> **这里额外提一点,可以通过父节点使用 `DefaultTextStyle` 来实现局部样式的共享哦。**
|
||||
|
||||
## 8、Builder
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image16)
|
||||
|
||||
在 Flutter 中存在 `Builder` 这样一个 Widget,看源码发现它其实就是 `StatelessWidget` 的简单封装,那为什么还需要它的存在呢?
|
||||
|
||||
|
||||
如下图所示,相信一些 Flutter 开发者在使用 `Scaffold.of(context).showSnackBar(snackbar)` 时,可能 遇到过如下错误,这是因为传入的 `context` 属于错误节点导致的,**因为此处传入的 `context` 并不能找到页面所在的 `Scaffold` 节点。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image17)
|
||||
|
||||
所以这时候 `Builder` 的作用就体现了,如下所示,通过 `builder` 方法返回赋予的 `context` ,在向上查找 `Scaffold` 的时候,就可以顺利找到父节点的 `Scaffold` 了,这也一定程度上体现了 `ComponentElement` 的作用之一。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image18)
|
||||
|
||||
## 9、快速实现动画切换效果
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image19)
|
||||
|
||||
要实现如上图所示动画效果,在 Flutter 中提供了 `AnimatedSwitcher` 封装简易实现。
|
||||
|
||||
如下图所示,**通过嵌套 `AnimatedSwitcher` ,指定 `transitionBuilder` 动画效果,然后在数据改变时,同时改变需要执行动画的 `key` 值,即可达到动画切换的效果。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image20)
|
||||
|
||||
## 10、多语言显示异常
|
||||
|
||||
在官方的 [https://github.com/flutter/flutter/issues/36527](https://github.com/flutter/flutter/issues/36527) issue 中可以发现,**Flutter 在韩语/日语 与中文同时显示,会导致 iOS 下出现文字渲染异常的问题** ,如下图所示,左边为异常情况。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image21)
|
||||
|
||||
改问题解决方案暂时有两种:
|
||||
|
||||
- **增加字体 ttf ,全局指定改字体显示。**
|
||||
|
||||
- **修改主题下所有 `TextTheme` 的 `fontFamilyFallback` :**
|
||||
|
||||
```
|
||||
getThemeData() {
|
||||
var themeData = ThemeData(
|
||||
primarySwatch: primarySwatch
|
||||
);
|
||||
|
||||
var result = themeData.copyWith(
|
||||
textTheme: confirmTextTheme(themeData.textTheme),
|
||||
accentTextTheme: confirmTextTheme(themeData.accentTextTheme),
|
||||
primaryTextTheme: confirmTextTheme(themeData.primaryTextTheme),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
/// 处理 ios 上,同页面出现韩文和简体中文,导致的显示字体异常
|
||||
confirmTextTheme(TextTheme textTheme) {
|
||||
getCopyTextStyle(TextStyle textStyle) {
|
||||
return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
|
||||
}
|
||||
|
||||
return textTheme.copyWith(
|
||||
display4: getCopyTextStyle(textTheme.display4),
|
||||
display3: getCopyTextStyle(textTheme.display3),
|
||||
display2: getCopyTextStyle(textTheme.display2),
|
||||
display1: getCopyTextStyle(textTheme.display1),
|
||||
headline: getCopyTextStyle(textTheme.headline),
|
||||
title: getCopyTextStyle(textTheme.title),
|
||||
subhead: getCopyTextStyle(textTheme.subhead),
|
||||
body2: getCopyTextStyle(textTheme.body2),
|
||||
body1: getCopyTextStyle(textTheme.body1),
|
||||
caption: getCopyTextStyle(textTheme.caption),
|
||||
button: getCopyTextStyle(textTheme.button),
|
||||
subtitle: getCopyTextStyle(textTheme.subtitle),
|
||||
overline: getCopyTextStyle(textTheme.overline),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> ps :**通过`WidgetsBinding.instance.window.locale;` 可以获取到手机平台本身的当前语言情况,不需要 `context` ,也不是你设置后的 `Locale` 。**
|
||||
|
||||
|
||||
## 11、长按输入框导致异常的情况
|
||||
|
||||
如果项目存在多语言和主题切换的场景,可能会遇到长按输入框导致异常的场景,目前可推荐两种解放方法:
|
||||
|
||||
- 1、可以给你的自定义 `ThemeData` 强制指定固定一个平台,但是该方式会导致平台复制粘贴弹出框没有了平台特性:
|
||||
|
||||
```
|
||||
///防止输入框长按崩溃问题
|
||||
platform: TargetPlatform.android
|
||||
```
|
||||
|
||||
|
||||
- 2、增加一个自定义的 `LocalizationsDelegate` , 实现多语言环境下的自定义支持:
|
||||
|
||||
```
|
||||
|
||||
|
||||
class FallbackCupertinoLocalisationsDelegate
|
||||
extends LocalizationsDelegate<CupertinoLocalizations> {
|
||||
const FallbackCupertinoLocalisationsDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => true;
|
||||
|
||||
@override
|
||||
Future<CupertinoLocalizations> load(Locale locale) => loadCupertinoLocalizations(locale);
|
||||
|
||||
@override
|
||||
bool shouldReload(FallbackCupertinoLocalisationsDelegate old) => false;
|
||||
}
|
||||
|
||||
class CustomZhCupertinoLocalizations extends DefaultCupertinoLocalizations {
|
||||
const CustomZhCupertinoLocalizations();
|
||||
|
||||
@override
|
||||
String datePickerMinuteSemanticsLabel(int minute) {
|
||||
if (minute == 1) return '1 分钟';
|
||||
return minute.toString() + ' 分钟';
|
||||
}
|
||||
|
||||
@override
|
||||
String get anteMeridiemAbbreviation => '上午';
|
||||
|
||||
@override
|
||||
String get postMeridiemAbbreviation => '下午';
|
||||
|
||||
@override
|
||||
String get alertDialogLabel => '警告';
|
||||
|
||||
@override
|
||||
String timerPickerHourLabel(int hour) => '小时';
|
||||
|
||||
@override
|
||||
String timerPickerMinuteLabel(int minute) => '分';
|
||||
|
||||
@override
|
||||
String timerPickerSecond(int second) => '秒';
|
||||
|
||||
@override
|
||||
String get cutButtonLabel => '裁剪';
|
||||
|
||||
@override
|
||||
String get copyButtonLabel => '复制';
|
||||
|
||||
@override
|
||||
String get pasteButtonLabel => '粘贴';
|
||||
|
||||
@override
|
||||
String get selectAllButtonLabel => '全选';
|
||||
}
|
||||
|
||||
class CustomTCCupertinoLocalizations extends DefaultCupertinoLocalizations {
|
||||
const CustomTCCupertinoLocalizations();
|
||||
|
||||
@override
|
||||
String datePickerMinuteSemanticsLabel(int minute) {
|
||||
if (minute == 1) return '1 分鐘';
|
||||
return minute.toString() + ' 分鐘';
|
||||
}
|
||||
|
||||
@override
|
||||
String get anteMeridiemAbbreviation => '上午';
|
||||
|
||||
@override
|
||||
String get postMeridiemAbbreviation => '下午';
|
||||
|
||||
@override
|
||||
String get alertDialogLabel => '警告';
|
||||
|
||||
@override
|
||||
String timerPickerHourLabel(int hour) => '小时';
|
||||
|
||||
@override
|
||||
String timerPickerMinuteLabel(int minute) => '分';
|
||||
|
||||
@override
|
||||
String timerPickerSecond(int second) => '秒';
|
||||
|
||||
@override
|
||||
String get cutButtonLabel => '裁剪';
|
||||
|
||||
@override
|
||||
String get copyButtonLabel => '復制';
|
||||
|
||||
@override
|
||||
String get pasteButtonLabel => '粘貼';
|
||||
|
||||
@override
|
||||
String get selectAllButtonLabel => '全選';
|
||||
}
|
||||
|
||||
Future<CupertinoLocalizations> loadCupertinoLocalizations(Locale locale) {
|
||||
CupertinoLocalizations localizations;
|
||||
if (locale.languageCode == "zh") {
|
||||
switch (locale.countryCode) {
|
||||
case 'HK':
|
||||
case 'TW':
|
||||
localizations = CustomTCCupertinoLocalizations();
|
||||
break;
|
||||
default:
|
||||
localizations = CustomZhCupertinoLocalizations();
|
||||
}
|
||||
} else {
|
||||
localizations = DefaultCupertinoLocalizations();
|
||||
}
|
||||
return SynchronousFuture<CupertinoLocalizations>(localizations);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
> 自此,第十七篇终于结束了!(///▽///)
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190902_Flutter-17/image22)
|
387
Flutter-18.md
387
Flutter-18.md
|
@ -1,387 +0,0 @@
|
|||
作为系列文章的第十八篇,本篇将通过 ScrollPhysics 和 Simulation ,带你深入走进 Flutter 的滑动新世界,为你打开 Flutter 滑动操作的另一扇窗。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
## 一、前言
|
||||
|
||||
如下图所示是Flutter 默认的可滑动 `Widget` 效果,在 Android 和 iOS 上出现了不同的 **滑动速度与边缘拖拽效果** ,这是因为在不同平台上,默认使用了不同的 **`ScrollPhysics` 与 `Simulation`** ,后面我们将逐步介绍这两大主角的实现原理,**最终让你对 Flutter 世界的滑动拖拽进阶到 *“为所欲为”* 的境界。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image1)
|
||||
|
||||
> 下方开始高能干货,请自带茶水食用。
|
||||
|
||||
## 二、 ScrollPhysics
|
||||
|
||||
首先介绍 `ScrollPhysics` ,在 Flutter 官方的介绍中,`ScrollPhysics` 的作用是 **确定可滚动控件的物理特性,** 常见的有以下四大金刚:
|
||||
|
||||
* **`BouncingScrollPhysics`** :允许滚动超出边界,但之后内容会**反弹**回来。
|
||||
* **`ClampingScrollPhysics`** : 防止滚动超出边界,**夹住** 。
|
||||
* **`AlwaysScrollableScrollPhysics`** :始终**响应**用户的滚动。
|
||||
* **`NeverScrollableScrollPhysics`** :**不响应**用户的滚动。
|
||||
|
||||
在开发过程中,一般会通过如下代码进行设置:
|
||||
|
||||
```
|
||||
CustomScrollView(physics: const BouncingScrollPhysics())
|
||||
ListView.builder(physics: const AlwaysScrollableScrollPhysics())
|
||||
GridView.count(physics: NeverScrollableScrollPhysics())
|
||||
```
|
||||
|
||||
但在一般我们都不会主动去设置 **`physics` 属性,** 那么默认情况下,为什么在 Flutter 中的 `ListView` 、`CustomScrollView` 等 `Scrollable` 控件中,在 Android 和 iOS 平台的滚动和边界拖拽效果,会出现如下图所示的平台区别呢?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image2)
|
||||
|
||||
这里的关键就在于 **`ScrollConfiguration`** 和 **`ScrollBehavior`** 。
|
||||
|
||||
### 2.1、ScrollConfiguration 和 ScrollBehavior
|
||||
|
||||
我们知道所有的滑动控件都是通过 `Scrollable` 对触摸进行响应从而进行滑动的。
|
||||
|
||||
如下代码所示,在 `Scrollable` 的 **`_updatePosition`** 方法内,当 `widget.physics == null` 时,**`_physics` 默认是从 `ScrollConfiguration.of(context)` 的 `getScrollPhysics(context)` 方法获取** ,而 **`ScrollConfiguration.of(context)`** 返回的是一个 **`ScrollBehavior`** 对象。
|
||||
|
||||
```
|
||||
// Only call this from places that will definitely trigger a rebuild.
|
||||
void _updatePosition() {
|
||||
_configuration = ScrollConfiguration.of(context);
|
||||
_physics = _configuration.getScrollPhysics(context);
|
||||
if (widget.physics != null)
|
||||
_physics = widget.physics.applyTo(_physics);
|
||||
final ScrollController controller = widget.controller;
|
||||
final ScrollPosition oldPosition = position;
|
||||
if (oldPosition != null) {
|
||||
controller?.detach(oldPosition);
|
||||
scheduleMicrotask(oldPosition.dispose);
|
||||
}
|
||||
_position = controller?.createScrollPosition(_physics, this, oldPosition)
|
||||
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
|
||||
assert(position != null);
|
||||
controller?.attach(position);
|
||||
}
|
||||
```
|
||||
|
||||
**所以默认情况下 ,`ScrollPhysics` 是和 `ScrollConfiguration` 和 `ScrollBehavior` 有关系。**
|
||||
|
||||
那么 **`ScrollBehavior`** 是这么工作的?
|
||||
|
||||
查看 **`ScrollBehavior`** 的源码可知,它的 `getScrollPhysics` 方法中,**默认实现了平台返回了不同的 `ScrollPhysics`** ,所以默认情况下,在不同平台上的滚动和边缘推拽,会出现不一样的效果:
|
||||
|
||||
```
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.iOS:
|
||||
return const BouncingScrollPhysics();
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return const ClampingScrollPhysics();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
前面说过, **`ScrollPhysics` 是确定可滚动控件的物理特性** ,那么如上图所示,**Android 平台上拖拽溢出的蓝色半圆的怎么来的?`ScrollConfiguration` 的 `ScrollBehavior` 是在什么时候被设置的?**
|
||||
|
||||
查看 `ScrollConfiguration` 的源码我们得知, **`ScrollConfiguration` 和 `Theme`、`Localizations` 等一样是 `InheritedWidget`,那么它应该是从上层往下共享的。**
|
||||
|
||||
所以查看 `MaterialApp` 的源码,得到如下代码,可以看到 **`ScrollConfiguration ` 是在 `MaterialApp` 内默认嵌套的,并且通过 `_MaterialScrollBehavior` 设置了 `ScrollBehavior`, 其 override 的`buildViewportChrome ` 方法,就是实现了Android 上溢出拖拽的半圆效果,** 其中 `GlowingOverscrollIndicator` 就是半圆效果的绘制控件。
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
····
|
||||
return ScrollConfiguration(
|
||||
behavior: _MaterialScrollBehavior(),
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
class _MaterialScrollBehavior extends ScrollBehavior {
|
||||
@override
|
||||
TargetPlatform getPlatform(BuildContext context) {
|
||||
return Theme.of(context).platform;
|
||||
}
|
||||
@override
|
||||
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.iOS:
|
||||
return child;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return GlowingOverscrollIndicator(
|
||||
child: child,
|
||||
axisDirection: axisDirection,
|
||||
color: Theme.of(context).accentColor,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
到这里我们就知道了,在默认情况下可滑动控件的 `ScrollPhysics` 是如何配置的:
|
||||
|
||||
- 1、**`ScrollConfiguration` 是一个 `InheritedWidget` 。**
|
||||
- 2、**`MaterialApp` 内部利用 `ScrollConfiguration` 并共享了一个 `ScrollBehavior` 的子类 `_MaterialScrollBehavior`。**
|
||||
- 3、**`ScrollBehavior` 默认根据平台返回了特定的 `BouncingScrollPhysics` 和 `ClampingScrollPhysics` 效果。**
|
||||
- 4、**`_MaterialScrollBehavior` 中针对 Android 平台实现了 `buildViewportChrome` 的蓝色半球拖拽溢出效果。**
|
||||
|
||||
> ps :我们可以通过实现自己的 `ScrollBehavior` , 实现自定义的拖拽溢出效果。
|
||||
|
||||
## 三、ScrollPhysics 工作原理
|
||||
|
||||
**那么 `ScrollPhysics` 是怎么实现滚动和边缘拖拽的呢?** `ScrollPhysics` 默认是没有什么代码逻辑的,它的主要定义方法如下所示:
|
||||
|
||||
```
|
||||
|
||||
/// [position] 当前的位置, [offset] 用户拖拽距离
|
||||
/// 将用户拖拽距离 offset 转为需要移动的 pixels
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset)
|
||||
|
||||
/// 返回 overscroll ,如果返回 0 ,overscroll 就一直是0
|
||||
/// 返回边界条件
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value)
|
||||
|
||||
///创建一个滚动的模拟器
|
||||
Simulation createBallisticSimulation(ScrollMetrics position, double velocity)
|
||||
|
||||
///最小滚动数据
|
||||
double get minFlingVelocity
|
||||
|
||||
///传输动量,返回重复滚动时的速度
|
||||
double carriedMomentum(double existingVelocity)
|
||||
|
||||
///最小的开始拖拽距离
|
||||
double get dragStartDistanceMotionThreshold
|
||||
|
||||
///滚动模拟的公差
|
||||
///指定距离、持续时间和速度差应视为平等的差异的结构。
|
||||
Tolerance get tolerance
|
||||
```
|
||||
|
||||
上方代码标注了 `ScrollPhysics` 各个方法的大致作用,而在前面 [《十三、全面深入触摸和滑动原理》](https://juejin.im/post/5cd54839f265da03b2044c32) 中,我们深入解析过触摸和滑动的原理,大致流程从触摸开始往下传递, 最终触发 `layout` 实现滑动的现象:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image3)
|
||||
|
||||
|
||||
而 `ScrollPhysics` 的工作原理就穿插在其中,其流程如下图所示, 主要的逻辑在于红色标注的的三个方法:
|
||||
|
||||
- **`applyPhysicsToUserOffset`** :通过 physics 将用户拖拽距离 `offset` 转化为 `setPixels`(滚动) 的增量。
|
||||
- **`applyBoundaryConditions`** :通过 physics 计算当前滚动的边界条件。
|
||||
- **`createBallisticSimulation`** : 创建自动滑动的模拟器。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image4)
|
||||
|
||||
|
||||
这三个方法的触发时机在于 `_handleDragUpdate` 、 `_handleDragCancel` 和 `_handleDragEnd` ,也就是拖动过程和拖动结束的时机:
|
||||
|
||||
- **`applyPhysicsToUserOffset` 和 `applyBoundaryConditions` 是在 `_handleDragUpdate` 时被触发的。**
|
||||
- **`createBallisticSimulation` 是在 `_handleDragCancel` 和 `_handleDragEnd` 时被触发的。**
|
||||
|
||||
所以默认的 **`BouncingScrollPhysics`** 和 **`ClampingScrollPhysics`** 最大的差异也在这个三个方法。
|
||||
|
||||
### 3.1、applyPhysicsToUserOffset
|
||||
|
||||
`ClampingScrollPhysics` 默认是没有重载 `applyPhysicsToUserOffset` 方法的,**当 `parent == null` 时,用户的滑动 `offset` 是什么就返回什么:**
|
||||
|
||||
```
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
|
||||
if (parent == null)
|
||||
return offset;
|
||||
return parent.applyPhysicsToUserOffset(position, offset);
|
||||
}
|
||||
```
|
||||
|
||||
`BouncingScrollPhysics` 中对 `applyPhysicsToUserOffset` 方法进行了 `override` ,其中 **用户没有达到边界前,依旧返回默认的 `offset`,当用户到达边界时,通过算法来达到模拟溢出阻尼效果。**
|
||||
|
||||
|
||||
```
|
||||
|
||||
///摩擦因子
|
||||
double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1 - overscrollFraction, 2);
|
||||
|
||||
@override
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
|
||||
assert(offset != 0.0);
|
||||
assert(position.minScrollExtent <= position.maxScrollExtent);
|
||||
|
||||
if (!position.outOfRange)
|
||||
return offset;
|
||||
|
||||
final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
|
||||
final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
|
||||
final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd);
|
||||
final bool easing = (overscrollPastStart > 0.0 && offset < 0.0)
|
||||
|| (overscrollPastEnd > 0.0 && offset > 0.0);
|
||||
|
||||
final double friction = easing
|
||||
// Apply less resistance when easing the overscroll vs tensioning.
|
||||
? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension)
|
||||
: frictionFactor(overscrollPast / position.viewportDimension);
|
||||
final double direction = offset.sign;
|
||||
|
||||
return direction * _applyFriction(overscrollPast, offset.abs(), friction);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2、applyBoundaryConditions
|
||||
|
||||
`ClampingScrollPhysics` 的 `applyBoundaryConditions` 方法中,在计算边界条件值的时候,**滑动值会和边界值相减得到相反的数据,使得滑动边界相对静止,从而达到“夹住”的作用** ,也就是**动态边界** ,所以默认请下 Android 上滚动到了边界就会停止响应。
|
||||
|
||||
```
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
|
||||
return value - position.pixels;
|
||||
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
|
||||
return value - position.pixels;
|
||||
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
|
||||
return value - position.minScrollExtent;
|
||||
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
|
||||
return value - position.maxScrollExtent;
|
||||
return 0.0;
|
||||
}
|
||||
```
|
||||
|
||||
> ps: 前面说过蓝色的半圆是默认的 `ScrollBehavior` 内 `buildViewportChrome` 方法实现的。
|
||||
|
||||
`BouncingScrollPhysics` 中 `applyBoundaryConditions` 直接返回 0 ,**也就是达到 0 是就边界,过了 0 的就是边界外的拖拽效果了。**
|
||||
|
||||
```
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
|
||||
```
|
||||
|
||||
### 3.3、createBallisticSimulation
|
||||
|
||||
|
||||
因为 `createBallisticSimulation` 是在 `_handleDragCancel` 和 `_handleDragEnd` 时触发的,其实就是停止触摸的时候,**当 `createBallisticSimulation` 返回 `null` 时,`Scrllable` 将进入 `IdleScrollActivity` ,也就是停止滚动的状态。**
|
||||
|
||||
如下图所示,完全没有 `Simulation` 的列表滚动,是不会连续滚动的。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image5)
|
||||
|
||||
|
||||
`ClampingScrollPhysics` 的 `createBallisticSimulation` 方法中,**使用了 `ClampingScrollSimulation`(固定) 和 `ScrollSpringSimulation`(弹性) 两种 `Simulation`** ,如下代码所示,理论上只有 `position.outOfRange` 才会触发弹性的回弹效果,但 `ScrollPhysics` 采用了类似 **双亲代理模型** ,其 `parent` 可能会触发 `position.outOfRange` ,所以推测这里才会有 `ScrollSpringSimulation` 补充的判断。
|
||||
|
||||
如下代码可以看出,**只有在 `velocity` 速度大于默认加速度,并且是可滑动范围内,才返回 `ClampingScrollPhysics` 模拟滑动,否则返回 null 进入前面所说的 Idle 停止滑动,这也是为什么普通慢速拖动,不会触发自动滚动的原因。**
|
||||
|
||||
```
|
||||
@override
|
||||
Simulation createBallisticSimulation(
|
||||
ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = this.tolerance;
|
||||
if (position.outOfRange) {
|
||||
double end;
|
||||
if (position.pixels > position.maxScrollExtent)
|
||||
end = position.maxScrollExtent;
|
||||
if (position.pixels < position.minScrollExtent)
|
||||
end = position.minScrollExtent;
|
||||
assert(end != null);
|
||||
return ScrollSpringSimulation(
|
||||
spring,
|
||||
position.pixels,
|
||||
end,
|
||||
math.min(0.0, velocity),
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
if (velocity.abs() < tolerance.velocity) return null;
|
||||
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
|
||||
return null;
|
||||
if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
|
||||
return null;
|
||||
return ClampingScrollSimulation(
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`BouncingScrollPhysics` 的 `createBallisticSimulation` 则简单一些,**只有在结束触摸时,初始速度大于默认加速度或者超出区域,才会返回 `BouncingScrollSimulation` 进行模拟滑动计算,否则经进入前面所说的 Idle 停止滑动。**
|
||||
|
||||
```
|
||||
@override
|
||||
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = this.tolerance;
|
||||
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
|
||||
return BouncingScrollSimulation(
|
||||
spring: spring,
|
||||
position: position.pixels,
|
||||
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
|
||||
leadingExtent: position.minScrollExtent,
|
||||
trailingExtent: position.maxScrollExtent,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
可以看出,在停止触摸时,**列表是否会继续模拟滑动是和 `velocity` 和 `tolerance.velocity` 有关,也就是速度大于指定的加速度时才会继续滑动** ,并且在可滑动区域内 `ClampingScrollSimulation` 和 `BouncingScrollSimulation` 呈现的效果也不一样。
|
||||
|
||||
如下图所示,**第一页面的 `ScrollSpringSimulation` 在停止滚动前是有一定的减速效果的;而第二个页面 `ClampingScrollSimulation` 是直接快速滑动到边界。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image6)
|
||||
|
||||
|
||||
> **事实上,通过选择或者调整 `Simulation` ,就可以对列表滑动的速度、阻尼、回弹效果等实现灵活的自定义。**
|
||||
|
||||
## 四、Simulation
|
||||
|
||||
前面最后说到了,利用 `Simulation` 实现对列表的滑动、阻尼、回弹效果的实现处理,那么 `Simulation` 是如何工作的呢?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image7)
|
||||
|
||||
如上图所示,**在 `Simulation` 的创建是在 `ScrollPositionWithSingleContext` 的 `goBallistic` 方法中被调用的** ,然后通过 `BallisticScrollActivity` 去触发执行。
|
||||
|
||||
```
|
||||
@override
|
||||
void goBallistic(double velocity) {
|
||||
assert(pixels != null);
|
||||
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
|
||||
if (simulation != null) {
|
||||
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
|
||||
} else {
|
||||
goIdle();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `BallisticScrollActivity` 状态中,**`Simulation` 被用于驱动 `AnimationController` 的 `value` ,然后在动画的回调中获取 `Simulation` 计算后得到的 `value` 进行 `setPixels(value)` 实现滚动。**
|
||||
|
||||
|
||||
> 这里又涉及到了动画的绘制机制,动画的机制等新篇再详细说明,简单来说就是 **当系统 `drawFrame` 的 `vsync` 信号到来时,会执行到 `AnimationController` 内部的 `_tick` 方法,从而触发 `_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);` 改变和 ` notifyListeners();` 通知更新。**
|
||||
|
||||
|
||||
对于 `Simulation` 的内部计算逻辑这里就不展开了,大致上可知 **`ClampingScrollSimulation` 的摩擦因子是固定的,而 `BouncingScrollSimulation` 内部的摩擦因子和计算,是和传递的位置有关系。**
|
||||
|
||||
|
||||
**这里需要着重提及的就是,为什么 `BouncingScrollPhysics` 会自动回弹呢?**
|
||||
|
||||
其实也是 `BouncingScrollSimulation` 的功劳,因为 `BouncingScrollSimulation` 构建时,会传递有 `leadingExtent:position.minScrollExtent` 和 ` trailingExtent: position.maxScrollExtent` 两个参数,**在 underscroll 和 overscroll 的情况下,会利用 `ScrollSpringSimulation` 实现弹性的回滚到 `leadingExtent` 和 `trailingExtent` 的动画,从而达到如下图的效果:**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image8)
|
||||
|
||||
## 最后
|
||||
|
||||
到这里 Flutter 的 `ScrollPhysics` 和 `Simulation` 就基本分析完了,严格意义上, `Simulation` 应该是属于动画的部分,但是这里因为`ScrollPhysics` 也放到了一起。
|
||||
|
||||
**总结起来就是 `ScrollPhysics` 中控制了用户触摸转化和边界条件,并且在用户停止触摸时,利用 `Simulation` 实现了自动滚动与溢出回弹的动画效果。**
|
||||
|
||||
|
||||
> 自此,第十八篇终于结束了!(///▽///)
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image9)
|
209
Flutter-19.md
209
Flutter-19.md
|
@ -1,209 +0,0 @@
|
|||
作为系列文章的第十九篇,本篇将科普 Android 和 iOS 平台的打包和提交审核流程。
|
||||
|
||||
因为很多 Flutter 开发人员可能只有单端的开发经验,对于另外一端的打包和提审流程不熟悉,或者是前端人员没有提交审核的经验,所以本篇将科普这一流程,让大家少走弯路。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
|
||||
## 一、Android 打包和审核流程
|
||||
|
||||
|
||||
### 1、打包
|
||||
|
||||
事实上 Androd 的打包和审核流程都相对简单,打包 apk 只需要通过如下命令行就可以完成:
|
||||
|
||||
```
|
||||
flutter build apk --target-platform android-arm64
|
||||
|
||||
flutter build apk --target-platform android-arm64 -t lib/main_prod.dart
|
||||
```
|
||||
|
||||
- 其中 `--target-platform` 是针对打包后的 so 文件, 对需要支持的框架进行选择,因为现在无论是 Goole Play 或者国内平台,都多都有要求应用需要支持 `arm64-v8a` 的 ABI 架构,所以一般打包也会选择指定 `target-platform` 来减小 apk 的体积。
|
||||
|
||||
- `-t` 表示指定其他 `main.dart` 打包,也可以不指定。
|
||||
|
||||
- 另外需要注意,Android 上需要在 `android/app/src/build.gradle` 下配置 `signingConfigs` 来指定打包密钥等信息,具体生成密钥这里就不详说,之后把 `signingConfigs ` 配置到 `buildTypes` 就完成配置。
|
||||
|
||||
```
|
||||
android {
|
||||
····
|
||||
signingConfigs {
|
||||
config {
|
||||
keyAlias "xxxx"
|
||||
keyPassword "xxxx"
|
||||
storeFile file("../keystores/xxxxx.jks")
|
||||
storePassword "xxxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最后需要注意,如果你的 Apk 存在其他类型架构的 so 目录,比如 `armeabi-v7a` 等,那就需要在 `android/app/src/build.gradle` 的 `android { buildTypes {` 下加上 `ndk abiFilters` 进行过滤配置,因为 Android 下需要保证每个 ABI 目录内的 so 文件是完整齐全的,不然可能出现崩溃。
|
||||
|
||||
```
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.config
|
||||
ndk {
|
||||
//设置支持的SO库架构
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
debug {
|
||||
signingConfig signingConfigs.config
|
||||
ndk {
|
||||
//设置支持的SO库架构
|
||||
abiFilters 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最后打包完的 Apk 默认会在如下图所示路径
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image1)
|
||||
|
||||
### 2、提交审核
|
||||
|
||||
其实在 Android 上提交审核是比较简单的,因为 Android 只需要提供 Apk 下载链接就可以直接安装,所以很多厂家都在有自己服务器上直接放上 Apk 文件,但是为了更好的体验和分发,大多数情况下也会选上传到各大应用平台,比如华为上没有上架的话,会出现如下图所示问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image2)
|
||||
|
||||
> 甚至有些 Apk 因为没有上架,会因为 `app_name` 等原因被当作病毒提醒。
|
||||
|
||||
事实上国内的应用市场审核并不麻烦,只是因为平台多且各家条件可能不一样变得比较繁琐,目前主流要求的有:
|
||||
|
||||
- `targetSdkVersion` 28 (9.0);
|
||||
- ABI 需要支持 `arm64-v8a`;
|
||||
- 应用需要针对 AndroidQ(10.0)进行适配,比如文件读取权限变更;
|
||||
- [教育类应用需要备案](https://zhuanlan.zhihu.com/p/94698911);
|
||||
- 需要提供用户隐私协议和权限说明;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image3)
|
||||
|
||||
|
||||
之后就是一些平台的独立审核问题,比如 **360 平台审核要求你的 Apk 需要经过它们的应用加固**(加固后的作用就见仁见智),并且不少平台如**应用宝要求提供应用的版权说明等文件**,这些都是比较磨人的东西。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image4)
|
||||
|
||||
> 当然有些平台你可以不上,但是比如不上应用宝,你就很难获得*微信扫一扫后跳转打开应用和下载的能力*。
|
||||
|
||||
|
||||
另外比如华为平台会有:[根据工信部关于开展 APP 侵害用户权益专项整治工作的通知要求](http://www.miit.gov.cn/n1146295/n1652858/n1652930/n3757020/c7506353/content.html),应用内还需要提供帐号注销服务或销户功能能力。
|
||||
|
||||
可以看出 Android 的审核和条件其实并不繁琐,只是有些平台需要的东西比较磨人,具体需要上架可以根据需求自行斟酌了。
|
||||
|
||||
|
||||
## 二、iOS 打包和审核流程
|
||||
|
||||
|
||||
### 1、打包
|
||||
|
||||
iOS 的打包和审核流程相对复杂点,打包 iOS 首先你需要有*开发者账号、给应用申请和设置有 `Bundle Identifier` 、配置文件、证书等信息*,相信已经到打包阶段了,这系列文件你不会欠缺吧?
|
||||
|
||||
#### 1.1 创建 App Store Connect
|
||||
|
||||
通过登录 https://developer.apple.com 网站,在 `Account` 的 `Certificates,IDs & Profiles` 可以找到你应用的信息,同时在 `App Store Connect` 栏目可以前往 https://appstoreconnect.apple.com 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image5)
|
||||
|
||||
接着在 `我的 App` 按照提示创建应用,填写信息根据业务要求填写即可,这里主要说几个需要关注的点。
|
||||
|
||||
- 1、如下图所示在 App Store 的 App 信息里有一个隐私政策网站输入栏,这个是必填的,一般就是放一个 Html,具体可以参考类似的: https://guoshuyu.cn/home/index/privacy.html
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image6)
|
||||
|
||||
- 2、需要上传应用的截图,一般需要准备 3-5 张预览图,但是这里需要 6.5 寸和 5.5 寸两种,如果还需要支持 iPad 版本那就还需要上传 12.9 的 iPad 图。这里推荐下,**如果没有设计师出稿件,推荐使用模拟器进行截图(注意不要截入 DEBUG 的 Label), 6.5 寸可以用 iPhone 11promax 模拟器,5.5 寸的用 8plus 模拟器,打开具体页面后,按下 command + s 可以保存到桌面**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image7)
|
||||
|
||||
> 这里需要注意,截图的画面不要太简单,最好能替体现应用的具体内容,不然很容易被拒绝,这里同时提供需要尺寸对应的设备型号。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image8)
|
||||
|
||||
- 3、在版本的信息里还有技术支持网站的必填,这个具体可以参考 :https://guoshuyu.cn/home/index/tech.html ,如果此处不符合条件也会出现审核不通过的问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image9)
|
||||
|
||||
- 4、另外如果 App 需要登录,还需要提供用户的测试账号和密码等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image10)
|
||||
|
||||
#### 1.2 打包上传
|
||||
|
||||
打包 flutter iOS 首先需要执行 `flutter build ios` 命令,命令会生成 release 模式的下的 `framework` 文件,之后就可以进入 Xcode 流程。
|
||||
|
||||
如下图所示,首先确保🔨位置不要选中模拟器,之后在 Product > Archive 就会开始导出打包。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image11)
|
||||
|
||||
|
||||
打包成功后可以看到如下界面,找到你最新打包的那一项,选择 `Distribute App` 就可以进入下一步;另外打包过的项目在 Window > Organizer 也可以重新找到。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image12)
|
||||
|
||||
之后如下所示,就选择上传 `App Store Connect` 进行提交准备。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image13)
|
||||
|
||||
> 如果是选择导出测试 ipa 可以选择 `Development`,前提是对应机器的 `UDID` 等信息已经在打包配置文件内。
|
||||
|
||||
之后可以选择 Upload 或者 Export,Export 就是导出后再在本地上传,可以使用 `TransPorter` 工具再单独上传;Upload 就是前面之后直接上传。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image14)
|
||||
|
||||
接着出现的这个页面建议是不要勾选(不要问,问就是百度),然后直接 next,然后选择自动签名,等签名成功后最后点击上传就可以了。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image15)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image16)
|
||||
|
||||
### 2、审核
|
||||
|
||||
上传成功后就,过一段时间可以在`活动`和 `TestFlight` 看到你提交的构建版本,然后你可能会收到如下所示的一封邮件:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image17)
|
||||
|
||||
其中比如 **`ITMS-90683`** 说的是没有在 `plist` 内配置 `NSContactsUsageDescription` 的 key-value,也就是向用户解释你为什么需要用到读取用户联系人的权限。
|
||||
|
||||
诸如此类的还有后几个都是,如果你应用内用到了对应的权限。就需要在 `plist` 配置上对应的 `key-value` 。
|
||||
|
||||
另外就是 `Push Notification Entitlement` 的警告,是说你的应用没有配置推送相关的证书和设置,如果你的应用没有用到对应的功能,比如在 `Developer` 后台看如下图所示的推送是否勾选了,如果勾选了就需要在应用内配置对应的推送服务,iOS 上 APNS 还需要设置对应的推送证书,一般推送证书还会分开发和生产两种,如果没有使用推送可以忽略警告。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image18)
|
||||
|
||||
**还有就是 App 的启动页和 logo 尺寸记得配全,配置不全也会收到对应的警告,这个可能会影响审核。**
|
||||
|
||||
之后在版本信息里选择需要提交的构建版本,之后提交审核即可,一般审核会从等到*审核 > 正在审核 > 审核结果*,这个过程一般在 24 或者 48 小时之内,但是如果赶上了像圣诞节这样的节日,苹果会因为放假放慢审核,另外被拒绝的太多次的话,也会影响审核速度。
|
||||
|
||||
如下图所示,最后提一些审核建议,比如:
|
||||
|
||||
- 前面说过的应用截图要尽量体现应用的主要内容;
|
||||
- 不允许在应用内滥用应用更新提示,比如不允许应用自己跳转下载更新,只能是简单提示后跳转 app store ,如果把握不好尺度干脆在 iOS 上就不加;
|
||||
- 不要在应用内带有 fir.im ,蒲公英等资源、链接、文本和SDK,不然很容易被扫描然后拒绝。
|
||||
|
||||
以上这些都是属于常犯的问题,更多的还请看 :https://developer.apple.com/cn/app-store/review/guidelines/
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image19)
|
||||
|
||||
|
||||
> iOS 还有可以不用上架,只需要用户在手机上信任证书的可以使用 ipa 的开发者账号,但是这类开发者账号现在很难申请得到,并且这类账号的应用需要一年后重新打包一次更新。
|
||||
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200110_Flutter-19/image20)
|
66
Flutter-2.md
66
Flutter-2.md
|
@ -1,19 +1,13 @@
|
|||
作为系列文章的第二篇,本篇将为你着重展示:**如何搭建一个通用的Flutter App 常用功能脚手架,快速开发一个完整的 Flutter 应用**。
|
||||
作为系列文章的第二篇,继[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)之后,本篇将为你着重展示:**如何搭建一个通用的Flutter App 常用功能脚手架,快速开发一个完整的 Flutter 应用**。
|
||||
|
||||
>友情提示:本文所有代码均在 [**GSYGithubAppFlutter**](https://github.com/CarGuo/GSYGithubAppFlutter) ,文中示例代码均可在其中找到,看完本篇相信你应该可以轻松完成如下效果。相关基础还请看[篇章一](https://juejin.im/post/5b631d326fb9a04fce524db2)。
|
||||
|
||||
![我们的目标是!( ̄^ ̄)ゞ](http://img.cdn.guoshuyu.cn/20190604_Flutter-2/image1)
|
||||
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
## 前言
|
||||
|
||||
本篇内容结构如下图,主要分为: **基础控件、数据模块、其他功能** 三部分。每大块中的小模块,除了涉及的功能实现外,对于实现过程中笔者遇到的问题,会一并展开阐述,本系列的最终目的是: **让你感受 Flutter 的愉悦!** 那么就让我们愉悦的往下开始吧!
|
||||
本篇内容结构如下图,主要分为: **基础控件、数据模块、其他功能** 三部分。每大块中的小模块,除了涉及的功能实现外,对于实现过程中笔者遇到的问题,会一并展开阐述。本系列的最终目的是: **让你感受 Flutter 的愉悦!** 那么就让我们愉悦的往下开始吧!(◐‿◑)
|
||||
|
||||
|
||||
![我是简陋的下图](http://img.cdn.guoshuyu.cn/20190604_Flutter-2/image2)
|
||||
|
@ -26,9 +20,11 @@
|
|||
|
||||
### 1、Tabbar控件实现
|
||||
|
||||
Tabbar 页面是常有需求,而在Flutter中: **Scaffold + AppBar + Tabbar + TabbarView** 是 Tabbar 页面的最简单实现,但在加上 `AutomaticKeepAliveClientMixin` 用于页面 *keepAlive* 之后,早期诸如[#11895](https://github.com/flutter/flutter/issues/11895)的问题便开始成为Crash的元凶,直到 *flutter v0.5.7 sdk* 版本修复后,问题依旧没有完全解决,所以无奈最终修改了实现方案。(1.9.1 stable 中已经修复)
|
||||
Tabbar 页面是常有需求,而在Flutter中: **Scaffold + AppBar + Tabbar + TabbarView** 是 Tabbar 页面的最简单实现,但在加上 `AutomaticKeepAliveClientMixin` 用于页面 *keepAlive* 之后,诸如[#11895](https://github.com/flutter/flutter/issues/11895)的问题便开始成为Crash的元凶。直到 *flutter v0.5.7 sdk* 版本修复后,问题依旧没有完全解决,所以无奈最终修改了实现方案。
|
||||
|
||||
目前笔者是通过 **Scaffold + Appbar + Tabbar + PageView** 来组合实现效果,从而解决上述问题。下面我们直接代码走起,首先作为一个Tabbar Widget,它肯定是一个 `StatefulWidget` ,所以我们先实现它的 `State ` :
|
||||
目前笔者是通过 **Scaffold + Appbar + Tabbar + PageView** 来组合实现效果,从而解决上述问题。因为该问题较为常见,所以目前已经单独实现了测试Demo,有兴趣的可以看看 [TabBarWithPageView](https://github.com/CarGuo/TabBarWithPageView)。
|
||||
|
||||
下面我们直接代码走起,首先作为一个Tabbar Widget,它肯定是一个 `StatefulWidget` ,所以我们先实现它的 `State ` :
|
||||
|
||||
```
|
||||
class _GSYTabBarState extends State<GSYTabBarWidget> with SingleTickerProviderStateMixin {
|
||||
|
@ -89,7 +85,7 @@ Tabbar 页面是常有需求,而在Flutter中: **Scaffold + AppBar + Tabbar
|
|||
}
|
||||
```
|
||||
|
||||
如上代码所示,这是一个 *底部 TabBar* 的页面的效果。TabBar 和 PageView 之间通过 `_pageController` 和 `_tabController` 实现 Tab 和页面的同步,通过 `SingleTickerProviderStateMixin ` 实现 Tab 的动画切换效果 *(ps 如果有需要多个嵌套动画效果,你可能需要`TickerProviderStateMixin`)*,从代码中我们可以看到:
|
||||
如上代码所示,这是一个 *底部 TabBar* 的页面的效果。TabBar 和 PageView 之间通过 `_pageController` 和 `_tabController` 实现 Tab 和页面的同步,通过 `SingleTickerProviderStateMixin ` 实现 Tab 的动画切换效果 *(ps 如果有需要多个嵌套动画效果,你可能需要`TickerProviderStateMixin`)*。 从代码中我们可以看到:
|
||||
|
||||
* 手动左右滑动 `PageView ` 时,通过 `onPageChanged` 回调调用 `_tabController.animateTo(index);` 同步TabBar状态。
|
||||
|
||||
|
@ -204,15 +200,13 @@ class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {
|
|||
|
||||
![顶部TabBar效果](http://img.cdn.guoshuyu.cn/20190604_Flutter-2/image4)
|
||||
|
||||
在 TabBar 页面中,一般还会出现:**父页面需要控制 PageView 中子页的需求**,这时候就需要用到`GlobalKey`了,比如 `GlobalKey<PageOneState> stateOne = new GlobalKey<PageOneState>();` ,通过 globalKey.currentState 对象,你就可以调用到 PageOneState 中的公开方法,这里需要注意 `GlobalKey` 实例需要全局唯一。
|
||||
在 TabBar 页面中,一般还会出现:**父页面需要控制 PageView 中子页的需求**。这时候就需要用到`GlobalKey`了。比如 `GlobalKey<PageOneState> stateOne = new GlobalKey<PageOneState>();` ,通过 globalKey.currentState 对象,你就可以调用到 PageOneState 中的公开方法。这里需要注意 `GlobalKey` 需要全局唯一,一般可以在`build` 方法中创建。
|
||||
|
||||
### 2、上下刷新列表
|
||||
|
||||
*毫无争议,必备控件*。
|
||||
*毫无争议,必备控件*。Flutter 中 为我们提供了 `RefreshIndicator` 作为内置下拉刷新控件;同时我们通过给 `ListView` 添加 `ScrollController` 做滑动监听,在最后增加一个 Item, 作为上滑加载更多的 Loading 显示。
|
||||
|
||||
Flutter 中 为我们提供了 `RefreshIndicator` 作为内置下拉刷新控件;同时我们通过给 `ListView` 添加 `ScrollController` 做滑动监听,在最后增加一个 Item, 作为上滑加载更多的 Loading 显示。
|
||||
|
||||
如下代码所示,通过 `RefreshIndicator` 控件可以简单完成下拉刷新工作,这里需要注意一点是:**可以利用 `GlobalKey<RefreshIndicatorState>` 对外提供 `RefreshIndicator` 的 `RefreshIndicatorState`,这样外部就 可以通过 GlobalKey 调用 ` globalKey.currentState.show(); `,主动显示刷新状态并触发 `onRefresh`** 。
|
||||
如下代码所示,通过 `RefreshIndicator` 控件可以简单完成下拉刷新工作。这里需要注意一点是:**可以利用 `GlobalKey<RefreshIndicatorState>` 对外提供 `RefreshIndicator` 的 `RefreshIndicatorState`,这样外部就 可以通过 GlobalKey 调用 ` globalKey.currentState.show(); `,主动显示刷新状态并触发 `onRefresh`** 。
|
||||
|
||||
**上拉加载更多**在代码中是通过 ` _getListCount() ` 方法,在原本的数据基础上,增加实际需要渲染的 item 数量给 ListView 实现的,最后**通过 `ScrollController` 监听到底部,触发 `onLoadMore`**。
|
||||
|
||||
|
@ -354,7 +348,7 @@ class _GSYPullLoadWidgetState extends State<GSYPullLoadWidget> {
|
|||
|
||||
### 4、矢量图标库
|
||||
|
||||
**矢量图标**对笔者是必不可少的,比起一般的 png 图片文件,矢量图标在开发过程中:**可以轻松定义颜色,并且任意调整大小不模糊**。矢量图标库是引入 ttf 字体库文件实现,在 Flutter 中通过 `Icon` 控件,加载对应的 `IconData` 显示即可。
|
||||
**矢量图标**对笔者是必不可少的。比起一般的 png 图片文件,矢量图标在开发过程中:**可以轻松定义颜色,并且任意调整大小不模糊**。矢量图标库是引入 ttf 字体库文件实现,在 Flutter 中通过 `Icon` 控件,加载对应的 `IconData` 显示即可。
|
||||
|
||||
Flutter 中默认内置的 `Icons` 类就提供了丰富的图标,直接通过 `Icons` 对象即可使用,同时个人推荐阿里爸爸的 **iconfont** 。如下代码,通过在 `pubspec.yaml` 中添加字体库支持,然后在代码中创建 `IconData` 指向字体库名称引用即可。
|
||||
|
||||
|
@ -387,8 +381,6 @@ Flutter 中默认内置的 `Icons` 类就提供了丰富的图标,直接通过
|
|||
|
||||
Flutter 中的页面跳转是通过 `Navigator` 实现的,路由跳转又分为:**带参数跳转和不带参数跳转**。不带参数跳转比较简单,默认可以通过 MaterialApp 的路由表跳转;而带参数的跳转,参数通过跳转页面的构造方法传递。常用的跳转有如下几种使用:
|
||||
|
||||
> 新版本开始可以给 `pushNamed` 设置 `arguments` 参数,然后在新页面通过 `ModalRoute.of(context).settings.arguments` 获取。
|
||||
|
||||
```
|
||||
///不带参数的路由表跳转
|
||||
Navigator.pushNamed(context, routeName);
|
||||
|
@ -405,7 +397,7 @@ Navigator.push(context, new MaterialPageRoute(builder: (context) => new NotifyPa
|
|||
});
|
||||
```
|
||||
|
||||
同时我们可以看到,Navigator 的 push 返回的是一个 `Future`,这个`Future ` 的作用是**在页面返回时被调用的**。也就是你可以通过 `Navigator` 的 `pop` 时返回参数,之后在 `Future` 中可以的监听中处理页面的返回结果。
|
||||
  同时我们可以看到,Navigator 的 push 返回的是一个 `Future`,这个`Future ` 的作用是**在页面返回时被调用的**。也就是你可以通过 `Navigator` 的 `pop` 时返回参数,之后在 `Future` 中可以的监听中处理页面的返回结果。
|
||||
|
||||
|
||||
```
|
||||
|
@ -424,10 +416,10 @@ static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
|
|||
|
||||
### 1、网络请求
|
||||
|
||||
当前 Flutter 网络请求封装中,国内最受欢迎的就是 [Dio](https://github.com/flutterchina/dio) 了,Dio 封装了网络请求中的**数据转换、拦截器、请求返回**等。如下代码所示,通过对 Dio 的简单封装即可快速网络请求,真的很简单,更多的可以查 Dio 的官方文档,这里就不展开了。
|
||||
当前 Flutter 网络请求封装中,国内最受欢迎的就是 [Dio](https://github.com/flutterchina/dio) 了,Dio 封装了网络请求中的**数据转换、拦截器、请求返回**等。如下代码所示,通过对 Dio 的简单封装即可快速网络请求,真的很简单,更多的可以查 Dio 的官方文档,这里就不展开了。(真的不是懒(˶‾᷄ ⁻̫ ‾᷅˵))
|
||||
|
||||
```
|
||||
///创建网络请求对象,主要最好吧 dio 实例全局单里
|
||||
///创建网络请求对象
|
||||
Dio dio = new Dio();
|
||||
Response response;
|
||||
try {
|
||||
|
@ -442,7 +434,7 @@ static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
|
|||
|
||||
### 2、Json序列化
|
||||
|
||||
在 Flutter 中,json 序列化是有些特殊的,不同与 JS ,比如使用上述 Dio 网络请求返回,如果配置了返回数据格式为 **json** ,实际上的到会是一个Map。而 Map 的 key-value 使用,在开发过程中并不是很方便,所以你需要对Map 再进行一次转化,转为实际的 Model 实体。
|
||||
在 Flutter 中,json 序列化是有些特殊的。不同与 JS ,比如使用上述 Dio 网络请求返回,如果配置了返回数据格式为 **json** ,实际上的到会是一个Map。而 Map 的 key-value 使用,在开发过程中并不是很方便,所以你需要对Map 再进行一次转化,转为实际的 Model 实体。
|
||||
|
||||
所以 `json_serializable` 插件诞生了, [中文网Json](https://flutterchina.club/json/) 对其已有一段教程,这里主要补充说明下具体的使用逻辑。
|
||||
|
||||
|
@ -519,17 +511,17 @@ abstract class _$TemplateSerializerMixin {
|
|||
|
||||
```
|
||||
|
||||
## *注意:新版json序列化中做了部分修改,代码更简单了,详见demo* 。
|
||||
## *注意:新版json序列化中做了部分修改,代码更简单了,详见demo*
|
||||
|
||||
### 3、Redux
|
||||
### 3、Redux State
|
||||
|
||||
相信在前端领域、*Redux* 并不是一个陌生的概念,作为**全局状态管理机**,用于 Flutter 中再合适不过。如果你没听说过,**Don't worry**,简单来说就是:**它可以跨控件管理、同步State** 。所以 [flutter_redux](https://pub.flutter-io.cn/packages/flutter_redux) 等着你征服它。
|
||||
相信在前端领域、*Redux* 并不是一个陌生的概念。作为**全局状态管理机**,用于 Flutter 中再合适不过。如果你没听说过,**Don't worry**,简单来说就是:**它可以跨控件管理、同步State** 。所以 [flutter_redux](https://pub.flutter-io.cn/packages/flutter_redux) 等着你征服它。
|
||||
|
||||
大家都知道在 Flutter 中 ,是通过实现 `State` 与 `setState` 来渲染和改变 `StatefulWidget` 的,如果使用了`flutter_redux` 会有怎样的效果?
|
||||
大家都知道在 Flutter 中 ,是通过实现 `State` 与 `setState` 来渲染和改变 `StatefulWidget` 的。如果使用了`flutter_redux` 会有怎样的效果?
|
||||
|
||||
比如把用户信息存储在 `redux` 的 `store` 中, 好处在于: **比如某个页面修改了当前用户信息,所有绑定的该 State 的控件将由 Redux 自动同步修改,State 可以跨页面共享。**
|
||||
比如把用户信息存储在 `redux` 的 `store` 中, 好处在于: **比如某个页面修改了当前用户信息,所有绑定的该 State 的控件将由 Redux 自动同步修改。State 可以跨页面共享。**
|
||||
|
||||
更多 Redux 的详细就不再展开,后续会有详细介绍,接下来我们讲讲 flutter_redux 的使用,在 redux 中主要引入了 *action、reducer、store* 概念。
|
||||
更多 Redux 的详细就不再展开,接下来我们讲讲 flutter_redux 的使用。在 redux 中主要引入了 *action、reducer、store* 概念。
|
||||
|
||||
* action 用于定义一个数据变化的请求。
|
||||
* reducer 用于根据 action 产生新状态
|
||||
|
@ -556,7 +548,7 @@ GSYState appReducer(GSYState state, action) {
|
|||
}
|
||||
```
|
||||
|
||||
下面是上方使用的 `UserReducer` 的实现,这里主要通过 `TypedReducer` 将 reducer 的处理逻辑与定义的 Action 绑定,最后通过 `combineReducers` 返回 `Reducer<State>` 对象应用于上方 Store 中。
|
||||
下面是上方使用的 `UserReducer` 的实现。这里主要通过 `TypedReducer` 将 reducer 的处理逻辑与定义的 Action 绑定,最后通过 `combineReducers` 返回 `Reducer<State>` 对象应用于上方 Store 中。
|
||||
|
||||
```
|
||||
/// redux 的 combineReducers, 通过 TypedReducer 将 UpdateUserAction 与 reducers 关联起来
|
||||
|
@ -778,15 +770,21 @@ Android启动页,在 `android/app/src/main/res/drawable/launch_background.xml`
|
|||
|
||||
## 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : [https://github.com/CarGuo](https://github.com/CarGuo)
|
||||
* 本文代码 :[https://github.com/CarGuo/GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
|
||||
#### 完整开源项目推荐:
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
#### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
|
||||
![我们还会再见的](http://img.cdn.guoshuyu.cn/20190604_Flutter-2/image9)
|
||||
|
|
135
Flutter-20.md
135
Flutter-20.md
|
@ -1,135 +0,0 @@
|
|||
作为系列文章的第二十篇,本篇将结合[官方的技术文档](https://github.com/flutter/flutter/wiki/Android-Platform-Views#text-input)科普 Android 上 `PlatformView` 的实现逻辑,并且解释为什么在 Android 上 `PlatformView` 的键盘总是有问题。
|
||||
|
||||
> 为什么 iOS 上相对稳定,文中也做了对应介绍。
|
||||
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
|
||||
## 1、为什么有 PlatformView
|
||||
|
||||
因为 Flutter 的实现在概念上类似于 Android 上的 `WebView`,Flutter 是通过将 `Widget Tree` 转化为纹理后通过 Skia 实现控件绘制,这造就了优秀的跨平台效果的同时,也带来了不可逆的兼容问题。
|
||||
|
||||
### 1.1、无法集成原生平台控件
|
||||
|
||||
**这就像 WebView 一样,Flutter UI 不会转换为 Android 控件,而是由 Flutter Engine 使用 Skia 直接在 `SurfaceView` 上渲染出来**。
|
||||
|
||||
这意味着默认情况下 Flutter UI 永远不会包含 Android Native 的控件,也就是说无法在 Flutter 中集成如 `WebView` 或 `MapView` 这些常用的控件。
|
||||
|
||||
**所以为解决这个问题,Flutter 创建了一个叫 `AndroidView` 的控件逻辑, 开发者使用该 Widget 可以将 Android Native 组件嵌入到 Flutter UI 中**。
|
||||
|
||||
### 1.2、AndroidView 的实现
|
||||
|
||||
`AndroidView` 这个 Widget 需要和 Flutter 相结合才能完整显示:**在 Flutter 中通过将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中
|
||||
,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到**。
|
||||
|
||||
> `VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,一般在副屏显示或者录屏场景下会用到。`VirtualDisplay` 会将虚拟显示区域的内容渲染在一个 `Surface` 上。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200225_Flutter-20/image1)
|
||||
|
||||
如上图所示,**简单来说就是原生控件的内容被绘制到内存里,然后 Flutter Engine 通过相对应的 `textureId` 就可以获取到控件的渲染数据并显示出来**。
|
||||
|
||||
通过从 `VirtualDisplay` 输出中获取纹理,并将其和 Flutter 原有的 UI 渲染树混合,使得 Flutter 可以在自己的 Flutter Widget tree 中以图形方式插入 Android 原生控件。
|
||||
|
||||
### 1.3、 有其他可以实现的方式吗?
|
||||
|
||||
在 iOS 平台上就不使用类似 `VirtualDisplay` 的方法,而是**通过将 Flutter UI 分为两个透明纹理来完成组合:一个在 iOS 平台视图之下,一个在其上面**。
|
||||
|
||||
所以这样的好处就是:需要在“iOS平台”视图下方呈现的Flutter UI,最终会被绘制到其下方的纹理上;而需要在“平台”上方呈现的Flutter UI,最终会被绘制在其上方的纹理。**它们只需要在最后组合起来就可以了**。
|
||||
|
||||
通常这种方法更好,因为这意味着 Android Native View 可以直接添加到 Flutter 的 UI 层次结构中。
|
||||
|
||||
但是,Android 平台并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:*当 iOS 视图向下移动 `2px` 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 `2px`*。
|
||||
|
||||
但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。**如果强行以这种方式在 Android 上使用,最终将产生很多如 `AndroidView` 与 Flutter UI 不同步的问题**。
|
||||
|
||||
> 有关此替代方法的详细讨论,详见 https://flutter.dev/go/nshc
|
||||
|
||||
## 2、相关问题和解决方法
|
||||
|
||||
尽管前面可以使用 `VirtualDisplay` 将 Android 控件嵌入到 Flutter UI 中 ,但这种 `VirtualDisplay` 的介入还有其他麻烦的问题需要处理。
|
||||
|
||||
### 2.1、触摸事件
|
||||
|
||||
**默认情况下, `PlatformViews` 是没办法接收触摸事件**。
|
||||
|
||||
因为 `AndroidView` 其实是被渲染在 `VirtualDisplay` 中 ,而每当用户点击看到的 `"AndroidView"` 时,其实他们就真正”点击的是正在渲染的 `Flutter` 纹理 。**用户产生的触摸事件是直接发送到 Flutter View 中,而不是他们实际点击的 `AndroidView`**。
|
||||
|
||||
#### 2.1.1、解决方法
|
||||
|
||||
- `AndroidView` 使用 Flutter Framework 中的点击测试逻辑来检测用户的触摸是否在需要特殊处理的区域内。
|
||||
|
||||
> 类似可见:[《Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)》](https://juejin.im/post/5cd54839f265da03b2044c32)
|
||||
|
||||
- 当触摸成功时会向 [Android embedding](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L595) 发送一条消息,其中包含 touch 事件的详细信息。
|
||||
|
||||
- 在 [Android embedding](https://github.com/flutter/flutter/blob/068fa84/packages/flutter/lib/src/rendering/platform_view.dart#L595) 中,该事件的坐标最后会匹配到 `AndroidView` 在 `VirtualDisplay` 中的坐标,然后会创建一个 `MotionEvent` 用于 描述触摸的新控件,并将其转发到内部 `VirtualDisplay` 中真实的 `AndroidView` 中进行响应。
|
||||
|
||||
#### 2.1.2、局限性
|
||||
|
||||
- 该实现逻辑会将新的 `MotionEvent` 直接分发给 `AndroidView` ,如果这个 View 又派生了其他视图,那么就可能会出现触摸信息被发送到错误的位置。
|
||||
|
||||
- `MotionEvent` 的转化过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。
|
||||
|
||||
|
||||
### 2.2、文字输入
|
||||
|
||||
**通常,`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的位置会始终被认为是 `unfocused` 的状态**。
|
||||
|
||||
Android 目前不提供任何 API 来动态设置或更改的焦点 `Window`,`Flutter` 中`focused` 的 `Window` 通常是实际持有“真实的” Flutter 纹理和 UI ,并且对于用户直接可见。
|
||||
|
||||
而 **`InputConnections`(如何在 Android 中 输入文本)在 `unfocused` 的 View 中通常是会被丢弃**。
|
||||
|
||||
#### 2.2.1、解决方法
|
||||
|
||||
- **Flutter 重写了 `checkInputConnectionProxy` 方法,这样 Android 会认为 Flutter View 是作为 `AndroidView` 和输入法编辑器(IME)的代理**,这样 Android 就可以从 Flutter View 中获取到 `InputConnections` 然后作用于 `AndroidView` 上面。
|
||||
|
||||
- **在 Android Q 开始 `InputMethodManager`(IMM)改为每个 `Window` 自己实例化而不是全局单例**。因此之前幼稚的“设置代理”的模式在 Q 开始不起作用。为了进一步解决这个问题,**Flutter 创建了一个 `Context` 的子类, 该子类返回的内容与 Flutter View 中的 `IMM` 相同,这样就不会需要在查询 `IMM` 时需要返回的真实的 `Window`**。这意味着当 Android 需要 `IMM` 时,`VirtualDisplay` 仍然会使用 Flutter View 的 `IMM` 作为代理。
|
||||
|
||||
- 当要求 `AndroidView` 提供 `InputConnection` 时,它会检查 `AndroidView` 是否确实是输入的目标。如果是,那 [`AndroidView` 中的 `InputConnection` 将被获取并返回给 Android ](https://github.com/flutter/engine/blob/036ddbb0ee6858ae532df82a2747aa93faee4487/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java#L206) 。
|
||||
|
||||
- Android 认为 Flutter View 是 `focused` 且可用的,因此 `AndroidView` 的 `InputConnection` 可以成功被获取并使用。
|
||||
|
||||
#### 2.2.2、 Platforview 中的 WebView 键盘输入
|
||||
|
||||
**在 Android N 之前的版本上 `WebView` 输入比较复杂,因为它们具有自己内部的逻辑来创建和设置输入连接,而这些输入连接并没有完全遵循 Android 的协议**。在 `flutter_webview` 插件中,还需要添加其他解决方法以便在可以在 `WebView` 启用文本输入。
|
||||
|
||||
- [设置一个代理 View ,该 View 与 `WebView` 在相同的线程上侦听输入连接](https://github.com/flutter/plugins/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L113)。如果没有此功能,`WebView` 将在内部消耗所有 `InputConnection` 的呼叫,而不会通知 Flutter View 代理。
|
||||
- [在代理线程中,返回 Flutter View 以创建输入。](https://github.com/flutter/plugins/blob/27f3de3e1e6ed1c0f2cd23b0d1477ff3f0955aaa/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java#L67)。
|
||||
- [`WebView` 失去焦点时,将输入连接重置回 Flutter 线程](https://github.com/flutter/plugins/blob/27f3de3/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java#L128)。这样可以防止文本输入“卡”在 WebView 内。
|
||||
|
||||
#### 2.2.3、局限性
|
||||
|
||||
- 通常这个逻辑取决于 Android 的内部行为,并且可能会十分脆弱,比如: *[1.12 版本下针对华为等设备出现的键盘输入异常等问题](https://github.com/flutter/flutter/issues/51254)*。
|
||||
|
||||
- 某些文本功能仍然不可用,例如:*“复制”和“共享”对话框当前不可用*。
|
||||
|
||||
|
||||
## 3、总结
|
||||
|
||||
`PlatformView` 的实现模式增加了 Flutter 的生命力和活力,但是相对的也引出了很多问题,比如 [#webview-keyboard](https://github.com/flutter/flutter/labels/p%3A%20webview-keyboard)、[#webview](https://github.com/flutter/flutter/labels/p%3A%20webview)、[#platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) 相关的 issue 专题高居不下,并且如 [webview_flutter](https://pub.dev/packages/webview_flutter) 插件的文档所述:
|
||||
|
||||
> 该插件依赖 Flutter 的新机制来嵌入 Android 和 iOS 视图。由于该机制当前处于开发人员预览中,因此该插件也应被视为开发人员预览。
|
||||
>
|
||||
> `webview_flutter` 的键盘支持也尚未准备好用于生产,因为 Webview 中的键盘支持目前还处于实验性的阶段。
|
||||
|
||||
**所以到这里相信你应该知道,为什么 Flutter 中的 `PlatforView` 在 Android 上如此之难兼容,并且键盘输入问题会那么多坑了**。
|
||||
|
||||
> 自此,第二十篇终于结束了!(///▽///)
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200225_Flutter-20/image2)
|
344
Flutter-200.md
344
Flutter-200.md
|
@ -1,344 +0,0 @@
|
|||
今天很高兴地宣布 Flutter 2 的发布,距离Flutter 1.0 的发布已经两年多了,但是在很短的时间内, Flutter 已经关闭了 24,541 issues,并合并了 765 个贡献者的 17,039个PR。
|
||||
|
||||
自去年9月 Flutter 1.22 发布以来,Flutter 已经关闭了 5807 issues 并合并了 298位贡献者的 4091 个PR。
|
||||
|
||||
|
||||
### Web
|
||||
|
||||
截止到今天,Flutter 的 Web 支持已经从 Beta 过渡到稳定 Channel 。在此初始稳定版本中,Flutter 在 Web 平台下将代码的可重用性提高到另一个层次,因此现在当开发者创建Flutter 应用程序时,Web 只是该应用程序的另一个可支持的目标设备。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image1)
|
||||
|
||||
通过利用 Web 平台的众多优势,Flutter 为构建丰富的交互式Web应用程序奠定了基础,Flutter 主要专注于性能和渲染保真度的改进,除了 HTML renderer 之外,我们还添加了一个新的基于 CanvasKit 的渲染器,另外我们还添加了特定于 Web 的功能,例如 [Link Widget](https://pub.dev/documentation/url_launcher/latest/link/Link-class.html) 以确保在浏览器中运行的应用感觉像Web应用。
|
||||
|
||||
> 你可以在Flutter的 Web 支持博客文章中找到有关此稳定版本的更多详细信息: https://medium.com/flutter/web-post-d6b84e83b425
|
||||
|
||||
### Sound Null Safety
|
||||
|
||||
空安全声明是 Dart 语言的重要补充,它通过区分可空类型和非可空类型进一步增强了类型系统,这使开发人员能够防止 null 错误崩溃。
|
||||
|
||||
通过将空检查合并到类型系统中,开发者可以在开发过程中捕获这些错误,从而防止生产崩溃。从 Flutter 2 开始,包含 Dart 2.12 的稳定版完全支持空安全声明。
|
||||
|
||||
> 有关更多详细信息,请参见 Dart 2.12博客文章:https://medium.com/dartlang/announcing-dart-2-12-499a6e689c87
|
||||
|
||||
pub.dev 包存储库已经发布了 1,000 多个空安全软件包,其中包括 Dart,Flutter,Firebase 和 Material 团队的数百个软件包。
|
||||
|
||||
> 如果你是软件包作者,请查看迁移指南并考虑立即进行迁移: https://dart.dev/null-safety/migration-guide
|
||||
|
||||
### Desktop
|
||||
|
||||
在此版本中,Flutter的桌面支持已经发布在稳定 Channel,这意味着 Flutter 已经准备好让你尝试一下使用它开发桌面应用,当然你可以将其视为“beta snapshot”,以预览将于今年晚些时候发布的最终稳定版本。
|
||||
|
||||
> PS :所以这是为了赶 KPI 才发布的么?
|
||||
|
||||
为了使 Flutter 桌面达到发布的质量,Flutter 从大小上进行了改进,从确保文本编辑像在每个受支持的平台上的原生体验一样开始,包括诸如:[text selection pivot points](https://github.com/flutter/flutter/pull/71756)以及 [a keyboard event once it’s been handled](https://github.com/flutter/flutter/issues/33521)的能力。
|
||||
|
||||
在处理完键盘事件后,在鼠标输入端现在可以立即开始使用高精度定点设备进行拖动,而不必等待处理触摸输入时所需的延迟。
|
||||
|
||||
此外内置的上下文菜单已添加到 Materia l和 Cupertino 设计语言的 `TextField` 和 `TextFormField` 控件中。
|
||||
|
||||
最后,[grab handles have been added](https://github.com/flutter/flutter/pull/74299) 已经被添加到 `ReorderableListView` 控件中。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image2)
|
||||
|
||||
|
||||
|
||||
`ReorderableListView` 现在具有可拖住的功能,可通过鼠标轻松拖放,
|
||||
在移动项目中 `ReorderableListView` 要求用户长按才能启动拖动,这哥场景在移动设备上适用,但是很少有台式机用户会想到用鼠标长按某个项目来移动它,因此此版本还包括适用于鼠标或触摸输入的移动方式。另外常用功能的另一项改进是更新的滚动条,该滚动条可以正确显示桌面形状因素。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image3)
|
||||
|
||||
|
||||
此版本还包括一个更新的 `Scrollbar` 控件,该控件在桌面环境中非常使用,包括拖动预览、单击轨道以上下滚动页面以及在鼠标悬停在鼠标的任何部分上时显示轨道的功能。
|
||||
|
||||
此外由于 `Scrollbar` 是使用新 `ScrollbarTheme` 主题,因此开发者可以设置其样式以使其与应用程序的外观和风格相匹配。
|
||||
|
||||
对于其他特定于桌面的功能,此版本还启用了 Flutter 应用程序的命令行参数处理功能,以便可以使用诸如 Windows File Explorer 中的文件双击之类的简单操作来打开应用程序中的文件。
|
||||
|
||||
另外 Flutter 在致力于在应用在 Windows 和 macOS 的上调整大小变得更加流畅,并为国际用户启用IME(输入法编辑器)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image4)
|
||||
|
||||
> 此外,我们还提供了更新的文档,介绍了开始准备将桌面应用程序部署到特定操作系统商店时需要执行的操作。https://flutter.dev/desktop#distribution
|
||||
|
||||
|
||||
在尝试使用 Flutter 桌面 Beta 时,开发者可以通过按预期方式切换到 Beta 通道并根据flutter.dev 上的指导为目标平台设置配置标志来访问它。此外,我们还制作了稳定通道上可用的 beta 快照。
|
||||
|
||||
如果开发者使用 `flutter config` 启用某个桌面配置设置(例如 `enable-macos-desktop`),则可以尝试桌面支持的 beta 功能,而不必经历漫长的过程如删除 Flutter SDK 后才能转移到 beta 频道等,这非常适合尝试一下或将桌面支持用作简单的 “Flutter Emulator.”。
|
||||
|
||||
|
||||
但是,如果您选择停留在 Stable Channel 上以访问桌面 Beta,则不会像切换到Beta或dev频道那样快地获得新功能或错误修复,因此如果你正在积极地针对 Windows,macOS 或 Linux,我们建议您切换到可更快提供更新的渠道。
|
||||
|
||||
|
||||
> 当 Flutter 桌面的第一个完整的生产版本快完全发布时,我们知道还有更多工作要做,包括对与本机顶级菜单集成的支持,更接近各个平台的文本编辑体验以及可访问性支持,以及常规的
|
||||
|
||||
|
||||
### New iOS features
|
||||
|
||||
此版本带来了与 iOS 相关的178个PR合并,包括 [#23495](https://github.com/flutter/engine/pull/23495)(将状态恢复带到iOS),[#67781](https://github.com/flutter/flutter/pull/67781)(它满足了长期存在的直接从命令行构建IPA而无需打开Xcode的要求),以及 [#69809](https://github.com/flutter/flutter/pull/69809),更新了CocoaPods版本以匹配最新工具。
|
||||
|
||||
此外,Cupertino 设计语言实现中还添加了一些 iOS 控件,如新的`CupertinoSearchTextField` 提供了 iOS 搜索栏 UI。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image5)
|
||||
|
||||
|
||||
`CupertinoFormSection`,`CupertinoFormRow` 和 `CupertinoTextFormFieldRow` 控件更容易满足 iOS 的设计风格。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image6)
|
||||
|
||||
除了适用于iOS的功能外,在着色器和动画方面,我们还将继续 iOS 和 Flutter 的性能改进,iOS仍然是 Flutter 的主要平台,我们将继续努力带来重要的新功能和性能改进。
|
||||
|
||||
### New widgets: Autocomplete and ScaffoldMessenger
|
||||
|
||||
此版本的 Flutter 附带了两个新控件: `AutocompleteCore` 和 `ScaffoldMessenger`。
|
||||
|
||||
`AutocompleteCore` 表示将自动完成功能纳入 Flutter 应用程序所需的基本
|
||||
功能。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image7)
|
||||
|
||||
> 自动完成是 Flutter 经常需要的功能,如果对完整功能的设计感到好奇,请查看自动完成设计文档。 https://docs.google.com/document/d/1fV4FDNdcza1ITU7hlgweCDUZdWyCqd-rjz_J7K2KkfY/
|
||||
|
||||
|
||||
`ScaffoldMessenger` 用来处理许多与 `SnackBar` 相关的问题,包括能够轻松创建`SnackBar` 以响应 `AppBar` 动作;创建 `SnackBars` 以在 `Scaffold` 过渡之间持久存在的能力;能够在 `SnackBars` 完成时显示 `SnackBars`的能力,即使用户已导航到具有其他 `Scaffold` 的页面,也将执行异步操作。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image8)
|
||||
|
||||
|
||||
所有这些优点可以从现在开始使用几行代码来显示 SnackBars :
|
||||
|
||||
```
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(SnackBar(content: Text(‘I can fly.’)));
|
||||
|
||||
```
|
||||
|
||||
### Multiple Flutter instances with Add-to-App
|
||||
|
||||
|
||||
从与许多 Flutter 开发人员的交谈中我们了解到,许多人没有使用 Flutter 开发全新应用程序的想法,但他们可以通过将 Flutter 添加到现有的 iOS 和 Android 应用程序中来利用 Flutter。
|
||||
|
||||
此功能称为 [Add-to-App](https://flutter.dev/docs/development/add-to-app),是在两个移动平台上重用 Flutter 代码同时仍保留现有本机代码库的绝佳方法。但是在此之前我们有时会听到,不清楚如何将第一个页面集成到 Flutter 中。
|
||||
|
||||
将 Flutter 和本机交织在一起会使得导航状态难以维护,并且在视图级别集成多个 Flutter 会占用大量内存。
|
||||
|
||||
过去其他 Flutter 实例的存储成本与第一个实例相同,在Flutter 2 中,我们将创建额外的Flutter 引擎的静态内存成本降低了约 99%,每个实例约为 180kB。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image9)
|
||||
|
||||
|
||||
支持此功能的新 API 可以在 beta 通道上预览,并在 flutter.dev 上记录了一系列演示此新模式的示例项目,通过此更改,我们不再犹豫建议在本机应用程序中创建Flutter引擎的多个实例。
|
||||
|
||||
|
||||
### Flutter Fix
|
||||
|
||||
每当任何框架成熟并使用越来越多的代码库聚集用户时,随着时间的推移,趋势就是避免对框架API进行任何更改,以避免破坏越来越多的代码行。
|
||||
|
||||
Flutter 2 拥有超过 500,000 个Flutter开发人员,涉及的平台数量越来越多,因此它很快就面临了这样的问题。但是为了使我们能够随着时间的推移不断改进 Flutter,我们希望能够对 API 进行重大更改。问题是如何在不中断开发人员的情况下继续改进Flutter API?
|
||||
|
||||
我们的答案是 Flutter Fix:(http://flutter.dev/docs/development/tools/flutter-fix)
|
||||
|
||||
Flutter Fix 是事物的组合。首先,`dartCLI ` 工具有一个新的命令行选项,名为 dart fix ,它知道在哪里可以查找已弃用的 API 列表以及如何使用这些 API 更新代码。其次它是可用修补程序本身的列表,最后它是针对 VS Code,IntelliJ 和 Android Studio IDE 的更新的 Flutter 扩展集,它们知道哪些改变是属于公开相同的内容,展示可用的修复程序列表,如带小划线的快速修复程序,可帮助您单击鼠标来更改代码。
|
||||
|
||||
举例来说,假设您的应用中包含以下代码行:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image10)
|
||||
|
||||
由于不推荐使用此构造函数的参数,因此应将其替换为以下内容:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image11)
|
||||
|
||||
|
||||
即使你熟悉的 Flutter 中所有被弃用的内容,但在代码中必须进行的更改数量也就越大,应用所有修补程序的难度就越大,并且更容易出错。
|
||||
|
||||
人类在这类重复性任务上并不擅长。但是计算机很擅长;通过执行以下命令,就可以看到我们如何在整个项目中进行的所有修复:
|
||||
|
||||
```
|
||||
$ dart fix --dry-run
|
||||
```
|
||||
|
||||
如果您想批量应用它们,可以轻松地这样做:
|
||||
|
||||
```
|
||||
dart fix --apply
|
||||
```
|
||||
|
||||
或者,如果您想在自己喜欢的IDE中以交互方式应用这些修补程序,也可以这样做:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image12)
|
||||
|
||||
多年来我们一直在将旧的API标记为已弃用,但是现在有了关于何时删除实际已弃用的API的政策,Flutter 2 是我们第一次这样做。
|
||||
|
||||
即使我们尚未捕获所有已弃用的API作为数据来提供 Flutter Fix,但我们仍将继续从先前已弃用的 API 中添加更多信息,并将在未来的重大更改中继续这样做。
|
||||
|
||||
我们的目标是尽最大努力使 Flutter 的 API 达到最佳状态,同时还要使您的代码保持最新。
|
||||
|
||||
### Flutter DevTools
|
||||
|
||||
为了清楚说明 DevTools 是用于调试Flutter应用程序的工具,我们在调试 Flutter 应用程序时将其重命名为 Flutter DevTools 。此外我们还做了很多工作,以使其达到 Flutter 2的生产质量。
|
||||
|
||||
在您启动 DevTools 之前也可以帮助开发者解决问题的新功能是:Android Studio,IntelliJ 或 Visual Studio Code 能够在出现常见异常时发出通知,并提供将其引入DevTools 中以帮助您调试的功能它。
|
||||
|
||||
例如,以下内容显示您的应用程序中已引发溢出异常,该异常会在 Visual Studio Code 中弹出一个选项,用于调试DevTools中的问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image13)
|
||||
|
||||
按下该按钮可打开出现问题控件上的 DevTools 中的 Flutter Inspector,因此可以对其进行修复。
|
||||
|
||||
今天我们仅针对布局溢出异常执行此操作,但我们的计划是针对所有常见异常提供这种处理,DevTools可以解决这些异常。
|
||||
|
||||
一旦运行了 DevTools,选项卡上的新错误标记将帮助开发者跟踪应用程序中的特定问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image14)
|
||||
|
||||
DevTools 的另一个新功能是能够轻松查看分辨率比显示的图像高的图像,这有助于跟踪过多的应用程序大小和内存使用情况。要启用此功能,请在 Flutter Inspector 中启用“反转超大图像”。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image15)
|
||||
|
||||
现在,当开发者显示分辨率明显大于其显示尺寸的图像时,该图像将上下颠倒显示,以便在开发者的应用中轻松查找。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image16)
|
||||
|
||||
此外,根据大众的需求,除了在 Flutter Inspector 的“布局资源管理器”中显示有关灵活布局的详细信息外,我们还添加了显示固定布局的功能,使开发者能够调试各种布局。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image17)
|
||||
|
||||
这还不是全部,这只是Flutter DevTools 2中更多新功能的摘要:
|
||||
- 在 Flutter 框架图中添加了平均 FPS 信息并提高了可用性;
|
||||
- 用红色错误标签在网络事件探查器中调出失败的网络请求。
|
||||
- 更快的新内存视图图表,更小且更易于使用,其中包括用于描述特定时间活动的新悬浮卡。
|
||||
- 将搜索和过滤添加到“日志记录”选项卡。
|
||||
- 在启动DevTools之前跟踪日志,因此启动时可以查看完整的日志历史记录。
|
||||
- 将“性能”视图重命名为“ CPU Profiler”,以使其更清楚地提供什么功能。
|
||||
- 向 CPU Profiler 火焰图添加了时序网格。
|
||||
- 将“时间轴”视图重命名为“性能”,以便更清楚地了解其提供的功能。
|
||||
|
||||
### Android Studio / IntelliJ扩展
|
||||
|
||||
用于 IntelliJ 系列 IDE 的 Flutter 插件也为 Flutter 2 提供了许多新功能,首先有一个新的项目向导,它与 IntelliJ 中的新向导样式匹配。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image18)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image19)
|
||||
|
||||
另外如果正在 Linux 上使用 IntelliJ 或 Android Studio 对从 Snap Store 安装的Flutter SDK 进行编程,则 Flutter 快照路径已添加到已知 SDK 路径列表中,这使Flutter 快照的用户可以更轻松地在“设置”中配置 Flutter SDK。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image20)
|
||||
|
||||
### Visual Studio代码扩展
|
||||
|
||||
Visual Studio Code 的 Flutter 扩展也对 Flutter 2 进行了改进,从许多测试增强功能开始,包括重新运行仅失败的测试的功能。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image21)
|
||||
|
||||
经过两年的开发,对 Dart 的LSP(语言服务器协议)支持现已作为默认方式提供给 Dart 分析器,以将其集成到 Flutter 扩展的 Visual Studio Code 中。
|
||||
|
||||
LSP 支持对 Flutter 开发进行了许多改进,包括能够在当前Dart文件中应用某种类型的所有修复程序,并使代码完成生成完整的函数调用(包括括号和必需的参数)的功能。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image22)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image23)
|
||||
|
||||
LSP 的支持不仅限于Dart,它还支持 pubspec.yaml 和 analysis_options.yaml 文件中的代码完成。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image24)
|
||||
|
||||
|
||||
### DartPad updated to support Flutter 2
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image25)
|
||||
|
||||
现在,开发者可以尝试使用 Flutter 的新的空安全版本,而无需离开自己喜欢的浏览器。
|
||||
|
||||
### Ecosystem updates
|
||||
|
||||
Flutter 的开发经验不仅包含框架和工具,还包括其他内容,它还包括适用于 Flutter 应用程序的各种软件包和插件。
|
||||
|
||||
自上一次 Flutter 稳定版本发布以来,该领域也发生了很多事情。例如,在 `camera` 和`video_player` 插件之间已合并了将近30个PR,以大大提高两者的质量。
|
||||
|
||||
另外,如果你是一个 `Firebase` 的用户,我们很高兴地宣布最流行的插件质量已经得到了新的提升,包括空安全的支持,以及全套的支持 Android,iOS,Web,和 MacOS,这些插件包括:
|
||||
|
||||
- Core
|
||||
- Authentication
|
||||
- Cloud Firestore
|
||||
- Cloud Functions
|
||||
- Cloud Messaging
|
||||
- Cloud Storage
|
||||
- Crashlytics
|
||||
|
||||
另外,如果您正在寻找应用程序的崩溃报告,则可能需要考虑 Sentry,该公司已经发布了适用于Flutter应用程序的新SDK:https://blog.sentry.io/2021/03/03/with-flutter-and-sentry-you-can-put-all-your-eggs-in-one-repo/ 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image26)
|
||||
|
||||
借助Sentry的Flutter SDK,可以实时收到在 Android,iOS或本机平台上发生的错误的通知。
|
||||
|
||||
此外,如果还没有看到 Flutter Community 的 “plus” 插件,则需要将其签出。
|
||||
|
||||
他们分叉了 Flutter 团队最初开发的许多受欢迎的插件,并添加了 null 安全支持,对其他平台的支持和一整套全新的文档,以及开始修复 flutter/plugins 存储库中的适当问题,该插件包括以下内容:
|
||||
|
||||
- Android Alarm+
|
||||
- Android Intent+
|
||||
- Battery+
|
||||
- Connectivity+
|
||||
- Device Info+
|
||||
- Network Info+
|
||||
- Package Info+
|
||||
- Sensors+
|
||||
- Share+
|
||||
|
||||
> http://plus.fluttercommunity.dev/
|
||||
|
||||
此时与 Flutter 兼容的软件包和插件集的数量超过 15,000,这会使得很开发者难找到优质的的软件包和插件。
|
||||
|
||||
因此,我们会发布发布点数(静态分析得分),受欢迎程度,喜欢度,并且,对于特别高的质量,会发布那些特别标记为 Flutter Favorite 的包装,为了及时应对 Flutter 2,我们在收藏夹列表中添加了几个新软件包:
|
||||
|
||||
- animated_text_kit
|
||||
- bottom_navy_bar
|
||||
- chopper
|
||||
- font_awesome_flutter
|
||||
- flutter_local_notifications
|
||||
- just_audio
|
||||
|
||||
|
||||
最后但并非最不重要的一点是,对于对软件包的是否适用于 Flutter 的最新版本感兴趣的软件包作者或软件包用户,您将需要访问 Codemagic 的新 pub.green 网站。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-200/image27)
|
||||
|
||||
Codemagic 新的 pub.green 网站显示了最新 Flutter 版本与顶级软件包的兼容性,
|
||||
pub.green 网站测试了 pub.dev 上可用的 Flutter 和 Dart 软件包与不同Flutter版本的兼容性。
|
||||
|
||||
> 详细可见:https://blog.codemagic.io/pub-green/
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
我们对 Flutter 2 进行了以下重大更改,其中许多可以使用 dart fix 命令或所选 IDE 中的快速修复程序自动缓解:
|
||||
|
||||
- [#61366](https://github.com/flutter/flutter/pull/61366) Continue the clipBehavior breaking change。
|
||||
- [#66700](https://github.com/flutter/flutter/pull/66700) 默认 `FittedBox`的 `clipBehavior` 为无。
|
||||
- [#68905](https://github.com/flutter/flutter/pull/68905) 从 `Cupertino` 颜色分辨率 API 删除 nullOk 参数
|
||||
- [#69808](https://github.com/flutter/flutter/pull/69808) 从 `Scaffold.of` 和 `ScaffoldMessenger.of` 删除 nullOk 参数
|
||||
- [#68910](https://github.com/flutter/flutter/pull/68910) 从 `Router.of ` 中删除 nullOk 参数,并使其返回不可为空的值
|
||||
- [#68911](https://github.com/flutter/flutter/pull/68911) 添加 `maybeLocaleOf` 到本地化
|
||||
- [#68736](https://github.com/flutter/flutter/pull/68736) 在 `Media.queryOf` 删除 nullOK
|
||||
- [#68917](https://github.com/flutter/flutter/pull/68917) 从 `Focus.of` 、 `FocusTraversalOrder.of` 和 `FocusTraversalGroup.of` 中删除 nullOk 参数
|
||||
- [#68921](https://github.com/flutter/flutter/pull/68921) 从 `Shortcuts.of` ,`Actions.find` 和 `Actions.handler` 中删除 nullOk 参数
|
||||
- [#68925](https://github.com/flutter/flutter/pull/68925) 从`AnimatedList.of` 和 `SliverAnimatedList.of` 中删除nullOk参数
|
||||
- [#69620](https://github.com/flutter/flutter/pull/69620) 从 `BuildContex` 中删不推荐使用的方法
|
||||
- [#70726](https://github.com/flutter/flutter/pull/70726) 从 `Navigator.of` 中删除 nullOk 参数,并添加 `Navigator.maybeOft`
|
||||
- [#72017](https://github.com/flutter/flutter/pull/72017) 删除不推荐使用的`CupertinoTextThemeData.brightness`
|
||||
- [#72395](https://github.com/flutter/flutter/pull/72395) 从 `HoverEvent` 中删除不建议使用的 `PointerEnterEvent`,`PointerExitEvent` 。
|
||||
- [#72532](https://github.com/flutter/flutter/pull/72532) 删除不建议使用的`showDialog.child`
|
||||
- [#72890](https://github.com/flutter/flutter/pull/72890) 删除不推荐使用的`Scaffold.resizeToAvoidBottomPadding`
|
||||
- [#72893](https://github.com/flutter/flutter/pull/) 删除不推荐使用的`WidgetsBinding`.[`deferFirstFrameReport`,`allowFirstFrameReport`]
|
||||
- [#72901](https://github.com/flutter/flutter/pull/#72901) 删除不推荐使用的 `StatefulElement.inheritFromElement`
|
||||
- [#72903](https://github.com/flutter/flutter/pull/#72903) 删除不推荐使用的 `Element` 方法
|
||||
- [#73604](https://github.com/flutter/flutter/pull/73604) 删除不建议使用的 `CupertinoDialog`
|
||||
- [#73745](https://github.com/flutter/flutter/pull/73745) 从 [CupertinoSliver] `NavigationBar` 删除不推荐使用的 `actionForegroundColor`
|
||||
- [73746](https://github.com/flutter/flutter/pull/#73746) 删除不赞成使用的 `ButtonTheme.bar`
|
||||
- [#73747](https://github.com/flutter/flutter/pull/73747) 删除 span deprecations
|
||||
- [#73748](https://github.com/flutter/flutter/pull/73748) 删除弃用的 `RenderView.scheduleInitialFrame`
|
||||
- [#73749](https://github.com/flutter/flutter/pull/73749) 删除不赞成使用的 `Layer.findAll`
|
||||
- [#75657](https://github.com/flutter/flutter/pull/75657) 从 `Localizations.localeOf` 删除残留的 nullOk 参数
|
||||
- [#74680](https://github.com/flutter/flutter/pull/74680) 从`Actions.invoke` 删除 nullOk ,添加 `Actions.maybeInvoke` .
|
|
@ -1,74 +0,0 @@
|
|||
|
||||
最近 Flutter 发布了官方关于 2022 的 [战略](https://docs.google.com/document/d/e/2PACX-1vTI9X2XHN_IY8wDO4epQSD1CkRT8WDxf2CEExp5Ef4Id206UOMopkYqU73FvAnnYG6NAecNSDo9TaEO/pub) 和 [路线图](https://github.com/flutter/flutter/wiki/Roadmap) ,本篇主要针对内容做一些总结和解读,给正在使用 Flutter 或者正打算使用 Fluter 的人做个参考。
|
||||
|
||||
|
||||
## 总结陈述相关
|
||||
|
||||
目前 Flutter 社区的发展已经很大,官方统计在过去的一年里,**数据上 Flutter 已经基本超过超过其他跨平台框架,成为最受欢迎的移动端跨平台开发工具,截至 2022 年 2 月,有近 50 万个应用程序使用 Flutter**。
|
||||
|
||||
在过去一年里, Flutter 社区有数千人为该项目提供了贡献和支持,从个人到 `Canonical`、`Microsoft`、`ByteDance` 和`阿里巴巴`等大公司都对 Flutter 提供了不少帮助。
|
||||
|
||||
当然 Flutter 也不是尽善尽美,Flutter 虽然也有被一些大型应用所使用,例如:`SHEIN` (顶级时尚零售商)、`微信`(10 亿+用户 IM 应用程序)和` PUBG` (7.5 亿+玩家大逃杀游戏),但是它在大型应用中使用并不明显。
|
||||
|
||||
因为在大型应用中有大量的历史需求和代码,还有重构所需的成本限制,**使用 Flutter 进行混合开发其实支持不如 `Jetpack Compose`** ,是的, Flutter 官方表示:
|
||||
|
||||
> *相反,Android 的 Jetpack Compose 产品非常适合这一类产品,因为它可以轻松地基于 JVM 的框架,逐步添加到现有的 Android 应用程序中*。
|
||||
|
||||
|
||||
**也就是从官方的角度看,混合开发下,特别是 Android 平台,其实 `Compose` 更适合混合开发,感觉这也是 `add-to-app` 的维护和推进到现在好像并不乐观的原因**。
|
||||
|
||||
|
||||
## 展望
|
||||
|
||||
**Flutter 在 2022 年首要的战略目标就是月活跃用户的增长**,官方的理念就是:
|
||||
|
||||
|
||||
> 一个 `SDK` 再优秀,如果只有少部分人在使用,那它也不能体现价值;但是一个 `SDK` 即使平庸,但是有大量开发者使用,那也会拥有一个健康繁荣的生态氛围,这样使用框架的人才能从中受益。
|
||||
|
||||
|
||||
### 1、提升开发体验
|
||||
|
||||
**目前谷歌认为虽然 Dart 和 Flutter 相对原生平台会给开发者带来学习成本,但是也会带来了不错的收益**,另外得益于社区良好的发展和维护,目前 Flutter 和 Dart 丰富的开发工具和文档,可以让开发人员顺利地迁移到 Flutter,所以 Dart 和 Flutter 未来的开发体验会越来越好。
|
||||
|
||||
而官方未来也将持续优化 Flutter 的一些开发体验,例如: DevTools 中有助于调试性能问题的新功能。
|
||||
|
||||
|
||||
**但是事实上在新版 `Android Stuio Bumblebee` 和 `Flutter 插件` 的体验目前并不好**,一些 `Plugin` 上功能的消失或者无法正常使用的问题其实比较让人难受,例如:**出现 iOS 运行提示 Cocospod 不存在,但是其实已经安装的问题**。
|
||||
|
||||
虽然这种问题通过其他方式解决并不麻烦,比如命令行运行,但是显得就很低级。目前 `Android Stuio Bumblebee Patch1` 已经解决了该问题,**但是这次更新无法增量,只能全量覆盖**。另外
|
||||
|
||||
还有关于 Flutter 插件上关于 module 的自动导入消失的等等 ···
|
||||
|
||||
> 可以看到 Flutter 已经投入很多精力和时间在改进 Flutter 的开发体验,作为目前最大体量的跨平台开发框架,时不时有些瑕疵还是可以理解,希望 2022 Flutter 能更加注重细节的问题。
|
||||
|
||||
### 2、跨平台
|
||||
|
||||
关于跨平台上体验上,在 iOS 和 Android 上 Flutter 目前已经可以说得做到了不错的体验和质量,而随着 Window 第一个稳定版本已经发布了,今年的大目标之一就是继续提高 Web 和 Desktop 相关的开发体验和交付质量。
|
||||
|
||||
另外 Android 开发人员正在对 `Material` 的进行支持,同时对新硬件功能和外形尺寸等进行适配,以及与 Jetpack 库和 Kotlin 代码的更好集成也都是计划之一。
|
||||
|
||||
最后 Flutter 在 Web 上目前已经使用了 `CanvasKit`、`WebGPU`、`AOM` 和`带有 GC 的 WebAssembly` 等新技术,在新的一年也会继续维护和提高 Web 的交付质量,例如: **在 Web 上的 hotload 以及改进 Dart-to-JS 的使用场景**。
|
||||
|
||||
|
||||
## 2022 年路线图
|
||||
|
||||
- **正如前面解读的,Desktop 的投入是最主要的目标之一,从 Windows开始,然后是 Linux 和 macOS ,将尽快推进 Desktop 平台全部 Stable**。
|
||||
|
||||
|
||||
- 关于 Web 方面,在高兼容和提高性能的同时,也打算尝试让 Flutter Web 可以嵌入到其他非 Flutter 的 HTML 页面里。
|
||||
|
||||
- Flutter 的 framewok 和 engine 方面, **Material 3 和支持从单个 `Isolate` 渲染到多个窗口会是很重要的一部分内容,另外还有一个大头就是改进各个平台上本编辑的体验**。其实个人认为,Flutter 在文本编辑和键盘方便的体验确实还不够好。
|
||||
|
||||
|
||||
- Dart 语言方法主要是 2022 可能会引入静态元编程,另外语法改进,计划扩展 Dart 的编译工具链以支持编译到 `Wasm` 也在计划当中。
|
||||
|
||||
- 关于 Jank 问题,Flutter 已经开始考虑重构着色器了,其中 **2022 年 iOS 将会迁移到新的着色器框架上,并在后续再移植到其他平台**,但是从 [#85737](https://github.com/flutter/flutter/issues/85737) 上看,任重道远,希望不会有什么大坑吧~
|
||||
|
||||
|
||||
## 最后
|
||||
|
||||
总的来看, Flutter 团队的今年的投入和计划还是占比不低,Flutter 社区的活跃也加速着 Flutter 的成熟。
|
||||
|
||||
但是同样随着 Flutter 项目越来越庞大,例如 [#95343](https://github.com/flutter/flutter/issues/95343) 这样的问题可能也会越来越多,因为使用的人多了,需要面对的需求就多了,细节的把控上就更具备挑战性。
|
||||
|
||||
同样就如官方所说,虽然 Flutter 团队有在推进混合开发的支持,但是 Flutter 从根源实现上,对于混合开发其实就很不友好,例如:`渲染同步`、`路由同步`、`混合内存优化`、`混合数据共享`等等,不是说不支持,而是成本和收获的问题,所以可以看到最近这些稳定版本,Flutter 关于 `add-to-app` 的提及相对较少,目前看来 Flutter 官方主要还是计算在**维护好 Andorid 和 iOS 平台的基础上,继续优化 Web 的质量和推进 Desktop 全平台正式发布更主流。**
|
641
Flutter-21.md
641
Flutter-21.md
|
@ -1,641 +0,0 @@
|
|||
作为系列文章的第二十一篇,本篇将通过不一样的角度来介绍 Flutter Framework 的整体渲染原理,深入剖析 Flutter 中构成 Layer 后的绘制流程,让开发者对 Flutter 的渲染原理和实现逻辑有更清晰的认知。
|
||||
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
## 一、Layer 相关的回顾
|
||||
|
||||
先回顾下,我们知道在 Flutter 中的控件会经历 `Widget` -> `Element` -> `RenderObject` -> `Layer` 这样的变化过程,而其中 `Layer` 的组成由 `RenderObject` 中的 `isRepaintBoundary` 标志位决定。
|
||||
|
||||
> 当调用 `setState` 时,`RenderObject` 就会往上的父节点去查找,根据 `isRepaintBoundary `是否为 true,会决定是否从这里开始往下去触发重绘,换个说法就是:**确定要更新哪些区域**。
|
||||
>
|
||||
|
||||
比如 `Navigator` 跳转不同路由页面,每个页面内部就有一个 `RepaintBoundary` 控件,这个控件对应的 `RenderRepaintBoundary` 内的 `isRepaintBoundary` 标记位就是为 `true` ,从而路由页面之间形成了独立的 `Layer` 。
|
||||
|
||||
**所以相关的 `RenderObject` 在一起组成了 `Layer`,而由 `Layer` 构成的 `Layer Tree` 最后会被提交到 Flutter Engine 绘制出画面**。
|
||||
|
||||
|
||||
那 `Layer` 是怎么工作的?它的本质又是什么? Flutter Framework
|
||||
中 `Layer` 是如何被提交到 Engine 中?
|
||||
|
||||
## 二、Flutter Framework 中的绘制
|
||||
|
||||
带着前面 `Layer` 的问题,我们先做个假设:如果抛开 Flutter Framework 中封装好的控件,我们应该如何绘制出一个画面?或者说如何创建一个 `Layer` ?
|
||||
|
||||
举个例子,如下代码所示,运行后可以看到一个居中显示的 100 x 100 的蓝色方块,并且代码里没有用到任何 `Widget` 、 `RenderObject` 甚至 `Layer`,而是使用了 **`PictureRecorder` 、`Canvas` 、 `SceneBuilder` 这些相对陌生的对象完成了画面绘制,并且在最后执行的是 `window.render`** 。
|
||||
|
||||
```
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
void main() {
|
||||
ui.window.onBeginFrame = beginFrame;
|
||||
|
||||
ui.window.scheduleFrame();
|
||||
}
|
||||
|
||||
void beginFrame(Duration timeStamp) {
|
||||
final double devicePixelRatio = ui.window.devicePixelRatio;
|
||||
|
||||
///创建一个画板
|
||||
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
||||
|
||||
///基于画板创建一个 Canvas
|
||||
final ui.Canvas canvas = ui.Canvas(recorder);
|
||||
canvas.scale(devicePixelRatio, devicePixelRatio);
|
||||
|
||||
var centerX = ui.window.physicalSize.width / 2.0;
|
||||
var centerY = ui.window.physicalSize.height / 2.0;
|
||||
|
||||
///画一个 100 的剧中蓝色
|
||||
canvas.drawRect(
|
||||
Rect.fromCenter(
|
||||
center: Offset.zero,
|
||||
width: 100,
|
||||
height: 100),
|
||||
new Paint()..color = Colors.blue);
|
||||
|
||||
///结束绘制
|
||||
final ui.Picture picture = recorder.endRecording();
|
||||
|
||||
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder()
|
||||
..pushOffset(centerX, centerY)
|
||||
..addPicture(ui.Offset.zero, picture)
|
||||
..pop();
|
||||
|
||||
ui.window.render(sceneBuilder.build());
|
||||
}
|
||||
```
|
||||
|
||||
因为在 Flutter 中 `Canvas` 的创建是必须有 `PictureRecorder` ,而 `PictureRecorder` 顾名思义就是创建一个图片用于记录绘制,所以在上述代码中:
|
||||
- 先是创建了 `PictureRecorder`;
|
||||
- 然后使用 `PictureRecorder` 创建了 `Canvas` ;
|
||||
- 之后使用 `Canvas` 绘制蓝色小方块;
|
||||
- 结束绘制后通过 `SceneBuilder` 的 `pushOffset` 和 `addPicture` 加载了绘制的内容;
|
||||
- 通过 `window.render` 绘制出画面。
|
||||
|
||||
> 需要注意⚠️: `render` 方法被限制必须在 `onBeginFrame` 或 `onDrawFrame` 中调用,所以上方代码才会有 `window.onBeginFrame = beginFrame;`。在官方的[examples/layers/raw/](https://github.com/flutter/flutter/blob/449f4a6673f6d89609b078eb2b595dee62fd1c79/examples/layers/raw/) 下有不少类似的例子。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image1)
|
||||
|
||||
|
||||
可以看到 Flutter Framework 在底层绘制的最后一步是 `window.render` ,而如下代码所示: `render` 方法需要的参数是 `Scene` 对象,并且 `render` 方法是一个 `native` 方法,**说明 Flutter Framework 最终提交给 Engine 的是一个 `Scene`**。
|
||||
|
||||
|
||||
```
|
||||
void render(Scene scene) native 'Window_render';
|
||||
```
|
||||
|
||||
**那 `Scene ` 又是什么?前面所说的 `Layer` 又在哪里呢?它们之间又有什么样的关系?**
|
||||
|
||||
|
||||
## 三、Scene 和 Layer 之间的苟且
|
||||
|
||||
在 Flutter 中 `Scene` 其实是一个 `Native` 对象,它对应的其实是 `Engine` 中的 [`scene.cc`](https://github.com/flutter/engine/blob/78a1c7ebf9adfc988b66381245502536695bfd75/lib/ui/compositing/scene.cc#L44) 结构,而 Engine 中的 `scene.cc` 内包含了一个 `layer_tree_` 用于绘制,所以**首先可以知道`Scene` 在 `Engine` 是和 `layer_tree_` 有关系**。
|
||||
|
||||
然后就是在 **Flutter Framework 中 `Scene` 只能通过 `SceneBuilder` 构建**,而 `SceneBuilder` 中存在很多方法比如: `pushOffset`、`pushClipRect`、`pushOpacity` 等,这些方法的执行后,可以通过 Engine 会创建出一个对应的 `EngineLayer`。
|
||||
|
||||
```
|
||||
OffsetEngineLayer pushOffset(double dx, double dy, { OffsetEngineLayer oldLayer }) {
|
||||
assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushOffset'));
|
||||
final OffsetEngineLayer layer = OffsetEngineLayer._(_pushOffset(dx, dy));
|
||||
assert(_debugPushLayer(layer));
|
||||
return layer;
|
||||
}
|
||||
EngineLayer _pushOffset(double dx, double dy) native 'SceneBuilder_pushOffset';
|
||||
```
|
||||
|
||||
**所以 `SceneBuilder` 在 `build` 出 `Scene` 之前,可以通过 `push` 等相关方法产生 `EngineLayer`**, 比如前面的蓝色小方块例子,`SceneBuilder` 就是通过 `pushOffset` 创建出对应的图层偏移。
|
||||
|
||||
接着看 Flutter Framework 中的 `Layer` ,如下代码所示,在 `Layer` 默认就存在 `EngineLayer` 参数,所以可以得知 `Layer` 肯定和 `SceneBuilder` 有一定关系。
|
||||
|
||||
```
|
||||
@protected
|
||||
ui.EngineLayer get engineLayer => _engineLayer;
|
||||
|
||||
@protected
|
||||
set engineLayer(ui.EngineLayer value) {
|
||||
_engineLayer = value;
|
||||
if (!alwaysNeedsAddToScene) {
|
||||
|
||||
if (parent != null && !parent.alwaysNeedsAddToScene) {
|
||||
parent.markNeedsAddToScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.EngineLayer _engineLayer;
|
||||
|
||||
/// Override this method to upload this layer to the engine.
|
||||
///
|
||||
/// Return the engine layer for retained rendering. When there no
|
||||
/// corresponding engine layer, null is returned.
|
||||
|
||||
@protected
|
||||
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
|
||||
|
||||
```
|
||||
|
||||
其次在 `Layer` 中有一个关键方法: **`addToScene`**,先通过注释可以得知这个方法是由子类实现,并且执行后可以得到一个 `EngineLayer` ,并且这个方法需要一个 `SceneBuilder` ,而查询该方法的实现恰好就有`OffsetLayer` 和 `PictureLayer` 等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image2)
|
||||
|
||||
所以如下代码所示,在 `OffsetLayer` 和 `PictureLayer` 的 `addToScene` 方法实现中可以看到:
|
||||
|
||||
- `PictureLayer` 调用了 `SceneBuilder` 的 `addPicture`;
|
||||
- `OffsetLayer` 调用了 `SceneBuilder` 的 `pushOffset` ;
|
||||
|
||||
```
|
||||
class PictureLayer extends Layer {
|
||||
···
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
|
||||
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
|
||||
}
|
||||
···
|
||||
}
|
||||
|
||||
class OffsetLayer extends ContainerLayer {
|
||||
···
|
||||
OffsetLayer({ Offset offset = Offset.zero }) : _offset = offset;
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
|
||||
engineLayer = builder.pushOffset(
|
||||
layerOffset.dx + offset.dx,
|
||||
layerOffset.dy + offset.dy,
|
||||
oldLayer: _engineLayer as ui.OffsetEngineLayer,
|
||||
);
|
||||
addChildrenToScene(builder);
|
||||
builder.pop();
|
||||
}
|
||||
···
|
||||
}
|
||||
```
|
||||
|
||||
所以到这里 **`SceneBuilder` 和 `Layer` 通过 `EngineLayer` 和 `addToScene` 方法成功关联起来,而 `window.render` 提交的 `Scene` 又是通过 `SceneBuilder` 构建得到,所以如下图所示, `Layer` 和 `Scene` 就这样“苟且”到了一起**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image3)
|
||||
|
||||
|
||||
对面前面的蓝色小方块代码,如下代码所示,这里修改为使用 `Layer` 的方式实现,可以看到这样的实现更接近 Flutter Framework 的实现:**通过 `rootLayer` 一级一级 `append` 构建出`Layer` 树,而 `rootLayer` 调用 `addToScene` 方法后,因为会执行 `addChildrenToScene` 方法,从而往下执行 child `Layer` 的 `addToScene`**。
|
||||
|
||||
```
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
void main() {
|
||||
ui.window.onBeginFrame = beginFrame;
|
||||
|
||||
ui.window.scheduleFrame();
|
||||
}
|
||||
|
||||
void beginFrame(Duration timeStamp) {
|
||||
final double devicePixelRatio = ui.window.devicePixelRatio;
|
||||
|
||||
///创建一个画板
|
||||
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
||||
|
||||
///基于画板创建一个 Canvas
|
||||
final ui.Canvas canvas = ui.Canvas(recorder);
|
||||
canvas.scale(devicePixelRatio, devicePixelRatio);
|
||||
|
||||
var centerX = ui.window.physicalSize.width / 2.0;
|
||||
var centerY = ui.window.physicalSize.height / 2.0;
|
||||
|
||||
///画一个 100 的剧中蓝色
|
||||
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 100, height: 100),
|
||||
new Paint()..color = Colors.blue);
|
||||
|
||||
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
|
||||
|
||||
OffsetLayer rootLayer = new OffsetLayer();
|
||||
|
||||
|
||||
OffsetLayer offsetLayer = new OffsetLayer(offset: Offset(centerX, centerY));
|
||||
rootLayer.append(offsetLayer);
|
||||
|
||||
PictureLayer pictureLayer = new PictureLayer(Rect.zero);
|
||||
pictureLayer.picture = recorder.endRecording();
|
||||
offsetLayer.append(pictureLayer);
|
||||
|
||||
|
||||
rootLayer.addToScene(sceneBuilder);
|
||||
|
||||
|
||||
ui.window.render(sceneBuilder.build());
|
||||
}
|
||||
|
||||
```
|
||||
## 四、Layer 的品种
|
||||
|
||||
这里额外介绍下 Flutter 中常见的 `Layer`,如下图所示,一般 Flutter 中 `Layer` 可以分为 `ContainerLayer` 和非 `ContainerLayer` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image4)
|
||||
|
||||
`ContainerLayer` 是可以具备子节点,也就是带有 `append` 方法,大致可以分为:
|
||||
|
||||
- 位移类(`OffsetLayer`/`TransformLayer`);
|
||||
- 透明类(`OpacityLayer`)
|
||||
- 裁剪类(`ClipRectLayer`/`ClipRRectLayer`/`ClipPathLayer`);
|
||||
- 阴影类 (`PhysicalModelLayer`)
|
||||
|
||||
为什么这些 `Layer` 需要是 `ContainerLayer` ?**因为这些 `Layer` 都是一些像素合成的操作,其本身是不具备“描绘”控件的能力,就如前面的蓝色小方块例子一样,如果要呈现画面一般需要和 `PictureLayer` 结合**。
|
||||
|
||||
> 比如 `ClipRRect` 控件的 `RenderClipRRect` 内部,在 `pushClipRRect` 时可以会创建 `ClipRRectLayer` ,而新创建的 `ClipRRectLayer ` 会通过 `appendLayer` 方法触发 `append` 操作添加为父 `Layer` 的子节点。
|
||||
|
||||
而非 `ContainerLayer` 一般不具备子节点,比如:
|
||||
|
||||
- `PictureLayer` 是用于绘制画面,Flutter 上的控件基本是绘制在这上面;
|
||||
- `TextureLayer` 是用于外界纹理,比如视频播放或者摄像头数据;
|
||||
- `PlatformViewLayer` 是用于 iOS 上 `PlatformView` 相关嵌入纹理的使用;
|
||||
|
||||
举个例子,控件绘制时的 `Canvas` 来源于 `PaintingContext` , 而如下代码所示 `PaintingContext` 通过 `_repaintCompositedChild` 执行绘制后得到的 `Picture` 最后就是提交给所在的 `PictureLayer.picture`。
|
||||
|
||||
```
|
||||
void stopRecordingIfNeeded() {
|
||||
if (!_isRecording)
|
||||
return;
|
||||
_currentLayer.picture = _recorder.endRecording();
|
||||
_currentLayer = null;
|
||||
_recorder = null;
|
||||
_canvas = null;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 五、Layer 的内外兼修
|
||||
|
||||
|
||||
了解完 `Layer` 是如何提交绘制后,接下来介绍的就是 `Layer` 的刷新和复用。
|
||||
|
||||
我们知道当 `RenderObject` 的 `isRepaintBoundary` 为 `ture` 时,Flutter Framework 就会自动创建一个 `OffsetLayer` 来“承载”这片区域,而 `Layer` 内部的画面更新一般不会影响到其他 `Layer` 。
|
||||
|
||||
**那 `Layer` 是如何更新?这就涉及了 `Layer` 内部的 `markNeedsAddToScene` 和 `updateSubtreeNeedsAddToScene` 这两个方法。**
|
||||
|
||||
如下代码所示,`markNeedsAddToScene` 方法其实就是把 `Layer` 内的 `_needsAddToScene` 标记为 `true` ; 而 `updateSubtreeNeedsAddToScene ` 方法就是遍历所有 child `Layer`,通过递归调用 `updateSubtreeNeedsAddToScene()` 判断是否有 `child` 需要 `_needsAddToScene` ,如果是那就把自己也标记为 `true`。
|
||||
|
||||
```
|
||||
@protected
|
||||
@visibleForTesting
|
||||
void markNeedsAddToScene() {
|
||||
// Already marked. Short-circuit.
|
||||
if (_needsAddToScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
_needsAddToScene = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSubtreeNeedsAddToScene() {
|
||||
super.updateSubtreeNeedsAddToScene();
|
||||
Layer child = firstChild;
|
||||
while (child != null) {
|
||||
child.updateSubtreeNeedsAddToScene();
|
||||
_needsAddToScene = _needsAddToScene || child._needsAddToScene;
|
||||
child = child.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
是不是和 `setState` 调用 `markNeedsBuild` 把自己标志为 `_dirty` 很像?**当 `_needsAddToScene` 等于 `true` 时,对应 `Layer` 的 `addToScene` 才会被调用;而当 `Layer` 的 `_needsAddToScene` 为 `false` 且 `_engineLayer` 不为空时就触发 `Layer` 的复用**。
|
||||
|
||||
```
|
||||
void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
|
||||
|
||||
if (!_needsAddToScene && _engineLayer != null) {
|
||||
builder.addRetained(_engineLayer);
|
||||
return;
|
||||
}
|
||||
addToScene(builder);
|
||||
|
||||
_needsAddToScene = false;
|
||||
}
|
||||
```
|
||||
|
||||
是的,当一个 `Layer` 的 `_needsAddToScene` 为 `false` 时 表明了自己不需要更新,那这个 `Layer` 的 `EngineLayer` 又存在,那 就可以被复用。举个例子:当一个新的页面打开时,底部的页面并没有发生变化时,它只是参与画面的合成,所以对于底部页面来说它 “`Layer`” 是可以直接被复用参与绘制。
|
||||
|
||||
|
||||
**那 `markNeedsAddToScene` 在什么时候会被调用?**
|
||||
|
||||
如下图所示,当 `Layer` 子的参数,比如: `PictureLayer` 的 `picture`、`OffsetLayer` 的 `offset` 发生变化时,`Layer` 就会主动调用 `markNeedsAddToScene` 标记自己为“脏”区域。另外当 `Layer` 的 `engineLayer` 发生变化时,就会尝试触发父节点的 `Layer` 调用 `markNeedsAddToScene` ,这样父节点也会对应产生变化。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image5)
|
||||
|
||||
```
|
||||
@protected
|
||||
set engineLayer(ui.EngineLayer value) {
|
||||
_engineLayer = value;
|
||||
if (!alwaysNeedsAddToScene) {
|
||||
if (parent != null && !parent.alwaysNeedsAddToScene) {
|
||||
parent.markNeedsAddToScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
而 `updateSubtreeNeedsAddToScene` 是在 `buildScene` 的时候触发,在 `addToScene` 之前调用 `updateSubtreeNeedsAddToScene` 再次判断 child 节点,从而确定是否需要发生改变。
|
||||
|
||||
```
|
||||
ui.Scene buildScene(ui.SceneBuilder builder) {
|
||||
List<PictureLayer> temporaryLayers;
|
||||
assert(() {
|
||||
if (debugCheckElevationsEnabled) {
|
||||
temporaryLayers = _debugCheckElevations();
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
updateSubtreeNeedsAddToScene();
|
||||
addToScene(builder);
|
||||
|
||||
_needsAddToScene = false;
|
||||
final ui.Scene scene = builder.build();
|
||||
|
||||
return scene;
|
||||
}
|
||||
```
|
||||
|
||||
## 六、Flutter Framework 的 Layer 构成
|
||||
|
||||
最后回归到 Flutter Framework ,在 Flutter Framework 中 `_window.render` 是在 `RenderView` 的 `compositeFrame` 方法中被调用;而 `RenderView ` 是在`RendererBinding` 的 `initRenderView` 被初始化;`initRenderView` 是在 `initInstances` 时被调用,也就是 `runApp` 的时候。
|
||||
|
||||
简单来说就是:**`runApp` 的时候创建了 `RenderView` ,并且 `RenderView` 内部的 `compositeFrame` 就是通过 `_window.render`来提交 `Layer` 的绘制。**
|
||||
|
||||
```
|
||||
void compositeFrame() {
|
||||
Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
|
||||
try {
|
||||
final ui.SceneBuilder builder = ui.SceneBuilder();
|
||||
final ui.Scene scene = layer.buildScene(builder);
|
||||
if (automaticSystemUiAdjustment)
|
||||
_updateSystemChrome();
|
||||
_window.render(scene);
|
||||
scene.dispose();
|
||||
assert(() {
|
||||
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
|
||||
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
|
||||
return true;
|
||||
}());
|
||||
} finally {
|
||||
Timeline.finishSync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所以 `runApp` 的时候 Flutter 创建了 `RenderView`,并且在 `Window` 的 `drawFrame` 方法中调用了 `renderView.compositeFrame();` 提交了绘制,而 **`RenderView` 作为根节点,它携带的 `rootLayer` 为 `OffsetLayer` 的子类 `TransformLayer`,属于是 Flutter 中 `Layer` 的根节点**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image6)
|
||||
|
||||
这里举个例子,如下图所示是一个简单的不规范代码,运行后出现的结果是一个黑色空白页面,这里我们通过 `debugDumpLayerTree` 方法打印出 `Layer` 的机构。
|
||||
|
||||
```
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
new Future.delayed(Duration(seconds: 1), () {
|
||||
debugDumpLayerTree();
|
||||
});
|
||||
return MaterialApp(
|
||||
title: 'GSY Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: Container(),
|
||||
//routes: routers,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
打印出的结果如下 LOG 所示,正如前面所说 `TransformLayer` 作为 `rooterLayer` 它的 `owner` 是 `RenderView`,然后它有两个 child 节点: child1 `OffsetLayer` 和 child2 `PictureLayer` 。
|
||||
|
||||
> 默认情况下因为 `Layer` 的形成机制(`isRepaintBoundary` 为 `ture` 自动创建一个 `OffsetLayer`)和 `Canvas` 绘制需要,至少会有一个 `OffsetLayer` 和 `PictureLayer`。
|
||||
|
||||
```
|
||||
I/flutter (32494): TransformLayer#f8fa5
|
||||
I/flutter (32494): │ owner: RenderView#2d51e
|
||||
I/flutter (32494): │ creator: [root]
|
||||
I/flutter (32494): │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ transform:
|
||||
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
|
||||
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
|
||||
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
|
||||
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): ├─child 1: OffsetLayer#4503b
|
||||
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
|
||||
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
|
||||
I/flutter (32494): │ │ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#e1be1]
|
||||
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#95107] ←
|
||||
I/flutter (32494): │ │ Stack ← _Theatre ←
|
||||
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey<OverlayState>#ceb36] ← ⋯
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: OffsetLayer#e8309
|
||||
I/flutter (32494): │ creator: RepaintBoundary-[GlobalKey#bbad8] ← IgnorePointer ←
|
||||
I/flutter (32494): │ FadeTransition ← FractionalTranslation ← SlideTransition ←
|
||||
I/flutter (32494): │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
|
||||
I/flutter (32494): │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
|
||||
I/flutter (32494): │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): └─child 2: PictureLayer#be4f1
|
||||
I/flutter (32494): paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2030.0)
|
||||
```
|
||||
|
||||
根据上述 LOG 所示,首先看:
|
||||
|
||||
- `OffsetLayer` 的 `creator` 是 `RepaintBoundary`,而其来源是 `Overlay`,我们知道 Flutter 中可以通过 `Overlay` 做全局悬浮控件,而 `Overlay` 就是在 `MaterialApp` 的 `Navigator` 中创建,并且它是一个独立的`Layer` ;
|
||||
- 而 `OffsetLayer` 的 child 是 `PageStorage` ,`PageStorage` 是通过 `Route` 产生的,也即是默认的路由第一个页面。
|
||||
|
||||
|
||||
**所以现在知道为什么 `Overlay` 可以在 `MaterialApp` 的所有路由页面下全局悬浮显示了吧。**
|
||||
|
||||
|
||||
如下代码所示,再原本代码的基础上增加 `Scaffold` 后继续执行 `debugDumpLayerTree`。
|
||||
|
||||
```
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
new Future.delayed(Duration(seconds: 1), () {
|
||||
debugDumpLayerTree();
|
||||
});
|
||||
return MaterialApp(
|
||||
title: 'GSY Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: Scaffold(
|
||||
body: Container(),
|
||||
),
|
||||
//routes: routers,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到这里多了一个 `PhysicalModelLayer` 和 `PictureLayer` ,`PhysicalModelLayer` 是用于设置阴影等效果的,比如关闭 `debugDisablePhysicalShapeLayers` 后 `AppBar` 的阴影会消失,而之后的 `PictureLayer` 也是用于绘制。
|
||||
|
||||
```
|
||||
I/flutter (32494): TransformLayer#ac14b
|
||||
I/flutter (32494): │ owner: RenderView#f5ecc
|
||||
I/flutter (32494): │ creator: [root]
|
||||
I/flutter (32494): │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ transform:
|
||||
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
|
||||
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
|
||||
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
|
||||
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): ├─child 1: OffsetLayer#c0128
|
||||
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
|
||||
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
|
||||
I/flutter (32494): │ │ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#fe143]
|
||||
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#9cb60] ←
|
||||
I/flutter (32494): │ │ Stack ← _Theatre ←
|
||||
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey<OverlayState>#ee455] ← ⋯
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: OffsetLayer#fb2a6
|
||||
I/flutter (32494): │ │ creator: RepaintBoundary-[GlobalKey#fd46b] ← IgnorePointer ←
|
||||
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← SlideTransition ←
|
||||
I/flutter (32494): │ │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
|
||||
I/flutter (32494): │ │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: PhysicalModelLayer#f1460
|
||||
I/flutter (32494): │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
|
||||
I/flutter (32494): │ │ PrimaryScrollController ← _ScaffoldScope ← Scaffold ← Semantics
|
||||
I/flutter (32494): │ │ ← Builder ← RepaintBoundary-[GlobalKey#fd46b] ← IgnorePointer ←
|
||||
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← ⋯
|
||||
I/flutter (32494): │ │ elevation: 0.0
|
||||
I/flutter (32494): │ │ color: Color(0xfffafafa)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: PictureLayer#f800f
|
||||
I/flutter (32494): │ paint bounds: Rect.fromLTRB(0.0, 0.0, 392.7, 738.2)
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): └─child 2: PictureLayer#af14d
|
||||
I/flutter (32494): paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2030.0)
|
||||
I/flutter (32494):
|
||||
|
||||
```
|
||||
|
||||
最后通过再使用 `Navigator` 跳到另外一个页面,再新页面打印 `Layer` 树,可以看到又可以多了个 `PictureLayer` 、`AnnotatedRegionLayer` 和 `TransformLayer` : 其中多了的 `AnnotatedRegionLayer` 是用于处理新页面顶部状态栏的显示效果。
|
||||
|
||||
```
|
||||
I/flutter (32494): TransformLayer#12e21
|
||||
I/flutter (32494): │ owner: RenderView#aa5c7
|
||||
I/flutter (32494): │ creator: [root]
|
||||
I/flutter (32494): │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ transform:
|
||||
I/flutter (32494): │ [0] 2.8,0.0,0.0,0.0
|
||||
I/flutter (32494): │ [1] 0.0,2.8,0.0,0.0
|
||||
I/flutter (32494): │ [2] 0.0,0.0,1.0,0.0
|
||||
I/flutter (32494): │ [3] 0.0,0.0,0.0,1.0
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): ├─child 1: OffsetLayer#fc176
|
||||
I/flutter (32494): │ │ creator: RepaintBoundary ← _FocusMarker ← Semantics ← FocusScope
|
||||
I/flutter (32494): │ │ ← PageStorage ← Offstage ← _ModalScopeStatus ←
|
||||
I/flutter (32494): │ │ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#43140]
|
||||
I/flutter (32494): │ │ ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#46f19] ←
|
||||
I/flutter (32494): │ │ Stack ← _Theatre ←
|
||||
I/flutter (32494): │ │ Overlay-[LabeledGlobalKey<OverlayState>#af6f4] ← ⋯
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: OffsetLayer#b6e14
|
||||
I/flutter (32494): │ │ creator: RepaintBoundary-[GlobalKey#0ce90] ← IgnorePointer ←
|
||||
I/flutter (32494): │ │ FadeTransition ← FractionalTranslation ← SlideTransition ←
|
||||
I/flutter (32494): │ │ _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundary
|
||||
I/flutter (32494): │ │ ← _FocusMarker ← Semantics ← FocusScope ← PageStorage ← ⋯
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: PhysicalModelLayer#4fdc6
|
||||
I/flutter (32494): │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
|
||||
I/flutter (32494): │ │ PrimaryScrollController ← _ScaffoldScope ← Scaffold ←
|
||||
I/flutter (32494): │ │ ClipDemoPage ← Semantics ← Builder ←
|
||||
I/flutter (32494): │ │ RepaintBoundary-[GlobalKey#0ce90] ← IgnorePointer ←
|
||||
I/flutter (32494): │ │ FadeTransition ← ⋯
|
||||
I/flutter (32494): │ │ elevation: 0.0
|
||||
I/flutter (32494): │ │ color: Color(0xfffafafa)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ ├─child 1: PictureLayer#6ee26
|
||||
I/flutter (32494): │ │ paint bounds: Rect.fromLTRB(0.0, 0.0, 392.7, 738.2)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ ├─child 2: AnnotatedRegionLayer<SystemUiOverlayStyle>#cbeaf
|
||||
I/flutter (32494): │ │ │ value: {systemNavigationBarColor: 4278190080,
|
||||
I/flutter (32494): │ │ │ systemNavigationBarDividerColor: null, statusBarColor: null,
|
||||
I/flutter (32494): │ │ │ statusBarBrightness: Brightness.dark, statusBarIconBrightness:
|
||||
I/flutter (32494): │ │ │ Brightness.light, systemNavigationBarIconBrightness:
|
||||
I/flutter (32494): │ │ │ Brightness.light}
|
||||
I/flutter (32494): │ │ │ size: Size(392.7, 83.6)
|
||||
I/flutter (32494): │ │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │ │
|
||||
I/flutter (32494): │ │ └─child 1: PhysicalModelLayer#edb15
|
||||
I/flutter (32494): │ │ │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
|
||||
I/flutter (32494): │ │ │ AnnotatedRegion<SystemUiOverlayStyle> ← Semantics ← AppBar ←
|
||||
I/flutter (32494): │ │ │ FlexibleSpaceBarSettings ← ConstrainedBox ← MediaQuery ←
|
||||
I/flutter (32494): │ │ │ LayoutId-[<_ScaffoldSlot.appBar>] ← CustomMultiChildLayout ←
|
||||
I/flutter (32494): │ │ │ AnimatedBuilder ← ⋯
|
||||
I/flutter (32494): │ │ │ elevation: 4.0
|
||||
I/flutter (32494): │ │ │ color: MaterialColor(primary value: Color(0xff2196f3))
|
||||
I/flutter (32494): │ │ │
|
||||
I/flutter (32494): │ │ └─child 1: PictureLayer#418ce
|
||||
I/flutter (32494): │ │ paint bounds: Rect.fromLTRB(0.0, 0.0, 392.7, 83.6)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 3: TransformLayer#7f867
|
||||
I/flutter (32494): │ │ offset: Offset(0.0, 0.0)
|
||||
I/flutter (32494): │ │ transform:
|
||||
I/flutter (32494): │ │ [0] 1.0,0.0,0.0,-0.0
|
||||
I/flutter (32494): │ │ [1] -0.0,1.0,0.0,0.0
|
||||
I/flutter (32494): │ │ [2] 0.0,0.0,1.0,0.0
|
||||
I/flutter (32494): │ │ [3] 0.0,0.0,0.0,1.0
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: PhysicalModelLayer#9f36b
|
||||
I/flutter (32494): │ │ creator: PhysicalShape ← _MaterialInterior ← Material ←
|
||||
I/flutter (32494): │ │ ConstrainedBox ← _FocusMarker ← Focus ← _InputPadding ←
|
||||
I/flutter (32494): │ │ Semantics ← RawMaterialButton ← KeyedSubtree-[GlobalKey#9ead9]
|
||||
I/flutter (32494): │ │ ← TickerMode ← Offstage ← ⋯
|
||||
I/flutter (32494): │ │ elevation: 6.0
|
||||
I/flutter (32494): │ │ color: Color(0xff2196f3)
|
||||
I/flutter (32494): │ │
|
||||
I/flutter (32494): │ └─child 1: PictureLayer#2a074
|
||||
I/flutter (32494): │ paint bounds: Rect.fromLTRB(320.7, 666.2, 376.7, 722.2)
|
||||
I/flutter (32494): │
|
||||
I/flutter (32494): └─child 2: PictureLayer#3d42d
|
||||
I/flutter (32494): paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2030.0)
|
||||
I/flutter (32494):
|
||||
|
||||
```
|
||||
|
||||
所以可以看到,Flutter 中的 `Widget` 在最终形成各式各样的 `Layer` ,每个 `Layer` 都有自己单独的区域和功能,比如 `AnnotatedRegionLayer`在新的页面处理状态栏颜色的变化,而这些 `Layer` 最终通过 `SceneBuilder` 转化为 `EngineLayer` ,最后提交为 `Scene` 经由 Engine 绘制。
|
||||
|
||||
|
||||
最后总结一下:**Flutter Framework 的 `Layer` 在绘制之前,需要经历 `SceneBuinlder` 的处理得到 `EngineLayer`,其实 Flutter Framework 中的 `Layer` 可以理解为 `SceneBuinlder` 的对象封装,而 `EngineLayer` 才是真正的 Engine 图层 ,在之后得到的 `Scene` 会被提交 Engine 绘制**。
|
||||
|
||||
|
||||
|
||||
> 自此,第二十一篇终于结束了!(///▽///)
|
||||
|
||||
## 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200327_Flutter-21/image7)
|
|
@ -1,242 +0,0 @@
|
|||
> **相信大家已经都在对 Flutter 2.10 版本跃跃欲试,本篇就目前升级用 Flutter 2.10 版本遇到的问题做一些总结提炼。**
|
||||
|
||||
|
||||
**事实上按照 Flutter 每个版本的投入使用规律,应该是第三个小版本最稳**,以 Flutter 目前庞大的用户量,每次正式版的发布必然带来各种奇奇怪怪的问题,**一般情况下我推荐 2.10 版本等到 2.10.3 发布再投入生产会更稳妥**,但是如果你等不及官方 `hotfix` ,那么后面的内容可能可以帮助到你。
|
||||
|
||||
|
||||
> 本次如果你是从 2.8 升级的到 2.10 ,那么 dart 层需要调整几乎等于零。
|
||||
|
||||
|
||||
## Kotlin 版本
|
||||
|
||||
**首先就项目升级的第一个,也就是最重要的一个,就是升级你的 kotlin 插件版本,这个是强制的**,因为之前的旧版本使用的基本都是 `1.3.x` 的版本,而这些 Flutter 2.10 强制要求 `1.5.31` 以上的版本。
|
||||
|
||||
```gradle
|
||||
buildscript {
|
||||
- ext.kotlin_version = '1.3.50'
|
||||
+ ext.kotlin_version = '1.5.31'
|
||||
```
|
||||
|
||||
这里需要注意,**这次升级 Kotlin 版本,会带来一些 Kotlin 包的 API 出现一些 break 的变化** ,所以如果你本身 App 使用了较多 Kotlin 开发,或者插件里使用了一些 Kotlin 的包,就需要注意升级带来的适配成本,例如:
|
||||
|
||||
> `ProducerScope` 需要 `override` 新的 `trySend` 方法,但是这个方法需要 `return` 一个 `ChannelResult` , `ChannelResult` 是 `@InternalCoroutinesApi` 。
|
||||
|
||||
|
||||
## Gradle 版本
|
||||
|
||||
因为 Kotlin 版本升级了,所以 AGP 插件必须使用最低 `4.0.0` 配合 Gradle `6.1.1` 的版本,也就是:
|
||||
|
||||
```gradle
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
/
|
||||
distributionUrl=https://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
```
|
||||
|
||||
因为以前的老版本使用的 AGP 可能是 AGP `3.x` 配合 Gradle `5.x` 的版本,**所以如果升级了 Kotlin 版本,这一步升级就必不可少。**
|
||||
|
||||
> 这里顺便放一张 AGP 和 Gradle 之间的版本对应截图
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-210-FIX/image1)
|
||||
|
||||
|
||||
## Android SDK 问题
|
||||
|
||||
|
||||
### cmdline-tools & license
|
||||
|
||||
这个问题可能大家不一定会遇到,首先如果你在执行 `flutter doctor` 的时候出现以下情况
|
||||
|
||||
```
|
||||
[!] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
|
||||
✗ cmdline-tools component is missing
|
||||
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
|
||||
See https://developer.android.com/studio/command-line for more details.
|
||||
✗ Android license status unknown.
|
||||
Run `flutter doctor --android-licenses` to accept the SDK licenses.
|
||||
See https://flutter.dev/docs/get-started/install/macos#android-setup for
|
||||
more details.
|
||||
```
|
||||
|
||||
|
||||
也就是 `cmdline-tools` 和 `Android license` 都是 `✗` 的显示时,那可能你还需要额外做一些步骤来完善配置。
|
||||
|
||||
|
||||
首先你需要安装 `cmdline-tools` ,如下图所示直接安装就可以了
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-210-FIX/image2)
|
||||
|
||||
|
||||
然后执行 `flutter doctor --android-licenses` ,就可以很简单地完善你的环境的配置。
|
||||
|
||||
|
||||
### Build Tools
|
||||
|
||||
其次,如果你在编译 Android Apk 的过程中出现 : `Installed Build Tools revision 31.0.0 is corrupted` 之类的问题:
|
||||
|
||||
```
|
||||
Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'.
|
||||
> Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.
|
||||
```
|
||||
|
||||
那么可以通过执行如下命令行来完成配置 :
|
||||
|
||||
```
|
||||
# change below to your Android SDK path
|
||||
cd ~/Library/Android/sdk/build-tools/31.0.0 \
|
||||
&& mv d8 dx \
|
||||
&& cd lib \
|
||||
&& mv d8.jar dx.jar
|
||||
```
|
||||
> Window 用户可以看 https://stackoverflow.com/questions/68387270/android-studio-error-installed-build-tools-revision-31-0-0-is-corrupted
|
||||
|
||||
|
||||
### NDK
|
||||
|
||||
如果你在编译过程中出现 `No version of NDK matched` 的问题:
|
||||
|
||||
|
||||
```
|
||||
Execution failed for task ':app:stripDebugDebugSymbols'.
|
||||
> No version of NDK matched the requested version 21.0.6113669. Versions available locally: 19.1.5304403
|
||||
```
|
||||
|
||||
这个问题其实很简单,如图打开你的 `SDK Manager` 下载对应的版本就可以了。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-210-FIX/image3)
|
||||
|
||||
|
||||
|
||||
## 本地 AAR 文件问题
|
||||
|
||||
|
||||
因为前面升级了 AGP 版本,这时候就带来一个问题,这个问题仅存在于**你使用的 Flutter Plugin 里的本地的 aar 文件**。
|
||||
|
||||
正常情况下编译时就会遇到如果的提示:
|
||||
|
||||
```
|
||||
> Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR. Previous versions of the Android Gradle Plugin produce broken AARs in this case too (despite not throwing this error). The following direct local .aar file dependencies of the :********* project caused this error: /Users/guoshuyu/.pub-cache/git/*********-01d03bf549e512f6e15dd539411a8c236d77cd47/android/libs/libc*********.aar, /Users/guoshuyu/.pub-cache/git/*********-01d03bf549e512f6e15dd539411a8c236d77cd47/android/libs/*********.aar, /Users/guoshuyu/.pub-cache/git/*********-01d03bf549e512f6e15dd539411a8c236d77cd47/android/libs/*********.aar
|
||||
```
|
||||
|
||||
|
||||
这时候听我一声劝,**什么办法都不好使,直接搭一个私服 Maven ,很简单的,把 aar 上传上去,然后远程依赖进来就可以了**。
|
||||
|
||||
[Alex](https://juejin.cn/user/606586150596360) 大佬建议的本地 maven 构建也可以:https://www.kikt.top/posts/flutter/plugin/flutter-sdk-import-aar/ 主要就是构建得到一个如下结构的目录:
|
||||
|
||||
```
|
||||
tree .
|
||||
.
|
||||
├── com
|
||||
│ └── pgyer
|
||||
│ └── sdk
|
||||
│ ├── 3.0.9
|
||||
│ │ ├── sdk-3.0.9.aar
|
||||
│ │ ├── sdk-3.0.9.aar.md5
|
||||
│ │ ├── sdk-3.0.9.aar.sha1
|
||||
│ │ ├── sdk-3.0.9.pom
|
||||
│ │ ├── sdk-3.0.9.pom.md5
|
||||
│ │ └── sdk-3.0.9.pom.sha1
|
||||
│ ├── maven-metadata.xml
|
||||
│ ├── maven-metadata.xml.md5
|
||||
│ └── maven-metadata.xml.sha1
|
||||
└── sdk.aar
|
||||
|
||||
```
|
||||
|
||||
然后配置 android 下的 gradle
|
||||
|
||||
```gradle
|
||||
// 定义一个方法, 用于获取当前moudle的dir
|
||||
def getCurrentProjectDir() {
|
||||
String result = ""
|
||||
rootProject.allprojects { project ->
|
||||
if (project.properties.get("identityPath").toString() == ":example_for_flutter_plugin_local_maven") { // 这里是flutter的约定, 插件的module名是插件名, :是gradle的约定. project前加:
|
||||
result = project.properties.get("projectDir").toString()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
// 这个闭包是循环所有project, 我们让这个仓库可以被所有module找到
|
||||
def dir = getCurrentProjectDir()
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { // 添加这个指向本地的仓库目录
|
||||
url "$dir/aar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.pgyer:sdk:3.0.9" // 添加这个, 接着点sync project with gradle file 刷新一下项目就可以了. 是使用api还是implementation根据你的实际情况来看就好了
|
||||
}
|
||||
```
|
||||
|
||||
## 强制 V2
|
||||
|
||||
|
||||
Android 上在这个版本上就强制要求 V2 的,例如如果之前使用了 `android:name="io.flutter.app.FlutterApplication"` ,那么在编译时你会看到:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Warning
|
||||
──────────────────────────────────────────────────────────────────────────────
|
||||
Your Flutter application is created using an older version of the Android
|
||||
embedding. It is being deprecated in favor of Android embedding v2. Follow the
|
||||
steps at
|
||||
|
||||
https://flutter.dev/go/android-project-migration
|
||||
|
||||
to migrate your project. You may also pass the --ignore-deprecation flag to
|
||||
ignore this check and continue with the deprecated v1 embedding. However,
|
||||
the v1 Android embedding will be removed in future versions of Flutter.
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
The detected reason was:
|
||||
|
||||
/Users/guoshuyu/workspace/***/*********/android/app/src/main/AndroidManifest.xml uses `android:name="io.flutter.app.FutterApplication"`
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
这里如果你只需要简单删除 `android:name="io.flutter.app.FutterApplication"` 就可以了。
|
||||
|
||||
> 更多关于 V2 的可以参考:https://flutter.dev/go/android-project-migration
|
||||
|
||||
|
||||
## Material 图标出现异常
|
||||
|
||||
Flutter 2.10 针对 Material Icon 做了一次升级,结果很明显这次发布不小心又挖了个坑,目前问题看起来是**因为某个 issue 的回滚导致部分 icon 的提交也被回退**,所以这部分只能静待 hotfix ,目前官方已经知道这个问题,具体可见:
|
||||
|
||||
> https://github.com/flutter/flutter/issues/97767
|
||||
|
||||
## iOS CocoaPods not installed
|
||||
|
||||
|
||||
**如果你运行 iOS 出现 `CocoaPods not installed` 的错误提示,那么不要着急,这个是 Android Studio 团队的锅**。
|
||||
|
||||
```
|
||||
Warning: CocoaPods not installed. Skipping pod install.
|
||||
CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
|
||||
Without CocoaPods, plugins will not work on iOS or macOS.
|
||||
For more info, see https://flutter.dev/platform-plugins
|
||||
To install see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.
|
||||
|
||||
Exception: CocoaPods not installed or not in valid state.
|
||||
```
|
||||
|
||||
其实你在执行 `flutter doctor` 时可能就是看到提示,说你本地缺少 `CocoaPods` , 但是实际上你本地是有 `CocoaPods` 的,这时候解决的方案有几个可以选择:
|
||||
|
||||
- 直接通过命令行 `flutter run` 运行就不会有这个问题;
|
||||
- 通过命令行 `open /Applications/Android\ Studio.app` 启动 Android Studio ;
|
||||
- 执行 `chmod +x /Applications/Android\ Studio.app/Contents/bin/printenv` (如果你使用了 `JetBrains Toolbox` ,那 `printenv` 文件路径可能会有所变化)
|
||||
- 静待 Android Studio 的小版本更新
|
||||
|
||||
|
||||
> 更多可以参考 : https://github.com/flutter/flutter/issues/97251
|
||||
|
||||
更新:**新版 Android Studio Patch1 更新已经修复该问题**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-210-FIX/image4)
|
179
Flutter-2100.md
179
Flutter-2100.md
|
@ -1,179 +0,0 @@
|
|||
> 原文链接:https://medium.com/flutter/whats-new-in-flutter-2-10-5aafb0314b12
|
||||
|
||||
欢迎来到 Flutter 2.10 稳定版本的更新,自上次发布至今还不到两个月,但即使在这么短的时间内,**Flutter 2.10 也关闭了 1,843 个 issues,合并了来自全球 155 位贡献者的 1,525 个 PR**,所以非常感谢大家这段时间出色的工作,尤其是在 2021 年假期期间。
|
||||
|
||||
作为此版本的重要组成部分,这里有几件令人兴奋的事情要宣布,包括:
|
||||
|
||||
- **Flutter 对 Windows 支持的重大更新;**
|
||||
- **一些关于性能方面的重大改进;**
|
||||
- **关于对框架中图标和颜色相关的新功能支持;**
|
||||
- **一些开发工具方便的改进;**
|
||||
|
||||
此外还有一些关于**移除 dev channel 的更新、减少对旧版 iOS 的支持以及简短的重要变更列表等等**。
|
||||
|
||||
## 为 Windows 上的生产应用做好准备
|
||||
|
||||
首先,Flutter 2.10 版本给我们带来了稳定版本的 Windows 支持,现在开发者可以不再通过设置 flag 来启用 Windows 的支持,因为**在 Flutter 2.10 上现在默认支持编译生成 Windows 应用**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image1)
|
||||
|
||||
|
||||
当然,此版本还包括对**文本处理、键盘处理和键盘快捷键相关的改进,以及更好地和 Windows 进行集成,支持命令行参数,全球化多语言文本输入和辅助功能**等等。
|
||||
|
||||
> 有关 Windows 稳定版发布的更多信息,请参阅[Flutter for Windows 博客文章](https://timsneath.medium.com/6979d0d01fed),该文章描述了 Flutter 在 Windows 上的架构实现,让你了解目前有多少 Flutter 包和插件已经支持 Windows,你还可以查看我们的工具和应用合作伙伴在 Windows 上使用 Flutter 所做的一些 Demo!
|
||||
|
||||
|
||||
## 性能改进
|
||||
|
||||
Flutter 2.10 包括了对 Flutter 社区成员 [knopp](https://github.com/knopp) 所提供的**脏区管理**支持,他为 [ iOS/Metal 上的单个脏区域启用了部分重绘的支持](https://github.com/flutter/engine/pull/28801),在基准测试中这一变动降低了 90% - 99% 的光栅化时间,并将 GPU 利用率从 90% 以上降低到 10% 以下。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image2)
|
||||
|
||||
> 我们希望在未来的版本中将这部分重绘带到来的好处支持到[其他平台](https://github.com/flutter/engine/pull/29591)。
|
||||
|
||||
在 Flutter 2.8 版本中,我们[发布了自己的 picture recording format](https://github.com/flutter/flutter/issues/53501),现在 Flutter 2.10 中我们开始使用它进行功能优化,例如现在 Flutter 可以[**更简单地实现**](https://github.com/flutter/engine/pull/29775) **opacity layers**,即使在最坏的情况下,**基准测试中的帧光栅时间也下降到了之前的三分之一以下。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image3)
|
||||
|
||||
> 随着我们继续开发 picture recording format ,预计这些优化可以将扩展到更多的场景。
|
||||
|
||||
在 profile 和 release 模式下,Dart 代码会提前编译为 native 代码,这里面提高性能和降低其大小的关键在于整个程序的 type flow 分析,它解锁了许多编译器优化和激进的 tree-shaking。
|
||||
|
||||
但是由于 type flow 分析必须涵盖整个程序,因此开销可能会有些昂贵,所以此版本增加了[**更快的 type flow 分析实现**](https://dart.googlesource.com/sdk.git/+/e698500693603374ecc409e158f36c25bff45b12),在我们的基准测试中,**Flutter 应用程序的总体构建时间下降了约 10%**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image4)
|
||||
|
||||
> 与往常一样,增强性能、减少内存使用和减少延迟是 Flutter 团队的首要任务,期待未来版本的进一步改进。
|
||||
|
||||
## iOS 更新
|
||||
|
||||
除了性能改进之外,我们还添加了一些特定平台的增强功能,其中一项新增的功能是来自[luckysmg](https://github.com/luckysmg)[的 iOS 中更流畅的键盘动画](https://github.com/flutter/engine/pull/29281),它会默认被应用于你的 App 而无需你做任何事情。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image5)
|
||||
|
||||
**我们还通过修复一些[边缘](https://github.com/flutter/plugins/pull/4608)条件下[崩溃](https://github.com/flutter/plugins/pull/4619)的[情况](https://github.com/flutter/plugins/pull/4661)来提高了 iOS 相机插件的稳定性。**
|
||||
|
||||
最后,**通过[压缩](https://github.com/flutter/engine/pull/30077)[指针](https://github.com/flutter/engine/pull/30333)使得 64 位的 iOS 架构可以减少内存的使用**。
|
||||
|
||||
> 64 位架构将指针表示为 4 字节的数据结构,当你有很多对象时,指针本身占用的空间会增加 APP 的整体内存使用量,特别是如果你的 App 规模比较庞大和复杂的时候,会导致更多的 GC 流失,但是 iOS App 很大一部分不太可能有对象需要占用 32 位地址空间(20 亿个对象),更不用说庞大的 64 位地址空间(900 亿个对象)了。
|
||||
|
||||
Dart 2.15 中提供了压缩指针,在这个 Flutter 版本中,我们使用它们来减少 64 位 iOS 应用程序的内存使用量,您可以[查看 Dart 2.15 博客文章来了解详细信息](https://medium.com/dartlang/dart-2-15-7e7a598e508a)。
|
||||
|
||||
> 在阅读 Dart 博客文章时,不要忘记[查看 Dart 2.16 的公告](https://medium.com/dartlang/dd87abd6bad1),了解有关支持 Flutter for Windows 的更新,包括包平台标记和 pub.dev 上的新搜索体验。
|
||||
|
||||
|
||||
## 安卓更新
|
||||
|
||||
此版本还包含许多针对 Android 的改进。
|
||||
|
||||
默认情况下当创建新应用时,**Flutter 会默认支持最新版本的 Android** 12 版本 (API 级别 31),此外,在此版本中**我们自动启用了**[multidex](https://developer.android.com/studio/build/multidex)**支持**。
|
||||
|
||||
如果您的应用支持低于 21 的 Android SDK 版本,并且超过了 64K 方法限制,**只需将`--multidex` 标志传递给 `flutter build appbundle` 或者 `flutter build apk` 就可以让你的应用支持 multidex。**
|
||||
|
||||
最后,**Flutter 工具现在会在 Gradle 发生错误时提供常见的问题解决步骤**,例如如果在应用中添加了一个插件,需要你提高最低 Android SDK 版本时,你现在会在日志中看到 “Flutter Fix” 建议。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image6)
|
||||
|
||||
|
||||
## Web 更新
|
||||
|
||||
此版本同样包含对 Web 的一些改进。
|
||||
|
||||
例如在以前的版本中,在 Web 上滚动多行的 `TextField` 到边缘时它不会正确滚动,而在 Flutter 2.10 下 [**edge scrolling for text selection**](https://github.com/flutter/flutter/pull/93170) 支持当选中滚动超过 `TextField` 的范围时,内容依然可以继续正常滚动,改更新适用于 Web 和桌面应用。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image7)
|
||||
|
||||
此外 Flutter 还包括对 Web 的另一项显着改进:**减少将 Flutter 映射到 Web 的开销。**
|
||||
|
||||
在以前的版本中,每次我们想要将原生的 HTML 控件引入 Flutter 应用时,我们都需要一个 overlay 作为我们对 Web 的平台视图的支持,这些叠加层中的每一个都支持自定义绘制,但也代表着一定数量的开销。
|
||||
|
||||
> 如果你的应用中有大量原生 HTML 小部件(例如 links),则会因此增加大量性能开销。在这个版本中,**我们为 Web 创建了一个新的“non-painting platform view”,基本上消除了这种开销**。
|
||||
|
||||
我们已经在 [Link 控件](https://pub.dev/documentation/url_launcher/latest/link/Link-class.html) 中利用了这种优化,这意味着如果你的 Flutter Web 应用程序中有很多 Link,它们不会再有任何重大开销,而随着时间的推移,我们会将此优化应用到其他控件上。
|
||||
|
||||
## Material 3
|
||||
|
||||
Flutter 2.10 版本是向 Material 3 过渡的开始,其中包括[**从 single seed color 生成整个配色方案**](https://github.com/flutter/flutter/pull/93463)的能力。
|
||||
|
||||
你可以使用使用任何颜色构造 `ColorScheme` 实例:
|
||||
|
||||
```dart
|
||||
final lightScheme = ColorScheme.fromSeed(seedColor: Colors.green);
|
||||
final darkScheme = ColorScheme.fromSeed(seedColor: Colors.green, brightness: Brightness.dark);
|
||||
```
|
||||
|
||||
`ThemeData` 其 factory 构造函数还有一个新的 `colorSchemeSeed` 参数,可生成主题的配色方案:
|
||||
|
||||
```dart
|
||||
final lightTheme = ThemeData(colorSchemeSeed: Colors.orange, …);
|
||||
final darkTheme = ThemeData(colorSchemeSeed:Colors.orange, brightness: Brightness.dark, …);
|
||||
```
|
||||
|
||||
此外,此版本包括还包含了 `ThemeData.useMaterial3` 标识位,它用于将组件切换到新的 Material 3 外观支持。
|
||||
|
||||
最后,**我们添加了[1,028 个新的 Material 图标](https://github.com/flutter/flutter/pull/95007)**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image8)
|
||||
|
||||
|
||||
## 集成测试改进
|
||||
|
||||
2020 年 12 月 开始我们宣布了一种[使用 integration_test 包进行端到端测试](https://medium.com/flutter/updates-on-flutter-testing-f54aa9f74c7e)的新方法,这个新包取代了 flutter_driver 包作为进行集成测试的推荐方式,提供了如 Firebase Test Lab对 Web 和桌面端的支持。
|
||||
|
||||
从那时起我们对集成测试进行了进一步的改进,包括**将 integration_test 包捆绑到 Flutter SDK 本身**中,使其更容易与开发者的应用进行集成。
|
||||
|
||||
> 如果你想将现有的 flutter_driver 测试移动到 integration_test,可以参考迁移指南:https://docs.flutter.dev/testing/integration-tests/migration
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image9)
|
||||
|
||||
|
||||
## Flutter 开发工具
|
||||
|
||||
在这个版本中我们也对 Flutter DevTools 做了一些改动,包括更便捷地从命令后使用 DevTools,现在可以直接**通过 `dart devtools` 去会下载和执行更新版本而不是使用`pub global activate`**。
|
||||
|
||||
我们还进行了许多关于[可用性](https://github.com/flutter/devtools/pull/3526) 的[更新](https://github.com/flutter/devtools/pull/3493) 其中包括[**改进了变量窗格中检查大型列表和映射的支持**](https://github.com/flutter/devtools/pull/3497)(感谢[elliette](https://github.com/elliette))。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image10)
|
||||
|
||||
|
||||
|
||||
## VSCode 改进
|
||||
|
||||
Flutter 的 Visual Studio Code 扩展也获得了许多增强功能,包括**代码中更多位置的颜色预览**和[**更新代码的颜色选择器**](https://github.com/Dart-Code/Dart-Code/issues/3240)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image11)
|
||||
|
||||
此外,如果你想成为 VSCode 的 Dart 和 Flutter 扩展插件的预发布版本的测试人员,可以[在扩展设置中切换到预发布版本](https://github.com/Dart-Code/Dart-Code/issues/3729)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-2100/image12)
|
||||
|
||||
## 删除开发通道
|
||||
|
||||
在[Flutter 2.8 版本](https://medium.com/flutter/whats-new-in-flutter-2-8-d085b763d181) 已经宣布我们正在努力**移除 dev channel**,从而简化开发者的选择并移除工程开销,而在这个版本中[我们已经完成了这项工作](https://github.com/flutter/flutter/issues/94962),包括:
|
||||
|
||||
- 更新了 Flutter 工具以帮助将开发人员迁移出 dev channel
|
||||
- 更新了 wiki 以反映更新
|
||||
- 更新了弃用政策
|
||||
- 从 DartPad、预提交测试和网站中删除了dev channel支持
|
||||
|
||||
## 对 iOS 9.3.6 的不再支持
|
||||
|
||||
由于实验室中目标设备的使用减少和维护难度增加,现在将对**iOS 9.3.6的**[**支持**](http://flutter.dev/go/rfc-32-bit-ios-support)[**从“支持”层转移到“尽力而为”层**](https://docs.flutter.dev/development/tools/sdk/release-notes/supported-platforms),这意味着对 iOS 9.3.6 的支持和对 32 位 iOS 设备的支持将仅通过临时修复和社区测试来维持。
|
||||
|
||||
> https://docs.flutter.dev/development/tools/sdk/release-notes/supported-platforms)
|
||||
|
||||
**在 2022 年第三季度的稳定版本中,我们预计从 Flutter 稳定版本中放弃对 32 位 iOS 设备以及 iOS 版本 9 和 10 的支持**,这意味着在那之后基于稳定的 Flutter SDK 构建的应用将不再在 32 位 iOS 设备上运行,并且 **Flutter 支持的最低 iOS 版本将增加到 iOS 11**。
|
||||
|
||||
## 重大变化
|
||||
|
||||
|
||||
- 所需的 Kotlin 版本:https://docs.flutter.dev/release/breaking-changes/kotlin-version
|
||||
- 在 v2.5 之后删除了已弃用的 API:https://docs.flutter.dev/release/breaking-changes/2-5-deprecations)
|
||||
- Web 上的原始图像使用正确的来源和颜色:https://docs.flutter.dev/release/breaking-changes/raw-images-on-web-uses-correct-origin-and-colors
|
||||
- Scribble Text Input Client:https://docs.flutter.dev/release/breaking-changes/scribble-text-input-client
|
||||
|
||||
如果你仍在使用这些 API,可以[阅读 flutter.dev 上的迁移指南](https://docs.flutter.dev/release/breaking-changes),与往常一样,非常感谢社区[提供的测试](https://github.com/flutter/tests/blob/master/README.md),帮助我们识别这些重大变化。FL
|
590
Flutter-220.md
590
Flutter-220.md
|
@ -1,590 +0,0 @@
|
|||
|
||||
> 原文链接:https://medium.com/flutter/whats-new-in-flutter-2-2-fd00c65e2039
|
||||
|
||||
|
||||
**本次 Flutter 2.2 正式版主要着重于优化:包括 iOS 性能改进,Android 延迟加载组件,针对 Flutter Web 的更新等等**
|
||||
|
||||
|
||||
每个 Flutter 新稳定版本的发布都会带来一些更新,无论是性能增强、新功能还是错误修复,尽管 Flutter 2 才发行了两个月,但 2.2 依旧在 Flutter 2 的基础上做了很多改进,**该版本合并了 2456 个 PR,涉及 Framework、 engine 和 plugins 的 issue 关闭了 3105 个 **。
|
||||
|
||||
|
||||
## Flutter 2.2 更新稳定
|
||||
|
||||
此版本在 Flutter 2 之上进行了大量的改进,包括 Android,iOS 和 Web 上的更新,如新的`Material` 图标,文本处理,滚动条行为的更新以及对 `TextSpan` 控件的鼠标光标支持。
|
||||
|
||||
### Dart 2.13
|
||||
|
||||
Flutter 2.2 包含了 Dart 2.13 版本,此 Dart 更新主要包含一个**新的类型别名功能**,使开发者能够为类型和函数创建别名:
|
||||
|
||||
```dart
|
||||
// Type alias for functions (existing)
|
||||
typedef ValueChanged<T> = void Function(T value);
|
||||
|
||||
// Type alias for classes (new!)
|
||||
typedef StringList = List<String>;
|
||||
|
||||
// Rename classes in a non-breaking way (new!)
|
||||
@Deprecated("Use NewClassName instead")
|
||||
typedef OldClassName<T> = NewClassName<T>;
|
||||
```
|
||||
|
||||
使用类型别名可以为复杂的长类型提供“漂亮”的短名称,还可以让开发者以连续的方式重命名类。
|
||||
|
||||
> 更多 dart 2.13 内容 :https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067
|
||||
|
||||
### Flutter Web 更新
|
||||
|
||||
Flutter Web 作为 Flutter 最新的稳定平台,Web 在此版本中做了很多的改进。
|
||||
|
||||
首先,**使用新的 service 加载机制优化了缓存行为**,并修复了的重复下载 `main.dart.js` 的问题。
|
||||
|
||||
在 Flutter Web 的早期版本中,后台在更新下载到应用程序后,用户不刷新浏览器是不会看到这些更改,而从 Flutter 2.2 开始,当检测到更改时用户可以直接看到更新,而无需再次手动刷新页面。
|
||||
|
||||
> 启用此更改要求重新生成 Flutter 应用的 `index.html`,所以你可以先保存 `index.html` 里的修改,然后删除 `index.html` 文件,再通过 `flutter create .` 在项目目录中运行从而重新创建它。
|
||||
|
||||
**Flutter 2.2 还对两个 Web 渲染器进行了改进:**
|
||||
|
||||
- **对于 HTML 添加了对字体功能的支持**,启用设置 `FontFeature` 以及使用画布 API 渲染文本,以便在悬停时将其显示在正确的位置。
|
||||
|
||||
- **对于 HTML 和 CanvasKit都添加了 `computeLineMetrics` 和对着色器蒙板的支持**,以解决 Flutter Web 和移动应用程序之间的差距,例如:开发人员现在可以使用不透明蒙板,使用着色器蒙板执行淡出过渡,并使用 `computeLineMetrics` 像在移动应用程序中一样使用。
|
||||
|
||||
对于 Flutter Web 而言,`Semantics` 是的首要任务之一, Flutter 通过构建`SemanticsNode` 树来实现可访问性。Flutter Web 用户启用 `Semantics` 后,框架将生成与 `DOM` 树平行的 `RenderObjectDOM`树,并将语义属性转换为 `Aira`。
|
||||
|
||||
在此版本中改进了语义节点的位置,以缩小使用转换时移动和桌面 Web 应用程序之间的距离,这意味着在使用转换为 `Widget` 设置样式时,焦点框应正确显示在元素上。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image1)
|
||||
|
||||
我们还在配置文件和发布模式下使用命令行标志公开了语义节点调试树,以帮助开发人员通过可视化为其 Web 应用程序创建的语义节点来调试可访问性。
|
||||
|
||||
要在 Flutter Web 应用启用此功能,请运行以下命令:
|
||||
|
||||
```
|
||||
$ flutter run -d chrome --profile \
|
||||
--dart-define = FLUTTER_WEB_DEBUG_SHOW_SEMANTICS = true
|
||||
```
|
||||
|
||||
激活该标志后将能够在 `Widget` 顶部看到语义节点,就可以调试并查看语义元素是否放置在不应放置的位置。
|
||||
|
||||
虽然在支持一系列核心辅助功能方面取得了比较大的进步,但我们将继续改善辅助功能的支持。在 2.2 稳定版之后的 `master` 和 `dev` 通道上可用的内部版本中,我们还添加了一个 API,使得开发人员能够以编程方式自动启用其应用程序的可访问性,并解决了将 Tab 与屏幕阅读器配合使用的问题。
|
||||
|
||||
**最后最新版本的 Flutter DevTools 现在支持 Flutter Web 应用**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image2)
|
||||
|
||||
### iOS页面过渡和增量安装
|
||||
|
||||
**在此版本中对于 iOS 我们通过将渲染动画帧所需的时间减少了 75%** ,使在 `Cupertino` 中的页面过渡更加平滑么。
|
||||
|
||||
在此版本中还实现了在开发过程中增量的iOS安装,**基准测试中我们发现安装更新版本的 iOS 应用程序的时间减少了40%**。
|
||||
|
||||
### 使用 Flutter 构建自适应平台应用
|
||||
|
||||
随着 Flutter 稳定版的支持平台越来越多,不仅需要考虑支持不同形式的设备(例如移动设备,平板电脑和台式机),还需要支持不同输入类型(触摸与鼠标+键盘)以及具有不同平台的应用,所以我们将:**可以根据不同目标平台的详细信息,进行自我调整的应用称为“平台自适应”应用**。
|
||||
|
||||
> 更多可见:https://flutter.dev/docs/development/ui/layout/building-adaptive-apps
|
||||
|
||||
对于根据这些原则为多个平台编写的 Demo 的应用程序,我们推荐参考 `gSkinner` 的 [Flokk](https://flutter.gskinner.com/flokk) 和 [Flutter Folio](https://flutter.gskinner.com/folio) 应用程序。
|
||||
|
||||
Flutter 平台自适应应用指南的 UX 部分基于新的大屏幕 `Material` 指南,`Material` 团队的新指南包括对一些主要布局文章的处理,以及对多个组件的更新和更新的设计套件,所有这些都考虑到了大屏幕。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image3)
|
||||
|
||||
|
||||
### 更多材料图标
|
||||
|
||||
在“ `Material` 指南”的主题上,在此发行版中我们分割出两个单独的 PR,为 Flutter 添加了新的 `Material` 图标,包括 `Dash` 自己的图标。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image4)
|
||||
|
||||
这些更新使开发者的应用程序的 `Material` 图标总数达到了 7,000 多个,现在可以在fonts.google.com/icons 上按类别和名称进行搜索。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image5)
|
||||
|
||||
|
||||
> 找到合适的图标后,新的 Flutter 标签会显示如何使用它,或者可以选择仅下载该图标以用作应用程序中。
|
||||
|
||||
|
||||
### 改善文字处理
|
||||
|
||||
文本处理一直是 Flutter 里着重处理的领域,在此版本中已经开始重构处理文本输入的方式,以启用诸如在 **`Widget` 点击冒泡时取消 `keystroke` 之类的功能,并引入完全自定义与文本操作相关的 `keystrokes` 的功能**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image6)
|
||||
|
||||
|
||||
能够取消 `keystrokes` 使 **Flutter 能够实现使用空格键和箭头键之类触发滚动的功能**,从而为最终用户提供更直观的体验。在 `keystrokes` 进入到应用程序中的父窗口 `Widget` 之前,开发者可以使用相同的功能来处理 `keystrokes`。
|
||||
|
||||
另一个示例是可以在 `TextField` 和按钮之间使用 Tab 键切换:
|
||||
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
class App extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Flutter Text Editing Fun',
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
TextField(),
|
||||
OutlinedButton(onPressed: () {}, child: const Text('Press Me')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image7)
|
||||
|
||||
**自定义文本操作让开发者可以执行诸如 `TextField`中 `Enter` 键的特殊处理之类的操作**,例如可以触发在聊天客户端中发送消息,同时允许通过 `Ctrl` + `Enter` 插入换行符。
|
||||
|
||||
> 这些文本操作使 Flutter 本身可以提供不同的 `keystrokes` ,以将文本编辑的行为与主机 OS 本身进行匹配,如 Windows 和 Linux上 的 `Ctrl + C` 和 macOS 上的 `Cmd + C`。
|
||||
|
||||
下面的示例将覆盖默认的向左箭头操作,并为 `Backspace`和 `Delete` 键提供新的操作:
|
||||
|
||||
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Flutter TextField Key Binding Demo',
|
||||
home: Scaffold(body: UnforgivingTextField()),
|
||||
);
|
||||
}
|
||||
|
||||
/// A text field that clears itself if the user tries to back up or correct
|
||||
/// something.
|
||||
class UnforgivingTextField extends StatefulWidget {
|
||||
@override
|
||||
State<UnforgivingTextField> createState() => _UnforgivingTextFieldState();
|
||||
}
|
||||
|
||||
class _UnforgivingTextFieldState extends State<UnforgivingTextField> {
|
||||
// The text editing controller used to clear the text field.
|
||||
late TextEditingController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
// This overrides the left arrow key binding that the text field normally
|
||||
// has in order to move the cursor back by a character. The default is
|
||||
// created by the MaterialApp, which has a DefaultTextEditingShortcuts
|
||||
// widget in it.
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const ClearIntent(),
|
||||
|
||||
// This binds the delete and backspace keys to also clear the text field.
|
||||
// You can bind any key, not just those already bound in
|
||||
// DefaultTextEditingShortcuts.
|
||||
LogicalKeySet(LogicalKeyboardKey.delete): const ClearIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.backspace): const ClearIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
// This binds the intent that indicates clearing a text field to the
|
||||
// action that does the clearing.
|
||||
ClearIntent: ClearAction(controller: controller),
|
||||
},
|
||||
child: Center(child: TextField(controller: controller)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// An intent that is bound to ClearAction.
|
||||
class ClearIntent extends Intent {
|
||||
const ClearIntent();
|
||||
}
|
||||
|
||||
/// An action that is bound to ClearIntent that clears the TextEditingController
|
||||
/// passed to it.
|
||||
class ClearAction extends Action<ClearIntent> {
|
||||
ClearAction({required this.controller});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
Object? invoke(covariant ClearIntent intent) {
|
||||
controller.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image8)
|
||||
|
||||
### 自动滚动行为
|
||||
|
||||
实际显示滚动条时 Android 和 iOS 的逻辑是相同的,而对于桌面应用程序,当内容大于容器时通常会自动显示滚动条,这需要添加 `Scrollbar` 作为父 `Widget`,**为了在手机或 PC 上都能正常,此版本`Scrollbar` 会在必要时会自动添加**。
|
||||
|
||||
例如下面所示的无滚动条的代码:
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
class App extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Automatic Scrollbars',
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: ListView.builder(
|
||||
itemCount: 100,
|
||||
itemBuilder: (context, index) => Text('Item $index'),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
在桌面上运行它时,将显示一个滚动条:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image9)
|
||||
|
||||
如果你不喜欢滚动条的外观或始终显示滚动条的逻辑,**可以设置一个 `ScrollBarTheme`,则可以在整个应用范围内或在特定实例上,通过设置来更改它 `ScrollBehavior` 来完成修改**。
|
||||
|
||||
### 鼠标光标在文本范围内
|
||||
|
||||
在 Flutter 的早期版本中,开发者可以在任何窗口小部件上添加鼠标光标(如指示可点击内容的手),而实际上 Flutter 本身在大多数情况下会添加这些鼠标光标,例如:在所有按钮上添加一个手形鼠标光标。
|
||||
|
||||
但是如果要运行带有不同文本跨度,且具有各自样式并且可能足够长的自动换行的格式丰富的文本,那么`TextSpan` 就不会是一个 `Widget`,因此不能用作鼠标光标的可视范围...而从此版本开始,**当拥有 `TextSpan` 带有手势识别器的时将自动获得相应的鼠标光标**:
|
||||
|
||||
|
||||
```dart
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
class App extends StatelessWidget {
|
||||
static const title = 'Flutter App';
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: title,
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: Text(App.title)),
|
||||
body: Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(fontSize: 48),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'This is not a link, ',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'but this is',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
urlLauncher.launch('https://flutter.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
现在可以拥有所需的自动换行文字跨度,并且其中任何带有识别器的文字都将获得适当的鼠标光标。
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image10)
|
||||
|
||||
在此版本中,`TextSpan` 还支持 `onEnter`和 `onExit` ,并且对应的拥有 `mouseCursor`。
|
||||
|
||||
## Flutter 2.2 更新预览
|
||||
|
||||
除了可用于生产的新功能外,Flutter 2.2 还提供了许多预览功能,包括 **iOS 着色器编译器性能改进,Android 延迟组件支持,Flutter 桌面更新以及 Sony 的 ARM64 Linux 主机支持**。
|
||||
|
||||
### 预览:iOS着色器编译改进
|
||||
|
||||
用图形渲染术语来说,“着色器” 是要在最终用户设备上可用的 GPU 编译并运行的程序。自成立以来 Flutter 一直在底层 Skia 图形库中使用着色器,以其自身的高质量图形效果(包括颜色,阴影,动画等)提供本机性能。
|
||||
|
||||
由于 Flutter API 的灵活性,着色器可以实时生成和编译,并与需要它们的帧工作负载同步,所以当编译着色器的时间超出框架预算时,体验结果对于用户来说会很明显。
|
||||
|
||||
为了避免出现问题,**Flutter 提供了在运行期间训练缓存着色器的功能,然后将它们打包并捆绑到应用程序中,并在 Flutter Engine 启动时在第一帧之前进行编译**。这意味着预编译的着色器不必在帧工作负载期间进行编译,也不会造成垃圾回收,但是 Skia 最初仅为 `OpenGL` 实现了此功能。
|
||||
|
||||
> 因此当我们默认情况下在 iOS 上启用 `Metal` 以响应 Apple 弃用 `OpenGL` 时,根据我们的基准测试,渲染帧时间增加了,而用户报告的产生的垃圾也增加了。
|
||||
|
||||
我们的测量数据表明,这些报告通常是由于着色器编译时间增加,Skia 为 `Metal` 后端生成的着色器数量,增加以及已编译的着色器无法在各次运行之间缓存,而使得 jank 持续到第一次运行之外而导致的一个应用程序。
|
||||
|
||||
> 因此直到现在,在 iOS 上避免这种麻烦的唯一方法是简化场景和动画,但这并不理想。
|
||||
|
||||
但是,**现在在 dev 通道上是 Skia 中对 `Metal` 的着色器预热的新支持的预览**,通过 Skia,Flutter 现在可以在第一帧工作负载开始之前编译带捆绑的着色器。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image11)
|
||||
|
||||
但是,此解决方案有一些警告:
|
||||
|
||||
- Skia 仍然需要为 `Metal` 生成比 `OpenGL` 后端更多的着色器;
|
||||
- 最终的着色器对机器代码的编译仍需要与框架工作负载同步发生,但这比在框架渲染时间中进行整个着色器生成和编译要快;
|
||||
- 首次运行应用程序后,将缓存生成的机器代码,直到重新启动设备为止;
|
||||
|
||||
如果想在应用程序中利用此新支持,可以按照 flutter.dev 上的说明进行操作。
|
||||
|
||||
但是,我们还没有完成这项工作。在 Android 和 iOS 上此实现都有一些缺点:
|
||||
|
||||
- 部署的应用程序的大小较大,因为它包含捆绑的着色器;
|
||||
- 应用程序启动等待时间更长,因为捆绑的着色器需要预先编译;
|
||||
- 开发人员暗示了我们对这种实现所带来的体验不满意;
|
||||
|
||||
我们认为最后一个问题最重要,特别是查看了执行培训运行的过程,并推理了因应用程序大小和应用程序启动延迟而带来过于繁琐的折衷。
|
||||
|
||||
**因此我们将继续研究,消除不依赖此实现的着色器编译垃圾以及所有垃圾的方法**。特别是我们正在与 Skia 团队合作,以减少响应 Flutter 的要求而生成的着色器的数量,并研究使用 Flutter Engine 捆绑的一小套静态定义的着色器实现。
|
||||
|
||||
> 可以在Flutter 中关注该项目,以了解我们的进度:https://github.com/flutter/flutter/projects/188
|
||||
|
||||
### Android 延迟加载组件
|
||||
|
||||
对于 Android 版本,使用 Dart 的拆分 AOT 编译功能,[允许 Flutter 应用程序在运行时下载包含提前编译的代码和 assets 的模块](https://github.com/flutter/flutter/pull/76192)。
|
||||
|
||||
**将这些可安装拆分的模块称为延迟组件**,通过仅在需要时才推迟下载代码和 assets ,可以大大减小初始安装大小,**例如我们实施了 `Flutter Gallery` 版本初始安装尺寸减少了 46 %**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image12)
|
||||
|
||||
在启用延迟组件的情况下进行构建时,**Dart 会将仅使用 `deferred` 关键字导入的代码编译到单独的共享库中,这些共享库与 assets 一起打包到延迟组件中**。
|
||||
|
||||
目前延迟组件仅在 Android 上可用,并且此功能作为早期预览版提供,在 flutter.dev 上新的[`Deferred components`](https://flutter.dev/docs/perf/deferred-components) 页面中了解如何实现延迟的组件。
|
||||
|
||||
> https://flutter.dev/docs/perf/deferred-components
|
||||
>
|
||||
> http://github.com/flutter/flutter/issues
|
||||
|
||||
|
||||
### Flutter Windows UWP Alpha
|
||||
|
||||
Flutter 的另一个更新是针对 PC 的,对Windows UWP 的支持已在移至了 alpha。UWP 允许将Flutter 应用程序带到无法运行标准 Windows 应用程序的设备(包括Xbox)。
|
||||
|
||||
要进行尝试首先需要设置 UWP 先决条件。然后切换到 dev 通道并启用 UWP 支持:
|
||||
|
||||
```
|
||||
$ flutter channel dev
|
||||
$ flutter upgrade
|
||||
$ flutter config — enable-windows-uwp-desktop
|
||||
```
|
||||
|
||||
启用后,创建 Flutter 应用程序将包括一个新 winuwp 文件夹,该文件夹可让在 UWP 容器中构建和运行应用程序:
|
||||
|
||||
|
||||
```
|
||||
$ flutter create uwp_fun
|
||||
$ cd uwp_fun
|
||||
$ flutter pub get
|
||||
$ flutter run -d winuwp
|
||||
```
|
||||
|
||||
因为要构建 Windows UWP 应用程序在 Windows 的沙箱环境中运行,所以在开发过程中需要在本地主机上的应用程序防火墙上打一个洞,以启用诸如热重载和调试器断点之类的功能。
|
||||
|
||||
可以按照 Flutter 桌面文档页面 `checknetisolation` 上的说明使用命令执行此操作,完成此操作后可以在 Windows 上看到 Flutter 应用程序作为 UWP 应用程序运行。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image13)
|
||||
|
||||
当然也可以运行更多有趣的UWP应用,例如在 Xbox 上运行的 Flutter 应用。
|
||||
|
||||
> 请查看flutter.dev/desktop/#windows-uwp。
|
||||
|
||||
|
||||
### 索尼对 ARM64 Linux 主机的支持
|
||||
|
||||
|
||||
Flutter 社区的另一项杰出成就来自 Sony 的软件工程师 HidenoriMatsubayashi,他为针对ARM64 Linux 的支持做出了贡献,通过此 PR 可以在 ARM64 Linux 上构建和运行 Flutter 应用程序。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image14)
|
||||
|
||||
> https://github.com/flutter/flutter/pull/61221
|
||||
|
||||
|
||||
### Flutter 生态系统和工具更新
|
||||
|
||||
Flutter Engine 和 Framework 只是整个 Flutter 生态的一部分,软件包生态系统和工具的更新对 Flutter 开发人员来说同样重要。
|
||||
|
||||
在生态系统方面,本次将发布许多新的 Flutter Favorite 软件包,以及 FlutterFire(Flutter对 Firebase 的支持)的一些更新,其中 FlutterFire 支持新的 Firebase App Check 预览,因此 Flutter 开发人员可以马上就用到它。
|
||||
|
||||
在工具方面,**Flutter DevTools 进行了新的更新以优化应用程序的内存占用**,并为提供程序包增加了一个新选项卡,另外 **VS Code 和 Android Studio / IntelliJ 的 IDE 插件都有显着更新**,并且如果是针对 Flutter 的内容作者,则可以采用全新的方式将 DartPad 集成到作品中。
|
||||
|
||||
最后有一个名为 `FlutterFlow` 的新的低代码应用程序设计和构建工具,该工具针对 Flutter 并在Web上运行,因为它本身是由 Flutter 构建的。
|
||||
|
||||
### Flutter 最受欢迎的更新
|
||||
|
||||
作为该版本的一部分,Flutter 生态系统委员会认证了 24 个新的 Flutter Favorite 软件包,这是我们迄今为止最大的扩展,新标记的 Flutter 收藏夹包括:
|
||||
|
||||
- FlutterFire :`cloud_firestore`,`cloud_functions`,`firebase_auth`,`firebase_core`,`firebase_crashlytics`,`firebase_messaging` 和`firebase_storage` ;
|
||||
|
||||
> http://firebase.flutter.dev/
|
||||
|
||||
- 社区 plus 包:`android_alarm_manager_plus`,`android_intent_plus`,`battery_plus`,`connectivity_plus`,`device_info_plus`,`network_info_plus`,`package_info_plus`,`sensors_plus` 和 `share_plus`;
|
||||
|
||||
> http://plus.fluttercommunity.dev/
|
||||
|
||||
- `googleapis`
|
||||
- `win32`
|
||||
- `intl` 和 `characters`
|
||||
- Sentry packages :`sentry`和 `sentry_flutter`
|
||||
- `infinite_scroll_pagination`和 `flutter_native_splash`
|
||||
|
||||
|
||||
所有这些软件包都已迁移到空安全的状态,并视情况支持 Android,iOS 和 Web 。
|
||||
|
||||
> 例如:firebase_crashlytics 上没有底层 SDK,android_alarm_manager_plus 是专门为Android 设计的。
|
||||
|
||||
社区 plus 提供从 Flutter 团队包的超集。例如自 Flutter 最初发行之前,Flutter 团队就由Google 的电池组提供了 `bettery package`,并且已迁移至零安全状态,但仅在 Android 和 iOS 上受支持,而 **`battery_plus` 包另一方面它支持所有六个 Flutter 平台,包括 Web,Windows,macOS 和 Linux**。
|
||||
|
||||
> 九个 “plus” 软件包都获得了 Flutter 受欢迎的奖项,这代表了 Flutter 整个社区在成熟度上迈出的一大步。
|
||||
|
||||
`googleapis` 插件提供了约 185 个 Google API 的自动生成的 Dart 包装器,可在客户端或服务器端 Dart应 用程序(包括Flutter应用程序)中使用。
|
||||
|
||||
`win32` 程序包是工程学的奇迹,**它使用 Dart FFI 封装了大多数常用的 Win32 API 调用**,以使 Dart 代码可以直接访问它们,而无需使用 C 编译器或 Windows SDK 。
|
||||
|
||||
随着 Flutter 在 Windows 平台上的流行,该 win32 软件包已成为许多流行插件(包括`path_provider`)最流行的插件的关键依赖项。作为完整性的测试,作者 timsneath 使用原始 Dart 在原始 Win32 中做了一些有趣的事情,例如实现记事本,蛇和俄罗斯方块:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image15)
|
||||
|
||||
> 该 win32 软件包绝对值得一试,看看你是否能够在 Windows 上使用 Dart 或 Flutter 进行了任何操作。
|
||||
|
||||
### FlutterFire 更新和 Firebase 应用程序检查
|
||||
|
||||
`FlutterFire` 是 Flutter 对 Firebase 的支持,是 Flutter 上最受欢迎的插件集合之一。
|
||||
|
||||
`Invertase` 在 Flutter 2 版本上投入生产以来一直在进行改进方面做得非常出色。实际上自FlutterFire 首次发布以来,`Invertase` 处理了 79 %的未解决问题,并将未完成的 PR 数减少了88%。
|
||||
|
||||
此外他们不仅在生产质量插件方面做得很好,而且还将 Beta 质量插件迁移到了零安全性,并使其在同一内核上构建和运行,以便开发者可以混合和匹配。
|
||||
|
||||
此外,`Invertase` 继续为 `FlutterFire` 插件添加新功能,其中包括对该版本 Flutter 进行的 Flutter 与 `Cloud Firebase` 集成的许多更新:
|
||||
|
||||
|
||||
- [`Typesafe`](https://firebase.flutter.dev/docs/firestore/usage/#typing-collectionreference-and-documentreference) 用于读取和写入数据的API
|
||||
- 支持 Firebase 本地仿真器套件;
|
||||
- 使用数据包优化数据查询
|
||||
|
||||
最后 `FlutterFire` 支持新 Firebase 产品的 Beta 版本:`Firebase App Check`。
|
||||
|
||||
`Firebase App Check` 可保护您的后端资源(如 Cloud Storage )免受计费欺诈或网络钓鱼之类的滥用,借助 App Check 运行 Flutter 应用程序的设备,会使用应用程序身份证明提供程序来证明它确实是您的真实应用程序,并且还可以检查它是否在未受干扰的真实设备上运行。
|
||||
|
||||
> https://firebase.flutter.dev/docs/app-check/overview
|
||||
|
||||
### Flutter DevTools 更新
|
||||
|
||||
Flutter DevTools 在此版本中进行了许多值得注意的更新,**包括两项内存跟踪改进以及一个仅用于 provider 插件的全新标签**。
|
||||
|
||||
此版本的 DevTools 中的第一个内存跟踪改进功能,**提供了跟踪对象分配位置的功能,让开发者在代码中查找内存泄漏的位置非常方便。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image16)
|
||||
|
||||
第二种是**将自定义消息注入到内存时间轴的功能,这样开发者就可以提供特定于应用程序的标记,例如在完成一些占用大量内存的工作之前和之后,以便可以检查自己是否清理正确**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image17)
|
||||
|
||||
> 随着 Flutter 应用的规模越来越大,我们将继续确保 Flutter 开发人员拥有跟踪和修复各种内存泄漏和运行时问题所需的工具。
|
||||
|
||||
在使用要跟踪的 Flutter 框架时,不仅是运行时问题,而且还存在一些其他问题:**有时开发者也想跟踪与软件包有关的问题**。
|
||||
|
||||
pub.dev 上有超过 15,000 个与 Flutter 兼容的软件包和插件,应用随着时间的推移使用更多软件包的可能性越来越大。考虑到这一点,我们一直在尝试向 Flutter DevTools 添加新的 “Provider”选项卡。
|
||||
|
||||
事实上,这个标签是由 Remi Roussel 创建,provider 包维护人员。如果你正在运行最新版本的Flutter DevTools,并且正在调试使用 provider 插件的 Flutter 应用程序,则将自动获得新的“Provider” 选项卡。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image18)
|
||||
|
||||
|
||||
“Provider”选项卡向开发者显示与每个提供程序相关的数据,包括**在运行应用程序时的实时更改,它可以让您直接更改数据,以测试应用程序的主要情况!**
|
||||
|
||||
这只是此发行版中Flutter DevTools中一些很酷的新功能,有关完整列表,请在此处查看各个公告:
|
||||
|
||||
- Flutter DevTools 2.1 :https://groups.google.com/g/flutter-announce/c/tCreMfJaJFU/m/38p1BBeiCAAJ
|
||||
- Flutter DevTools 2.2.1 :https://groups.google.com/g/flutter-announce/c/t8opLnUyiFQ/m/dJth-jKxAAAJ
|
||||
- Flutter DevTools 2.2.3 :https://groups.google.com/g/flutter-announce/c/t8opLnUyiFQ/m/YX5Ds_q0AgAJ
|
||||
|
||||
|
||||
### IDE插件更新
|
||||
|
||||
Flutter 的 Visual Studio Code 和 IntelliJ / Android Studio IDE 扩展也已在此版本中更新,例如 Visual Studio Code 扩展现在支持两个附加的 Dart 代码重构:**内联方法和内联局部变量**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image19)
|
||||
|
||||
在 Android Studio / IntelliJ 扩展中,我们添加了**使用选项将所有堆栈跟踪打印到控制台的功能**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image20)
|
||||
|
||||
|
||||
### DartPad workshops
|
||||
|
||||
为了确保我们能够在迅速发展的 Flutter 开发人员社区中准备好文档,Dart 和 Flutter 团队一直在寻找改进和扩展创建教育内容的方法。
|
||||
|
||||
在此版本中,我们为 DartPad 添加了一个新的分步 UI,开发人员可以使用该 UI 跟随讲师指导的讲习班。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-220/image21)
|
||||
|
||||
通过直接向 DartPad 添加说明,我们可以为 I/O 提供指导性的工作室体验,但是我们不只是为自己的工作室构建它;如果你想在 Dart 或 Flutter Workshop 中使用它,可以按照 DartPad Workshop 创作指南进行操作。
|
||||
|
||||
> https://github.com/dart-lang/dart-pad/wiki/Workshop-Authoring-Guide
|
||||
|
||||
这样的主旨在于利用 DartPad 共享代码,并在自己的网站中嵌入 DartPad 。
|
||||
|
||||
|
||||
### 社区聚焦:FlutterFlow
|
||||
|
||||
**FlutterFlow 是一款“低代码”应用程序设计和开发工具,可以通过浏览器中构建所有应用程序,它提供了一种所见即所得的环境**,可以使用 Firebase 的真实数据跨多个页面布置你的应用程序。
|
||||
|
||||
低代码工具的目标是轻松完成大多数常见的事情,从而开发者可以编写尽可能少的自定义代码行。实际上作为演示,他们构建了一个完整的多页移动应用程序,用于在不到一个小时的时间内,你可以在YouTube 上看到整个过程。
|
||||
|
||||
> https://youtu.be/TXsjnd_4SBo
|
||||
|
||||
|
||||
FlutterFlow 输出 Flutter 代码,因此如果需要添加代码以进一步自定义应用程序,你可以在flutterflow.io 上了解有关 FlutterFlow 产品发布的信息。
|
||||
|
||||
> https://flutterflow.io/blog/launch
|
||||
|
||||
## 重大变化
|
||||
|
||||
与往常一样,我们一直努力减少重大更改的数量,在此版本中,我们已将其限制为消除以下弃用项:
|
||||
- 73750 删除不建议使用的BinaryMessages:https://github.com/flutter/flutter/pull/73750
|
||||
- 73751 删除不推荐使用的 `TypeMatcher`类 :https://github.com/flutter/flutter/pull/73751
|
||||
|
||||
## 概括
|
||||
|
||||
Play商店中有八分之一以上的新应用是使用 Flutter 构建,仅 Play 商店中有超过 20 万个Flutter 应用,这样的持续增长令人震惊,世界各地各种规模的应用程序都将其 UI 委托给Flutter,以打造精美的多平台体验,以迎合他们所处的任何地方的用户。
|
364
Flutter-250.md
364
Flutter-250.md
|
@ -1,364 +0,0 @@
|
|||
> 原文链接 https://medium.com/flutter/whats-new-in-flutter-2-5-6f080c3f3dc
|
||||
|
||||
**Flutter 2.5 是 Flutter 版本历史上排名第二的大版本更新**,该版本:
|
||||
|
||||
- 关闭了 4600 个 issues;
|
||||
- 合并了 252 contributors 和 216 reviewers 的 3932 个 PR;
|
||||
|
||||
回顾过去一年,可以看到有 1337 位 contributors 创建了 21,072 个 PR 这样庞大的数据,其中有15,172 个被合并。
|
||||
|
||||
事实上该版本依然是对性能和开发工具进行了改进,同时还有增加许多新功能,包括:
|
||||
|
||||
- 对 Android 的全屏支持、更多 Material You(也称为 v3)支持;
|
||||
- 更新文本编辑功能以支持可切换的键盘快捷键;
|
||||
- Widget Inspector 可查阅更多详细信息;
|
||||
- Visual Studio Code 项目中对添加依赖项增加新的支持;
|
||||
- IntelliJ/Android Studio 中新增测试运行获取覆盖率信息;
|
||||
- 一个全新的应用程序模板,为 Flutter 应用程序提供更好的开发基础;
|
||||
|
||||
|
||||
## 性能:iOS 着色器预热、异步任务、GC 和消息传递
|
||||
|
||||
[#25644](https://github.com/flutter/engine/pull/25644) 中的第一个 PR 就是**用于离线训练运行 Metal 着色器预编译**,如基准测试所示,它将最坏情况的帧光栅化时间减少了 2/3 秒,将第 99 个百分位帧减少了一半。
|
||||
|
||||
然而着色器预热只是卡顿的来源之一,在**之前的版本处理来自网络、文件系统、插件或其他 isolate 的异步事件都可能会中断动画**,这是另一个卡顿的来源。
|
||||
|
||||
|
||||
所以 [#25789](https://github.com/flutter/engine/pull/25789) 改进了调度策略,在此版本 isolate 的 UI 事件循环里,帧处理现在优先于处理其他异步事件,从而在测试中消除了此类的卡顿。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image1)
|
||||
|
||||
|
||||
另一个导致卡顿的原因是**垃圾收集器 (GC) 暂停 UI 线程以回收内存**。
|
||||
|
||||
以前某些图像的内存在响应 Dart VM 的 GC 执行时会延迟回收,作为早期版本中的解决方法,Flutter 引擎会通过 Dart VM 的 GC 回收暗示图像内存可以回收,这在理论上可以实现了更及时的内存回收。
|
||||
|
||||
不幸的是这也导致了太多的主要 GC,并且有时仍然无法足够快地回收内存,以避免内存受限设备上的低内存情况,而**在这个版本中未使用的图像的内存被急切地回收**([#26219](https://github.com/flutter/engine/pull/26219)、[#82883](https://github.com/flutter/flutter/pull/82883)、[#84740](https://github.com/flutter/flutter/pull/84740)),大大减少了 GC。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image2)
|
||||
|
||||
例如在一项测试用例中,**播放 20 秒动画 GIF 从需要 400 多次 GC 变为只需要 4 次**,更少的主要 GC 意味着涉及图像出现和消失的动画将减少卡顿,并消耗更少的 CPU 和功率。
|
||||
|
||||
Flutter 2.5 的另一个性能改进是在 **Dart 和 Objective-C/Swift (iOS) 或 Dart 和 Java/Kotlin (Android) 之间发送消息时的延迟。**
|
||||
|
||||
通常作为[调整](https://docs.google.com/document/d/1oNLxJr_ZqjENVhF94-PqxsGPx0qGXx-pRJxXL6LSagc/edit#heading=h.9gabvat7tlxf) 消息频道的一部分,从消息编解码器中删除不必要的副本可将延迟减少高达 50% ,当然具体取决于消息大小和设备([#25988](https://github.com/flutter/engine/pull/25988),[#26331](https://github.com/flutter/engine/pull/26331))。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image3)
|
||||
|
||||
> 你可以在此链接获取到更多关于平台通道性能的信息: https://medium.com/flutter/improving-platform-channel-performance-in-flutter-e5b4e5df04af
|
||||
|
||||
## Dart 2.14:格式、语言特性、发布和 linting 开箱即用
|
||||
|
||||
此版本的 Flutter 和 Dart 2.14 一起发布。
|
||||
|
||||
[新版本的 Dart](https://medium.com/@mit.mit/announcing-dart-2-14-b48b9bb2fb67) 带有新的格式,使[级联](https://dart.dev/guides/language/language-tour#cascade-notation) 更加清晰,**新的 pub 支持忽略文件,以及新的语言功能,包括三重移位运算符的回归**。
|
||||
|
||||
**此外 Dart 2.14 创建了一组标准的 lint,在新的 Dart 和 Flutter 项目之间共享,开箱即用**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image4)
|
||||
|
||||
开发者不仅会在创建新的 Dart 或 Flutter 项目时获得这些 lint,而且只需几个步骤就可以将相同的分析添加到现有应用程序中。
|
||||
|
||||
> 合并迁移lint:https://flutter.dev/docs/release/breaking-changes/flutter-lints-package#migration-guide
|
||||
>
|
||||
> 有关这些 lint 的详细信息、新语言功能等,请查看 https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067
|
||||
|
||||
## Framework:Android 全屏、Material You & 文本编辑快捷方式
|
||||
|
||||
从 [#81303](https://github.com/flutter/flutter/pull/81303) 开始, [我们修复了 Android 一系列与全屏模式相关的问题](https://github.com/flutter/flutter/pull/81303),此更改还添加了一种在其他模式下收听全屏更改的方法。
|
||||
|
||||
> 例如用户与应用互动时,当系统 UI 返回时,**开发人员现在可以编写代码在返回全屏时执行其他操作**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image5)
|
||||
|
||||
|
||||
> *新的 Android 全屏模式:普通模式(左)、边到边模式(中)、带有自定义 SystemUIOverlayStyle 的边到边(右)*
|
||||
|
||||
|
||||
在此版本中,我们对新 Material You(又名 v3)的规范增加了支持,**包括对浮动操作按钮大小和主题的更新**([#86441](https://github.com/flutter/flutter/pull/86441)),在`MaterialState.scrolledUnder` 可以使用 Demo 中的示例代码查看的新状态 PR 式例 ( [#79999](https://github.com/flutter/flutter/pull/79999) )。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image6)
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image7)
|
||||
|
||||
> *新的 MaterialState.scrolledUnder 状态在起作用*
|
||||
|
||||
另一个改进是**添加了 scroll metrics notifications**([#85221](https://github.com/flutter/flutter/pull/85221)、[#85499](https://github.com/flutter/flutter/pull/85499)),即使用户没有滚动,它也会提供可滚动区域的通知,例如下面显示了 `ListView` 根据的基础大小适当地出现或消失滚动条:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image8)
|
||||
|
||||
在这种情况下不必编写任何代码,但如果想捕获 `ScrollMetricNotification` 更改,则可以通过此监听来完成。
|
||||
|
||||
> 特别感谢社区贡献者[xu-baoolin](https://github.com/xu-baolin),他为此付出了努力并提出了一个很好的解决方案。
|
||||
|
||||
|
||||
另一个出色的社区贡献是为 `ScaffoldMessenger` , 你可能还记得 Flutter 2.0 开始 `ScaffoldMessenger` 作为一个更强大的方式来显示 `SnackBars` , 在屏幕的底部为用户提供通知,而在 **Flutter 2.5 中,现在可以在 `Scaffold` 的顶部添加一个横幅**,该横幅会一直保持到用户关闭它为止。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image9)
|
||||
|
||||
应用程序可以通过调用以下 `showMaterialBanner` 方法来获得此行为 `ScaffoldMessenger` :
|
||||
|
||||
```dart
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('The MaterialBanner is below'),
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Show MaterialBanner'),
|
||||
onPressed: () => ScaffoldMessenger.of(context).showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('Hello, I am a Material Banner'),
|
||||
leading: const Icon(Icons.info),
|
||||
backgroundColor: Colors.yellow,
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Dismiss'),
|
||||
onPressed: () => ScaffoldMessenger.of(context)
|
||||
.hideCurrentMaterialBanner(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
[Material 指南规定](https://material.io/components/banners#usage) 开发者的应用[横幅](https://material.io/components/banners#usage)一次只能显示一个,因此如果你调用多次 `showMaterialBanner`,`ScaffoldMessenger` 它将维护一个队列,在前一个横幅已被关闭之后,再显示一个新横幅。
|
||||
|
||||
> 感谢 [Calamity210](https://github.com/Calamity210) 对 Flutter 中的 Material 支持做出了如此出色的补充!
|
||||
|
||||
在此版本中,我们添加了**文本编辑键盘快捷键可覆盖的功能** [#85381](https://github.com/flutter/flutter/pull/85381),这是在 Flutter 2.0 及其新的文本编辑功能的基础上进一步构建,例如文本选择以及能够在处理键盘事件后停止它的传播。
|
||||
|
||||
> 如果您希望 **Ctrl-A** 执行一些自定义操作而不是选择所有文本。
|
||||
|
||||
`DefaultTextEditingShortcuts` 类包含每个平台上受支持的键盘快捷键列表,如果开发者想覆盖任何内容,可以使用 Flutter 的现有 `Shortcuts` 将任何快捷方式重新映射到现有或自定义意图。
|
||||
|
||||
> API 参考: https://api.flutter.dev/flutter/widgets/DefaultTextEditingShortcuts-class.html
|
||||
|
||||
## 插件:相机、图像选择器和 plus 插件
|
||||
|
||||
另一个具有有很多改进的插件是[相机插件](https://pub.dev/packages/camera):
|
||||
|
||||
- [#3795](https://github.com/flutter/plugins/pull/3795) [相机] android-rework 第 1 部分:支持 Android 相机功能的基类
|
||||
- [#3796](https://github.com/flutter/plugins/pull/3796) [相机] android-rework 第 2 部分:Android 自动对焦功能
|
||||
- [#3797](https://github.com/flutter/plugins/pull/3797) [camera] android-rework part 3:Android曝光相关功能
|
||||
- [#3798](https://github.com/flutter/plugins/pull/3798) [相机] android-rework 第 4 部分:Android 闪光和变焦功能
|
||||
- [#3799](https://github.com/flutter/plugins/pull/3799) [相机] android-rework 第 5 部分:Android FPS 范围、分辨率和传感器方向功能
|
||||
- [#4039](https://github.com/flutter/plugins/pull/4039) [相机] android-rework 第 6 部分:Android 曝光和焦点功能
|
||||
- [#4052](https://github.com/flutter/plugins/pull/4052) [camera] android-rework part 7:Android降噪功能
|
||||
- [#4054](https://github.com/flutter/plugins/pull/4054) [相机] android-rework 第 8 部分:最终实现的支持模块
|
||||
- [#4010](https://github.com/flutter/plugins/pull/4010) [camera] 在 iOS 上不触发设备方向
|
||||
- [#4158](https://github.com/flutter/plugins/pull/4158) [相机] 修复坐标旋转以在 iOS 上设置焦点和曝光点
|
||||
- [#4197](https://github.com/flutter/plugins/pull/4197) [相机] 修复相机预览并不总是在方向改变时重建
|
||||
- [#3992](https://github.com/flutter/plugins/pull/3992) [camera] 设置不受支持的 FocusMode 时防止崩溃
|
||||
- [#4151](https://github.com/flutter/plugins/pull/4151) [camera] 引入camera_web包
|
||||
|
||||
[image_picker 插件](https://pub.dev/packages/image_picker) 也做了很多工作,专注于端到端的相机体验:
|
||||
|
||||
- [#3898](https://github.com/flutter/plugins/pull/3898) [image_picker] 图像选择器修复相机设备
|
||||
- [#3956](https://github.com/flutter/plugins/pull/3956) [image_picker] 将相机捕获的存储位置更改为 Android 上的内部缓存,以符合新的 Google Play 存储要求
|
||||
- [#4001](https://github.com/flutter/plugins/pull/4001) [image_picker] 删除了对相机权限的冗余请求
|
||||
- [#4019](https://github.com/flutter/plugins/pull/4019) [image_picker] 当相机是 source 时修复旋转
|
||||
|
||||
这项工作改进了 Android 的相机和 image_picker 插件的功能和稳健性。
|
||||
|
||||
此外你会注意到 [摄像头插件](https://pub.dev/packages/camera_web) 的早期版本可用于网络支持 ( [#4151](https://github.com/flutter/plugins/pull/4151) )。
|
||||
|
||||
此预览为在 Web 上查看相机预览、拍照、使用闪光灯和缩放控件提供基本支持,它目前还不是被[认可的插件](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin),因此您需要[明确添加它](https://pub.dev/packages/camera_web/install)以在才能在 web 中使用。
|
||||
|
||||
> 详细内容: https://pub.dev/packages/camera_web/install
|
||||
|
||||
在此版本的 Flutter 中,Flutter 团队的每个相应插件现在都带有一个类似 [电池](https://pub.dev/packages/battery) 的建议:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image10)
|
||||
|
||||
此外,由于这些插件不再被积极维护,它们不再被标记为 Flutter 最喜欢的插件,我们建议使用以下插件的 plus 版本:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image11)
|
||||
|
||||
## Flutter DevTools:性能、Widget 检查器和 Polish
|
||||
|
||||
首先最重要的是 DevTools 中增加利用引擎更新的支持([#26205](https://github.com/flutter/engine/pull/26205)、[#26233](https://github.com/flutter/engine/pull/26233)、[#26237](https://github.com/flutter/engine/pull/26237)、[#26970](https://github.com/flutter/engine/pull/26970)、[#27074](https://github.com/flutter/engine/pull/27074)、[#26617](https://github.com/flutter/engine/pull/26617))。
|
||||
|
||||
其中一组更新使 **Flutter 能够更好地将跟踪事件与特定框架相关联**,这有助于开发人员确定框架可能超出预算的原因。
|
||||
|
||||
可以在 DevTools Frames 图表中看到这一点,该图表已被重建为“实时”,可以在应用程序呈现时填充在此图表中,从此图表中选择一个帧导航到该帧的时间线事件:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image12)
|
||||
|
||||
Flutter 引擎现在还可以识别时间线中的着色器编译事件,Flutter DevTools 使用这些事件来帮助诊断应用程序中的着色器编译卡顿。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image13)
|
||||
|
||||
借助这项新功能,DevTools 会检测何时因着色器编译丢失帧,以便可以解决卡顿问题。
|
||||
|
||||
在 `flutter run` 时与 `--purge-persistent-cache` 标志一起使用,这会清除缓存以确保重现用户在 “首次运行” 或 “重新打开” (iOS) 体验中看到的环境。
|
||||
|
||||
> 此功能仍在开发中,如果有任何问题,可以查阅:https://b.corp.google.com/issues/new?component=775375&template=1369639
|
||||
|
||||
此外跟踪应用程序中的 CPU 性能问题时,可能会被来自 Dart 和 Flutter 库或引擎本机代码的分析数据淹没,如果想关闭其中任何一个以专注于您自己的代码,您可以**使用新的 CPU Profiler 功能 [#3236](https://github.com/flutter/devtools/pull/3236) 来实现,该功能可以从这些来源中隐藏分析器信息**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image14)
|
||||
|
||||
对于没有过滤掉的任何类别,它们现在已经进行了颜色编码([#3310](https://github.com/flutter/devtools/pull/3310)、[#3324](https://github.com/flutter/devtools/pull/3324)),以便可以轻松查看 **CPU 帧图表来自系统的哪些部分**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image15)
|
||||
|
||||
> 彩色框架图,用于识别应用中的应用、原生、Dart 和 Flutter 代码活动
|
||||
|
||||
性能并不时调试的唯一因素,此版本的 DevTools 附带了对 Widget Inspector 的更新,**允许将鼠标悬停在 Widget 时评估对象、视图属性、小部件状态等**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image16)
|
||||
|
||||
而当选择一个 Widget 时,它会自动填充在新的小部件检查器控制台中,这样就可以在其中浏览 Widget 的属性。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image17)
|
||||
|
||||
**在断点处暂停时还可以从控制台计算表达式**。
|
||||
|
||||
除了新功能外 Widget Inspector 还进行了翻新,为了让 DevTools 成为了解和调试 Flutter 应用程序的更有用,我们与芬兰的一家创意技术机构[Codemate](https://codemate.com/)合作进行了一些更新。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image18)
|
||||
|
||||
在此屏幕截图中可以看到以下更改:
|
||||
|
||||
|
||||
- **更好地传达调试切换按钮的作用**——这些按钮具有新图标、面向任务的标签,以及描述它们的作用和何时使用它们的丰富工具提示,每个工具提示进一步链接到该功能的详细文档。
|
||||
|
||||
- **更容易扫描和定位感兴趣的 Widgets**——Flutter 框架中常用的 Widget 现在在检查器左侧的 Widget 树视图中显示图标,它们根据类别进一步进行颜色编码,例如布局 Widget 显示为蓝色,而内容Widget 显示为绿色。此外每个文本 Widget 现在显示其内容的预览。
|
||||
|
||||
- **对齐布局资源管理器和小部件树的配色方案**- 现在可以更轻松地从布局资源管理器和 Widget 树中识别相同的 Widget。例如屏幕截图中的“列” Widget 位于布局浏览器中的蓝色背景上,并且在 Widget 树视图中具有蓝色图标。
|
||||
|
||||
## IntelliJ/Android Studio:集成测试、测试覆盖率和图标预览
|
||||
|
||||
|
||||
Flutter 的 IntelliJ/Android Studio 插件在此版本中也进行了许多改进,首先是运行集成测试的能力 ( [#5459](https://github.com/flutter/flutter-intellij/pull/5459) )。
|
||||
|
||||
集成测试是在设备上运行的整个应用程序测试,位于 integration_test 目录中,并使用与`testWidgets()` 单元测试相同的功能。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image19)
|
||||
|
||||
要将集成测试添加到项目,请 [按照 flutter.dev 上的说明进行操作](https://flutter.dev/docs/testing/integration-tests),要将测试与 IntelliJ 或 Android Studio 连接,请添加启动集成测试的运行配置并连接设备以供测试使用,运行配置可以让开发者运行测试,包括设置断点、步进等。
|
||||
|
||||
此外,Flutter 最新的 IJ/AS 插件允许查看单元测试和集成测试运行的覆盖率信息,可以从“调试”按钮旁边的工具栏按钮访问它:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image20)
|
||||
|
||||
覆盖信息在编辑器的装订线中使用红色和绿色条显示,在这个例子中第 9-13 行被测试,但第 3 和 4 行没有被测试。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image21)
|
||||
|
||||
最新版本还包括预览来自 pub.dev 包中使用的图标的新功能,这些包是围绕 TrueType 字体文件([#5504](https://github.com/flutter/flutter-intellij/pull/5504)、[#5595](https://github.com/flutter/flutter-intellij/pull/5595)、[#5677](https://github.com/flutter/flutter-intellij/pull/5677)、[#5704](https://github.com/flutter/flutter-intellij/pull/5704))构建的,就像 Material 和 Cupertino 图标支持预览一样。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image22)
|
||||
|
||||
要启用图标预览,您需要告诉插件您正在使用哪些软件包,settings/preferences 中有一个新的文本字段:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image23)
|
||||
|
||||
|
||||
请注意,如屏幕截图中的示例代码所示,此设置仅适用于在类中定义为静态常量的图标,它不适用于表达式,例如 `LineIcons.addressBook()` or `LineIcons.values['code']` 。
|
||||
|
||||
## Visual Studio Code:依赖项、Fix All 和 Test Runner
|
||||
|
||||
Flutter 的 Visual Studio Code 插件也在此版本中得到了改进,两个新命令 “Dart: Add Dependency” and “Dart: Add Dev Dependency” ([#3306](https://github.com/Dart-Code/Dart-Code/issues/3306), [#3474](https://github.com/Dart-Code/Dart-Code/issues/3474))。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image24)
|
||||
|
||||
这些命令提供的功能类似于[Jeroen Meijer 的 Pubspec Assist 插件](https://marketplace.visualstudio.com/items?itemName=jeroen-meijer.pubspec-assist),新命令开箱即用,并提供定期从 pub.dev 获取的包类型过滤列表。
|
||||
|
||||
开发者可能还对适用于 Dart 文件的“Fix All”命令([#3445](https://github.com/Dart-Code/Dart-Code/issues/3445)、[#3469](https://github.com/Dart-Code/Dart-Code/issues/3469))感兴趣,并且可以一步修复所有与[dart fix](https://dart.dev/tools/dart-fix)相同的问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image25)
|
||||
|
||||
这也可以通过添加 `source.fixAll` 到 `editor.codeActionsOnSave` 的 VS Code 设置来设置为在保存时运行,或者想尝试预览功能,可以启用该 `dart.previewVsCodeTestRunner` 设置并查看通过新的 Visual Studio Code 测试运行程序运行的 Dart 和 Flutter 测试。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image26)
|
||||
|
||||
Visual Studio Code 测试运行器看起来与当前的 Dart 和 Flutter 测试运行器略有不同,它将跨会话保留结果。Visual Studio Code 测试运行器还添加了新的装订线图标,显示测试的最后状态,可以单击以运行测试(或右键单击以获取上下文菜单)。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image27)
|
||||
|
||||
在即将发布的版本中,现有的 Dart 和 Flutter 测试运行器将被移除,以支持新的 Visual Studio Code 测试运行器。
|
||||
|
||||
|
||||
## 工具:异常、新应用模板和 Pigeon 1.0
|
||||
|
||||
在此版本中,调试器现在可以在未处理的异常上正确中断,而这些异常以前时被 framework 捕获 ( [#17007](https://github.com/flutter/flutter/issues/17007) )。
|
||||
|
||||
这改善了调试体验,**因为调试器现在可以直接指向他们在代码中的抛出行,而不是指向框架深处的随机行。**
|
||||
|
||||
一个相关的新功能使开发者能够决定 FutureBuilder 是否应该重新抛出或吞下错误 ([#84308](https://github.com/flutter/flutter/pull/84308)),这应该会为开发者提供大量额外的例外情况,以帮助追踪 Flutter 应用程序中的问题。
|
||||
|
||||
自 Flutter 诞生以来,就出现了 Counter 应用模板,它具有许多优点:
|
||||
|
||||
- 它展示了 Dart 语言的许多特性;
|
||||
- 展示了几个关键的 Flutter 概念,并且它足够小;
|
||||
- 可以放入单个文件中,即使有很多的解释性评论;
|
||||
|
||||
然而它没有为Flutter 应用程序提供一个特别好的起点,在此版本中,通过以下命令提供了一个新模板 ( [#83530](https://github.com/flutter/flutter/pull/83530) ):
|
||||
|
||||
`$ flutter create -t skeleton my_app`
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image28)
|
||||
|
||||
骨架模板生成一个遵循社区最佳实践的两页列表视图,它的开发经过大量内部和外部审查,为构建生产质量应用程序提供了更好的基础,并支持以下功能:
|
||||
|
||||
- 用于 `ChangeNotifier` 协调多个 Widget
|
||||
- 默认情况下使用 arb 文件生成本地化
|
||||
- 包括示例图像并为图像资产建立 1x、2x 和 3x 文件夹
|
||||
- 使用“功能优先”的文件夹组织
|
||||
- 支持共享首选项
|
||||
- 支持明暗主题
|
||||
- 支持多页面间导航
|
||||
|
||||
随着时间的推移,随着 Flutter 最佳实践的发展预计这个新模板也会随之发展。
|
||||
|
||||
另一方面,**如果你正在开发插件而不是应用程序,那么可能会对 Pigeon 的 1.0 版本感兴趣**。
|
||||
|
||||
Pigeon 是一个代码生成工具,用于在 Flutter 及其主机平台之间生成类型安全的互操作代码,它允许定义插件 API 的描述,并为 Dart、Java 和 Objective-C(分别可用于 Kotlin 和 Swift)生成框架代码。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-250/image29)
|
||||
|
||||
Flutter 团队的一些插件中已经使用了 Pigeon,在此版本中它提供了更多有用的错误消息,增加了对泛型、原始数据类型作为参数和返回类型以及多个参数的支持,预计开发者将来会更频繁地使用它。
|
||||
|
||||
|
||||
## 重大更改和弃用
|
||||
|
||||
以下是 Flutter 2.5 版本中的重大变化:
|
||||
|
||||
- [默认拖动滚动设备](https://flutter.dev/docs/release/breaking-changes/default-scroll-behavior-drag)
|
||||
- [在 v2.2 之后删除了弃用的 API](https://flutter.dev/docs/release/breaking-changes/2-2-deprecations)
|
||||
- [引入包:flutter_lints](https://flutter.dev/docs/release/breaking-changes/flutter-lints-package)
|
||||
- [ThemeData 的 accent 属性已被弃用](https://flutter.dev/docs/release/breaking-changes/theme-data-accent-properties)
|
||||
- [GestureRecognizer Cleanup](https://flutter.dev/docs/release/breaking-changes/gesture-recognizer-add-allowed-pointer)
|
||||
- [用 collate 替换 AnimationSheetBuilder.display](https://flutter.dev/docs/release/breaking-changes/animation-sheet-builder-display)
|
||||
- [使用 HTML 插槽在 Web 中呈现平台视图](https://flutter.dev/docs/release/breaking-changes/platform-views-using-html-slots-web)
|
||||
- [将 LogicalKeySet 迁移到 SingleActivator](https://github.com/flutter/flutter/pull/80756)
|
||||
|
||||
随着继续更新 Flutter Fix(在您的 IDE 中和通过`dart fix`命令可用),总共有 157 条规则来自动迁移受这些或过去的重大更改以及任何弃用影响的代码。
|
||||
|
||||
此外随着 Flutter 2.5 的发布,**我们将弃用[2020 年 9 月宣布的](http://flutter.dev/go/rfc-ios8-deprecation)对 iOS 8 的支持**。放弃对市场份额不到 1% 的 iOS 8 的支持,使 Flutter 团队能够专注于更广泛使用的新平台,弃用意味着这些平台可以工作,但我们不会在这些平台上测试 Flutter 的新版本或插件。
|
||||
|
||||
> 您可以在 flutter.dev 上查看当前支持的 Flutter 平台列表: https://flutter.dev/docs/development/tools/sdk/release-notes/supported-platforms
|
256
Flutter-280.md
256
Flutter-280.md
|
@ -1,256 +0,0 @@
|
|||
|
||||
> 原文链接:https://medium.com/flutter/whats-new-in-flutter-2-8-d085b763d181
|
||||
|
||||
|
||||
欢迎来到 Flutter 2.8!该版本包含了 207 位 contributors 和 178 位 reviewers 的内容,其中一共有 2,424 个合并的 PR,并 Closed 了 2976 个问题。
|
||||
|
||||
与往常一样,Flutter 的工作的第一位就是保证质量,我们花费了大量时间来确保 Flutter 在支持的设备范围内可以尽可能平稳和稳健地运行。
|
||||
|
||||
## Startup
|
||||
|
||||
**该版本改进了应用的启动延迟问题**,这个改进在 Google Pay 中进行了, Google Pay 作为一个主流的大型应用程序,代码超过 100 万行,使用它进行测试可以确保这些更改所产生的影响是可以被感知的。
|
||||
|
||||
**所有这些改进使得 Google Pay 在低端 Android 设备上运行时的启动延迟降低了 50%,在高端设备上降低了 10%**。
|
||||
|
||||
Flutter 通过影响 Dart VM 的垃圾收集策略的方式,可以有助于避免在应用启动期间出现不合时宜的 GC 。
|
||||
|
||||
> 例如在 Android 上渲染第一帧之前,Flutter 现在 [只通知 Dart VM `TRIM_LEVEL_RUNNING_CRITICAL` 及以上的内存压力信号](https://github.com/flutter/flutter/issues/90551),在本地测试中,这个更改将低端设备上的第一帧时间减少了多达 300 毫秒。
|
||||
|
||||
出于[严谨的考虑](https://github.com/flutter/engine/pull/29145#pullrequestreview-778935616),在之前的版本中 Flutter 创建平台视图时会阻塞平台线程,这次通过[详细的推理和测试](https://github.com/flutter/flutter/issues/91711) 确定了可以删除一些序列化,这个改进消除了在低端设备上启动 Google Pay 期间超过 100 毫秒的阻塞。
|
||||
|
||||
另外,以前设置默认字体管理器时,会在设置第一个 Dart isolate 时添加人为的延迟,而[延迟默认字体管理器](https://github.com/flutter/engine/pull/29291) 和 Dart `Isolate` 设置,这样既改善了启动延迟,又使上述优化的效果更加明显。
|
||||
|
||||
|
||||
## # Memory
|
||||
|
||||
由于 Flutter 频繁地加载 Dart VM 的 “service isolate”,这部分 AOT 代码与应用程序捆绑在一起,因此 Flutter 会同时将这两者都读入内存,因此针对内存受限的设备, Flutter 开发人员在进行性能跟踪时[遇到了问题](https://github.com/flutter/flutter/issues/91382)。
|
||||
|
||||
在 2.8 版本中针对 Android 设备, Dart VM 的 service isolate [被拆分为](https://github.com/flutter/engine/pull/29245)可以单独加载的[自己的包](https://github.com/flutter/engine/pull/29245),这样的调整让设备可节省最多 40 MB 的内存。
|
||||
|
||||
通过[ Dart VM informing the OS ](https://github.com/flutter/flutter/issues/92120),内存占用进一步减少了 10% ,AOT 程序使用的内存将可能不需要再次读取文件,因此,之前保存文件备份数据副本的页面可以被回收并用于其他用途。
|
||||
|
||||
## Profiling
|
||||
|
||||
以便更好地了解应用程序中的性能问题,在应用程序启动时启用,2.8 版本现在会将跟踪事件发送到 `Android systrace` 记录器,即使 Flutter 应用程序构建在发布模式下也会发送这些事件。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image1)
|
||||
|
||||
此外为了创建更少卡顿的动画效果,开发者可能会想要更多关于光栅缓存行为的性能跟踪信息,因为这个行为对于 Flutter 来说是比较昂贵的,**可以重复使用的图片进行 blit, 而不是在每一帧上重新绘制它们,在性能跟踪中的新事件流现在允许跟踪光栅缓存图片的生命周期**。
|
||||
|
||||
## Flutter DevTools
|
||||
|
||||
对于调试性能问题,**该版本的 `DevTools` 添加了一个新的“Enhance Tracing”功能,它可以帮助开发者诊断因昂贵的构建、布局和绘制操作而导致的 UI 卡顿**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image2)
|
||||
|
||||
启用这些跟踪功能中的任何一个后,时间轴将包含用于构建的 Widget、布置的渲染对象和绘制渲染对象的新事件(视情况而定)。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image3)
|
||||
|
||||
|
||||
**此外该版本的 `DevTools` 增加了分析应用程序启动性能的支持**,该配置文件包含从 Dart VM 初始化到第一个 Flutter 帧渲染的 CPU 样本。
|
||||
|
||||
在按下 “Profile app start up” 按钮并加载应用程序启动配置文件后,开发者将看到为配置文件选择的 “AppStartUp” 用户标签,另外还可以通过在可用用户标签列表中,选择此用户标签过滤器(如果有)来加载应用程序启动配置文件。
|
||||
|
||||
> 选择此标签会显示应用启动的配置文件数据。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image4)
|
||||
|
||||
|
||||
## Web platform views
|
||||
|
||||
|
||||
**Android 和 iOS 并不是唯一获得性能改进的平台,该版本还改进了 Flutter web 平台的性能**。
|
||||
|
||||
Flutter Web 使用 `HtmlElementView` Widget 实现了这一点,它允许开发者在 Flutter Web 应用程序中托管 HTML 元素。
|
||||
|
||||
如果开发者使用的是 google_maps_flutter 插件或 video_player 插件的 web 版本,或者你已经遵循了 Flutter 团队关于[如何优化网络上显示图像的建议](https://docs.flutter.dev/development/platform-integration/web-images#use-img-in-a-platform-view),那么您其实已经在使用 platform views。
|
||||
|
||||
|
||||
在之前版本的 Flutter 中,platform view 会立即创建一个新的画布,每个额外的平台视图都会添加另一个画布,可是创建额外的画布是很昂贵的,因为每个画布都是整个窗口的大小。
|
||||
|
||||
**所以该版本会复用早期平台视图创建的画布**,这意味着开发者可以在 `HtmlElementView` 的 Web 应用中拥有多个实例而不会降低性能,同时还可以减少使用平台视图时的滚动卡顿。
|
||||
|
||||
|
||||
|
||||
## WebView 3.0
|
||||
|
||||
**这次 `webview_flutter` 的另一个新版本是,这里提高了版本号,是因为新功能的数量增加了,而且还因为 Web 视图在 Android 上的工作方式可能发生了重大变化**。
|
||||
|
||||
在之前的版本中, `webview_flutter` 的 hybrid composition 模式已经可用,但并不是默认设置。
|
||||
|
||||
hybrid composition 修复了先前默认 virtual displays 模式存在的许多问题,根据用户反馈和问题跟踪的结果,我们认为是时候让 hybrid composition 成为默认设置了,另外 `webview_flutter` 还增加了一些要求很高的功能:
|
||||
|
||||
|
||||
- 支持 POST 和 GET 来填充内容([4450](https://github.com/flutter/plugins/pull/4450)、[4479](https://github.com/flutter/plugins/pull/4479)、[4480](https://github.com/flutter/plugins/pull/4480)、[4573](https://github.com/flutter/plugins/pull/4573))
|
||||
- 从文件和字符串([4446](https://github.com/flutter/plugins/pull/4446)、[4486](https://github.com/flutter/plugins/pull/4486)、[4544](https://github.com/flutter/plugins/pull/4544)、[4558](https://github.com/flutter/plugins/pull/4558))加载 HTML
|
||||
- 透明背景支持([3431](https://github.com/flutter/plugins/pull/4569)、[3431](https://github.com/flutter/plugins/pull/4569)、[4570](https://github.com/flutter/plugins/pull/4570))
|
||||
- 在加载内容之前编写 cookie([4555](https://github.com/flutter/plugins/pull/4555)、[4555](https://github.com/flutter/plugins/pull/4556)、[4557](https://github.com/flutter/plugins/pull/4557))
|
||||
|
||||
此外在 3.0 版本中,`webview_flutter` 为新平台提供了初步支持:web,这个支持允许开发者从单个代码库构建 mobile 和 web 应用,在 Flutter Web 应用程序中托管 Web 视图是什么样的?从代码的角度来看它看起来是一样的:
|
||||
|
||||
```dart
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_web/webview_flutter_web.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MaterialApp(home: HomePage()));
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// required while web support is in preview
|
||||
if (kIsWeb) WebView.platform = WebWebViewPlatform();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Flutter WebView example')),
|
||||
body: const WebView(initialUrl: 'https://flutter.dev'),
|
||||
);
|
||||
}
|
||||
```
|
||||
在 Web上运行时它也会按开发者的预期工作:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image5)
|
||||
|
||||
请注意,**其实当前的 `webview_flutter` for web 的实现还有许多限制,因为它是使用 构建的 `iframe` 实现的**。
|
||||
|
||||
它仅支持简单的 URL 加载,无法控制加载的内容或者和加载的内容交互
|
||||
|
||||
> 有关更多信息,请查看 [webview_flutter_web Readme](https://pub.dev/packages/webview_flutter_web)
|
||||
|
||||
但是 `webview_flutter_web` 由于太收欢迎,我们将作为 [未经认可的插件提供](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#endorsed-federated-plugin),如果你想尝试一下,请将以下行添加到 pubspec.yaml 中:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
webview_flutter: ^3.0.0
|
||||
webview_flutter_web: ^0.1.0 # add unendorsed plugin explicitly
|
||||
```
|
||||
|
||||
## Flutter Favorites
|
||||
|
||||
Flutter Ecosystem Committee 生态系统委员会再次召开会议,指定以下 Flutter Favorites 包:
|
||||
|
||||
- 三种自定义路由器包:[`beamer`](https://pub.dev/packages/beamer),[`routemaster`](https://pub.dev/packages/routemaster) 和 [`go_router`](https://pub.dev/packages/go_router)
|
||||
- [`drift`](https://pub.dev/packages/drift),一个功能强大且流行的 Flutter 和 Dart 响应式持久化库的重命名,构建在 `sqlite`
|
||||
- [`freezed`](https://pub.dev/packages/freezed),一个 Dart “语言补丁” 为定义模型、克隆对象、模式匹配等提供简单的语法
|
||||
- [`dart_code_metrics`](https://pub.dev/packages/dart_code_metrics)
|
||||
- 几个非常好看的图形用户界面包:[`flex_color_scheme`](https://pub.dev/packages/flex_color_scheme),[`flutter_svg`](https://pub.dev/packages/flutter_svg),[`feedback`](https://pub.dev/packages/feedback),[`toggle_switch`](https://pub.dev/packages/toggle_switch),和 [`auto_size_text`](https://pub.dev/packages/auto_size_text)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image6)
|
||||
|
||||
# 特定于平台的软件包
|
||||
|
||||
如果你是软件包作者,必须选择哪些平台是将支持的,如果正在使用特定于平台的本机代码构建插件,可以[使用](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms)`pluginClass`[项目中的属性](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms)来实现,[该](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms)[属性](https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-platforms)`pubspec.yaml`指示提供功能的 native 类:
|
||||
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: com.example.hello
|
||||
pluginClass: HelloPlugin
|
||||
ios:
|
||||
pluginClass: HelloPlugin
|
||||
```
|
||||
|
||||
但是随着 [Dart FFI](https://dart.dev/guides/libraries/c-interop) 变得更加成熟,可以像 `path_provider_windows` 包一样在 100% Dart 中实现用于特定平台的功能,所以当没有任何本机类可以使用,但你仍想将你的包指定为仅支持某些平台时,请改用该`dartPluginClass` 属性:
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
plugin:
|
||||
implements: hello
|
||||
platforms:
|
||||
windows:
|
||||
dartPluginClass: HelloPluginWindows
|
||||
```
|
||||
|
||||
使用这个配置后,即使没有任何 native 代码,也已将包指定为仅支持某些平台,另外还必须提供 Dart 插件类;可以在 flutter.dev 上的 Dart-only 平台实现文档中了解更多信息。
|
||||
|
||||
|
||||
## Firebase
|
||||
|
||||
> 关于它的一系列升级和更新,很大一块,反正国内用不上,懒得写了
|
||||
|
||||
## Desktop
|
||||
|
||||
**Flutter 2.8 版本在 Windows、macOS 和 Linux 稳定版本的道路上又迈出了一大步。** 包括国际化和本地化支持,如最近的 中文IME支持、韩语IME支持和汉字IME支持。
|
||||
|
||||
一个为稳定版本准备的例子:完全重构 Flutter 处理键盘事件以允许同步响应,这使 Widget 能够处理按键并取消其在 tree 的其余部分中传播。
|
||||
|
||||
最初是在 Flutter 2.5 和 Flutter 2.8 中添加了对问题的回归和修复,这是重新设计处理特定于设备的键盘输入的方式,重构 Flutter 处理文本编辑方式来达到补充的目的,所有这些都是键盘输入密集型桌面应用程序所必需。
|
||||
|
||||
此外我们会继续扩展 Flutter 对视觉密度的支持并为对话框公开对齐方式,以实现更加桌面友好的 UI。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image7)
|
||||
|
||||
最后 Flutter 团队并不是唯一一个在 Flutter 桌面上工作的人,举个例子,Canonical 的桌面团队正在与 Invertase 合作,在 Linux 和 Windows 上实现最流行的 Flutter Firebase 插件。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image8)
|
||||
|
||||
## DartPad
|
||||
|
||||
DartPad 的改进,其中最大的改进是对更多包的支持,事实上现在有 23 个包可供导入,除了几个 Firebase 服务,该名单包含常用软件如 `bloc`,`characters`,`collection`,`google_fonts`,和 `flutter_riverpod` ,DartPad 团队会继续添加新的软件包,因此如果想查看当前支持哪些软件包,请单击右下角的信息图标。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image9)
|
||||
|
||||
还有另一个新的 DartPad 功能也非常方便。以前 DartPad 总是运行最新的稳定版本,在此版本中可以使用状态栏中的新频道菜单,来选择最新的 Beta 频道版本以及之前的稳定版本(称为“旧频道”)。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-280/image10)
|
||||
|
||||
## Removing the dev channel
|
||||
|
||||
Flutter “channel” 控制着底层 Flutter 框架和引擎在你的开发机器上变化的速度,Stable 代表最少的问题,而 master 代表最多。
|
||||
|
||||
**由于资源限制,我们最近停止更新 `dev` channel**。虽然为此我们确实收到了一些关于此的问题,但我们发现只有不到 3% 的 Flutter 开发人员使用该`dev`渠道。
|
||||
|
||||
因此我们决定正式退役的进程`dev`渠道,因为很少有开发人员使用 dev 频道,但 Flutter 工程师需要花费大量时间和精力来维护它。
|
||||
|
||||
你可以使用该 `flutter channel` 命令决定想要哪个频道,以下是 Flutter 团队对每个频道的看法:
|
||||
|
||||
- `stable`频道代表我们拥有的最高质量的构建。它们每季度(大致)发布一次,并针对中间的关键问题进行热修复,这就是“慢”通道:安全、成熟、长期服务。
|
||||
|
||||
- `beta` 频道为那些习惯于更快节奏的人提供了一种快速移动的替代方案。目前每月发布。
|
||||
|
||||
- `master` 频道是我们活跃的开发频道,我们不提供对该频道的支持,但我们针对它运行了一套全面的单元测试。
|
||||
|
||||
当 `dev` 在未来几个月停用该频道时,请考虑 `beta` 或 `master `频道,具体取决于对问题的容忍度以及对最新和最好的需求。
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
与往常一样,我们都在努力减少每个版本中重大更改的数量,在此版本中,Flutter 2.8 除了已过期并根据我们的重大变更政策已被删除的已弃用 API 之外,没有重大变更:
|
||||
|
||||
- [90292](https://github.com/flutter/flutter/pull/90292)删除autovalidate弃用
|
||||
- [90293](https://github.com/flutter/flutter/pull/90293)删除FloatingHeaderSnapConfiguration.vsync弃用
|
||||
- [90294](https://github.com/flutter/flutter/pull/90294)删除AndroidViewController.id弃用
|
||||
- [90295](https://github.com/flutter/flutter/pull/90295)删除BottomNavigationBarItem.title弃用
|
||||
- [90296](https://github.com/flutter/flutter/pull/90296)删除不推荐使用的文本输入格式类
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
**看完 Flutter 2.8 的更新,最主要是关于性能、稳定性和 WebView 的调整,本质上这个版本应该会比较友好,因为几乎没有 Breaking Changes ,所以值得一试,推荐等 2.8.3 之后的版本。**
|
37
Flutter-3.md
37
Flutter-3.md
|
@ -1,15 +1,12 @@
|
|||
作为系列文章的第三篇,本篇将为你着重展示:**Flutter开发过程的打包流程、APP包对比、细节技巧与问题处理**,本篇主要描述的 Flutter 的打包、在开发过程中遇到的各类问题与细节,算是对上两篇的补全。
|
||||
作为系列文章的第三篇,继[篇章一](https://juejin.im/post/5b631d326fb9a04fce524db2)和[篇章二](https://juejin.im/post/5b685a2a5188251ac22b71c0)之后,本篇将为你着重展示:**Flutter开发过程的打包流程、APP包对比、细节技巧与问题处理**。本篇主要描述的Flutter的打包、在开发过程中遇到的各类问题与细节,算是对上两篇的补全。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 友情提示:本文所有代码均在 [**GSYGithubAppFlutter**](https://github.com/CarGuo/GSYGithubAppFlutter) ,要不试试?(◐‿◑)。
|
||||
|
||||
|
||||
## 一、打包
|
||||
|
||||
首先我们先看结果,如下表所示,是 **Flutter 与 React Native 、iOS 与 Android 的纵向与横向对比** 。
|
||||
首先我们先看结果,如下表所示,是 **Flutter 与 React Native 、IOS 与 Android 的纵向与横向对比** 。
|
||||
|
||||
| 项目 | IOS |Android|
|
||||
| ---------------------- | ---------------------------------------- | ---------------------------------------- |
|
||||
|
@ -23,16 +20,16 @@
|
|||
|
||||
* 对上述内容有兴趣的可以看看[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)。
|
||||
|
||||
### 1、Android 打包
|
||||
### 1、Android打包
|
||||
|
||||
![I'm Android](http://img.cdn.guoshuyu.cn/20190604_Flutter-3/image5)
|
||||
|
||||
在 Android 的打包上,笔者基本没有遇到什么问题,在`android/app/build.grade`文件下,配置`applicationId`、`versionCode`、`versionName` 和签名信息,最后通过 `flutter build app` 即可完成编译。编程成功的包在 `build/app/outputs/apk/release` 下。
|
||||
在Android的打包上,笔者基本没有遇到什么问题,在`android/app/build.grade`文件下,配置`applicationId`、`versionCode`、`versionName` 和签名信息,最后通过 `flutter build app` 即可完成编译。编程成功的包在 `build/app/outputs/apk/release` 下。
|
||||
|
||||
|
||||
### 2、iOS 打包与真机运行
|
||||
### 2、IOS打包与真机运行
|
||||
|
||||
在 iOS 的打包上,笔者倒是经历了一波曲折,这里主要讲笔者遇到的问题。
|
||||
在IOS的打包上,笔者倒是经历了一波曲折,这里主要讲笔者遇到的问题。
|
||||
|
||||
首先你需要一个 apple 开发者账号,然后创建证书、创建AppId,创建配置文件、最后在`info.plist`文件下输入相关信息,更详细可看官方的[《发布的IOS版APP》](https://flutterchina.club/ios-release/)的教程。
|
||||
|
||||
|
@ -53,7 +50,7 @@
|
|||
#import <url_launcher/UrlLauncherPlugin.h>
|
||||
```
|
||||
|
||||
通过 Android Studio 运行到 iOS 模拟器时没有任何问题,说明这不是第三方包问题。通过查找问题发现,在 iOS 执行 `Archive` 之前,需要执行 `flutter build release`,如下图在命令执行之后,Pod 的执行目录会发现改变,并且生成打包需要的文件。(*ps 普通运行时自动又会修改回来*)
|
||||
通过 Android Studio 运行到 IOS 模拟器时没有任何问题,说明这不是第三方包问题。通过查找问题发现,在 IOS 执行 `Archive` 之前,需要执行 `flutter build release`,如下图在命令执行之后,Pod 的执行目录会发现改变,并且生成打包需要的文件。(*ps 普通运行时自动又会修改回来*)
|
||||
|
||||
|
||||
![文件变化](http://img.cdn.guoshuyu.cn/20190604_Flutter-3/image6)
|
||||
|
@ -75,7 +72,7 @@ drop Pods/Pods.xcodeproj into Runner/Pods.
|
|||
"Valid architectures" to only "arm64" (I removed armv7 armv7s)
|
||||
```
|
||||
|
||||
最后终于成功打包,心累啊(///▽///)。同时如果希望直接在真机上调试 Flutter,可以参考 :[《Flutter基础—开发环境与入门》](https://blog.csdn.net/hekaiyou/article/details/52874796?locationNum=4&fps=1) 下的 **iOS 真机**部分。
|
||||
最后终于成功打包,心累啊(///▽///)。同时如果希望直接在真机上调试 Flutter,可以参考 :[《Flutter基础—开发环境与入门》](https://blog.csdn.net/hekaiyou/article/details/52874796?locationNum=4&fps=1) 下的 **IOS 真机**部分。
|
||||
|
||||
|
||||
|
||||
|
@ -258,15 +255,23 @@ Flutter 在 Debug 和 Release 下分别是 *JIT* 和 *AOT* 模式,而在 DEBUG
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-3/image9)
|
||||
|
|
313
Flutter-300.md
313
Flutter-300.md
|
@ -1,313 +0,0 @@
|
|||
# Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O
|
||||
|
||||
> 本次 Flutter 3.0 主要包括 macOS 和 Linux 的稳定版发布,以及相关的性能改进等。原文链接 https://medium.com/flutter/whats-new-in-flutter-3-8c74a5bc32d0
|
||||
|
||||
|
||||
又到了发布 Flutter 稳定版本的时候,在三个月前我们发布了 Flutter 关于 Windows 的稳定版,而今天,除 Windows 之外,**Flutter 也正式支持 macOS 和 Linux 上的稳定运行**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image1)
|
||||
|
||||
在这里感谢所有 Flutter contributors 的辛勤工作,本次版本发布合并了 5248 个 PR。
|
||||
|
||||
**Flutter 3.0 的发布,主要包括 Flutter 对 macOS 和 Linux 正式版支持、进一步的性能改进、手机端和 Web 端相关的更新等等。此外还有关于减少对旧版本 Windows 的支持,以及一些 breaking changes 列表**。
|
||||
|
||||
# 稳定版 Flutter 已经支持所有桌面平台
|
||||
|
||||
Linux 和 macOS 已达进入稳定版本阶段,包括以下功能:
|
||||
|
||||
## 级联菜单和对 macOS 系统菜单栏的支持
|
||||
|
||||
现在可以使用 `PlatformMenuBar` 在 macOS 上创建菜单栏,该 Widget 支持仅插入平台菜单,并控制 macOS 菜单中显示的内容。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image2)
|
||||
|
||||
## 所有桌面平台全面支持国际化文本输入
|
||||
|
||||
包括使用[input method editors](https://en.wikipedia.org/wiki/Input_method)(IME),如中文、日文和韩文,在 Flutter 3.0 上所有桌面平台上都得到支持,包括第三方输入法如搜狗和谷歌日文输入法。
|
||||
|
||||
## 所有桌面平台的 Accessibility
|
||||
|
||||
Flutter for Windows、macOS 和 Linux 全面支持 Accessibility 服务,例如屏幕阅读、无障碍导航和倒置颜色等。
|
||||
|
||||
## macOS 上默认的 Universal binaries
|
||||
|
||||
从 Flutter 3 开始,Flutter macOS 桌面应用都将被构建为 universal binaries,从而支持现有的基于 Intel 处理器的 Mac, 和 Apple 的 Apple Silicon 设备。
|
||||
|
||||
|
||||
## 放弃 Windows 7/8
|
||||
|
||||
在 Flutter 3.0 中,推荐将 Windows 的版本提升到 Windows 10,虽然目前 Flutter 团队不会阻止在旧版本(Windows 7、Windows 8、Windows 8.1)上进行开发,但 [Microsoft 不再支持](https://docs.microsoft.com/en-us/lifecycle/faq/windows) 这些版本,虽然 Flutter 团队将继续为旧版本提供“尽力而为”的支持,但还是鼓励开发者升级。
|
||||
|
||||
> **注意**:目前还会继续为在 Windows 7 和 Windows 8 上能够正常*运行* Flutter 提供支持;此更改仅影响开发环境。
|
||||
|
||||
# 移动端更新
|
||||
|
||||
对移动端的更新包括以下内容:
|
||||
|
||||
## 折叠手机的支持
|
||||
|
||||
Flutter 3 版本开始支持可折叠的移动设备。在 Microsoft 发起的合作中,新功能和 Widget 可让开发者在可折叠设备上拥有更舒适的体验。
|
||||
|
||||
**其中包括 `MediaQuery` 现在包含一个 `DisplayFeatures` 列表,用于描述设备的边界和状态**,如铰链、折叠和切口等。此外 `DisplayFeatureSubScreen` 现在可以通过定位其子 Widget 的位置不会与 `DisplayFeatures` 的边界重叠,并且目前已经与 framework 的默认对话框和弹出窗口集成,使得 Flutter 能够立即感知和响应这些**元素**。
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image3)
|
||||
|
||||
这里非常感谢 Microsoft 团队,尤其是[@andreidiaconu](https://github.com/andreidiaconu),感谢他们的 contributions!另外可以试用一下[Surface Duo 模拟器示例](https://docs.microsoft.com/en-us/dual-screen/flutter/samples),它包括一个带有 Flutter Gallery 特殊分支的示例,可以用于了解 Flutter 在折叠屏中的实际应用。
|
||||
|
||||
## iOS 可变刷新率支持
|
||||
|
||||
**Flutter 现在支持 iOS 上的 ProMotion 刷新率,包括 iPhone 13 Pro 和 iPad Pro 等**。
|
||||
|
||||
在这些设备上,Flutter 可以以达到 120 hz的刷新率进行渲染,再次之前 iOS 上的刷新率限制为 60hz,有关更多详细信息,请参阅[flutter.dev/go/variable-refresh-rate](http://flutter.dev/go/variable-refresh-rate)。
|
||||
|
||||
> 更多可见:[《Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结》](https://juejin.cn/post/7081273509690736653)
|
||||
|
||||
## 简化 iOS 的发布
|
||||
|
||||
Flutter 团队 [为 flutter build ipa 命令添加了新选项](https://github.com/flutter/flutter/pull/97672)支持以简化发布 iOS 应用。
|
||||
|
||||
当开发者准备好分发到 TestFlight 或 App Store 时,可以通过运行 `flutter build ipa` 以构建 Xcode 存档(`.xcarchive`文件)和应用程序包(`.ipa`文件)。 这时候可以选择添加 `—-export-method ad-hoc`、` —-export-method development` 或 `—-export-method enterprise` 来定制发布支持。
|
||||
|
||||
[构建应用程序包后,可以通过 Apple Transport macOS 应用](https://apps.apple.com/us/app/transporter/id1450874784)或在命令行上使用 `xcrun altool`(运行 `man altool` 用于 App Store Connect API 的密钥身份验证)将其上传到 Apple 。上传后,应用就可以可发布到[TestFlight 或 App Store](https://docs.flutter.dev/deployment/ios#release-your-app-to-the-app-store)。
|
||||
|
||||
**通过这个简化流程,在设置初始的[Xcode 项目设置后](https://docs.flutter.dev/deployment/ios#review-xcode-project-settings),例如名称和应用图标,开发者可以不再需要打开 Xcode 来发布 iOS 应用**。
|
||||
|
||||
## Gradle 版本更新
|
||||
|
||||
现在使用 Flutter 工具创建新项目,会发现生现在开始使用最新版本的 Gradle 和 Android Gradle Plugin,对于现有项目,需要手动将版本升级到 Gradle 的 7.4 和 Android Gradle 插件的 7.1.2。
|
||||
|
||||
## 停用 32 位 iOS/iOS 9/iOS 10
|
||||
|
||||
正如 2022 年 2 月发布的 2.10 稳定版本时所说的那样,Flutter 对 32 位 iOS 设备以及 iOS 9 和 10 版本的支持即将结束。此更改影响 iPhone 4S、iPhone 5、iPhone 5C 以及第 2、3 和 4 代 iPad 设备。Flutter 3 是它们最后一个支持 iOS 版本支持。
|
||||
|
||||
> 要了解有关此更改的更多信息,请查看[RFC:End of support for 32-bit iOS devices](http://flutter.dev/go/rfc-32-bit-ios-unsupported)。
|
||||
|
||||
|
||||
# Web 更新
|
||||
|
||||
Web 应用更新包括以下内容:
|
||||
|
||||
## 图像解码
|
||||
|
||||
Flutter web 现在会在支持它的浏览器中自动检测和使用 ImageDecoder API,而截至今天大多数基于 Chromium 的浏览器(Chrome、Edge、Opera、三星浏览器等)都添加了此 API。
|
||||
|
||||
**新的 API 使用浏览器的内置图像编解码器从主线程异步解码图像,这将图像解码速度提高了 2 倍,并且它从不阻塞主线程,从而消除了以前由图像引起的所有卡顿问题**。
|
||||
|
||||
## Web 应用的生命周期
|
||||
|
||||
Flutter Web 应用程序的新生命周期 API 使开发者可以更灵活地从托管 HTML 页面控制 Flutter 应用的引导过程,并帮助 Lighthouse 分析应用的性能,包括以下经常请求的场景:
|
||||
|
||||
- 启动画面。
|
||||
- 加载指示器。
|
||||
- 在 Flutter 应用程序之前显示的纯 HTML 交互式登录页面。
|
||||
|
||||
> 有关更多信息,请查看docs.flutter.dev 上的[自定义 Web 应用程序初始化](https://docs.flutter.dev/development/platform-integration/web/initialization)。
|
||||
|
||||
# 工具更新
|
||||
|
||||
Flutter 和 Dart 工具的更新包括:
|
||||
|
||||
## 更新的 lint 包
|
||||
|
||||
lint 包的 2.0 版已发布:
|
||||
|
||||
- Flutter:[https ://pub.dev/packages/flutter_lints/versions/2.0.0](https://pub.dev/packages/flutter_lints/versions/2.0.0)
|
||||
- Dart:[https ://pub.dev/packages/lints/versions/2.0.0](https://pub.dev/packages/lints/versions/2.0.0)
|
||||
|
||||
**在 Flutter 3 中生成的应用程序会通过 `flutter create` 自动启用 v2.0 的 lints 集**。Flutter 现在鼓励现有的应用、包和插件都迁移到 v2.0 以遵循该协议,迁移支持可以通过运行 `flutter pub upgrade --major-versions flutter_lints`.
|
||||
|
||||
**v2 中大多数新添加的 lint 警告都带有自动修复功能**。因此在 `pubspec.yaml` 文件中升级到最新的包版本后,可以运行 `dart fix —-apply` 自动修复大多数 lint 警告(可能一些警告仍然需要一些手动工作。
|
||||
|
||||
> 尚未使用 `package:flutter_lints` 的应用、软件包或插件可以按照[迁移指南](https://docs.flutter.dev/release/breaking-changes/flutter-lints-package#migration-guide)进行迁移。
|
||||
|
||||
## 性能改进
|
||||
|
||||
感谢 contributor [knopp](https://github.com/knopp),[局部重绘](https://github.com/flutter/engine/pull/29591)的支持已在 Android 设备上启用。
|
||||
|
||||
在本地测试中,此更改将Pixel 4 XL 设备在 `backdrop_filter_perf` 基准测试上, 90th percentile 和 99th 的帧光栅化时间减少了 5 倍,**现在在 iOS 和基础此更新的 Android 设备上都启用了,当存在单个矩形脏区域时的部分重绘支持**。
|
||||
|
||||
另外,Flutter 3.0 还进一步改进了[不透明动画相关的性能](https://github.com/flutter/engine/pull/30957),特别是当一个 `Opacity` Widget 只包含一个渲染 primitive 时, `Opacity` 下关于 `saveLayer` 的调用通常会被省略。在基准测试下中,这种情况下的光栅化时间提高了[一个数量级](https://flutter-flutter-perf.skia.org/e/?begin=1643063115&end=1644004520&keys=X32827d8819e8271e025f50e77bf2bec0&requestType=0&xbaroffset=27447),在未来的版本中,我们计划将此优化应用于更多场景。
|
||||
|
||||
再次感谢 contributor [JsouLiang](https://github.com/JsouLiang) 的提交,现在引擎的光栅和 UI 线程在 Android 和 iOS 上运行的优先级高于其他线程,例如 Dart VM 后台垃圾回收线程,而在我们的基准测试中,这导致平均框架构建时间[加快了约 20%](https://flutter-flutter-perf.skia.org/e/?begin=1644581114&end=1644647407&keys=X3999dc0a0c89054eaa9f66bcff27d882&num_commits=50&request_type=1&xbaroffset=27549)。
|
||||
|
||||
在 Flutter 3.0 之前,光栅缓存的准入策略仅查看图片中绘制操作的数量,不幸的是这会导致引擎花费更多的内存,来缓存实际上渲染速度非常快的图片。新版本[引入了一种机制](https://github.com/flutter/engine/pull/31417),该机制会根据其图片绘制操作的成本来估计图片的渲染复杂性,将其用作光栅缓存准入策略从而[减少内存使用量](https://flutter-flutter-perf.skia.org/e/?begin=1644790212&end=1646044276&keys=X4c7dd4e4903a38523816c00b31d4d787&requestType=0&xbaroffset=27636),并且不会在我们的基准测试中降低性能。
|
||||
|
||||
感谢 contributor [ColdPaleLight](https://github.com/ColdPaleLight),他修复了[帧调度](https://github.com/flutter/engine/pull/31513) 中的一个错误,该错误导致 iOS 上的少量动画帧被丢弃的问题。
|
||||
|
||||
## Impeller
|
||||
|
||||
团队一直在努力寻找解决 iOS 和其他平台上卡顿的解决方案。**在 Flutter 3 版本中可以在 iOS 上preview 一个名为[Impeller](https://github.com/flutter/engine/tree/main/impeller) 的实验性渲染工具,Impeller 在引擎构建时会预编译[一组更小、更简单的着色器](https://github.com/flutter/flutter/issues/77412),这样它们就不会在应用程序运行时编译,这一直是 Flutter 中卡顿的主要来源**。
|
||||
|
||||
Impeller 尚未准备好正式发布,目前还远未到完成阶段,所以并非所有 Flutter 功能都能实现,但我们对它在 Flutter [/gallery](https://github.com/flutter/gallery) 应用程序中的保真度和性能感到非常满意,特别是 Gallery 应用里过渡动画中最差的帧快了大约 [20 倍](https://flutter-flutter-perf.skia.org/e/?begin=1650297849&end=1651261748&queries=sub_result%3Dworst_frame_rasterizer_time_millis%26test%3Dnew_gallery_impeller_ios__transition_perf%26test%3Dnew_gallery_ios__transition_perf&requestType=0)。
|
||||
|
||||
**Impeller 可以在 iOS 上通过启动 tag 来启动,开发者可以传递 `—-enable-impeller` 到`flutter run` 或将 `Info.plist` 文件中的 `FLTEnableImpeller` 标志设置为 `true` 来尝试 Impeller**。
|
||||
|
||||
|
||||
## Android上的内嵌广告
|
||||
|
||||
使用 `google_mobile_ads` 时,开发者应该会在用户关键交互(例如页面之间的滚动和转换)中得到更好的性能。
|
||||
|
||||
在底层,Flutter 现在使用新的异步组合来实现 Android 视图,它们通常称为[platform views](https://docs.flutter.dev/development/platform-integration/platform-views)。这意味着 Flutter 光栅线程不再需要等待 Android 视图渲染。相反,Flutter 引擎会使用它管理的 OpenGL 纹理将视图放置在屏幕上。
|
||||
|
||||
# 更多更新
|
||||
|
||||
Flutter 生态系统的其他更新包括:
|
||||
|
||||
## Material 3
|
||||
|
||||
Flutter 3 支持[Material Design 3](https://m3.material.io/),即下一代 Material Design。
|
||||
|
||||
Flutter 3 为 Material 3 提供了更多可选支持,包括 Material You 功能如:**动态颜色,新的颜色系统和排版、组件的更新以及 Android 12 中引入的新视觉效果,如新的触摸波纹设计和拉伸过度滚动效果**。
|
||||
|
||||
>开发者可以在 codelab 的 [Take your Flutter app from Boring to Beautiful](https://codelabs.developers.google.com/codelabs/flutter-boring-to-beautiful) 中尝试 Material 3 功能,有关如何选择加入这些新功能,以及哪些组件支持 Material 3 的详细信息,请参阅[API 文档](https://api.flutter.dev/flutter/material/ThemeData/useMaterial3.html)。
|
||||
|
||||
## 主题扩展
|
||||
|
||||
Flutter 现在可以使用名为 *Theme extensions* 的概念向 Material 的 `ThemeData` 添加任何内容,开发者可以通过 `ThemeData`.extensions 去添加自己想要的内容,而不是(在 Dart 意义上)继承 `ThemeData` 并重新实现其`copyWith`、`lerp`和其他方法。
|
||||
|
||||
此外,作为 package 开发人员,你可以提供 `ThemeExtensions` 相关内容,有关此内容的更多详细信息,请参阅[flutter.dev/go/theme-extensions并](https://flutter.dev/go/custom-colors-m3) 和 GitHub 上 的[示例](https://github.com/guidezpl/flutter/blob/master/examples/api/lib/material/theme/theme_extension.1.dart)。
|
||||
|
||||
## Ads
|
||||
|
||||
对于发布商而言,个性化广告征求同意并处理 Apple 的 App Tracking Transparency (ATT) 非常重要。
|
||||
|
||||
为了支持这些要求,Google 提供了用户消息传递平台 (UMP) SDK,它取代了之前的开源 [Consent SDK](https://github.com/googleads/googleads-consent-sdk-ios),在即将发布的 GMA SDK for Flutter 中,我们将添加对 UMP SDK 的支持,以帮助发布者获得用户同意。
|
||||
|
||||
> 有关更多详细信息,请查看 pub.dev 上的[google_mobile_ads](https://pub.dev/packages/google_mobile_ads)页面。
|
||||
|
||||
# Breaking changes
|
||||
|
||||
随着 Flutter 的不断改进 ,我们的目标是尽量减少重大更改的数量,而随着 Flutter 3 的发布,Flutter 有以下重大变化:
|
||||
|
||||
- [在 v2.10 之后删除了已弃用的 API](https://docs.flutter.dev/release/breaking-changes/2-10-deprecations)
|
||||
- [由 ZoomPageTransitionsBuilder 替换的页面过渡](https://docs.flutter.dev/release/breaking-changes/page-transition-replaced-by-ZoomPageTransitionBuilder)
|
||||
- [迁移 useDeleteButtonTooltip 到 Chips 的 deleteButtonTooltipMessage](https://docs.flutter.dev/release/breaking-changes/chip-usedeletebuttontooltip-migration)
|
||||
- [ThemeData 的 toggleableActiveColor 属性已被弃用](https://docs.flutter.dev/release/breaking-changes/toggleable-active-color)
|
||||
|
||||
> 如果你正在使用这些 API,请参阅[Flutter.dev 上的迁移指南](https://docs.flutter.dev/release/breaking-changes)。
|
||||
|
||||
|
||||
|
||||
# Flutter 3 相关介绍,包括Flutter桌面端、Flutter firebase 、Flutter游戏- 谷歌2022 I/O 大会,
|
||||
|
||||
> 原本链接 https://medium.com/flutter/introducing-flutter-3-5eb69151622f
|
||||
|
||||
|
||||
Flutter 3 作为 Google I/O 主题演讲的主要部分,Flutter 3 完成了 Flutter 从以移动为中心到多平台框架的路线图,本次提供了 **macOS 和 Linux 桌面应用相关的支持,以及对 Firebase 集成的改进、提高生产力和性能以及对 Apple Silicon 的支持等等**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image4)
|
||||
|
||||
# Flutter 3 之旅
|
||||
|
||||
Flutter 为了彻底改变应用的开发方式:将 Web 的迭代开发模型与以前游戏保留的硬件加速图形渲染和像素级控制相结合。
|
||||
|
||||
自 Flutter 1.0 beta 发布以来的过去四年里,Flutter 团队逐渐在这些基础上进行构建,添加了新的 framework 功能和新的 Widget,与底层平台更深入地集成,还有丰富的packages 支持以及许多性能和工具改进。
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image5)
|
||||
|
||||
随着产品的成熟,越来越多的人开始使用 Flutter 构建应用程序。如今有超过 500,000 个使用 Flutter 构建的应用程序。
|
||||
|
||||
来自 [data.ai](https://www.data.ai/en/)等研究公司的分析以及公开推荐表明,Flutter 被许多领域的[客户](https://flutter.dev/showcase)使用:
|
||||
|
||||
- [微信等社交应用](https://play.google.com/store/apps/details?id=com.tencent.mm&hl=en_US&gl=US)
|
||||
- [Betterment](https://apps.apple.com/us/app/betterment-investing-saving/id393156562)和[Nubank](https://play.google.com/store/apps/details?id=com.nu.production&hl=en_US&gl=US)等金融和银行应用;
|
||||
|
||||
- [SHEIN](https://play.google.com/store/apps/details?id=com.zzkko&hl=en_US&gl=US)和[trip.com](https://apps.apple.com/us/app/trip-com-hotels-flights-trains/id681752345)等商务应用;
|
||||
- [Fastic](https://fastic.com/)和[Tabcorp](https://auspreneur.com.au/tabcorp-adopts-googles-flutter-platform/)等生活方式应用;
|
||||
- [My BMW](https://www.press.bmwgroup.com/global/article/detail/T0328610EN/the-my-bmw-app:-new-features-and-tech-insights-for-march-2021?language=en)等配套应用
|
||||
- [巴西政府](https://apps.apple.com/app/id1506827551)等公共机构;
|
||||
|
||||
> **如今,有超过 500,000 个使用 Flutter 构建的应用程序**。
|
||||
|
||||
开发人员告诉我们,Flutter 可以更快地为更多平台构建精美的应用。在我们最近的用户研究中:
|
||||
|
||||
- 91% 的开发人员同意 Flutter 减少了构建和发布应用所需的时间。
|
||||
- 85% 的开发者同意 Flutter 让他们的应用比以前更漂亮。
|
||||
- 85% 的人同意 Flutter 让他们能够为比以前更方便地在更多的平台发布他们的应用。
|
||||
|
||||
在 [Sonos 最近的一篇博客文章中](https://tech-blog.sonos.com/posts/renovating-setup-with-flutter/),他讨论了他们关于体验方便的改进,强调了其中的第二点:
|
||||
|
||||
> “毫不夸张地说,解锁 [Flutter] 是有一定程度的‘*溢价*’,这与我们团队之前交付的任何东西都不同。对我们的设计师来说最重要的是,Flutter 可以轻松地构建新的 UI,这意味着我们的团可以花更少的时间对规范说“不”,而将更多的时间用于迭代规范。这听起来值得,所以我们建议大家可以尝试一下 Flutter。”
|
||||
|
||||
|
||||
# Flutter 3 介绍
|
||||
|
||||
**借助 Flutter 3,开发者可以通过一个代码库为六个平台构建应用**,为开发人员提供无与伦比的生产力,并帮助初创公司在一开始就将新想法快速得带入完整的目标市场。
|
||||
|
||||
在之前的版本中,我们在 iOS 和 Android 的技术上添加了[Web](https://medium.com/flutter/flutter-web-support-hits-the-stable-milestone-d6b84e83b425) 和 [Windows 支持](https://medium.com/flutter/announcing-flutter-for-windows-6979d0d01fed),现在**Flutter 3 增加了对 macOS 和 Linux 应用程序的稳定支持**。
|
||||
|
||||
添加对应平台的支持不仅仅是渲染像素:**它包括新的输入和交互模型、编译和构建支持、accessibility 和国际化以及特定于平台的集成等等,Flutter 团队的目标是让开发者能够灵活地利用底层操作系统,同时根据开发者的选择尽可能多的共享 UI 和逻辑**。
|
||||
|
||||
在 macOS 上,现在支持 Intel 和 Apple Silicon,提供[Universal Binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary)的支持,允许应用打包支持两种架构上的可执行文件。在 Linux 上,Canonical 和 Google 合作提供了一个最佳的开发选项。
|
||||
|
||||
[Superlist](https://superlist.com/)是 Flutter 如何实现 Desktop 应用的一个很好的例子,它会今天在测试版中发布。
|
||||
|
||||
Superlist 通过将列表、任务和自由格式内容组合成全新的待办事项列表和个人计划的新应用程序,提供协作能力,而 Superlist 团队之所以选择 Flutter,是因为它能够提供快速、高度品牌化的桌面体验,我们认为他们迄今为止的进步证明了为什么 Flutter 是一个不错的选择。
|
||||
|
||||
|
||||
Flutter 3 还改进了许多基础功能,包括了改性能、Material You 支持和开发效率的提高。
|
||||
|
||||
除了上面提到的工作,在这个版本中,Flutter 现在支持完全给予原生[Apple 芯片](https://support.apple.com/en-us/HT211814)进行开发,虽然 Flutter 自发布以来一直与基于 M1 的 Apple 设备兼容,但 Flutter 现在可以充分利用了[Dart 对 Apple 芯片的支持](https://medium.com/dartlang/announcing-dart-2-14-b48b9bb2fb67),从而能够在基于 M1 的设备上更快地编译并支持 macOS 应用程序的 [Universal Binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary) 文件
|
||||
|
||||
我们对[Material Design 3](https://m3.material.io/)的工作也在此版本中基本完成,它允许开发人员提供动态配色方案和新的视觉组件,以适应性强的跨平台设计系统:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image6)
|
||||
|
||||
|
||||
Flutter 由 Dart 提供支持,Dart 是一种用于多平台开发的高生产力、可移植语言,我们在这个周期中对 Dart 的改进工作包括有:
|
||||
- 减少样板文件;
|
||||
- 提高可读性的新语言功能;
|
||||
- 实验性 RISC-V 支持;
|
||||
- 升级的 linter 和新文档;
|
||||
|
||||
>有关 Dart 2.17 中所有新改进的更多详细信息,请查看[博客](https://medium.com/dartlang)。
|
||||
|
||||
|
||||
# Firebase 和 Flutter
|
||||
|
||||
当然,构建应用的不仅仅是 UI , 应用的发布者需要一整套工具来构建、发布和运行应用,包括: 身份验证、数据存储、云功能和设备测试等服务。
|
||||
|
||||
目前有多种服务都已经支持 Flutter,包括[Sentry](https://docs.sentry.io/platforms/flutter/)、[AppWrite](https://appwrite.io/docs/getting-started-for-flutter)和 [AWS Amplify](https://docs.amplify.aws/start/q/integration/flutter/)。
|
||||
|
||||
Google 提供的应用服务是 Firebase,[SlashData ](https://www.slashdata.co/developer-program-benchmarking/?)的开发者基准测试研究表明,62% 的 Flutter 开发者在他们的应用中使用 Firebase。
|
||||
|
||||
因此,在过去的几个版本中,我们一直在与 Firebase 合作,以便能更好地将 Flutter 的集成。这包括将 Flutter 的 Firebase 插件发布到 1.0,添加更好的文档和工具,以及[FlutterFire UI](https://pub.dev/packages/flutterfire_ui)等新 Widget,为开发人员提供可重用的身份验证和配置文件界面 UI 等等。
|
||||
|
||||
而在今天,我们宣布将 Flutter/Firebase 集成升级为 Firebase 产品的核心支持。我们正在将源代码和文档转移到 Firebase 存储库和站点中,开发者可以期待我们与 Android 和 iOS 同步发展 Firebase 对 Flutter 的支持。
|
||||
|
||||
此外,我们还进行了一些重大改进,以支持使用 Firebase 时支持崩溃报告服务 Crashlytics。通过Flutter [Crashlytics 插件](https://firebase.google.com/docs/crashlytics),开发者可以实时跟踪致命错误,提供与 iOS 和 Android 开发人员相同的功能集。
|
||||
|
||||
这包括重要的警报和指标,如“无崩溃用户”可帮助开发者掌握应用的稳定性。Crashlytics 分析管道已升级和改进对 Flutter 崩溃的支持,从而更快可以地对问题进行分类、优先排序和修复问题。
|
||||
|
||||
最后我们简化了插件设置过程,因此只需几个步骤即可从 Dart 代码中启动和运行 Crashlytics。
|
||||
|
||||
|
||||
|
||||
# Flutter 休闲游戏工具包
|
||||
|
||||
对于大多数开发者来说,Flutter 是一个应用框架。但是随着休闲游戏开发社区也在不断壮大,利用 Flutter 提供的硬件加速图形支持以及[Flame](https://flame-engine.org/)等开源游戏引擎的需求一致在提高。
|
||||
|
||||
我们想让休闲游戏开发者更容易上手,因此在今天的 I/O 上,我们宣布发布[休闲游戏工具包](https://flutter.dev/games),它提供的模板和最佳实践的入门工具包以及广告和云服务。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image7)
|
||||
|
||||
尽管 Flutter 并非专为高强度 3D 动作游戏而设计的渲染引擎,但其中一些游戏的非游戏 UI 已经开始转向 Flutter ,包括拥有数亿用户的热门游戏,如[PUBG Mobile ](https://play.google.com/store/apps/details?id=com.tencent.ig)。
|
||||
|
||||
对于 I/O,我们想看看我们可以将这项技术推到多远,所以 Flutter 团队创建了一个有趣的弹球游戏,由 Firebase 和 Flutter 的网络支持提供支持。
|
||||
|
||||
[I/O Pinball](https://pinball.flutter.dev/ ) 提供了一个围绕 Google 的吉祥物设计的游戏:Flutter 的 Dash、Firebase 的 Sparky、Android 机器人和 Chrome 恐龙,我们认为这是展示 Flutter 的一种有趣方式。
|
||||
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image8)
|
||||
|
||||
# 由 Google 赞助,由社区提供支持
|
||||
|
||||
我们喜欢 Flutter 的原因,不仅仅是一款 Google 开发的产品——而是因为它是一款“所有人”的产品。
|
||||
|
||||
开源意味着我们都可以参与并受益于它的成功,无论是通过贡献新代码或文档,创建核心框架软件包,编写书籍和培训课程来教授他人。
|
||||
|
||||
为了展示社区的最佳状态,我们最近与 DevPost 合作赞助了 Puzzle Hack 挑战赛,让开发人员有机会通过使用 Flutter 重新构想经典的滑动拼图来展示他们的技能,这将展示 web, desktop 和 mobile如何结合。
|
||||
|
||||
> 相关的视频链接:https://youtu.be/l6hw4o6_Wcs
|
441
Flutter-310.md
441
Flutter-310.md
|
@ -1,441 +0,0 @@
|
|||
# Google I/O 2023 - Flutter 3.10 发布,快来看看有什么更新吧
|
||||
|
||||
|
||||
|
||||
> 核心部分原文链接:https://medium.com/flutter/whats-new-in-flutter-3-10-b21db2c38c73
|
||||
|
||||
|
||||
|
||||
虽然本次 I/O 的核心 keynote 主要是 AI ,但是按照惯例依然发布了新的 Flutter 稳定版,不过并非大家猜测的 4.0,而是 3.10 ,Flutter 的版本号依然那么的出人意料。
|
||||
|
||||
**Flutter 3.10 主要包括有对 Web、mobile、graphics、安全性等方面的相关改进**,核心其实就是:
|
||||
|
||||
- iOS 默认使用了 Impeller
|
||||
- 一堆新的 Material 3 控件袭来
|
||||
- iOS 新能优化,Android 顺带可有可无的更新
|
||||
- Web 可以无 iframe 嵌套到其他应用
|
||||
|
||||
# Framework
|
||||
|
||||
|
||||
|
||||
## Material 3
|
||||
|
||||
看起来谷歌对于 Material 3 的设计规范很上心,根据最新的 [Material Design spec](https://m3.material.io/components) 规范 Flutter 也跟进了相关的修改,其中包括有**新组件和组件主题和新的视觉效果等**。
|
||||
|
||||
目前依然是由开发者可以在 `MaterialApp` 主题配置下,通过 `useMaterial3` 标志位选择是否使用 Material 3,**不过从下一个稳定版本开始,`useMaterial3` 默认会被调整为 `true`** 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image1.png)
|
||||
|
||||
> 对于 Material 3 ,可以通过 https://flutter.github.io/samples/material_3.html 上的相关 Demo 预览。
|
||||
|
||||
## ColorScheme.fromImageProvider
|
||||
|
||||
所有 M3 组件配置主题的默认颜色 `ColorScheme`,**默认配色方案使用紫色 shades,这有区别于之前默认的蓝色**。
|
||||
|
||||
除了可以从单一 “seed” 颜色来定制配置方案之后,通过 `fromImageProvider` 图像也可以创建自定义配色方案。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image2.gif)
|
||||
|
||||
## NavigationBar
|
||||
|
||||
本次还增加了一个 M3 版本的 `BottomNavigationBar` 控件效果,虽然 [M3](https://m3.material.io/components/navigation-bar/overview) 使用不同的颜色、highlighting 和 elevation,但它的工作方式其实还是和以前一样。
|
||||
|
||||
如果需要调整 `NavigationBars` 的默认外观,可以使用使用 `NavigationBarTheme` 来覆盖修改,虽然目前你不需要将现有 App 迁移到 `NavigationBars` ,但是官方建议还是尽可能在新项目里使用 `NavigationBars` 作为导航控件。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image3.gif)
|
||||
|
||||
|
||||
|
||||
## NavigationDrawer
|
||||
|
||||
M3 针对 Drawer 同样提供了新的 `NavigationDrawer `,它通过 `NavigationDestinations` 显示单选列表,也可以在该列表中包含其他控件。
|
||||
|
||||
> 同步M3下 `Drawer` 也更新了颜色和高度,同时对布局进行了一些小的更改。
|
||||
|
||||
`NavigationDrawer` 需要时可以滚动,如果要覆盖 `NavigationDrawer` 的默认外观,同样可以使用 `NavigationDrawerTheme` 来覆盖。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image4.gif)
|
||||
|
||||
|
||||
|
||||
## SearchBar 和 SearchAnchor
|
||||
|
||||
这是 Flutter 为搜索查询和提供预测效果新增的控件。
|
||||
|
||||
当用户在输入搜索查询时,会在 “search view” 中计算匹得到一个配响应列表,用户选择一个结果或调整匹配结果。
|
||||
|
||||
如果要覆盖 `SearchBarTheme` 的默认外观,同样可以使用 `SearchAnchorTheme` 来覆盖。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image5.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image6.gif) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ |
|
||||
|
||||
|
||||
|
||||
|
||||
## Secondary Tab Bar
|
||||
|
||||
M3 下 Flutter 现在默认提供创建第二层选项卡式内容的支持,针对二级 Tab 可以使用 `TabBar.secondary`。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image7.gif)
|
||||
|
||||
## DatePicker 和 TimePicker 更新
|
||||
|
||||
M3下 `DatePicker `更新了控件的日历、文本字段的颜色、布局和形状等,对应 API 没有变动,但会个新增了 `DatePickerTheme` 用于调整控件样式。
|
||||
|
||||
`TimePicker` 和`DatePicker` 一样,更新了控件的常规版本和紧凑版本的颜色、布局和形状。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image8.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image9.gif) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ |
|
||||
|
||||
|
||||
|
||||
## BottomSheet 更新
|
||||
|
||||
M3 下 `BottomSheet` 除了颜色和形状更新之外,还添加了一个可选的拖动手柄,当设置 `showDragHandle`为 `true` 时生效。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image10.gif)
|
||||
|
||||
|
||||
|
||||
## ListTile 更新
|
||||
|
||||
M3下 `ListTile` 更新了定位和间距,包括 content padding、leading 和 trailing 控件的对齐、minimum leading width, 和 vertical spacing 等,但是 API 保持不变。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image11.gif)
|
||||
|
||||
|
||||
|
||||
# TextField 更新
|
||||
|
||||
M3 更新了所有 `TextField` 对原生手势支持。
|
||||
|
||||
用鼠标双击或三次点击 `TextField` 和在触摸设备上双击或三次点击效果相同,默认情况下 `TextField` 和`CupertinoTextField ` 都可以使用该功能。
|
||||
|
||||
### `TextField` double click/tap 手势
|
||||
|
||||
- Double click + drag:扩展字块中的选择。
|
||||
- Double tap + drag:扩展字块中的选择。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image12.gif)
|
||||
|
||||
### `TextField` triple click/tap 手势
|
||||
|
||||
Triple click
|
||||
|
||||
- 在多行 `TextField`(Android/Fuchsia/iOS/macOS/Windows) 中选择点击位置的段落块。
|
||||
- 在多行 `TextField` (Linux) 内部时,在 click 位置选择一个行块。
|
||||
- 选择单行中的所有文本 `TextField`。
|
||||
|
||||
Triple tap
|
||||
|
||||
- 在 multi-line `TextField` 中选择点击位置的段落块 。
|
||||
- 选择单行 `TextField` 中的所有文本
|
||||
|
||||
Triple click+拖动
|
||||
|
||||
- 扩展段落块中的选择 (Android/Fuchsia/iOS/macOS/Windows)。
|
||||
- 扩展行块中的选择 (Linux)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image13.gif)
|
||||
|
||||
|
||||
|
||||
> 简单来说,就是手势和鼠标在双击和三击下,会触发不同的选择效果,并且 Linux 在三击效果下会有点差异
|
||||
|
||||
|
||||
|
||||
## Flutter 支持 SLSA 级别 1
|
||||
|
||||
Flutter Framework 现在使用软件工件供应链级别 ( [SLSA](https://slsa.dev/) ) 级别 1 进行编译,这里面支持了许多安全功能的实现,包括:
|
||||
|
||||
- **脚本化构建过程**:Flutter 的构建脚本现在允许在受信任的构建平台上自动构建,建立在受保护的架构上有助于防止工件篡改,从而提高供应链安全性。
|
||||
- **带有审计日志的多方批准**:Flutter 发布工作流程仅在多个工程师批准后执行,所有执行都会创建可审计的日志记录,这些更改确保没有人可以在源代码和工件生成之间引入更改。
|
||||
- **出处**:Beta 和稳定版本现在使用 [provenance](https://slsa.dev/provenance/v0.1) 构建,这意味着具有预期内容的可信来源构建了框架发布工件,每个版本都会发布链接以查看和验证 [SDK 存档](https://docs.flutter.dev/release/archive) 的出处。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image14.png)
|
||||
|
||||
这项工作还在朝着 SLSA L2 和 L3 合规性迈进,这两个级别侧重于在构建过程中和构建之后提供 artifacts 保护。
|
||||
|
||||
|
||||
|
||||
# Web
|
||||
|
||||
|
||||
|
||||
## 改进了加载时间
|
||||
|
||||
3.10 减小了图标字体的文件大小,它会从 Material 和 Cupertino 中删除了未使用的字形,从而提供了更快加载。
|
||||
|
||||
## CanvasKit 变小
|
||||
|
||||
基于 Chromium 的浏览器可以使用更小的自定义 CanvasKit 渠道,托管在 Google [gstatic.com ](http://gstatic.com/) 上的 CanvasKit 可以进一步提高性能。
|
||||
|
||||
## Element 嵌入
|
||||
|
||||
现在可以 [从页面中的特定 Element 来加载 Flutter Web](https://docs.flutter.dev/deployment/web#embedding-a-flutter-app-into-an-html-page) ,不需要 `iframe`,在这个版本之前 fluter web 是需要填充整个页面主体或显示在 `iframe` 标记内,简单说就是把 flutter web 嵌套到其他 Web 下更方便了。
|
||||
|
||||
> 具体 Demo 可见:https://github.com/flutter/samples/tree/main/web_embedding
|
||||
|
||||
## **着色器支持**
|
||||
|
||||
Web 应用可以使用 Flutter 的 [fragment shader](https://docs.flutter.dev/development/ui/advanced/shaders) :
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/myshader.frag
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Engine
|
||||
|
||||
## Impeller
|
||||
|
||||
在 3.7 稳定版中 iOS 提供了 [Impeller ](https://docs.flutter.dev/perf/impeller) 预览支持,从那时起 Impeller 就收到并解决了用户的大量反馈。
|
||||
|
||||
在 3.10 版本中,我们对 Impeller 进行了 250 多次提交,**现在我们将 Impeller 设置为 iOS 上的默认渲染器**。
|
||||
|
||||
默认情况下,所有使用 Flutter 3.10 为 iOS 构建的应用都使用 Impeller,这样 iOS 应用预计将会有更少的卡顿和更一致的性能。
|
||||
|
||||
自 3.7 版本以来,iOS 上的 Impeller 改进了内存占用,可以使用较少的渲染通道和中间渲染目标。
|
||||
|
||||
在较新的 iPhone 上,**启用有损纹理压缩可在不影响保真度的情况下减少内存占用,这些进步也显着提高了 iPad 的性能**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image15.png)
|
||||
|
||||
比如 [Wonderous](https://flutter.gskinner.com/wonderous/) 应用中的 “pull quote” 页面,**这些改进是的当前页面下的内存占用量减少了近一半**。
|
||||
|
||||
内存使用量的减少也适度降低了 GPU 和 CPU 负载,Wondrous 应用可能不会记录这些负载下降,它的框架之前已经优化的不错,但这一变化应该会延长续航能力。
|
||||
|
||||
Impeller 还释放了团队可以更快地交付流行功能请求的能力,例如在 iOS 上支持更广泛的 P3 色域。
|
||||
|
||||
> 社区贡献加速了我们的进步,特别是 GitHub 用 户[ColdPaleLight](https://github.com/ColdPaleLight) 和 [luckysmg](https://github.com/luckysmg ) ,他们编写了多个与 Impeller 相关的补丁,提高了保真度和性能。
|
||||
|
||||
虽然 Impeller 满足大多数 Flutter 应用的渲染需求,但你可以选择关闭 Impeller。如果选择退出,请考虑在[ GitHub 上提交问题](https://github.com/flutter/flutter/issues/new/choose)以告诉我们原因。
|
||||
|
||||
> ```
|
||||
> <key>FLTEnableImpeller</key>
|
||||
> <false/>
|
||||
> ```
|
||||
|
||||
用户可能会注意到 Skia 和 Impeller 在渲染时存在细微差别,这些差异可能是错误,所以请勿在 Github 上提出问题,**在未来的版本中,我们将删除适用于 iOS 的旧版 Skia 渲染器以减小 Flutter 的大小**。
|
||||
|
||||
另外,Impeller 的 Vulkan 后端然在支持当中,Android 上的 Impeller 仍在积极开发中,但尚未准备好进行预览。
|
||||
|
||||
> 要了解 Impeller 进展,请查看 https://github.com/orgs/flutter/projects/21。
|
||||
|
||||
# Performance
|
||||
|
||||
3.10 版本涵盖了除 Impeller 之外还有更多性能改进和修复。
|
||||
|
||||
## 消除卡顿
|
||||
|
||||
这里要感谢 [luckysmg](https://github.com/luckysmg), 他们发现可以缩短从 Metal 驱动获取下一个可绘制层的时间,而方式就是需要将 `FlutterViews` 背景颜色设置为非零值。
|
||||
|
||||
此更改消除了最近 iOS 120Hz 显示器上的低帧率问题,**在某些情况下它会使帧速率增加三倍**,这帮助我们解决了六个 GitHub issue。
|
||||
|
||||
**这一变化具有意义重大,以至于我们向后移植了一个修补程序到 3.7 版本中**。
|
||||
|
||||
在 3.7 稳定版中,我们将本地图像的加载从平台线程转移到 Dart 线程,以避免延迟来自平台线程的 vsync 事件。但是[用户](https://github.com/flutter/flutter/issues/121525)注意到 Dart 线程上的这项额外工作也导致了一些卡顿。
|
||||
|
||||
在 3.10 中,**我们将本地图像的打开和解码从 Dart 线程移至[后台线程](https://github.com/flutter/engine/pull/39918)**,这个更改消除了具有大量本地图像的屏幕上潜在的长时间停顿,同时避免了延迟 vsync 事件,在我们的本地测试和自动化基准测试中,这个更改将多个同步图像的加载时间缩短了一半。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image16.png)
|
||||
|
||||
|
||||
|
||||
我们继续在 Flutter 新的内部 DisplayList 结构之上构建优化,在 3.10 中,我们添加了 [R-Tree based culling](https://github.com/flutter/engine/pull/38429) 机制。
|
||||
|
||||
这种机制在我们的渲染器中更早地移除了绘制操作的处理。[例如](https://github.com/flutter/flutter/issues/92366) 优化加速了输出在屏幕外失败的自定义painter。
|
||||
|
||||
我们的 [microbenchmarks](https://flutter-engine-perf.skia.org/e/?begin=1671661938&end=1671754421&keys=X789f7ff76f30f8ccc672464f335fe09b&num_commits=50&request_type=1&xbaroffset=31974) 显示 DisplayList 处理时间最多减少了 50%,具有裁剪自定义绘画的 App 可能会看到不同效果的改进,改进的程度取决于隐藏绘制操作的复杂性和数量。
|
||||
|
||||
## 减少 iOS 启动延迟
|
||||
|
||||
之前应用中标识符查找的[低效策略](https://github.com/flutter/flutter/issues/37826)增加了应用启动延迟,这个启动延迟的增长与应用的大小成正比。
|
||||
|
||||
而在 3.10 中,[我们修复了 bundle identifier lookup](https://github.com/flutter/engine/pull/39975),这将大型应用的启动延迟减少了 100 毫秒或大约 30–50%。
|
||||
|
||||
## 缩小尺寸
|
||||
|
||||
Flutter 使用 `SkParagraph` 作为文本、布局和渲染的默认库,之前我们包括了一个标志以支持回退到遗留 `libtxt`和 `minikin` 。
|
||||
|
||||
由于我们对 `SkParagraph` 有充分的信心,[我们](https://github.com/flutter/engine/pull/39499)在 3.10 中删除了 `libtxt` 和 `minikin` 以及它们的标志,这将 Flutter 的压缩大小减少了 30KB。
|
||||
|
||||
> 看起来信心十足了。
|
||||
|
||||
## 稳定性
|
||||
|
||||
在 3.0 版本中,我们在渲染管道后期启用了一项 Android 功能,该功能使用高级 GPU 驱动,当只有一个“dirty” 区域发生变化时,这些驱动功能会重新绘制较少的屏幕内容。
|
||||
|
||||
我们之前已经将它添加到早期的优化中以达到类似的效果,尽管我们的基准测试结果不错,但还是出现了两个问题:
|
||||
|
||||
- 首先,改进最多的基准可能不代表实际用例。
|
||||
- 其次,[事实证明很难找到](https://github.com/flutter/engine/pull/37493)支持此 GPU 驱动功能的设备和 Android 版本集
|
||||
|
||||
鉴于有限的进步和支持,我们在 Android 上[禁用了](https://github.com/flutter/engine/pull/40898)部分重绘功能。
|
||||
|
||||
而使用 Skia 后端时,该功能在 iOS 上依然保持启用状态,我们希望在未来的版本中可以[通过 Impeller 启用它。](https://github.com/flutter/flutter/issues/124526)
|
||||
|
||||
|
||||
|
||||
# API 改进
|
||||
|
||||
## APNG解码器
|
||||
|
||||
Flutter 3.10 [解决了一个我们最受关注的问题](https://github.com/flutter/flutter/issues/37247),它增加了 `APNG` 解码图像的[能力](https://github.com/flutter/engine/pull/31098),现在可以使用 Flutter 现有的图片加载 API 来加载 `APNG` 图片。
|
||||
|
||||
## 图片加载 API 改进
|
||||
|
||||
3.10 添加了一个[新方法](https://master-api.flutter.dev/flutter/dart-ui/instantiateImageCodecWithSize.html) `instantiateImageCodecWithSize`,该方法满足以下三个条件的[用例支持:](https://github.com/flutter/flutter/issues/118543)
|
||||
|
||||
- 加载时宽高比未知
|
||||
- 边界框约束
|
||||
- 原始纵横比约束
|
||||
|
||||
# Mobile
|
||||
|
||||
## iOS
|
||||
|
||||
### 无线调试
|
||||
|
||||
**现在可以在无线的情况下运行和热重新加载的 Flutter iOS 应用**。
|
||||
|
||||
在 Xcode 中成功无线配对 iOS 设备后,就可以使用 flutter run 将应用部署到该设备,**如果遇到问题,请在 Window > Devices** 和 **Simulators > Devices**下验证网络图标是否出现在设备旁边。
|
||||
|
||||
> 要了解更多信息,可以查阅 https://docs.flutter.dev/get-started/install/macos#ios-setup。
|
||||
|
||||
### 宽色域图像支持
|
||||
|
||||
iOS 上的 Flutter 应用现在可以支持宽色域图像的精确渲染,要使用宽色域支持,应用必须使用 Impeller 并在 `Info.plist` 文件添加 `FLTEnableWideGamut ` 标志。
|
||||
|
||||
### 拼写检查支持
|
||||
|
||||
`SpellCheckConfiguration()` 控件现在默认支持 [Apple](https://developer.apple.com/documentation/uikit/uitextchecker) 在 iOS 上的拼写检查服务,可以使用 `spellCheckConfiguration` 中的参数对其进行设置 `CupertinoTextField` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image17.gif)
|
||||
|
||||
|
||||
|
||||
### 自适应复选框和单选
|
||||
|
||||
3.10 将 `CupertinoCheckBox` 和 `CupertinoRadio` 添加到库中 `Cupertino` ,他们创建符合 Apple 样式的复选框和单选按钮组件。
|
||||
|
||||
Material 复选框和单选控件添加了 `.adaptive` 构造函数,在 iOS 和 macOS 上,这些构造函数使用相应的 Cupertino 控件,在其他平台上使用 Material 控件。
|
||||
|
||||
### 优化 Cupertino 动画、过渡和颜色
|
||||
|
||||
Flutter 3.10 改进了一些动画、过渡和颜色以匹配 SwiftUI,这些改进包括:
|
||||
|
||||
- [更新](https://github.com/flutter/flutter/pull/122275) `CupertinoPageRoute`
|
||||
- [添加](https://github.com/flutter/flutter/pull/110127)标题放大动画 `CupertinoSliverNavigationBar`
|
||||
- 添加几种[新的 iOS 系统颜色 ](https://github.com/flutter/flutter/pull/118971)`CupertinoColors`
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image18.gif)
|
||||
|
||||
|
||||
|
||||
### PlatformView 性能
|
||||
|
||||
当 `PlatformViews `出现在屏幕上时,Flutter会限制 iOS 上的[刷新率以减少卡顿](https://github.com/flutter/engine/pull/39172),当应用显示动画或可滚动时,用户可能会在应用出现 `PlatformViews` 时注意到这一点。
|
||||
|
||||
### macOS 和 iOS 可以在插件中使用共享代码
|
||||
|
||||
Flutter 现在支持插件 `pubspec.yaml` 文件中的 `sharedDarwinSource` ,这个 key 表示 Flutter 应该共享 iOS 和 macOS 代码。
|
||||
|
||||
```
|
||||
ios:
|
||||
pluginClass: PathProviderPlugin
|
||||
dartPluginClass: PathProviderFoundation
|
||||
sharedDarwinSource: true
|
||||
macos:
|
||||
pluginClass: PathProviderPlugin
|
||||
dartPluginClass: PathProviderFoundation
|
||||
sharedDarwinSource: true
|
||||
```
|
||||
|
||||
### 应用扩展的新资源
|
||||
|
||||
我们为 Flutter 开发人员添加了使用 iOS 应用扩展文档,这些扩展包括实时活动、主屏幕控件和共享扩展。
|
||||
|
||||
为了简化创建主屏幕控件和共享数据,我们向 `path_provider` 和 `homescreen_widget` 插件添加了新方法。
|
||||
|
||||
> 具体可见:https://docs.flutter.dev/development/platform-integration/ios/app-extensions
|
||||
|
||||
### 跨平台设计的新资源
|
||||
|
||||
该文档现在包括针对特定 [UI 组件](https://docs.flutter.dev/resources/platform-adaptations#ui-components)的跨平台设计注意事项,要了解有关这些 UI 组件的更多信息,请查看Flutter UX GitHub 存储库中的讨论: https://github.com/flutter/uxr/discussions
|
||||
|
||||
> 具体可见:https://docs.flutter.dev/resources/platform-adaptations#ui-components
|
||||
|
||||
|
||||
|
||||
## Android
|
||||
|
||||
### Android CameraX 支持
|
||||
|
||||
[Camera X ](https://developer.android.com/training/camerax)是一个 Jetpack 库,可简化向 Android 应用添加丰富的相机功能。
|
||||
|
||||
该功能适用于多种 Android 相机硬件,在 3.10 中,我们为 Flutter Camera 插件添加了对 CameraX 的初步支持,此支持涵盖以下用例:
|
||||
|
||||
- 图像捕捉
|
||||
- 视频录制
|
||||
- 显示实时相机预览
|
||||
|
||||
```
|
||||
Dependencies:
|
||||
camera: ^0.10.4 # 最新相机版本
|
||||
camera_android_camerax: ^0.5.0
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 开发者工具
|
||||
|
||||
我们继续改进了 DevTools,这是一套用于 Dart 和 Flutter 的性能和调试工具,一些亮点包括:
|
||||
|
||||
- DevTools UI 使用 Material 3,这让外观现代化又增强了可访问性。
|
||||
- DevTools 控制台支持在调试模式下评估正在运行的应用,在 3.10 之前,只能在暂停应用时执行此操作。
|
||||
- 嵌入式 [Perfetto 跟踪查看器](https://perfetto.dev/)取代了以前的时间线跟踪查看器。
|
||||
|
||||
Perfetto 可以处理更大的数据集,并且比传统的跟踪查看器表现得更好,例如:
|
||||
|
||||
- 允许固定感兴趣的线程
|
||||
- 单击并拖动以从多个帧中选择多个时间轴事件
|
||||
- 使用 SQL 查询从时间轴事件中提取特定数据
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230511_F3/image19.png)
|
||||
|
||||
# 弃用和重大更改
|
||||
|
||||
## 弃用的 API
|
||||
|
||||
3.10 中的重大更改包括在 v3.7 发布后过期的弃用 API。
|
||||
|
||||
要查看所有受影响的 API 以及其他上下文和迁移指南,请查看[之前版本的弃用指南](https://docs.flutter.dev/release/breaking-changes/3-7-deprecations)。
|
||||
|
||||
> [Dart Fix ](https://docs.flutter.dev/development/tools/flutter-fix)可以修复其中的许多问题,包括在 IDE 中快速修复和使用`dart fix`命令批量应用。
|
||||
|
||||
## Android Studio Flamingo 升级
|
||||
|
||||
将 Android Studio 升级到 Flamingo 后,你可能会在尝试 `flutter run` 或 `flutter build` Flutter Android 应用时看到错误。
|
||||
|
||||
[发生此错误是因为 Android Studio Flamingo 将其捆绑的 Java SDK 从 11 更新到 17,](https://docs.gradle.org/current/userguide/compatibility.html#java)使用 Java 17 时,之前的 7.3 Gradle 版本无法运行。
|
||||
|
||||
我们[更新](https://github.com/flutter/flutter/pull/123916)来了 `flutter analyze --suggestions` 以验证是否由于 Java SDK 和 Gradle 版本之间的不兼容而发生此错误。
|
||||
|
||||
> 要了解修复此错误的不同方法,请查看我们的迁移指南:https://docs.flutter.dev/go/android-java-gradle-error。
|
||||
|
||||
## Window singleton 弃用
|
||||
|
||||
改版本弃用了 Window singleton,依赖它的应用和库需要开始[迁移](https://docs.flutter.dev/release/breaking-changes/window-singleton)。
|
||||
|
||||
当你的应用在未来版本的 Flutter 中做支持时,这会可以为你的应用提前做好多窗口准备支持。
|
||||
|
||||
> PS:还可以关注下本次 I/O 基于 Flutter 发布的新小游戏:[I/O FLIP 小游戏](https://juejin.cn/post/7231378331139997757)
|
|
@ -1,120 +0,0 @@
|
|||
# Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher
|
||||
|
||||
Flutter 3.10 发布之后,大家可能注意到,在它的 [release note](https://juejin.cn/post/7231565908631633979#heading-46) 里提了一句: **Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现**。
|
||||
|
||||
> 所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。
|
||||
|
||||
如下图所示,如果具体到对应的 API 场景,主要就是涉及 `WidgetsBinding.instance.window` 和 `MediaQueryData.fromWindow` 等接口的适配,因为 `WidgetsBinding.instance.window` 即将被弃用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image1.png)
|
||||
|
||||
> 你可以不适配,还能跑,只是升级的技术债务往后累计而已。
|
||||
|
||||
那首先可能就有一个疑问,为什么会有需要直接使用 `WidgetsBinding.instance.window` 的使用场景?简单来说,具体可以总结为:
|
||||
|
||||
- 没有 `BuildContext` ,不想引入 `BuildContext`
|
||||
- 不希望获取到的 ` MediaQueryData` 受到所在 `BuildContext` 的影响,例如键盘弹起导致 padding 变化重构和受到 `Scaffold` 下的参数影响等
|
||||
|
||||
> 这部分详细可见:[《MediaQuery 和 build 优化你不知道的秘密》 ](https://juejin.cn/post/7114098725600903175)。
|
||||
|
||||
那么从 3.10 开始,针对 `WidgetsBinding.instance.window` 可以通过新的 API 方式进行兼容:
|
||||
|
||||
- 如果存在 `BuildContex` , 可以通过 `View.of` 获取 `FlutterView`,这是官方最推荐的替代方式
|
||||
- 如果没有 `BuildContex` 可以通过 `PlatformDispatcher` 的 `views` 对象去获取
|
||||
|
||||
这里注意到没有,现在用的是 `View.of` ,获取的是 `FlutterView` ,对象都称呼为 View 而不是 「Window」,对应的 `MediaQueryData.fromWindow` API 也被弃用,修改为 `MediaQueryData.fromView` ,这个修改的依据在于:
|
||||
|
||||
> 起初 Flutter 假定了它只支持一个 Window 的场景,所以会有 `SingletonFlutterWindow` 这样的 instance window 对象存在,同时 `window` 属性又提供了许多和窗口本身无关的功能,在多窗口逻辑下会显得很另类。
|
||||
|
||||
那么接下来就让我们用「长篇大论」来简单介绍下这两个场景的特别之处。
|
||||
|
||||
# 存在 BuildContext
|
||||
|
||||
回归到本次的调整,首先是存在 BuildContext 的场景,如下代码所示,对于存在 `BuildContex` 的场景, `View.of` 相关的调整为:
|
||||
|
||||
```dart
|
||||
/// 3.10 之前
|
||||
double dpr = WidgetsBinding.instance.window.devicePixelRatio;
|
||||
Locale locale = WidgetsBinding.instance.window.locale;
|
||||
double width =
|
||||
MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width;
|
||||
|
||||
|
||||
/// 3.10 之后
|
||||
double dpr = View.of(context).devicePixelRatio;
|
||||
Locale locale = View.of(context).platformDispatcher.locale;
|
||||
double width =
|
||||
MediaQueryData.fromView(View.of(context)).size.width;
|
||||
|
||||
```
|
||||
|
||||
可以看到,这里的 `View` 内部实现肯定是有一个 `InheritedWidget` ,它将 `FlutterView` 通过 `BuildContext` 往下共享,从而提供类似 「window」 的参数能力,而通过 `View.of` 获取的参数:
|
||||
|
||||
- **当 `FlutterView` 本身的属性值发生变化时,是不会通知绑定的 `context` 更新,这个行为类似于之前的 ` WidgetsBinding.instance.window`**
|
||||
- 只有当 `FlutterView` 本身发生变化时,比如 `context` 绘制到不同的 `FlutterView` 时,才会触发对应绑定的 `context` 更新
|
||||
|
||||
可以看到 `View.of` 这个行为考虑的是「多 `FlutterView`」 下的更新场景,如果是需要绑定到具体对应参数的变动更新,如 `size` 等,还是要通过以前的 `MediaQuery.of` / `MediaQuery.maybeOf` 来实现。
|
||||
|
||||
而对于 `View` 来说,**每个 `FlutterView` 都必须是独立且唯一的**,在一个 Widget Tree 里,一个 `FlutterView` 只能和一个 `View` 相关联,这个主要体现在 `FlutterView` 标识 `GlobalObjectKey` 的实现上。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image2.png)
|
||||
|
||||
简单总结一下:**在存在 `BuildContex` 的场景,可以简单将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,不用担心绑定了 `context` 导致重构,因为 `View.of` 只对 `FlutterView` 切换的场景生效**。
|
||||
|
||||
# 不存在 BuildContext
|
||||
|
||||
对于不存在或者不方便使用 `BuildContext` 的场景,官方提供了 `PlatformDispatcher.views` API 来进行支持,不过因为 `get views` 对应的是 `Map` 的 `values` ,它是一个 `Iterable` 对象,**那么对于 3.10 我们需要如何使用 `PlatformDispatcher.views` 来适配没有 `BuildContext` 的 `WidgetsBinding.instance.window` 场面**?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image3.png)
|
||||
|
||||
> `PlatformDispatcher` 内部的` views` 维护了中所有可用 `FlutterView` 的列表,用于提供在没有 `BuildContext` 的情况下访问视图的支持。
|
||||
|
||||
你说什么情况下会有没有 `BuildContext` ?比如 Flutter 里 的 `runApp` ,如下图所示,3.10 目前在 `runApp` 时会通过 `platformDispatcher.implicitView` 来塞进去一个默认的 `FlutterView` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image4.png)
|
||||
|
||||
`implicitView` 又是什么?其实 `implicitView` 就是 `PlatformDispatcher._views` 里 id 为 0 的 `FlutterView` ,默认也是 `views` 这个 `Iterable` 里的 `first` 对象。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image5.png)
|
||||
|
||||
也就是在没有 `BuildContext` 的场景, 可以通过 `platformDispatcher.views.first` 的实现迁移对应的 `instance.window` 实现。
|
||||
|
||||
```dart
|
||||
/// 3.10 之前
|
||||
MediaQueryData.fromWindow(WidgetsBinding.instance.window)
|
||||
/// 3.10 之后
|
||||
MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)
|
||||
```
|
||||
|
||||
为什么不直接使用 `implicitView` 对象? 因为 `implicitView` 目前是一个过渡性方案,官方希望在多视图的场景下不应该始终存在 implicit view 的概念,而是应用自己应该主动请求创建一个窗口,去提供一个视图进行绘制。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230517_310/image6.png)
|
||||
|
||||
所以对于 `implicitView` 目前官方提供了 `_implicitViewEnabled` 函数,后续可以通过可配置位来控制引擎是否支持 `implicitView` ,也就是 **`implicitView` 在后续更新随时可能为 null ,这也是我们不应该在外部去使用它的理由**,同时它是在 `runApp` 时配置的,所以它在应用启动运行后永远不会改变,如果它在启动时为空,则它永远都会是 null。
|
||||
|
||||
> `PlatformDispatcher.instance.views[0]` 在之前的单视图场景中,无论是否有窗口存在,类似的 `implicitView` 会始终存在;而在多窗口场景下,`PlatformDispatcher.instance.views` 将会跟随窗口变化。
|
||||
|
||||
另外我们是通过 `WidgetsBinding.instance.platformDispatcher.views` 去访问 `views` ,而不是直接通过 `PlatformDispatcher.instance.views` ,因为通常官方更建议在 Binding 的依赖关系下去访问 `PlatformDispatcher` 。
|
||||
|
||||
> 除了需要在 `runApp()` 或者 `ensureInitialized()` 之前访问 PlatformDispatcher 的场景。
|
||||
|
||||
另外,如下图所示,通过 Engine 里对于 window 部分代码的实现,可以看到我们所需的默认` FlutterView` 是 id 为 0 的相关依据,所以这也是我们通过 `WidgetsBinding.instance.platformDispatcher.views` 去兼容支持的逻辑所在。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230517_310/image7.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image9.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image10.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
最后总结一下,说了那么多,其实不外乎就是将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,如果还有一些骚操作场景,可以使用 `WidgetsBinding.instance.platformDispatcher.views` ,如果不怕后续又坑,甚至可以直接使用 `WidgetsBinding.instance.platformDispatcher.implicitView` 。
|
||||
|
||||
整体上解释那么多,**主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解**,相信下一个版本多窗口应该就可以和大家见面了。
|
||||
|
||||
更多讨论可见:
|
||||
|
||||
- https://github.com/flutter/flutter/issues/120306
|
||||
- https://github.com/flutter/engine/pull/39553
|
||||
- https://github.com/flutter/flutter/issues/116929
|
||||
- https://github.com/flutter/flutter/issues/99500
|
||||
- https://github.com/flutter/engine/pull/39788
|
416
Flutter-316.md
416
Flutter-316.md
|
@ -1,416 +0,0 @@
|
|||
# Flutter 3.16 发布,快来看有什么更新吧
|
||||
|
||||
> 参考原文:https://medium.com/flutter/whats-new-in-flutter-3-16-dba6cb1015d1
|
||||
|
||||
Flutter 又又又发布新季度更新啦,同时随着而来的还有 Dart 3.2,本次 3.16 开始 Material 3 会成为新的默认主题,另外 Android 也迎来了 Impeller 的预览支持,另外还有 [Flutter Casual Games Toolkit ](https://medium.com/flutter/building-your-next-casual-game-with-flutter-716ef457e440) 的重大更新。
|
||||
|
||||
> **最重要的是,Impeller 的 Android 支持来了。**
|
||||
|
||||
# Framework
|
||||
|
||||
## Material default
|
||||
|
||||
现在,从 3.16 开始,`MaterialApp` 里的 `useMaterial3` 默认会是 true,如果你还希望使用 M2,可以使用 `useMaterial3: false` 来使用 M2 的主题效果,**不过 Material 2 相关的东西未来会被弃用并删除**。
|
||||
|
||||
另外在 M3 上其实有的 Widget 和 M2 并不是完全兼容,所以更新到 3.16 后一些 UI 你可以需要做手动迁移适配,例如 [NavigationBar](https://api.flutter.dev/flutter/material/NavigationBar-class.html) 的 UI 效果。
|
||||
|
||||
> 更多迁移问题可见:https://github.com/flutter/flutter/issues/91605 ,另外通过 https://flutter.github.io/samples/material_3.html 你可以比较两种主题下的不同效果。
|
||||
|
||||
M3 下的主题主要由 `ThemeData.colorScheme` 和 `ThemeData.textTheme` 来决定,创建 Material 3 配色首选是使用 `ColorScheme.fromSeed()` ,另外也可以通过 `ColorScheme.fromImageProvider` 来从图像下获取配色方案支持。
|
||||
|
||||
> 木已成舟,建议大家及早适配。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image1.gif)
|
||||
|
||||
另外对于 M3 motion 的改进还包括添加 `Easing` 和 `Durations` 类,而 Material 2 的 curves 现在被重命名,会包含 “legacy” 的警告,表示它最终将被弃用和删除。( [#129942](https://github.com/flutter/flutter/pull/129942) )
|
||||
|
||||
> 简单来说就是,新增加了 motion.dart 来替代老的 [curves.dart#L26 ](https://github.com/flutter/flutter/blob/main/packages/flutter/lib/src/material/curves.dart#L26)。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image2.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image3.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image4.gif)
|
||||
|
||||
## 在编辑菜单中添加附加选项
|
||||
|
||||
在 iOS 上,用户现在可以选择文本并启动提供多种标准服务的共享菜单,在 3.16 的版本中本次添加了查找、搜索和共享选项。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image5.gif)
|
||||
|
||||
## 增加 TextScaler
|
||||
|
||||
为了支持 Android 14 的[非线性字体缩放功能](https://blog.google/products/android/android-14/#:~:text=Also%2C you can improve readability,rate than smaller font size.)来帮助视力障碍,新 `TextScaler` 类替换了`Text.textScaleFactor ` 属性。( [#128522](https://github.com/flutter/flutter/pull/128522) )
|
||||
|
||||
## SelectionArea 更新
|
||||
|
||||
`SelectionArea` 现在支持鼠标单击、双击以及长按触摸设备相关的原生手势,默认情况下,这些新手势可通过`SelectionArea ` 和 `SelectableRegion` 来支持:
|
||||
|
||||
- 单击:在单击的位置设置折叠选区
|
||||
- 双击:选择单击位置的单词
|
||||
- 双击 + 拖动:扩展单词块中的选择范围
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image6.gif)
|
||||
|
||||
|
||||
|
||||
- 长按+拖动:扩展单词块中的选择范围。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image7.gif)
|
||||
|
||||
## 对焦点 Widget 进行操作的菜单项
|
||||
|
||||
3.16 开始增加清除使用菜单项时焦点更改的功能: `FocusManager` 的 `applyFocusChangesIfNeeded` 现在支持恢复菜单焦点 ,当用户单击菜单项时,焦点将返回到打开菜单之前具有焦点的项目。( [#130536](https://github.com/flutter/flutter/pull/130536) )
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image8.gif)
|
||||
|
||||
## iOS、macOS 的菜单项快捷方式自动重新排序
|
||||
|
||||
Mac 平台上的 Flutter 应用现在可以对菜单中的快捷方式修饰符进行排序,以遵循 Apple 人机界面指南。( [#129309](https://github.com/flutter/flutter/pull/129309) )
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image9.png)
|
||||
|
||||
## MatrixTransition 动画
|
||||
|
||||
新的 `MatrixTransition` 允许在创建动画过渡时进行矩阵变换,根据当前动画值,可以提供 child widget 的矩阵变换。( [#131084](https://github.com/flutter/flutter/pull/131084) )
|
||||
|
||||
```dart
|
||||
|
||||
class MatrixTransitionExampleApp extends StatelessWidget {
|
||||
const MatrixTransitionExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: MatrixTransitionExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixTransitionExample extends StatefulWidget {
|
||||
const MatrixTransitionExample({super.key});
|
||||
|
||||
@override
|
||||
State<MatrixTransitionExample> createState() =>
|
||||
_MatrixTransitionExampleState();
|
||||
}
|
||||
|
||||
class _MatrixTransitionExampleState extends State<MatrixTransitionExample>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: MatrixTransition(
|
||||
animation: _animation,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: FlutterLogo(size: 150.0),
|
||||
),
|
||||
onTransform: (double value) {
|
||||
return Matrix4.identity()
|
||||
..setEntry(3, 2, 0.004)
|
||||
..rotateY(pi * 2.0 * value);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image10.gif)
|
||||
|
||||
|
||||
|
||||
## PaintPattern 添加到 flutter_test
|
||||
|
||||
在 `flutter_test `包中,新 `PaintPattern` 类允许开发者验证 `CustomPainter` 和 `Decoration` (在单元测试中使用)等 Widget 对画布进行的绘制调用。
|
||||
|
||||
以前需要一个文件来验证是否绘制了正确的颜色和矩形,但现在可以使用 `PaintPattern` ,例如以下示例验证了 `MyWidget `在画布上绘制了一个圆圈:
|
||||
|
||||
```dart
|
||||
expect(
|
||||
find.byType(MyWidget),
|
||||
paints
|
||||
..circle(
|
||||
x: 10,
|
||||
y: 10,
|
||||
radius: 20,
|
||||
color: const Color(0xFFF44336),
|
||||
),
|
||||
);
|
||||
// Multiple paint calls can even be chained together.
|
||||
expect(
|
||||
find.byType(MyWidget),
|
||||
paints
|
||||
..circle(
|
||||
x: 10,
|
||||
y: 10,
|
||||
radius: 20,
|
||||
color: const Color(0xFFF44336),
|
||||
),
|
||||
..image(
|
||||
image: MyImage,
|
||||
x: 20,
|
||||
y: 20,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
## 滚动更新
|
||||
|
||||
继 Flutter 3.13 中首次发布二维滚动基础之后,3.16 带来了更多功能和完善, 2D foundation 现在支持 `KeepAlive` Widget,以及默认焦点遍历和隐式滚动。
|
||||
|
||||
3.13 版本发布后不久,[two_Dimension_scrollables](https://pub.dev/packages/two_dimensional_scrollables) 包就发布了,该包由 Flutter 团队维护,包含第一个构建在该框架基础上的 2D 滚动 widget - `TableView`,目前已添加了丰富的装饰和样式支持以及其他错误修复。
|
||||
|
||||
# Engine
|
||||
|
||||
## Impeller Android
|
||||
|
||||
在 3.16 版本中,Android 上的 Impeller 已准备好在 stable 上提供预览,该预览版包括有关支持 Vulkan 的设备上的 Impeller 特性。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image11.png)
|
||||
|
||||
> 图表显示了过去一年中在 Impeller 的 Vulkan 后端上运行的 Flutter Gallery 转换性能基准测试改进,用户将观察到卡顿更少且稳态帧速率更高。
|
||||
|
||||
目前 Impeller 在没有 Vulkan 支持的设备上会表现不佳,单在未来几个月内还会将 Impeller 的 OpenGL 后端功能继续完善。
|
||||
|
||||
Flutter 开发者现在可以在支持 Vulkan 的 Android 设备上试用 Impeller,方法是将标志 `— enable-impeller 传递`给 `flutter run`,或者将以下设置添加到 `AndroidManiest.xml`文件中的 `<application>`:
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="true" />
|
||||
```
|
||||
|
||||
> 通常,Impeller 在运行 Android API 29 或更高版本的 64 位操作系统的设备上会使用 Vulkan 。
|
||||
|
||||
目前 Android Vulkan 预览版已知问题有:
|
||||
|
||||
- platform view 尚未实现支持,包含 platform view 的框架性能会有些差。
|
||||
- 自定义着色器还未实现。
|
||||
|
||||
因为 Android 硬件生态系统更加多样化,预计 Android 的预览期将比 iOS 更长,这是无法避免的,另外,Impeller 的 Vulkan 在“调试”构建中启用了超出 Skia 使用的功能的额外调试功能,并且这些功能会产生额外的运行时开销,**所以有关 Impeller 性能的反馈来自最好来自 profile 或发布版本**。
|
||||
|
||||
## Impeller 性能, 保真度和稳定性
|
||||
|
||||
3.16 还对 Impeller 中的文本性能进行了多项改进,这对 Android 和 iOS 都是同样的。特别是改进了 Impeller 字形图集的管理,以及在引擎的 UI 和光栅线程之间划分文本工作负载的方式,所以 3.16 上用户会注意到文本繁重工作负载中的卡顿现象减少。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image12.png)
|
||||
|
||||
> 图表显示,在使用 Impeller 的 iPhone 11 上进行的一项文本密集型基准测试中平均帧光栅化时间(以毫秒为单位)有所减少。
|
||||
|
||||
本次 3.16 已经对 flutter/engine 存储库做出了 209 个 Impeller 相关承诺,解决了 217 个问题,其中包括 42 个有关保真度、稳定性或性能问题的用户报告。
|
||||
|
||||
## Engine 性能
|
||||
|
||||
为了在具有异构多处理功能的移动设备上支持更好的性能,本次 [修改了](https://github.com/flutter/engine/pull/45673) Engine 对性能敏感的线程(例如 UI 和光栅线程)与设备更强大的内核的支持。
|
||||
|
||||
本次修改在某些情况下改进非常显着,预计在 Android 上的 Skia 和 Impeller 进行本次更改后,用户将注意到卡顿现象减少,而在 iOS 设备上,这种影响不太明显,因为在 iOS 设备上,功能较强大的内核与功能较弱的内核之间的差异较小。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image13.png)
|
||||
|
||||
|
||||
|
||||
## Impeller 性能 overlay
|
||||
|
||||
在之前的版本中,Flutter 的[性能 overlay ](https://docs.flutter.dev/perf/ui-performance#the-performance-overlay)功能并未与 Impeller 一起发布,本次版本修复了该问题。现在在启用 Impeller 的情况下,性能 overlay 可以正确显示。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image14.png)
|
||||
|
||||
## 现在可以正确显示 Dithering
|
||||
|
||||
在 3.16 版本中, `Paint.enableDithering` 属性默认设置为 true,不再支持开发人员配置,在此之前,渐变在所有设备上都有很多色带,并且在使用某些动画时看起来也很奇怪,而解决方案是使渐变不透明,并使用 Skia 的Dithering 渐变。
|
||||
|
||||
> 而为了简化迁移过程, Impeller 永远不会支持除梯度之外的任何内容的 Dithering。
|
||||
|
||||
- 3.16 之前
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image15.png)
|
||||
|
||||
- 3.16 之后
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image16.png)
|
||||
|
||||
# 游戏
|
||||
|
||||
## Flutter 游戏工具包
|
||||
|
||||
在过去的几年里 Flutter 发布了数以万计的游戏,从简单但有趣的谜题到更复杂的街机游戏,其中包括:
|
||||
|
||||
- Etermax 的[Trivia Crack](https://triviacrack.com/)
|
||||
- Lotum 的[4 Pics 1 Word](https://flutter.dev/showcase/lotum)(猜词游戏)
|
||||
- Dong Digital 的 [Brick Mania](https://play.google.com/store/apps/details?id=net.countrymania.brick&hl=en)(街机游戏)
|
||||
- Onrizon 的[StopotS](https://play.google.com/store/apps/details?id=com.gartic.StopotS&hl=en)(类别游戏)
|
||||
- Flutter for I/O 的 [复古弹球游戏](https://pinball.flutter.dev/)
|
||||
- [PUBG](https://flutter.dev/showcase/pubg-mobile) 移动版在社交和菜单屏幕中使用 Flutter
|
||||
- ····
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image17.gif)
|
||||
|
||||
为了帮助游戏开发者提高工作效率,Flutter 今天推出了休闲游戏工具包的重大更新,它是一系列新资源的集合,可帮助开发者从概念转向推出更多特定类型的游戏模板,例如纸牌游戏、无尽跑酷游戏,以及 Play 游戏服务、应用内购买、广告、成就、crashlytics 等服务集成和多人游戏支持。
|
||||
|
||||
> 要了解更多信息,可以查看 https://medium.com/flutter/building-your-next-casual-game-with-flutter-716ef457e440
|
||||
|
||||
|
||||
|
||||
# Web
|
||||
|
||||
## Chrome DevTools 上的 Flutter 时间轴事件
|
||||
|
||||
Flutter 时间轴事件现在显示在 Chrome DevTools 的性能面板中。( [#130132](https://github.com/flutter/flutter/issues/130132) )
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image18.png)
|
||||
|
||||
# Android
|
||||
|
||||
## 鼠标滚轮支持
|
||||
|
||||
为了适配鼠标在平板电脑或可折叠设备的效果,3.16 版本中 flutter 支持对鼠标滚动与 Android 设备上的滚动速度相匹配。( [44724](https://github.com/flutter/engine/pull/44724) )
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image19.gif)
|
||||
|
||||
## 预测性后退导航
|
||||
|
||||
Android 14 版本包含预测性后退手势功能,3.16 更新为 Flutter 带来了预测性返回手势。
|
||||
|
||||
```dart
|
||||
PopScope(
|
||||
canPop: _myCondition,
|
||||
child: ...
|
||||
),
|
||||
|
||||
PopScope(
|
||||
canPop: true,
|
||||
onPopInvoked (bool didPop) {
|
||||
_myHandleOnPopMethod();
|
||||
},
|
||||
child: ...
|
||||
),
|
||||
|
||||
NavigatorPopHandler(
|
||||
onPop: () => _nestedNavigatorKey.currentState!.pop(),
|
||||
child: Navigator(
|
||||
key: _nestedNavigatorKey,
|
||||
…
|
||||
),
|
||||
)
|
||||
···
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image20.gif)
|
||||
|
||||
# iOS系统
|
||||
|
||||
## 应用扩展
|
||||
|
||||
Flutter 现在可以支持针对某些 [iOS 应用扩展](https://developer.apple.com/app-extensions/),这意味着可以使用 Flutter Widget 为某些类型的 iOS 应用绘制 UI,当然这并不适用于所有类型的应用扩展,因为 API(例如主屏幕空间)或内存可能存在限制。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image21.png)
|
||||
|
||||
由于应用扩展的内存限制,仅建议使用 Flutter 为内存限制大于 100MB 的扩展类型构建应用扩展 UI。
|
||||
|
||||
此外,Flutter 在调试模式下会使用额外的内存,因此当用于构建扩展 UI 时,Flutter 并不完全支持在物理设备上以调试模式运行应用扩展。
|
||||
|
||||
> 详细可见: https://docs.flutter.dev/platform-integration/ios/app-extensions
|
||||
|
||||
|
||||
|
||||
# 生态
|
||||
|
||||
目前 [Flutter Favorite](https://docs.flutter.dev/packages-and-plugins/favorites) 已经重新启动,在本周期中 Flutter 生态系统委员会将 [Flame](https://pub.dev/packages/flame)、[flutter_animate](https://pub.dev/packages/flutter_animate)、[flutter_rust_bridge](https://pub.dev/packages/flutter_rust_bridge)、[Riverpod](https://pub.dev/packages/riverpod)、[video_player](https://pub.dev/packages/video_player)、[macos_ui](https://pub.dev/packages/macos_ui) 和 [fpdart](https://pub.dev/packages/fpdart) 包指定为新的 Flutter Favorite。
|
||||
|
||||
## Camera X 改进
|
||||
|
||||
在 3.10 稳定版本中, Flutter 相机插件中添加了对 Camera X 的初步支持,而 CameraX 解决了该插件的 Camera 2 实现中存在的许多问题。
|
||||
|
||||
```yaml
|
||||
dependency:
|
||||
camera: ^0.10.4
|
||||
camera_android_camerax: ^0.5.0
|
||||
```
|
||||
|
||||
## macOS 视频播放器
|
||||
|
||||
[video_player](https://pub.dev/packages/video_player) 中添加了 macOS 支持。
|
||||
|
||||
# 开发工具
|
||||
|
||||
## 开发工具扩展
|
||||
|
||||
新的 [DevTools 扩展框架](https://pub.dev/packages/devtools_extensions) 支持:
|
||||
|
||||
- 包作者为其直接在 DevTools 中显示的包构建自定义工具。
|
||||
- 软件包作者可以编写强大的工具,利用 DevTools 中的现有框架和实用程序。
|
||||
- 使用 DevTools 调试应用以访问特定于其用例的工具的 Dart 和 Flutter 开发人员(由应用的依赖项以及哪些依赖项提供 DevTools 扩展决定)。
|
||||
|
||||
感谢 [Provider](https://pub.dev/packages/provider)、[Drift](https://pub.dev/packages/drift) 和 [Patrol](https://pub.dev/packages/patrol) 的软件包作者,这个生态系统已经建立起来,现在就可以使用这些软件包的 DevTools 扩展!
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image22.png) | ![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image23.png) | ![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image24.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
## 开发工具更新
|
||||
|
||||
此版本的 DevTools 的一些亮点包括:
|
||||
|
||||
- 添加了对 DevTools 扩展的支持
|
||||
- 添加了一个新的“主”屏幕,显示连接的应用的摘要
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image25.png)
|
||||
|
||||
其他改进包括:
|
||||
|
||||
- 整体表现
|
||||
- 热重启 robustness
|
||||
- 文本选择和复制行为
|
||||
- viewer polish 网络分析响应
|
||||
|
||||
## VS Code UI 的可发现性
|
||||
|
||||
感谢 Flutter 社区成员 [DanTup](https://github.com/DanTup) ,Flutter VS Code 扩展现在拥有一个 Flutter 侧边栏,可让轻松访问:
|
||||
|
||||
- 打开 Flutter DevTools 屏幕
|
||||
- 查看活动的调试会话
|
||||
- 查看可用设备
|
||||
- 创建新项目
|
||||
- 热重载并重启
|
||||
- 运行 Flutter Doctor -v
|
||||
- ····
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231116_Flutter316/image26.png)
|
||||
|
||||
# 最后
|
||||
|
||||
本次更新的还是属于比较“低调”的更新,最大的变化应该就是 M3 的默认主题和 Android Impeller ,其他的其实影响并不是很大,其中 M3 主题还是建议大家及早适配,因为 M2 的控件效果未来确实会慢慢剔除。
|
||||
|
||||
另外可以看到本次更新的核心还是集中在 Android 和 iOS ,PC 更新节奏看起来受到“某些影响”后慢了不少?同时关于 Jetbrains 的插件更新也没体现,核心 IDE 的资源都投入到 VSCode 了,只能说且行且珍惜。
|
||||
|
||||
好了,勇敢的少年,开始吃螃蟹了。
|
596
Flutter-319.md
596
Flutter-319.md
|
@ -1,596 +0,0 @@
|
|||
# Flutter 3.19 发布,快来看看有什么更新吧?
|
||||
|
||||
> 参考链接:https://medium.com/flutter/whats-new-in-flutter-3-19-58b1aae242d2
|
||||
|
||||
新年假期的尾巴,Flutter 迎来了 3.19 更新,该版本带来了 **Gemini(Google's most capable AI ) 的 Dart SDK,更好控制动画颗粒度的 Widget ,Impeller 的性能增强和 Android 优化支持,deep links 工具支持,Android 和 iOS 上的特定平台新支持,Windows Arm64 支持**等等。
|
||||
|
||||
> 普遍优化修复居多。
|
||||
|
||||
# Gemini Dart SDK
|
||||
|
||||
Google 的 AI Dart SDK Gemini 目前已经发布,pub 上的 [google_generative_ai](https://pub.dev/packages/google_generative_ai) 将 Gemini 的生成式 AI 功能支持到 Dart 或 Flutter 应用里,Google Generative AI SDK 可以更方便地让 Dart 开发人员在 App 里集成 LLM 的 AI 能力。
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:google_generative_ai/google_generative_ai.dart';
|
||||
|
||||
void main() async {
|
||||
// Access your API key as an environment variable (see first step above)
|
||||
final apiKey = Platform.environment['API_KEY'];
|
||||
if (apiKey == null) {
|
||||
print('No \$API_KEY environment variable');
|
||||
exit(1);
|
||||
}
|
||||
// For text-and-image input (multimodal), use the gemini-pro-vision model
|
||||
final model = GenerativeModel(model: 'gemini-pro-vision', apiKey: apiKey);
|
||||
final (firstImage, secondImage) = await (
|
||||
File('image0.jpg').readAsBytes(),
|
||||
File('image1.jpg').readAsBytes()
|
||||
).wait;
|
||||
final prompt = TextPart("What's different between these pictures?");
|
||||
final imageParts = [
|
||||
DataPart('image/jpeg', firstImage),
|
||||
DataPart('image/jpeg', secondImage),
|
||||
];
|
||||
final response = await model.generateContent([
|
||||
Content.multi([prompt, ...imageParts])
|
||||
]);
|
||||
print(response.text);
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image1.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image2.png)
|
||||
|
||||
# Framework
|
||||
|
||||
## 滚动优化
|
||||
|
||||
在 3.19 之前,使用两根手指在 Flutter 列表上进行滑动时,Flutter 的滚动速度会加快到两倍,这一直是一个饱受争议的问题,现在,从 3.19 开始,**开发者可以使用 `MultiTouchDragStrategy.latestPointer` 来配置默认的 `ScrollBehavior`** ,从而让滑动效果与手指数量无关。
|
||||
|
||||
`ScrollBehavior.multitouchDragStrategy` 默认情况下会防止多个手指同时与可滚动对象进行交互,从而影响滚动速度,如果之前你已经依赖老板本这个多指滑动能力,那么可以通过 `MaterialApp.scrollBehavior` / `CupertinoApp.scrollBehavior` 去恢复:
|
||||
|
||||
```dart
|
||||
class MyCustomScrollBehavior extends MaterialScrollBehavior {
|
||||
// Override behavior methods and getters like multitouchDragStrategy
|
||||
@override
|
||||
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
|
||||
}
|
||||
|
||||
// Set ScrollBehavior for an entire application.
|
||||
MaterialApp(
|
||||
scrollBehavior: MyCustomScrollBehavior(),
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
或者通过 `ScrollConfiguration` 进行局部配置:
|
||||
|
||||
```dart
|
||||
class MyCustomScrollBehavior extends MaterialScrollBehavior {
|
||||
// Override behavior methods and getters like multitouchDragStrategy
|
||||
@override
|
||||
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
|
||||
}
|
||||
|
||||
// ScrollBehavior can be set for a specific widget.
|
||||
final ScrollController controller = ScrollController();
|
||||
ScrollConfiguration(
|
||||
behavior: MyCustomScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
> 详细可参考:https://docs.flutter.dev/release/breaking-changes/multi-touch-scrolling
|
||||
|
||||
另外,本次 3.19 还修复了 [SingleChildScrollView#136871](https://github.com/flutter/flutter/pull/136871) 和 [ReorderableList#136828](https://github.com/flutter/flutter/pull/136828) 相关的崩溃问题,同时 [two_dimensional_scrollables](https://pub-web.flutter-io.cn/packages/two_dimensional_scrollables) 也修复了一些问题,比如在任一方向上正在进行滚动时出现拖动或者点击,scroll activity 将按预期停止。
|
||||
|
||||
最后,two_dimensional_scrollables 上的 TableView 控件也进行了多次更新,提供了需要改进,例如添加了对合并单元格的支持,并在上一个稳定版本 3.16 之后适配了更多 2D foundation。
|
||||
|
||||
## AnimationStyle
|
||||
|
||||
来自社区 [@TahaTesser](https://github.com/TahaTesser) 的贡献,现在 Flutter 开发者使用 AnimationStyle ,可以让用户快速覆盖 Widget 中的默认动画行为,就像 `MaterialApp` 、 `ExpansionTile` 和 `PopupMenuButton` :
|
||||
|
||||
```dart
|
||||
popUpAnimationStyle: AnimationStyle(
|
||||
curve: Easing.emphasizedAccelerate,
|
||||
duration: Durations.medium4,
|
||||
),
|
||||
```
|
||||
|
||||
```dart
|
||||
return MaterialApp(
|
||||
themeAnimationStyle: AnimationStyle.noAnimation,
|
||||
```
|
||||
|
||||
|
||||
|
||||
## SegmentedButton.styleFrom
|
||||
|
||||
来自社区成员 [@AcarFurkan](https://github.com/AcarFurkan) 的贡献,该静态方式就像其他按钮类型提供的方法一样。可以快速创建分段按钮的按钮样式,可以与其他分段按钮共享或用于配置应用的分段按钮主题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image3.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image4.png)
|
||||
|
||||
|
||||
|
||||
## Adaptive Switch
|
||||
|
||||
Adaptive Switch 可以让 Widget 在 macOS 和 iOS 上看起来和感觉是原生的效果,并且在其他地方具有 Material Design 的外观和感觉,它不依赖于 Cupertino 库,因此它的 API 在所有平台上都完全相同。
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [Switch.adaptive].
|
||||
|
||||
void main() => runApp(const SwitchApp());
|
||||
|
||||
class SwitchApp extends StatefulWidget {
|
||||
const SwitchApp({super.key});
|
||||
|
||||
@override
|
||||
State<SwitchApp> createState() => _SwitchAppState();
|
||||
}
|
||||
|
||||
class _SwitchAppState extends State<SwitchApp> {
|
||||
bool isMaterial = true;
|
||||
bool isCustomized = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = ThemeData(
|
||||
platform: isMaterial ? TargetPlatform.android : TargetPlatform.iOS,
|
||||
adaptations: <Adaptation<Object>>[
|
||||
if (isCustomized) const _SwitchThemeAdaptation()
|
||||
]);
|
||||
final ButtonStyle style = OutlinedButton.styleFrom(
|
||||
fixedSize: const Size(220, 40),
|
||||
);
|
||||
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('Adaptive Switches')),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
OutlinedButton(
|
||||
style: style,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isMaterial = !isMaterial;
|
||||
});
|
||||
},
|
||||
child: isMaterial
|
||||
? const Text('Show cupertino style')
|
||||
: const Text('Show material style'),
|
||||
),
|
||||
OutlinedButton(
|
||||
style: style,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isCustomized = !isCustomized;
|
||||
});
|
||||
},
|
||||
child: isCustomized
|
||||
? const Text('Remove customization')
|
||||
: const Text('Add customization'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SwitchWithLabel(label: 'enabled', enabled: true),
|
||||
const SwitchWithLabel(label: 'disabled', enabled: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchWithLabel extends StatefulWidget {
|
||||
const SwitchWithLabel({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
State<SwitchWithLabel> createState() => _SwitchWithLabelState();
|
||||
}
|
||||
|
||||
class _SwitchWithLabelState extends State<SwitchWithLabel> {
|
||||
bool active = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 150,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Text(widget.label)),
|
||||
Switch.adaptive(
|
||||
value: active,
|
||||
onChanged: !widget.enabled
|
||||
? null
|
||||
: (bool value) {
|
||||
setState(() {
|
||||
active = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
|
||||
const _SwitchThemeAdaptation();
|
||||
|
||||
@override
|
||||
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return defaultValue;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return SwitchThemeData(
|
||||
thumbColor: MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.yellow;
|
||||
}
|
||||
return null; // Use the default.
|
||||
}),
|
||||
trackColor: const MaterialStatePropertyAll<Color>(Colors.brown),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 详细可见:https://main-api.flutter.dev/flutter/material/Switch/Switch.adaptive.html
|
||||
|
||||
|
||||
|
||||
### SemanticsProperties 可访问性标识符
|
||||
|
||||
3.19 里`SemanticsProperties` 添加了新的可访问性标识符,为 native 可访问性层次结构中的语义节点提供标识符。
|
||||
|
||||
- 在 Android 上,它在辅助功能层次结构中显示为 “resource-id”
|
||||
|
||||
- 在 iOS 上是设置里 `UIAccessibilityElement.accessibilityIdentifier`
|
||||
|
||||
|
||||
|
||||
## MaterialStatesController
|
||||
|
||||
`TextField` 和 `TextFormField` 添加了 `MaterialStatesController` ,因为在此之前,开发者无法确定 `TextFormField` 当前是否处于错误状态,例如:
|
||||
|
||||
- 它显示错误消息并使用了`errorBorder`
|
||||
- 确定它是否 foucs,但前提是提供自己的 `FocusNode`
|
||||
|
||||
而现在允许开发者提供自己的 `MaterialStatesController`(类似于`ElevatedButton`),以便开发者可以完全访问有关这些控件的状态信息。
|
||||
|
||||
```dart
|
||||
final MaterialStatesController statesController = MaterialStatesController();
|
||||
statesController.addListener(valueChanged);
|
||||
|
||||
TextField(
|
||||
statesController: statesController,
|
||||
controller: textEditingController,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## UndoHistory stack
|
||||
|
||||
修复了 undo/redo 历史在日语键盘上可能消失的问题,并使其现在可以在将条目推送到 UndoHistory 堆栈之前对其进行修改。
|
||||
|
||||
`UndoHistory` 是一个提供撤消/重做功能的 Widget,它还具有绑定到特定于平台的实现的底层接口,监听了键盘事件以实现 undo/redo 操作。
|
||||
|
||||
从 Flutter 3.0.0 开始,可以将 `UndoHistoryController` 传递给 `TextField`它附带了 `UndoHistoryValue` 。
|
||||
|
||||
对于一个非常简单的展示,我将创建一个 UndoHistoryController 实例,将其传递给 TextField,使用 ValueListenableBuilder 监听该实例,并在构建器中的按钮上返回一行以执行撤消/重做操作。
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [UndoHistoryController].
|
||||
|
||||
void main() {
|
||||
runApp(const UndoHistoryControllerExampleApp());
|
||||
}
|
||||
|
||||
class UndoHistoryControllerExampleApp extends StatelessWidget {
|
||||
const UndoHistoryControllerExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final UndoHistoryController _undoController = UndoHistoryController();
|
||||
|
||||
TextStyle? get enabledStyle => Theme.of(context).textTheme.bodyMedium;
|
||||
TextStyle? get disabledStyle =>
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
maxLines: 4,
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
undoController: _undoController,
|
||||
),
|
||||
ValueListenableBuilder<UndoHistoryValue>(
|
||||
valueListenable: _undoController,
|
||||
builder: (BuildContext context, UndoHistoryValue value,
|
||||
Widget? child) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Undo',
|
||||
style: value.canUndo ? enabledStyle : disabledStyle),
|
||||
onPressed: () {
|
||||
_undoController.undo();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Redo',
|
||||
style: value.canRedo ? enabledStyle : disabledStyle),
|
||||
onPressed: () {
|
||||
_undoController.redo();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Engine
|
||||
|
||||
## Impeller 进度
|
||||
|
||||
### Android OpenGL 预览
|
||||
|
||||
在 3.16 稳定版本中,Flutter 官方邀请了用户在支持 Vulkan 的 Android 设备上试用 Impeller,覆盖了该领域 77% 的 Android 设备,而在过去的几个月里,Flutter 官方团队让 Impeller 的 OpenGL 达到了与 Vulkan 同等的功能,例如添加[支持 MSAA](https://github.com/flutter/engine/pull/47030)。
|
||||
|
||||
这意味着几乎所有 Android 设备上的 Flutter 应用都有望支持 Impeller 渲染,除了少数即将推出的剩余功能除外,例如自定义着色器和对外部纹理的完全支持,目前官方团队表示在今年晚些时候 Androd 也会将 Impeller 作为默认渲染器。
|
||||
|
||||
另外,Impeller 的 Vulkan 在“调试”构建中启用了超出 Skia 的附加调试功能,并且这些功能会产生额外的运行时开销。因此,有关 Impeller 性能的反馈你许来自 profile 或 release 版本,并且需要包括 DevTools 的时间表以及与同一设备上的 Skia 后端的比较。
|
||||
|
||||
### 路线
|
||||
|
||||
在实现了渲染保真度之后,在 Impeller Android 预览期间的主要关注点是性能,另外一些更大的改进也正在进行,例如能够利用 [Vulkan subpasses](https://github.com/flutter/flutter/issues/128911) 大大提高高级混合模式的性能。
|
||||
|
||||
此外,Flutter 官方还期望渲染策略发生变化,不再总是将 CPU 上的每条路径细分为[先模板后覆盖](https://github.com/flutter/flutter/issues/137714) ,这样的实现将大大降低 Android 和 iOS 上 Impeller 的 CPU 利用率。
|
||||
|
||||
最后,Flutter 还期望新的[高斯模糊](https://github.com/flutter/flutter/issues/131580)实施能匹配 Skia 实现的吞吐量,并改进 iOS 上模糊的惯用用法。
|
||||
|
||||
## API 改进
|
||||
|
||||
### 字形信息
|
||||
|
||||
3.19 版本包括两个新的 dart:ui 方法:`Paragraph ` 的 `getClosestGlyphInfoForOffset` 和 `getGlyphInfoAt`,这两个方式都会返回一个新类型的对象字形信息,包含段落内字符(或视觉上相连的字符序列)的尺寸。
|
||||
|
||||
- [Paragraph.getGlyphInfoAt](https://main-api.flutter.dev/flutter/dart-ui/Paragraph/getGlyphInfoAt.html),查找与文本中的代码单元关联的 [GlyphInfo ](https://main-api.flutter.dev/flutter/dart-ui/GlyphInfo-class.html)
|
||||
- [Paragraph.getClosestGlyphInfoForOffset](https://main-api.flutter.dev/flutter/dart-ui/Paragraph/getClosestGlyphInfoForOffset.html),查找屏幕上最接近给定 Offset 的字形的 GlyphInfo
|
||||
|
||||
### GPU追踪
|
||||
|
||||
Metal 下的 Impeller(iOS、macOS、模拟器) 和在支持 Vulkan 的 Android 设备上,Flutter 引擎现在将在调试和 profile 构建中报告时间线中每个帧的 GPU 时间,可以在 DevTools 中的 “GPUTracer” 下检查 GPU 帧时序。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image5.png)
|
||||
|
||||
请注意,由于非 Vulkan Android 设备可能会误报其对查询 GPU 计时的支持,因此只能通过在 AndroidManifest.xml 设置标志来启用 Impeller 的 GPU 跟踪:
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableOpenGLGPUTracing"
|
||||
android:value="true" />
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### Specialization Constants
|
||||
|
||||
Impeller 添加支持 [Specialization Constants](https://github.com/flutter/flutter/issues/119357) ,利用 Impeller 着色器中的这一功能,减少了 Flutter 引擎的未压缩二进制大小 350KB。
|
||||
|
||||
### Backdrop Filter 加速
|
||||
|
||||
3.19 版本包含一些不错的性能改进,其中就包括了 Impeller 的 Backdrop Filter 和模糊优化,特别是开源贡献者 [knopp](https://github.com/knopp) [noticed](https://github.com/flutter/flutter/issues/131567#issuecomment-1678210475) 注意到 Impeller 错误地请求了读取屏幕纹理的功能,[删除这个](https://github.com/flutter/engine/pull/47808)功能支持,在基准测试中,根据复杂程度,将包含多个背景滤镜的场景改进了 20-70%。
|
||||
|
||||
同时,Impeller 在每个背景滤镜上[不再无条件存储模板缓冲区](https://github.com/flutter/engine/pull/47397),相反,任何影响操作的剪辑都会被记录下来,并在恢复背景滤镜的保存层时重播到新的模板缓冲区中。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image6.png)
|
||||
|
||||
通过这一更改,在运行具有 Vulkan 的 Impeller 的 Pixel 7 Pro 上进行动画高级混合模式基准测试时,将平均 GPU 帧时间从 55 毫秒改进到 16 毫秒,并将 90% 的光栅线程 CPU 时间从大约 110 毫秒改进到 22 毫秒。
|
||||
|
||||
# Android
|
||||
|
||||
## Deeplinking web 验证器
|
||||
|
||||
3.19 开始,Flutter 的 Deeplinking web 验证器的早期版本将被推出使用。
|
||||
|
||||
在该版本中,Flutter Deeplinking 验证器支持 Android 上的 Web 检查,这意味着可以验证 assetlinks.json 文件的设置。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image7.png)
|
||||
|
||||
开发者可以打开DevTools,单击 “Deep Links” 选项,然后导入包含 Deeplinking 的 Flutter 项目,Deeplinking 验证器将告诉你配置是否正确。
|
||||
|
||||
Flutter 希望这个工具能够成为简化的 Deeplinking ,后续将继续补全 iOS 上的 Web 检查以及 iOS 和 Android 上的应用检查的支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image8.png)
|
||||
|
||||
> 更多可以查阅 :https://docs.google.com/document/d/1fnWe8EpZleMtSmP0rFm2iulqS3-gA86z8u9IsnXjJak/edit
|
||||
|
||||
## 支持 Share.invoke
|
||||
|
||||
Android 平台上 Flutter 之前缺少默认 “share” 按钮,而本次 3.19 里将开始支持它,作为 Flutter 持续努力的一部分,以确保所有默认上下文菜单按钮在每个平台上都可用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image9.png)
|
||||
|
||||
> 更多相关进度可见:https://github.com/flutter/flutter/issues/107578
|
||||
|
||||
## Native assets
|
||||
|
||||
如果需要 Flutter 代码中与其他语言的其他函数进行互操作,现在可以在 Android 上通过执行 FFI 来处理 Native assets 。
|
||||
|
||||
简单来说就是,在此之前, Dart interop 一直在全面支持与 [Java 和 Kotlin](https://link.juejin.cn/?target=https%3A%2F%2Fdart.dev%2Fguides%2Flibraries%2Fjava-interop) 和 [Objective C 和 Swift](https://link.juejin.cn/?target=https%3A%2F%2Fdart.dev%2Fguides%2Flibraries%2Fobjective-c-interop) 的直接调用支持,例如在 Dart 3.2 开始,Native assets 就作为实验性测试支持,一直在解决与依赖于 Native 代码的 Dart 包分发相关的许多问题,它通过提供统一的钩子来与构建 Flutter 和独立 Dart 应用所涉及的各种构建需要。
|
||||
|
||||
Native Assets 可以让 Dart 包更无缝依赖和使用 Native 代码,通过 `flutter run`/`flutter build `和 `dart run`/`dart build` 构建并捆绑 Native 代码 。
|
||||
|
||||
> 备注:可通过 `flutter config --enable-native-assets` 和 `flutter create --template=package_ffi [package name]` 启用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image10.png)
|
||||
|
||||
Demo [`native_add_library`](https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/native_add_library) 就展示了相关使用,当 Flutter 项目依赖 `package:native_add_library` 时, 脚本会自动在 `build.dart` 命令上调用:
|
||||
|
||||
```dart
|
||||
import 'package:native_add_library/native_add_library.dart';
|
||||
|
||||
void main() {
|
||||
print('Invoking a native function to calculate 1 + 2.');
|
||||
final result = add(1, 2);
|
||||
print('Invocation success: 1 + 2 = $result.');
|
||||
}
|
||||
```
|
||||
|
||||
> 更多可见:https://github.com/flutter/flutter/issues/129757
|
||||
|
||||
## 纹理层混合合成 (THLC) 模式
|
||||
|
||||
现在使 Google 地图 SDK 和文本输入框的放大镜功能时,他们都是工作在 TLHC 模式下,这会让 App 的性能得到不错的提升。
|
||||
|
||||
## 自定义 system-wide text selection toolbar 按键
|
||||
|
||||
Android 应用可以添加出现在所有文本选择菜单(长按文本时出现的菜单)中的自定义文本选择菜单项, Flutter 的 TextField 选择菜单现在包含了这些项目。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image11.png)
|
||||
|
||||
> 在 Android 上,一般可以编写一个应用,将自定义按钮添加到系统范围的文本选择工具栏上。例如上图这里 Android 应用 AnkiDroid 在文本选择工具栏中添加了 “Anki Card ”按钮,并且它可以出现在任何应用中。
|
||||
|
||||
# iOS
|
||||
|
||||
## Flutter iOS 原生字体
|
||||
|
||||
Flutter 文本现在在 iOS 上看起来更紧凑、更像 native,因为根据苹果设计指南,iOS 上较小的字体应该更加分散,以便在移动设备上更容易阅读,而较大的字体应该更加紧凑,以免占用太多空间。
|
||||
|
||||
在此之前,我们在所有情况下都错误地使用了更小、间距更大的字体,现在默认情况下 Flutter 将为较大的文本使用紧凑字体。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image12.png)
|
||||
|
||||
# 开发工具
|
||||
|
||||
## 开发工具更新
|
||||
|
||||
3.19 版本的 DevTools 的一些亮点包括:
|
||||
|
||||
● 在 DevTools 中添加了新功能以进行验证 Deeplinking
|
||||
|
||||
● 在 “Enhance Tracing” 菜单中添加了一个选项,用于跟踪平台 channel activity,这对于带有插件的应用很有用
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image13.jpg)
|
||||
|
||||
● 当没有连接的应用时,性能和 CPU 分析器现在也可以使用,可以重新加载之前从 DevTools 保存的性能数据或 CPU 配置文件
|
||||
|
||||
● VS Code 中的 Flutter 侧边栏现在能够在当前项目未启用的情况下启用新平台,并且侧边栏中的 DevTools 菜单现在有一个在外部浏览器窗口中使用 DevTools 的选项
|
||||
|
||||
|
||||
|
||||
# 桌面
|
||||
|
||||
## Windows Arm64 支持
|
||||
|
||||
Windows 上的 Flutter 现在开始初步支持 Arm64 架构,目前仍处于开发阶段, 可以在GitHub [#62597 ](https://github.com/flutter/flutter/issues/62597)上查看进度,虽然目前对于 Flutter 开发者来说可能用处不是特别明显,但是也算是一个难得的桌面增强。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 生态系统
|
||||
|
||||
## 隐私清单
|
||||
|
||||
**Flutter 现在包含 iOS 上的隐私清单以满足[即将推出的 Apple 要求](https://juejin.cn/post/7311876701909549065) ,所以看来一般情况下,这个 Flutter 3.19 非升不可**。
|
||||
|
||||
## 包生态的进展
|
||||
|
||||
2023 年,pub package 生态增长了 26%,从 1 月份的 38,000 个 package 增加到 12 月底的 48,000 个,截至 2024 年 1 月,Pub.dev 现在每月活跃用户超过 700,000 名,
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image14.png)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 弃用和重大变更
|
||||
|
||||
## 放弃 Windows 7 和 8 支持
|
||||
|
||||
Dart 3.3 和 Flutter 3.19 版本停止对 Windows 7 和 8 的支持
|
||||
|
||||
## Impeller Dithering flag
|
||||
|
||||
正如 3.16 发布时所说的,现在全局标志 `Paint.enableDithering` 已经[删除](https://github.com/flutter/engine/pull/46745)。
|
||||
|
||||
## 弃用 iOS 11
|
||||
|
||||
Flutter 不再支持 iOS 11,由于调用某些网络 API 时会发生[运行时崩溃](https://github.com/flutter/flutter/issues/136060), Flutter 3.16.6 及更高版本构建的应用将不再支持 iOS11 ,详细可见 :https://juejin.cn/post/7321410906427359258。
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
目前看来 Flutter 3.19 并没有什么大更新,属于季度正常迭代,主要是问题修复和性能高优化的版本,还是老规矩,坐等 3.19.6 版本。
|
||||
|
||||
最后,新年快乐~准备开工咯。
|
215
Flutter-330.md
215
Flutter-330.md
|
@ -1,215 +0,0 @@
|
|||
> 原文链接: https://medium.com/flutter/whats-new-in-flutter-3-3-893c7b9af1ff
|
||||
|
||||
**Flutter 3.3 正式发布啦,本次更新带来了 Flutter Web、桌面、文本性能处理等相关更新,另外,本次还为 `go_router` 、DevTools 和 VS Code 扩展引入了更多更新**。
|
||||
|
||||
# Framework
|
||||
|
||||
|
||||
## Global Selection
|
||||
|
||||
Flutter Web 在之前的版本中,经常会有选择文本时与预期的行为不匹配的情况,因为与 Flutter App 一样,原生 Web 是由 elements 树组成。
|
||||
|
||||
在传统的 Web 应用中,开发者可以通过一个拖动手势选择多个 Web 元素,但这在 Flutter Web 上无法轻松完成。
|
||||
|
||||
**但是从 3.3 开始,随着`SelectableArea` 的引入, `SelectableArea` Widget 的任何 Child 都可以自由启用改能力**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image1)
|
||||
|
||||
要利用这个强大的新特性,只需使用 `SelectionArea` 嵌套你的页面,比如路由下的 `Scaffold`,然后让 Flutter 就会完成剩下的工作。
|
||||
|
||||
> 要更全面地深入了解这个新功能,请访问 `SelectableArea` [API](https://api.flutter.dev/flutter/material/SelectionArea-class.html)
|
||||
|
||||
|
||||
## 触控板输入
|
||||
|
||||
**Flutter 3.3 改进了对触控板输入的支持**,这不仅提供了更丰富和更流畅的控制逻辑,还减少了某些情况下的错误识别。
|
||||
|
||||
举个例子,在[ Flutter cookbook ](https://docs.flutter.dev/cookbook) 中[拖动 UI 元素](https://docs.flutter.dev/cookbook/effects/drag-a-widget)页面,滚动到页面底部,然后执行以下步骤:
|
||||
|
||||
- 1. 缩小窗口大小,使上部呈现滚动条
|
||||
- 2. 悬停在上部
|
||||
- 3. 使用触控板滚动
|
||||
- 4. 在 Flutter 3.3 之前,在触控板上滚动会拖动项目,因为 Flutter 正在调度模拟的一般事件
|
||||
- 5. Flutter 3.3 后,在触控板上滚动会正确滚动列表,因为 Flutter 提供的是“滚动”手势,卡片无法识别,但滚动可以被识别。
|
||||
|
||||
有关更多信息,请参阅 [Flutter 触控板手势](https://docs.google.com/document/d/1oRvebwjpsC3KlxN1gOYnEdxtNpQDYpPtUFAkmTUe-K8/edit?resourcekey=0-pt4_T7uggSTrsq2gWeGsYQ) 设计文档,以及 GitHub 上的以下 PR:
|
||||
|
||||
- PR 89944:[在框架中支持触控板手势](https://github.com/flutter/flutter/pull/89944)
|
||||
- PR 31591:[iPad 触控板手势](https://github.com/flutter/engine/pull/31591)
|
||||
- PR 34060:[“ChromeOS/Android 触控板手势”](https://github.com/flutter/engine/pull/34060)
|
||||
- PR 31594:[Win32 触控板手势](https://github.com/flutter/engine/pull/31594)
|
||||
- PR 31592:[Linux 触控板手势](https://github.com/flutter/engine/pull/31592)
|
||||
- PR 31593:[Mac 触控板手势macOS](https://github.com/flutter/engine/pull/31593)
|
||||
|
||||
## Scribble
|
||||
|
||||
感谢社区成员[fbcouch](https://github.com/fbcouch)的贡献,Flutter 现在支持在 iPadOS 上使用 Apple Pencil 进行 [Scribble](https://support.apple.com/guide/ipad/enter-text-with-scribble-ipad355ab2a7/ipados) 手写输入。
|
||||
|
||||
**默认情况下,此功能在 `CupertinoTextField`、`TextField` 和 `EditableText` 上启用,启用此功能,只需升级到 Flutter 3.3**。
|
||||
|
||||
|
||||
![0_SlsnQUfdOTijdsyF.gif](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image2)
|
||||
|
||||
|
||||
## Text input
|
||||
|
||||
为了改进对富文本编辑的支持,该版本引入了平台的 ` TextInputPlugin`,**以前,`TextInputClient` 只交付新的编辑状态,没有新旧之间的差异信息,而 `TextEditingDeltas` 填补了 `DeltaTextInputClient` 这个信息空白**。
|
||||
|
||||
通过访问这些增量,开发者可以构建一个带有样式范围的输入字段,该范围在用户键入时会扩展和收缩。
|
||||
|
||||
> 要了解更多信息,请查看[富文本编辑器演示](https://flutter.github.io/samples/rich_text_editor.html)。
|
||||
|
||||
|
||||
# Material Design 3
|
||||
|
||||
Flutter 团队继续将更多 Material Design 3 组件迁移到 Flutter。此版本包括对`IconButton`、`Chips`以及`AppBar`.
|
||||
|
||||
要监控 Material Design 3 迁移的进度,请查看GitHub 上的[将 Material 3 带到 Flutter](https://github.com/flutter/flutter/issues/91605)。
|
||||
|
||||
|
||||
## 图标按钮
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image3)
|
||||
|
||||
## Chip
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image4)
|
||||
|
||||
## Medium and large AppBar
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image5)
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image6)
|
||||
|
||||
|
||||
|
||||
# Desktop
|
||||
|
||||
## Windows
|
||||
|
||||
|
||||
以前,Windows 的版本由特定于 Windows 应用的文件设置,但这个行为与其他平台设置其版本的方式不一致。
|
||||
|
||||
**但现在开发者可以在项目 `pubspec.yaml` 文件和构建参数中设置 Windows 桌面应用程序版本**。
|
||||
|
||||
> 有关设置应用程序版本的更多信息,请遵循 [docs.flutter.dev](https://docs.flutter.dev/deployment/windows#updating-the-apps-version-number)上的文档和 [迁移指南](https://docs.flutter.dev/development/platform-integration/windows/version-migration)
|
||||
|
||||
# Packages
|
||||
|
||||
## go_router
|
||||
|
||||
为了扩展 Flutter 的原生导航 API,团队发布了一个新版本的 `go_router` 包,它的设计使得移动端、桌面端和 Web 端的路由逻辑变得更加简单。
|
||||
|
||||
**`go router`包由 Flutter 团队维护,通过提供声明性的、基于 url 的 API 来简化路由**,从而更容易导航和处理深层链接。
|
||||
|
||||
> 最新版本 (5.0) 下应用能够使用异步代码进行重定向,并包括[迁移指南](https://docs.google.com/document/d/10l22o4ml4Ss83UyzqUC8_xYOv_QjZEi80lJDNE4q7wM/edit?usp=sharing&resourcekey=0-U-BXBQzNfkk4v241Ow-vZg)中描述的其他重大更改.有关更多信息,请查看 docs.flutter.dev 上的[导航和路由](https://docs.flutter.dev/development/ui/navigation)页面。
|
||||
|
||||
# VS Code 扩展增强
|
||||
|
||||
Flutter 的 Visual Studio Code 扩展有几个更新,包括添加依赖项的改进,**开发者现在可以使用Dart: Add Dependency**一步添加多个以逗号分隔的依赖项。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image7)
|
||||
|
||||
# Flutter 开发者工具更新
|
||||
|
||||
自上一个稳定的 Flutter 版本以来,DevTools 进行了许多更新,包括对数据显示表的 UX 和性能改进,以便更快、更少地滚动大型事件列表 ( [#4175](https://github.com/flutter/devtools/pull/4175) )。
|
||||
|
||||
有关 Flutter 3.0 以来更新的完整列表,请在此处查看各个公告:
|
||||
|
||||
- [Flutter DevTools 2.16.0 发布说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.16.0)
|
||||
- [Flutter DevTools 2.15.0 发行说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.15.0)
|
||||
- [Flutter DevTools 2.14.0 发布说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.14.0)
|
||||
|
||||
|
||||
# Performance
|
||||
|
||||
|
||||
## 光栅缓存改进
|
||||
|
||||
**此版本通过消除拷贝和减少 Dart 垃圾收集 (GC) 压力来提高从资产加载图像的性能**。
|
||||
|
||||
以前在加载资产图像时,`ImageProvider` API 需要多次复制压缩数据,当打开 assets 并将其作为类型化数据数组公开给 Dart 时,它会被复制到 native 堆中,然后当该类型化数据数组会被它被第二次复制到内部 `ui.ImmutableBuffer`。
|
||||
|
||||
通过 [#32999](https://github.com/flutter/engine/pull/32999),压缩的图像字节可以直接加载到`ui.ImmutableBuffer.fromAsset`用于解码的结构中,这种方法 [需要](https://github.com/flutter/flutter/pull/103496) 更改`ImageProviders`,这个过程也更快,因为它绕过了先前方法基于通道的加载器所需的一些额外的调度开销,**特别是在我们的微基准测试中,图像加载时间提高了近 2 倍**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image8)
|
||||
|
||||
> 有关更多信息和迁移指南,请参阅在 docs.flutter.dev 上[ImageProvider.loadBuffer 。](https://docs.flutter.dev/release/breaking-changes/image-provider-load-buffer)
|
||||
|
||||
|
||||
# Stability
|
||||
|
||||
## iOS 指针压缩已禁用
|
||||
|
||||
在 2.10 稳定版本中,我们在 iOS 上启用了 Dart 的[指针压缩优化](https://medium.com/dartlang/dart-2-15-7e7a598e508a#0c15),然而 GitHub 上的[Yeatse](https://github.com/Yeatse)[提醒我们](https://github.com/flutter/flutter/issues/105183) 优化的结果并不好。
|
||||
|
||||
Dart 的指针压缩通过为 Dart 的堆保留一个大的虚拟内存区域来工作,由于 iOS 上允许的总虚拟内存分配少于其他平台,因此这一大预留量减少了可供其他保留自己内存的组件使用的内存量,例如 Flutter 插件。
|
||||
|
||||
**虽然禁用指针压缩会增加 Dart 对象消耗的内存,但它也增加了 Flutter 应用程序的非 Dart 部分的可用内存,这总体上更可取的方向**。
|
||||
|
||||
Apple 提供了一项可以增加应用程序允许的最大虚拟内存分配的权利,但是此权利仅在较新的 iOS 版本上受支持,目前这并且不适用于运行 Flutter 仍支持的 iOS 版本的设备。
|
||||
|
||||
# API 改进
|
||||
|
||||
## PlatformDispatcher.onError
|
||||
|
||||
在以前的版本中,开发者必须手动配置自定义 `Zone` 项才能捕获应用程序的所有异常和错误,但是自定义 `Zone` 对 Dart 核心库中的一些优化是有害的,这会减慢应用程序的启动时间。
|
||||
|
||||
**在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。**
|
||||
|
||||
> 有关更多信息,请查看docs.flutter.dev 上 Flutter 页面中更新的 [PlatformDispatcher.onError](https://docs.flutter.dev/testing/errors)
|
||||
|
||||
|
||||
## FragmentProgram changes
|
||||
|
||||
用 GLSL 编写并在 `shaders:` 应用文件的 Flutter 清单中列出的片段着色器,`pubspec.yaml` 现在将自动编译为引擎可以理解的正确格式,并作为 assets 与应用捆绑在一起。
|
||||
|
||||
通过此次更改,开发者将不再需要使用第三方工具手动编译着色器,未来应该是将 Engine 的`FragmentProgram` API 视为仅接受 Flutter 构建工具的输出,当然目前还没有这种情况,但计划在未来的版本中进行此更改,如 [FragmentProgram API 支持改进](http://flutter.dev/go/fragment-program-support)设计文档中所述。
|
||||
|
||||
> 有关此更改的示例,请参阅此[Flutter 着色器示例](https://github.com/zanderso/fragment_shader_example)。
|
||||
|
||||
|
||||
## Fractional translation
|
||||
|
||||
以前,Flutter Engine 总是将 composited layers 与精确的像素边界对齐,因为它提高了旧款(32 位)iPhone 的渲染性能。
|
||||
|
||||
自从添加桌面支持以来,我们注意到这导致了可观察到的捕捉行为,因为屏幕设备像素比通常要低得多,例如,在低 DPR 屏幕上,可以看到工具提示在淡入时明显捕捉。
|
||||
|
||||
在确定这种像素捕捉对于新 iPhone 型号的性能不再必要后,[#103909](https://github.com/flutter/flutter/issues/103909) 从 Flutter 引擎中删除了这种像素捕捉以提高桌面保真度。
|
||||
|
||||
此外,我们还发现,去除这种像素捕捉可以稳定我们的一些黄金图像测试,这些测试会经常随着细微的细线渲染差异而改变。
|
||||
|
||||
|
||||
# 对支持平台的更改
|
||||
|
||||
## 32 位 iOS 弃用
|
||||
|
||||
正如我们之前在3.0 版本里宣布的一样 ,由于使用量减少,该版本是[最后一个支持 32 位 iOS 设备和 iOS 版本 9 和 10](http://flutter.dev/go/rfc-32-bit-ios-unsupported)的版本。
|
||||
|
||||
此更改影响 iPhone 4S、iPhone 5、iPhone 5C 以及第 2、3d 和第 4 代 iPad 设备。
|
||||
|
||||
Flutter 3.3 稳定版本和所有后续稳定版本不再支持 32 位 iOS 设备以及 iOS 9 和 10 版本,这意味着基于 Flutter 3.3 及更高版本构建的应用程序将无法在这些设备上运行。
|
||||
|
||||
## 停用 macOS 10.11 和 10.12
|
||||
|
||||
在 2022 年第四季度稳定版本中,我们预计将放弃对 macOS 版本 10.11 和 10.12 的支持。
|
||||
|
||||
这意味着在那之后针对稳定的 Flutter SDK 构建的应用程序将不再在这些版本上运行,并且 Flutter 支持的最低 macOS 版本将增加到 10.13 High Sierra。
|
||||
|
||||
|
||||
## Bitcode deprecation
|
||||
|
||||
|
||||
[在即将发布的 Xcode 14 版本中,iOS 应用程序提交将不再接受](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes) Bitcode ,并且启用了 bitcode 的项目将在此版本的 Xcode 中发出构建警告。鉴于此,Flutter 将在未来的稳定版本中放弃对位码的支持。
|
||||
|
||||
默认情况下,Flutter 应用程序没有启用 Bitcode,我们预计这不会影响许多开发人员。
|
||||
|
||||
但是,如果你在 Xcode 项目中手动启用了 bitcode,请在升级到 Xcode 14 后立即禁用它,可以通过打开 `ios/Runner.xcworkspace` 构建设置**Enable Bitcode**并将其设置为**No**来做到这一点,Add-to-app 开发者应该在宿主 Xcode 项目中禁用它。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220831_Flutter-330/image9)
|
|
@ -1,215 +0,0 @@
|
|||
> 原文链接: https://medium.com/flutter/whats-new-in-flutter-3-3-893c7b9af1ff
|
||||
|
||||
**Flutter 3.3 正式发布啦,本次更新带来了 Flutter Web、桌面、文本性能处理等相关更新,另外,本次还为 `go_router` 、DevTools 和 VS Code 扩展引入了更多更新**。
|
||||
|
||||
# Framework
|
||||
|
||||
|
||||
## Global Selection
|
||||
|
||||
Flutter Web 在之前的版本中,经常会有选择文本时与预期的行为不匹配的情况,因为与 Flutter App 一样,原生 Web 是由 elements 树组成。
|
||||
|
||||
在传统的 Web 应用中,开发者可以通过一个拖动手势选择多个 Web 元素,但这在 Flutter Web 上无法轻松完成。
|
||||
|
||||
**但是从 3.3 开始,随着`SelectableArea` 的引入, `SelectableArea` Widget 的任何 Child 都可以自由启用改能力**。
|
||||
|
||||
![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57f5252fa6b14a18b2aa9ffaa9849889~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
要利用这个强大的新特性,只需使用 `SelectionArea` 嵌套你的页面,比如路由下的 `Scaffold`,然后让 Flutter 就会完成剩下的工作。
|
||||
|
||||
> 要更全面地深入了解这个新功能,请访问 `SelectableArea` [API](https://api.flutter.dev/flutter/material/SelectionArea-class.html)
|
||||
|
||||
|
||||
## 触控板输入
|
||||
|
||||
**Flutter 3.3 改进了对触控板输入的支持**,这不仅提供了更丰富和更流畅的控制逻辑,还减少了某些情况下的错误识别。
|
||||
|
||||
举个例子,在[ Flutter cookbook ](https://docs.flutter.dev/cookbook) 中[拖动 UI 元素](https://docs.flutter.dev/cookbook/effects/drag-a-widget)页面,滚动到页面底部,然后执行以下步骤:
|
||||
|
||||
- 1. 缩小窗口大小,使上部呈现滚动条
|
||||
- 2. 悬停在上部
|
||||
- 3. 使用触控板滚动
|
||||
- 4. 在 Flutter 3.3 之前,在触控板上滚动会拖动项目,因为 Flutter 正在调度模拟的一般事件
|
||||
- 5. Flutter 3.3 后,在触控板上滚动会正确滚动列表,因为 Flutter 提供的是“滚动”手势,卡片无法识别,但滚动可以被识别。
|
||||
|
||||
有关更多信息,请参阅 [Flutter 触控板手势](https://docs.google.com/document/d/1oRvebwjpsC3KlxN1gOYnEdxtNpQDYpPtUFAkmTUe-K8/edit?resourcekey=0-pt4_T7uggSTrsq2gWeGsYQ) 设计文档,以及 GitHub 上的以下 PR:
|
||||
|
||||
- PR 89944:[在框架中支持触控板手势](https://github.com/flutter/flutter/pull/89944)
|
||||
- PR 31591:[iPad 触控板手势](https://github.com/flutter/engine/pull/31591)
|
||||
- PR 34060:[“ChromeOS/Android 触控板手势”](https://github.com/flutter/engine/pull/34060)
|
||||
- PR 31594:[Win32 触控板手势](https://github.com/flutter/engine/pull/31594)
|
||||
- PR 31592:[Linux 触控板手势](https://github.com/flutter/engine/pull/31592)
|
||||
- PR 31593:[Mac 触控板手势macOS](https://github.com/flutter/engine/pull/31593)
|
||||
|
||||
## Scribble
|
||||
|
||||
感谢社区成员[fbcouch](https://github.com/fbcouch)的贡献,Flutter 现在支持在 iPadOS 上使用 Apple Pencil 进行 [Scribble](https://support.apple.com/guide/ipad/enter-text-with-scribble-ipad355ab2a7/ipados) 手写输入。
|
||||
|
||||
**默认情况下,此功能在 `CupertinoTextField`、`TextField` 和 `EditableText` 上启用,启用此功能,只需升级到 Flutter 3.3**。
|
||||
|
||||
|
||||
![0_SlsnQUfdOTijdsyF.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7f945f93d21c414ab3fd1dedd7f1a2f3~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
|
||||
## Text input
|
||||
|
||||
为了改进对富文本编辑的支持,该版本引入了平台的 ` TextInputPlugin`,**以前,`TextInputClient` 只交付新的编辑状态,没有新旧之间的差异信息,而 `TextEditingDeltas` 填补了 `DeltaTextInputClient` 这个信息空白**。
|
||||
|
||||
通过访问这些增量,开发者可以构建一个带有样式范围的输入字段,该范围在用户键入时会扩展和收缩。
|
||||
|
||||
> 要了解更多信息,请查看[富文本编辑器演示](https://flutter.github.io/samples/rich_text_editor.html)。
|
||||
|
||||
|
||||
# Material Design 3
|
||||
|
||||
Flutter 团队继续将更多 Material Design 3 组件迁移到 Flutter。此版本包括对`IconButton`、`Chips`以及`AppBar`.
|
||||
|
||||
要监控 Material Design 3 迁移的进度,请查看GitHub 上的[将 Material 3 带到 Flutter](https://github.com/flutter/flutter/issues/91605)。
|
||||
|
||||
|
||||
## 图标按钮
|
||||
|
||||
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4cb96d60d0534254b29a7b2eb66419a4~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
## Chip
|
||||
|
||||
|
||||
![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b385a3d3e214a4181071ee6b0d07eb0~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
## Medium and large AppBar
|
||||
|
||||
|
||||
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51504518e30b4de1b66e2b2222fc9b1b~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
|
||||
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ecffcefdb0ac4772b935e4302ca3525b~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
|
||||
|
||||
# Desktop
|
||||
|
||||
## Windows
|
||||
|
||||
|
||||
以前,Windows 的版本由特定于 Windows 应用的文件设置,但这个行为与其他平台设置其版本的方式不一致。
|
||||
|
||||
**但现在开发者可以在项目 `pubspec.yaml` 文件和构建参数中设置 Windows 桌面应用程序版本**。
|
||||
|
||||
> 有关设置应用程序版本的更多信息,请遵循 [docs.flutter.dev](https://docs.flutter.dev/deployment/windows#updating-the-apps-version-number)上的文档和 [迁移指南](https://docs.flutter.dev/development/platform-integration/windows/version-migration)
|
||||
|
||||
# Packages
|
||||
|
||||
## go_router
|
||||
|
||||
为了扩展 Flutter 的原生导航 API,团队发布了一个新版本的 `go_router` 包,它的设计使得移动端、桌面端和 Web 端的路由逻辑变得更加简单。
|
||||
|
||||
**`go router`包由 Flutter 团队维护,通过提供声明性的、基于 url 的 API 来简化路由**,从而更容易导航和处理深层链接。
|
||||
|
||||
> 最新版本 (5.0) 下应用能够使用异步代码进行重定向,并包括[迁移指南](https://docs.google.com/document/d/10l22o4ml4Ss83UyzqUC8_xYOv_QjZEi80lJDNE4q7wM/edit?usp=sharing&resourcekey=0-U-BXBQzNfkk4v241Ow-vZg)中描述的其他重大更改.有关更多信息,请查看 docs.flutter.dev 上的[导航和路由](https://docs.flutter.dev/development/ui/navigation)页面。
|
||||
|
||||
# VS Code 扩展增强
|
||||
|
||||
Flutter 的 Visual Studio Code 扩展有几个更新,包括添加依赖项的改进,**开发者现在可以使用Dart: Add Dependency**一步添加多个以逗号分隔的依赖项。
|
||||
|
||||
|
||||
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5957872a2f114262b66a099b6bae7cdf~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
# Flutter 开发者工具更新
|
||||
|
||||
自上一个稳定的 Flutter 版本以来,DevTools 进行了许多更新,包括对数据显示表的 UX 和性能改进,以便更快、更少地滚动大型事件列表 ( [#4175](https://github.com/flutter/devtools/pull/4175) )。
|
||||
|
||||
有关 Flutter 3.0 以来更新的完整列表,请在此处查看各个公告:
|
||||
|
||||
- [Flutter DevTools 2.16.0 发布说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.16.0)
|
||||
- [Flutter DevTools 2.15.0 发行说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.15.0)
|
||||
- [Flutter DevTools 2.14.0 发布说明](https://docs.flutter.dev/development/tools/devtools/release-notes/release-notes-2.14.0)
|
||||
|
||||
|
||||
# Performance
|
||||
|
||||
|
||||
## 光栅缓存改进
|
||||
|
||||
**此版本通过消除拷贝和减少 Dart 垃圾收集 (GC) 压力来提高从资产加载图像的性能**。
|
||||
|
||||
以前在加载资产图像时,`ImageProvider` API 需要多次复制压缩数据,当打开 assets 并将其作为类型化数据数组公开给 Dart 时,它会被复制到 native 堆中,然后当该类型化数据数组会被它被第二次复制到内部 `ui.ImmutableBuffer`。
|
||||
|
||||
通过 [#32999](https://github.com/flutter/engine/pull/32999),压缩的图像字节可以直接加载到`ui.ImmutableBuffer.fromAsset`用于解码的结构中,这种方法 [需要](https://github.com/flutter/flutter/pull/103496) 更改`ImageProviders`,这个过程也更快,因为它绕过了先前方法基于通道的加载器所需的一些额外的调度开销,**特别是在我们的微基准测试中,图像加载时间提高了近 2 倍**。
|
||||
|
||||
|
||||
![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c447c57cf4b14809a78fd1117044100a~tplv-k3u1fbpfcp-watermark.image?)
|
||||
|
||||
> 有关更多信息和迁移指南,请参阅在 docs.flutter.dev 上[ImageProvider.loadBuffer 。](https://docs.flutter.dev/release/breaking-changes/image-provider-load-buffer)
|
||||
|
||||
|
||||
# Stability
|
||||
|
||||
## iOS 指针压缩已禁用
|
||||
|
||||
在 2.10 稳定版本中,我们在 iOS 上启用了 Dart 的[指针压缩优化](https://medium.com/dartlang/dart-2-15-7e7a598e508a#0c15),然而 GitHub 上的[Yeatse](https://github.com/Yeatse)[提醒我们](https://github.com/flutter/flutter/issues/105183) 优化的结果并不好。
|
||||
|
||||
Dart 的指针压缩通过为 Dart 的堆保留一个大的虚拟内存区域来工作,由于 iOS 上允许的总虚拟内存分配少于其他平台,因此这一大预留量减少了可供其他保留自己内存的组件使用的内存量,例如 Flutter 插件。
|
||||
|
||||
**虽然禁用指针压缩会增加 Dart 对象消耗的内存,但它也增加了 Flutter 应用程序的非 Dart 部分的可用内存,这总体上更可取的方向**。
|
||||
|
||||
Apple 提供了一项可以增加应用程序允许的最大虚拟内存分配的权利,但是此权利仅在较新的 iOS 版本上受支持,目前这并且不适用于运行 Flutter 仍支持的 iOS 版本的设备。
|
||||
|
||||
# API 改进
|
||||
|
||||
## PlatformDispatcher.onError
|
||||
|
||||
在以前的版本中,开发者必须手动配置自定义 `Zone` 项才能捕获应用程序的所有异常和错误,但是自定义 `Zone` 对 Dart 核心库中的一些优化是有害的,这会减慢应用程序的启动时间。
|
||||
|
||||
**在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。**
|
||||
|
||||
> 有关更多信息,请查看docs.flutter.dev 上 Flutter 页面中更新的 [PlatformDispatcher.onError](https://docs.flutter.dev/testing/errors)
|
||||
|
||||
|
||||
## FragmentProgram changes
|
||||
|
||||
用 GLSL 编写并在 `shaders:` 应用文件的 Flutter 清单中列出的片段着色器,`pubspec.yaml` 现在将自动编译为引擎可以理解的正确格式,并作为 assets 与应用捆绑在一起。
|
||||
|
||||
通过此次更改,开发者将不再需要使用第三方工具手动编译着色器,未来应该是将 Engine 的`FragmentProgram` API 视为仅接受 Flutter 构建工具的输出,当然目前还没有这种情况,但计划在未来的版本中进行此更改,如 [FragmentProgram API 支持改进](http://flutter.dev/go/fragment-program-support)设计文档中所述。
|
||||
|
||||
> 有关此更改的示例,请参阅此[Flutter 着色器示例](https://github.com/zanderso/fragment_shader_example)。
|
||||
|
||||
|
||||
## Fractional translation
|
||||
|
||||
以前,Flutter Engine 总是将 composited layers 与精确的像素边界对齐,因为它提高了旧款(32 位)iPhone 的渲染性能。
|
||||
|
||||
自从添加桌面支持以来,我们注意到这导致了可观察到的捕捉行为,因为屏幕设备像素比通常要低得多,例如,在低 DPR 屏幕上,可以看到工具提示在淡入时明显捕捉。
|
||||
|
||||
在确定这种像素捕捉对于新 iPhone 型号的性能不再必要后,[#103909](https://github.com/flutter/flutter/issues/103909) 从 Flutter 引擎中删除了这种像素捕捉以提高桌面保真度。
|
||||
|
||||
此外,我们还发现,去除这种像素捕捉可以稳定我们的一些黄金图像测试,这些测试会经常随着细微的细线渲染差异而改变。
|
||||
|
||||
|
||||
# 对支持平台的更改
|
||||
|
||||
## 32 位 iOS 弃用
|
||||
|
||||
正如我们之前在3.0 版本里宣布的一样 ,由于使用量减少,该版本是[最后一个支持 32 位 iOS 设备和 iOS 版本 9 和 10](http://flutter.dev/go/rfc-32-bit-ios-unsupported)的版本。
|
||||
|
||||
此更改影响 iPhone 4S、iPhone 5、iPhone 5C 以及第 2、3d 和第 4 代 iPad 设备。
|
||||
|
||||
Flutter 3.3 稳定版本和所有后续稳定版本不再支持 32 位 iOS 设备以及 iOS 9 和 10 版本,这意味着基于 Flutter 3.3 及更高版本构建的应用程序将无法在这些设备上运行。
|
||||
|
||||
## 停用 macOS 10.11 和 10.12
|
||||
|
||||
在 2022 年第四季度稳定版本中,我们预计将放弃对 macOS 版本 10.11 和 10.12 的支持。
|
||||
|
||||
这意味着在那之后针对稳定的 Flutter SDK 构建的应用程序将不再在这些版本上运行,并且 Flutter 支持的最低 macOS 版本将增加到 10.13 High Sierra。
|
||||
|
||||
|
||||
## Bitcode deprecation
|
||||
|
||||
|
||||
[在即将发布的 Xcode 14 版本中,iOS 应用程序提交将不再接受](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes) Bitcode ,并且启用了 bitcode 的项目将在此版本的 Xcode 中发出构建警告。鉴于此,Flutter 将在未来的稳定版本中放弃对位码的支持。
|
||||
|
||||
默认情况下,Flutter 应用程序没有启用 Bitcode,我们预计这不会影响许多开发人员。
|
||||
|
||||
但是,如果你在 Xcode 项目中手动启用了 bitcode,请在升级到 Xcode 14 后立即禁用它,可以通过打开 `ios/Runner.xcworkspace` 构建设置**Enable Bitcode**并将其设置为**No**来做到这一点,Add-to-app 开发者应该在宿主 Xcode 项目中禁用它。
|
||||
|
||||
|
||||
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12d3be467b014be6841dd22f4b6fa71a~tplv-k3u1fbpfcp-watermark.image?)
|
361
Flutter-370.md
361
Flutter-370.md
|
@ -1,361 +0,0 @@
|
|||
# 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 部分,所以按照惯例,等三个小版本会更稳。
|
36
Flutter-4.md
36
Flutter-4.md
|
@ -1,11 +1,10 @@
|
|||
|
||||
作为系列文章的第四篇,本篇主要介绍 Flutter 中 Redux 的使用,并结合Redux 完成实时的**主题切换**与**多语言切换**功能。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
>前文:
|
||||
>* [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
>* [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
>* [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
Flutter 作为**响应式框架**,通过 `state` 实现跨帧渲染的逻辑,难免让人与 *React* 和 *React Native* 联系起来,而其中 *React* 下*“广为人知”*的 **Redux 状态管理**,其实在 Flutter 中同样适用。
|
||||
|
||||
|
@ -19,11 +18,13 @@ Flutter 作为**响应式框架**,通过 `state` 实现跨帧渲染的逻辑
|
|||
|
||||
## 一、Redux
|
||||
|
||||
Redux 的概念是**状态管理**,那在已有 `state` 的基础上,为什么还需要 Redux ?因为使用 Redux 的好处是:**共享状态**和**单一数据**。
|
||||
Redux 的概念是**状态管理**,那在已有 `state` 的基础上,为什么还需要 Redux ?
|
||||
|
||||
因为使用 Redux 的好处是:**共享状态**和**单一数据**。
|
||||
|
||||
试想一下,App内有多个地方使用到登陆用户的数据,这时候如果某处对用户数据做了修改,各个页面的同步更新会是一件麻烦的事情。
|
||||
|
||||
但是引入 Redux 后,某个页面修改了当前用户信息,所有绑定了 Redux 的控件,将由 Redux 自动同步刷新。See!这在一定程度节省了我们的工作量,并且单一数据源在某些场景下也方便管理,同理我们后面所说的 *主题* 和 *多语言* 切换也是如此。
|
||||
但是引入 Redux 后,某个页面修改了当前用户信息,所有绑定了 Redux 的控件,将由 Redux 自动同步刷新。See!这在一定程度节省了我们的工作量,并且单一数据源在某些场景下也方便管理。同理我们后面所说的 *主题* 和 *多语言* 切换也是如此。
|
||||
|
||||
![大致流程图](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image2)
|
||||
|
||||
|
@ -43,7 +44,9 @@ Redux 的概念是**状态管理**,那在已有 `state` 的基础上,为什
|
|||
|
||||
4、更新 Store 中 state 绑定的 Widget。
|
||||
|
||||
根据这个流程,首先我们要创建一个 **Store** 。如下图,创建 Store 需要 `reducer` ,而 `reducer` 实际上是一个带有 `state` 和 `action` 的方法,并返回新的 State 。
|
||||
根据这个流程,首先我们要创建一个 **Store** 。
|
||||
|
||||
如下图,创建 Store 需要 `reducer` ,而 `reducer` 实际上是一个带有 `state` 和 `action` 的方法,并返回新的 State 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image3)
|
||||
|
||||
|
@ -424,17 +427,26 @@ class _GSYLocalizations extends State<GSYLocalizations> {
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : [https://github.com/CarGuo](https://github.com/CarGuo)
|
||||
* 本文代码 :[https://github.com/CarGuo/GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
##### 文章
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-4/image8)
|
55
Flutter-5.md
55
Flutter-5.md
|
@ -1,14 +1,20 @@
|
|||
作为系列文章的第五篇,本篇主要探索下 Flutter 中的一些有趣原理,帮助我们更好的去理解和开发。
|
||||
|
||||
## 文章汇总地址:
|
||||
>前文:
|
||||
>* [一、Dart语言和Flutter基础](https://xiaozhuanlan.com/topic/3269105784)
|
||||
>* [二、 快速开发实战篇](https://xiaozhuanlan.com/topic/1640729835)
|
||||
>* [三、 打包与填坑篇](https://xiaozhuanlan.com/topic/7938654021)
|
||||
>* [四、Redux、主题、国际化)](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
### 一、WidgetsFlutterBinding
|
||||
|
||||
*这是一个胶水类。*
|
||||
|
||||
#### 1、Mixins
|
||||
|
||||
混入其中( ̄. ̄)!,是的,Flutter 使用的是 Dart 支持 Mixin ,而 Mixin 能够更好的解决**多继承**中容易出现的问题,如:**方法优先顺序混乱、参数冲突、类结构变得复杂化**等等。
|
||||
混入其中( ̄. ̄)!
|
||||
|
||||
是的,Flutter 使用的是 Dart 支持 Mixin ,而 Mixin 能够更好的解决**多继承**中容易出现的问题,如:**方法优先顺序混乱、参数冲突、类结构变得复杂化**等等。
|
||||
|
||||
Mixin 的定义解释起来会比较绕,我们直接代码从中出吧。如下代码所示,在 Dart 中 `with` 就是用于 mixins。可以看出,`class G extends B with A, A2` ,在执行 G 的 a、b、c 方法后,输出了 `A2.a()、A.b() 、B.c()` 。所以结论上简单来说,就是**相同方法被覆盖了,并且 with 后面的会覆盖前面的**。
|
||||
|
||||
|
@ -304,8 +310,6 @@ PaintingBinding.instance.imageCache.maximumSizeBytes= 50 << 20;
|
|||
|
||||
其中 Android 的编译后二进制文件存在于 `data/data/包名/app_flutter/flutter_assets/`下。做过 Android 的应该知道,这个路径下是可以很简单更新的,所以你懂的  ̄ω ̄=。
|
||||
|
||||
> **⚠️注意,1.7.8 之后的版本,Android 下的 Flutter 已经编译为纯 so 文件。**
|
||||
|
||||
IOS?据我了解,貌似动态库 framework 等引用是不能用热更新的,除非你不需要审核!
|
||||
|
||||
|
||||
|
@ -314,16 +318,45 @@ IOS?据我了解,貌似动态库 framework 等引用是不能用热更新的
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : [https://github.com/CarGuo](https://github.com/CarGuo)
|
||||
* 本文代码 :[https://github.com/CarGuo/GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter)
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
##### 文章
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-5/image12)
|
||||
|
||||
|
||||
|
|
48
Flutter-6.md
48
Flutter-6.md
|
@ -1,10 +1,11 @@
|
|||
作为系列文章的第六篇,本篇主要在前文的探索下,针对描述一下 Widget 中的一些有意思的原理。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
>* [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
>* [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
>* [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
>* [四、Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
首先我们需要明白,Widget 是什么?这里有一个 *“总所周知”* 的答就是:**Widget并不真正的渲染对象** 。是的,事实上在 Flutter 中渲染是经历了从 `Widget` 到 `Element` 再到 `RenderObject` 的过程。
|
||||
|
||||
|
@ -59,14 +60,43 @@ RenderBox 避免了直接使用 `RenderObject` 的麻烦场景,其中 `Render
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
|
||||
##### 文章
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-6/image4)
|
53
Flutter-7.md
53
Flutter-7.md
|
@ -1,10 +1,12 @@
|
|||
作为系列文章的第七篇,本篇主要在前文的基础上,再深入了解 Widget 和布局中的一些常识性问题。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
|
||||
在第六篇中我们知道了 `Widget`、`Element`、`RenderObject` 三者之间的关系,其中我们最为熟知的 `Widget` ,作为“配置文件”的存在,在 Flutter 中它的功能都是比较单一的,属于 *“颗粒度比较细的存在”* ,写代码时就像拼乐高“积木”,那这“积木”究竟怎么拼的?下面就 **深入** 去挖挖有意思的东西吧。( ̄▽ ̄)
|
||||
|
@ -26,7 +28,7 @@
|
|||
|
||||
| Widget | RenderObject |
|
||||
| --------- | --------- |
|
||||
| ConstrainedBox | RenderConstrainedBox |
|
||||
| RenderConstrainedBox | RenderConstrainedBox |
|
||||
|
||||
![ConstrainedBox](http://img.cdn.guoshuyu.cn/20190604_Flutter-7/image2)
|
||||
|
||||
|
@ -40,7 +42,7 @@
|
|||
| Transform | RenderTransform |
|
||||
|Offstage|RenderOffstage|
|
||||
|
||||
所以我们可以总结:**真正的布局和大小计算等行为,都是在 `RenderBox` 上去实现的。** 不同的 Widget 通过各自的 `RenderBox ` 实现了“差异化”的布局效果。**所以找每个 Widget 的实现,找它的 `RenderBox ` 实现就可以了。**(当然,另外还有 `RenderSliver`,这里暂时不讨论)
|
||||
所以我们可以总结:**真正的布局和大小计算等行为,都是在 `RenderBox` 上去实现的。** 不同的 Widget 通过各自的 `RenderBox ` 实现了“差异化”的布局效果。**所以找每个 Widget 的实现,找它的 `RenderBox ` 实现就可以了。**
|
||||
|
||||
这里我们通过 **`Offstage`** 这个Widget 小结下,**`Offstage`** 这个 Widget 是通过 `offstage` 标志控制 **child** 是否显示的效果,同样的它也有一个 `RenderOffstage ` ,如下图,通过 `RenderOffstage ` 的源码我们可以“真实”看到 `offstage` 标志位的作用:
|
||||
|
||||
|
@ -156,6 +158,7 @@ CustomScrollView(
|
|||
)
|
||||
```
|
||||
|
||||
|
||||
-------
|
||||
|
||||
#### 不知道你看完本篇后,有没有对 Flutter 的布局有更深入的了解呢?*让我们愉悦的堆积木吧!*
|
||||
|
@ -164,10 +167,8 @@ CustomScrollView(
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -175,4 +176,34 @@ CustomScrollView(
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-7/image9)
|
50
Flutter-8.md
50
Flutter-8.md
|
@ -1,10 +1,13 @@
|
|||
作为系列文章的第八篇,本篇是主要讲述 Flutter 开发过程中的实用技巧,让你少走弯路少掉坑,全篇属于很干的干货总结,以实用为主,算是在深入原理过程中穿插的实用篇章。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
#### 1、Text 的 TextOverflow.ellipsis 不生效
|
||||
|
||||
|
@ -12,7 +15,7 @@
|
|||
|
||||
其实大部分时候,这是 `Text` 内部的 `RenderParagraph` 在判断 `final bool didOverflowWidth = size.width < textSize.width;` 时, *size.width* 和 *textSize.width* 是相等导致的。
|
||||
|
||||
所以你需要给 `Text` 设置一个 `Container` 之类的去约束它的大小,或者是 `Row` 中通过 `Expanded` + `Container` 去约束你的 `Text`,如果不知道于应该多大,可以通过 `LayoutBuilder` 设置。
|
||||
所以你需要给 `Text` 设置一个 `Container` 之类的去约束它的大小,或者是 `Row` 中通过 `Expanded` + `Container` 去约束你的 `Text`。
|
||||
|
||||
![请无视图片](http://img.cdn.guoshuyu.cn/20190604_Flutter-8/image1)
|
||||
|
||||
|
@ -137,10 +140,8 @@ Flutter 官方已经为你提供了 [android_intent](https://github.com/flutter/
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -148,6 +149,35 @@ Flutter 官方已经为你提供了 [android_intent](https://github.com/flutter/
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-8/image4)
|
50
Flutter-9.md
50
Flutter-9.md
|
@ -1,10 +1,14 @@
|
|||
作为系列文章的第九篇,本篇主要深入了解 Widget 中绘制相关的原理,探索 Flutter 里的 RenderObject 最后是如何走完屏幕上的最后一步,结尾再通过实际例子理解如何设计一个 Flutter 的自定义绘制。
|
||||
|
||||
## 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
> 前文:
|
||||
> * [一、Dart语言和Flutter基础](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
> * [二、 快速开发实战篇](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
> * [三、 打包与填坑篇](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
> * [四、 Redux、主题、国际化](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
> * [五、 深入探索](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
> * [六、 深入Widget原理](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
> * [七、 深入布局原理](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
> * [八、 实用技巧与填坑](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
在第六、第七篇中我们知道了 `Widget`、`Element`、`RenderObject` 的关系,同时也知道了`Widget` 的布局逻辑,最终所有 `Widget` 都转化为 `RenderObject` 对象, 它们堆叠出我们想要的画面。
|
||||
|
||||
|
@ -118,7 +122,7 @@ The [estimatedBounds] rectangle is in the [canvas] coordinate system.
|
|||
在 `_RenderSlider` 中,除了 **手势** 和 **动画** 之外,其余的每个绘制的部分,都是独立的 *Component* 去完成绘制,而这些 *Component* 都是通过 `SliderTheme` 的 `SliderThemeData` 提供的。
|
||||
|
||||
巧合的是,`SliderTheme` 本身就是一个 `InheritedWidget` 。看过以前篇章的同学应该会知道, `InheritedWidget` 一般就是用于做状态共享的,所以如果你需要自定义 `Slider` ,完成可以通过 `SliderTheme` 嵌套,然后通过 `SliderThemeData` 选择性的自定义你需要的模块。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20190604_Flutter-9/image12)
|
||||
|
||||
并且如下图,在 `_RenderSlider` 中注册时手势和动画,会在监听中去触发 `markNeedsPaint` 方法,这就是为什么你的触摸能够响应画面的原因了。
|
||||
|
@ -136,10 +140,8 @@ The [estimatedBounds] rectangle is in the [canvas] coordinate system.
|
|||
|
||||
### 资源推荐
|
||||
|
||||
* Github : [https://github.com/CarGuo/](https://github.com/CarGuo)
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* Github : https://github.com/CarGuo
|
||||
* 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter
|
||||
|
||||
##### 完整开源项目推荐:
|
||||
|
||||
|
@ -147,6 +149,34 @@ The [estimatedBounds] rectangle is in the [canvas] coordinate system.
|
|||
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp )
|
||||
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)
|
||||
|
||||
##### 文章
|
||||
|
||||
[《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》](https://juejin.im/post/5b631d326fb9a04fce524db2)
|
||||
|
||||
[《Flutter完整开发实战详解(二、 快速开发实战篇)》](https://juejin.im/post/5b685a2a5188251ac22b71c0)
|
||||
|
||||
[《Flutter完整开发实战详解(三、 打包与填坑篇)》](https://juejin.im/post/5b6fd4dc6fb9a0099e711162)
|
||||
|
||||
[《Flutter完整开发实战详解(四、Redux、主题、国际化)》](https://juejin.im/post/5b79767ff265da435450a873)
|
||||
|
||||
[《Flutter完整开发实战详解(五、 深入探索)》](https://juejin.im/post/5bc450dff265da0a951f032b)
|
||||
|
||||
[《Flutter完整开发实战详解(六、 深入Widget原理)》](https://juejin.im/post/5c7e853151882549664b0543)
|
||||
|
||||
[《Flutter完整开发实战详解(七、 深入布局原理)》](https://juejin.im/post/5c8c6ef7e51d450ba7233f51)
|
||||
|
||||
[《Flutter完整开发实战详解(八、 实用技巧与填坑)》](https://juejin.im/post/5c9e328251882567b91e1cfb)
|
||||
|
||||
[《Flutter完整开发实战详解(九、 深入绘制原理)》](https://juejin.im/post/5ca0e0aff265da309728659a)
|
||||
|
||||
[《Flutter完整开发实战详解(十、 深入图片加载流程)》](https://juejin.im/post/5cb1896ce51d456e63760449)
|
||||
|
||||
[《Flutter完整开发实战详解(十一、全面深入理解Stream)》](https://juejin.im/post/5cc2acf86fb9a0321f042041)
|
||||
|
||||
[《跨平台项目开源项目推荐》](https://juejin.im/post/5b6064a0f265da0f8b2fc89d)
|
||||
|
||||
[《移动端跨平台开发的深度解析》](https://juejin.im/post/5b395eb96fb9a00e556123ef)
|
||||
|
||||
[《React Native 的未来与React Hooks》](https://juejin.im/post/5cb34404f265da0384127fcd)
|
||||
|
||||
![我们还会再见吗?](http://img.cdn.guoshuyu.cn/20190604_Flutter-9/image15)
|
272
Flutter-BIO.md
272
Flutter-BIO.md
|
@ -1,272 +0,0 @@
|
|||
# 移动端系统生物认证技术详解
|
||||
|
||||
|
||||
相信大家对于生物认证应该不会陌生,使用指纹登陆或者 FaceId 支付等的需求场景如今已经很普遍,所以基本上只要涉及移动端开发,不管是 Android 、iOS 或者是 RN 、Flutter 都多多少少会接触到这一业务场景。
|
||||
|
||||
当然,不同之处可能在于大家对于平台能力或者接口能力的熟悉程度,**所以本篇主要介绍 Android 和 iOS 上使用系统的生物认证需要注意什么,具体流程是什么,给需要或者即将需要的大家出一份汇总的资料**。
|
||||
|
||||
> ⚠️注意:**本篇更倾向于调研资料的角度,适合需要接入或者在接入过程中出现疑问的方向,而不是 API 使用教程,另外篇幅较长警告~**
|
||||
|
||||
首先,先简单说一个大家都知道的概念,那就是不管是 Android 或者 iOS ,不管是指纹还是 FaceId ,只要使用的是系统提供的 API ,**作为开发者是拿不到任何用户的生物特征数据,所以简单来说你只能调用系统 API ,然后得到成功或者失败的结果**。
|
||||
|
||||
|
||||
|
||||
![image-20220329172335926](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image1)
|
||||
|
||||
|
||||
|
||||
## 一、Android
|
||||
|
||||
Android 上的生物认证发展史可以说是十分崎岖,目前简单来说经历了两个阶段:
|
||||
|
||||
- `FingerprintManager` (API 23)
|
||||
- `BiometricPrompt`(API 28)
|
||||
|
||||
所以如下图所示,你会看到其实底层有两套 `Service` 在支持生物认证的 API 能力,但是值得注意的是, **`FingerprintManager ` 在 Api28(Android P)被添加了 `@Deprecated` 标记 ,包括 androidx 里的兼容包 `FingerprintManagerCompat` 也是被标注了 `@Deprecated` ,因为官方提供更傻瓜式,更开箱即用的 `androidx.biometrics.BiometricPrompt`**。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
![image-20220329172806492](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image2)
|
||||
|
||||
|
||||
|
||||
### 1.1、使用 BiometricPrompt
|
||||
|
||||
简单介绍下接入 `BiometricPrompt` ,首先第一步是添加权限
|
||||
|
||||
```xml
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.test.biometric">
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
</manifest>
|
||||
```
|
||||
|
||||
接着调用 `BiometricPrompt` 构建系统弹出框信息,具体内容对应可见下图:
|
||||
|
||||
![image-20220329175140111](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image3)
|
||||
|
||||
最用设置 `AuthenticationCallback` 和调用 `authenticate` ,然后等待授权结果进入到成功的回调:
|
||||
|
||||
```java
|
||||
biometricPrompt = new BiometricPrompt(activity, uiThreadExecutor, authenticationCallback);
|
||||
biometricPrompt.authenticate(promptInfo);
|
||||
```
|
||||
|
||||
当然上述代码还少了很多细节:
|
||||
|
||||
- 比如需要是 `FragmentActivity` ;
|
||||
|
||||
- 检测设备是否支持生物认证(还有不支持的现在?);
|
||||
- 判断支持哪种生物认证,当然默认 `BiometricPrompt` 会帮你处理,如果有多种会弹出选择;
|
||||
|
||||
而认证不成功的时候可以在 `onAuthenticationError` 里获取到对应的错误码:
|
||||
|
||||
| onAuthenticationError | Type |
|
||||
| --------------------------------- | ------------------------------------------------------------ |
|
||||
| BIOMETRIC_ERROR_LOCKOUT | 操作被取消,因为 API 由于尝试次数过多而被锁定(一般就是在一次 `authenticate` 里例如多次指纹没通过,锁定了, 但是过一会还可以调用) |
|
||||
| BIOMETRIC_ERROR_LOCKOUT_PERMANENT | 由于 BIOMETRIC_ERROR_LOCKOUT 发生太多次,操作被取消,这个就是真的 LOCK 了。 |
|
||||
| BIOMETRIC_ERROR_NO_SPACE | 剩余存储空间不足 |
|
||||
| BIOMETRIC_ERROR_TIMEOUT | 超时 |
|
||||
| BIOMETRIC_ERROR_UNABLE_TO_PROCESS | 传感器异常或者无法处理当前信息 |
|
||||
| BIOMETRIC_ERROR_USER_CANCELED | 用户取消了操作 |
|
||||
| BIOMETRIC_ERROR_NO_BIOMETRIC | 用户没有在设备中注册任何生物特征 |
|
||||
| BIOMETRIC_ERROR_CANCELED | 由于生物传感器不可用,操作被取消 |
|
||||
| BIOMETRIC_ERROR_HW_NOT_PRESENT | 设备没有生物识别传感器 |
|
||||
| BIOMETRIC_ERROR_HW_UNAVAILABLE | 设备硬件不可用 |
|
||||
| BIOMETRIC_ERROR_VENDOR | 如果存在不属于上述之外的情况,Other |
|
||||
|
||||
### 1.2、BiometricPrompt 自定义
|
||||
|
||||
简单接入完 `BiometricPrompt` 之后, 你可能会有个疑问: *`BiometricPrompt` 是很方便,但是 UI 有点丑了,可以自定义吗?*
|
||||
|
||||
**抱歉,不可以** ,是的,`BiometricPrompt `不能自定义 UI,甚至你想改个颜色都“费劲”, 如果你去看 [biometric](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/biometric) 的源码,就会发现官方并没有让你自定义的打算,除非你 cv 这些代码自己构建一套,至于为什么会有这样的设计,我个人猜测**其中一条就是屏下指纹**。
|
||||
|
||||
> 在官方的 [《Migrating from FingerprintManager to BiometricPrompt》](https://medium.com/androiddevelopers/migrating-from-fingerprintmanager-to-biometricprompt-4bc5f570dccd)里也说了:丢弃指纹的布局文件,因为你将不再需要它们,AndroidX 生物识别库带有标准化的 UI。
|
||||
|
||||
什么是标准化的 UI ?如下所示是使用 `BiometricPrompt` 的三台手机,可以看到:
|
||||
|
||||
- 第一和第二台除了位置有些许不同,其他基本一致;
|
||||
- 第三胎手机是屏下指纹,可以看到整个指纹输入的 UI 效果完全是厂家自己的另外一种风格;
|
||||
|
||||
![image-20220329183129880](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image4)
|
||||
|
||||
**所以使用 `BiometricPrompt` 你将不需要关注 UI 问题,因为你没得选,甚至你也不需要关注手机上的生物认证类型的安全度问题,因为不管是 CDD 还是 UI ,OEM 厂商的都会直接实现好**,例如三星的 UI 是如下图所示:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image5)
|
||||
|
||||
|
||||
|
||||
> [Android 兼容性定义文档 (Android CDD)](https://source.android.com/compatibility/android-cdd#7_3_10_biometric_sensors)_里描述了生物认证传感器安全度的强弱,而在 framework 层面 `BiometricFragment` 和 `FingerprintDialogFragment` 都是 `@hide` ,甚至你单纯去翻 `androidx.biometric:biometric.aar` 的库,你都看不到 `BiometricFragment` 的布局,只能看到 `FingerprintDialogFragment` 的 layout。
|
||||
|
||||
那就没办法自定义 UI 了吗?还是有的,有两个选择:
|
||||
|
||||
- 继续使用 `FingerprintManager` ,虽然标注了弃用,但是目前还是可以用,在 Android 11 上也可以正常执行对应逻辑,下图是同一台手机在 Android 11 上使用 `FingerprintManager` 和 `BiometricPrompt ` 的对比:
|
||||
|
||||
![image-20220329202726403](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image6)
|
||||
|
||||
- 使用腾讯的 [soter ](https://github.com/Tencent/soter ) ,这个我们后面讲;
|
||||
|
||||
|
||||
|
||||
### 1.3、Login + BiometricPrompt
|
||||
|
||||
介绍完调用和 UI ,那就再结合 Login 场景聊聊 `BiometricPrompt` ,官方针对 Login 场景提供了一个 [Demo]( https://github.com/android/security-samples/tree/master/BiometricLoginKotlin) ,这里主要介绍整个业务流程,具体代码可以看官方的 [BiometricLoginKotlin]( https://github.com/android/security-samples/tree/master/BiometricLoginKotlin) ,前面说过生物认证只提供认证结果,那么结合 Login 业务,在官方的例子中 **`BiometricPrompt` 主要是用于做认证和加密的作用**:
|
||||
|
||||
![image-20220329162115306](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image7)
|
||||
|
||||
如上图所示,场景是在登陆之后,我们获取到了用户的 Token 信息,这个 Token 信息可能是服务器基于用户密码合并后的内容,所以它包含了一些敏感隐私,为了安全期间我们不能直接存储,而是利用 `BiometricPrompt` 去实现加密后存储:
|
||||
|
||||
- 首先通过 `KeyStore`,主要是得到一个包含密码的 `SecretKey` ,当然这里有一个关键操作,那就是 `setUserAuthenticationRequired(true)`,后面我们再解释;
|
||||
- 然后利用 `SecretKey` 创建 `Clipher` , `Clipher` 就是 Java 里常用于加解密的对象;
|
||||
- 利用 `BiometricPrompt.CryptoObject(cipher)` 去调用生物认证授权;
|
||||
- 授权成功后会得到一个 `AuthenticationResult` ,Result 里面包含存在密钥信息的 `cryptoObject?.cipher` 和 `cipher.iv` 加密偏移向量;
|
||||
- 利用授权成功后的 `cryptoObject?.cipher` 对 Token 进行加密,然后和 `cipher.iv` 一起保存到 `SharePerferences` ,就完成了基于 `BiometricPrompt` 的加密保存;
|
||||
|
||||
是不是觉得有点懵? 简单说就是:**我们通过一个只有用户通过身份验证时才授权使用的密钥来加密 Token ,这样不管这个 Token 是否泄漏,对于我们来说都是安全的。**
|
||||
|
||||
然后在 `KeyStore` 逻辑里这里有个 `setUserAuthenticationRequired(true)` 操作,这个操作的意思就是:是否仅在用户通过身份验证时才授权使用此密钥,也就是当设置为 `true` 时:
|
||||
|
||||
**用户必须通过使用其锁屏凭据的子集(例如密码/PIN/图案或生物识别)向此 Android 设备进行身份验证,才能够而授权使用密钥。**
|
||||
|
||||
也就是只有设置了安全锁屏时才能生成密钥,而一旦安全锁屏被禁用(重新配置为无、不验证用户身份的模式、被强制重置)时,密钥将*不可逆转地失效。*
|
||||
|
||||
> 另外可以设置了 `setUserAuthenticationValidityDurationSeconds` 来要求密钥必须至少有一个生物特征才可用,而一但它设置为 true,如果用户注册了新的生物特征,它也将不可逆转地失效。
|
||||
|
||||
**所以可以看到,这个流程下密钥会和系统安全绑定到一起,从而不害怕 Token 等信息的泄漏**,也因为授权成功后的 `CryptoObject` 和 `KeyStore` 集成到一起,可以更有效地抵御例如 root 的攻击。
|
||||
|
||||
而反之获取的流程也是类似,如下图所示:
|
||||
|
||||
- 在 `SharePerferences` 里获取加密后的 Token 和 iv 信息;
|
||||
- 同样是利用 `SecretKey` 创建 `Clipher` ,不过这次要带上保存的 iv 信息;
|
||||
- 利用 `BiometricPrompt.CryptoObject(cipher)` 去调用生物认证授权;
|
||||
- 通过授权成功后的 `cryptoObject?.cipher` 对 Token 进行加密,得到原始的 Token 信息;
|
||||
|
||||
|
||||
|
||||
![image-20220329162133600](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image8)
|
||||
|
||||
|
||||
|
||||
所以可以看到,**基本思路就是利用 `BiometricPrompt` 认证后得到 `CryptoObject?.Cipher` 去加解密,通过系统的安全等级要保护我们的隐私信息**。
|
||||
|
||||
最后补充一个知识点,虽然一般我们不关心,但是在 `BiometricPrompt` 里有 ***auth-per-use*** 和 ***time-bound*** 这两个概念:
|
||||
|
||||
- ***auth-per-use*** 密钥要求每次使用密钥时,都必须进行认证 ,前面我们通过 `BiometricPrompt.CryptoObject(cipher)` 去调用授权方法就是这类实现;
|
||||
- ***time-bound*** 密钥是一种在一定的时间段内有效的密钥,可以通过 `setUserAuthenticationValidityDurationSeconds` 设置有效时长,如果你设置为很短,例如 5 秒,那行为上和 auth-per-use 基本类似;
|
||||
|
||||
> 更多资料可以参考官方的 [biometric-authentication-on-android](https://medium.com/androiddevelopers/biometric-authentication-on-android-part-1-264523bce85d)
|
||||
|
||||
### 1.4、Tencent soter
|
||||
|
||||
前面说到 Android 上还有 soter ,腾讯在微信指纹支付全流程之上,将它的流程抽象为一套完备的生物识别标准:SOTER。
|
||||
|
||||
SOTER 会与手机厂商合作,在系统原有的接口能力之上提供安全加固,通过业务无关的安全域(TEE,即独立于手机操作系统的安全区域,root或越狱无法访问到)应用程序(TA)降低开发难度和适配成本,**做到即使外部环境不可信,依然可以安全授权。**
|
||||
|
||||
> TEE(Trusted Execution Environment)是独立于手机操作系统的一块独立运行的安全区域,SOTER标准中,所有的密钥生成、数据签名处理、指纹验证、敏感数据传输等敏感操作均在 TEE 中进行,**并且 SOTER使用的设备根密钥由厂商在产线上烧入,从根本上解决了根密钥不可信的问题,并以此根密钥为信任链根,派生密钥,从而完成**,与微信合作的所有手机厂商将均带有硬件TEE,并且通过腾讯安全平台和微信支付安全团队验收,符合SOTER标准。
|
||||
|
||||
![image-20220329214046518](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image9)
|
||||
|
||||
简而言之,这是一个支持直通厂商,并且具备后台服务对接校验的第三方库,目前最近 5 个月都还有在更新,那它有什么问题呢?
|
||||
|
||||
那就是**必须是与微信合作的所有手机厂商和机型才能正常使用** ,而且经常在一些厂商系统上出现奇奇怪怪的问题,比如:
|
||||
|
||||
- MiUI13 绑定服务异常;
|
||||
- 鸿蒙系统API层面报错;
|
||||
- 莫名其妙地出现崩溃;
|
||||
|
||||
但是它可以实现基本类似于微信支付的能力,所以如何取舍就看你的业务需求了。
|
||||
|
||||
> 支持机型可查阅 :[#有多少设备已经支持tencent-soter](https://github.com/Tencent/soter/wiki#有多少设备已经支持tencent-soter)
|
||||
|
||||
|
||||
|
||||
## iOS
|
||||
|
||||
相对来说 iOS 上的生物认证就舒适不少,相比较 Android 上需要区分系统版本和厂商的 `fingerprint` 、`face` 和 `iris` ,iOS 上的 Face ID 和 Touch ID 就十分统一和简洁。
|
||||
|
||||
简单介绍下 iOS 上使用生物认证,首先需要在 `Info.plist` 文件添加描述信息:
|
||||
|
||||
```xml
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Why is my app authenticating using face id?</string>
|
||||
```
|
||||
|
||||
然后导入头文件 `#import <LocalAuthentication/LocalAuthentication.h>` ,最后创建 `LAContext` 去执行授权操作,这里也简单展示对应的错误码:
|
||||
|
||||
| Error Code | Type |
|
||||
| --------------------------- | --------------------------------- |
|
||||
| LAErrorSystemCancel | 系统取消了授权,比如有其他APP切换 |
|
||||
| LAErrorUserCancel | 用户取消验证 |
|
||||
| LAErrorAuthenticationFailed | 授权失败 |
|
||||
| LAErrorPasscodeNotSet | 系统未设置密码 |
|
||||
| LAErrorBiometryNotAvailable | ID不可用,例如未打开 |
|
||||
| LAErrorBiometryNotEnrolled | ID不可用,用户未录入 |
|
||||
| LAErrorUserFallback | 用户选择输入密码 |
|
||||
|
||||
而同样关于自定义 UI 问题上,想必大家都知道了,**iOS 生物认证没有自定义 UI 的说法,也不支持自定义 UI ,系统怎么样就怎么样,你可以做的只有类似配置‘是否允许使用密码授权’这样的行为** 。
|
||||
|
||||
> 在这一点上相信 Android 开发都十分羡慕 iOS ,有问题也是系统问题,无法修复。
|
||||
|
||||
同样,简单说说在 iOS 上使用生物识别的 Login 场景流程:
|
||||
|
||||
- 获取到 Token 信息后,验证用户的 TouchID/FaceID ;
|
||||
- 验证通过后,将 Token 等信息保存到 keychain (keychain 只是一个数据存储,用于存储一些敏感数据如密码、证书等);
|
||||
- 保存成功后,下次再次登录时通过验证 TouchID/FaceID 获取对应信息;
|
||||
|
||||
![image-20220329223329042](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image10)
|
||||
|
||||
这里主要有两个关键点:
|
||||
|
||||
- **访问级别** : 例如是否需要每次都进行身份验证时才可以访问项目;
|
||||
- **身份验证级别**: 也就是什么场景下可以访问到存储的信息;
|
||||
|
||||
举个例子,访问 keychain 首先是需要创建 accessControl ,一般可以通过 **`SecAccessControlCreateWithFlags`** 来创建 accessControl ,这里有个关键参数用于指定访问级别:
|
||||
|
||||
- kSecAttrAccessibleAfterFirstUnlock 开机之后密钥不可用,需要等用户输入开机密码
|
||||
- kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: 开机之后密钥不可用,需要等用户输入开机密码,但是仅限于当前设备
|
||||
- kSecAttrAccessibleWhenUnlocked: 解锁过的设备密钥会保持可用状态
|
||||
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly: 解锁过的设备密钥会保持可用状态,仅当前设备
|
||||
- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly: 解锁过的设备密钥会保持可用状态,只有用户设置密码后密钥才可用
|
||||
- kSecAttrAccessibleAlways: 始终可用,已经 Deprecated
|
||||
- kSecAttrAccessibleAlwaysThisDeviceOnly: 密钥始终可用,但无法迁移到其他设备,已经 Deprecated
|
||||
|
||||
类似场景下一般使用 `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` ,另外还有 `SecAccessControlCreateFlags`标志,它主要是用于指定希望用户在访问钥匙串时的约束,一般类似场景会使用 `userPresence` :
|
||||
|
||||
- devicePasscode: 限制使用密码访问
|
||||
- biometryAny: 使用任何已注册 touch 或 face ID 访问
|
||||
- biometryCurrentSet: 限制使用当前注册 touch 或 face ID 访问
|
||||
- userPresence: 限制使用生物特征或密码访问
|
||||
- watch: 使用手表访问
|
||||
|
||||
创建完成 accessControl 之后,通过设置 kSecAttrAccessControl 后正常把信息存储到 keychain 就可以了,在存储 keychain 时也有可选的 `kSecClass` ,一般选用 `kSecClassGenericPassword`:
|
||||
|
||||
- kSecClassGenericPassword: 通用密码
|
||||
- kSecClassInternetPassword: Internet 密码
|
||||
- kSecClassCertificate:证书
|
||||
- kSecClassKey:加密密钥
|
||||
- kSecClassIdentity: 身份认证
|
||||
|
||||
*当然,此时你是否发现,在谈及 accessControl 和 keychain 时没有说明 `LAContext` ?*
|
||||
|
||||
其实在创建 accessControl 时是有对应 `kSecUseAuthenticationContext` 参数用于设置 `LAContext ` 到 keychain 认证,但是也可以不设置,具体为:
|
||||
|
||||
- 如果未指定,并且该项目需要 authentication 认证,那就会自动创建一个新的 `LAContext ` ,使用一次后丢弃;
|
||||
- 如果是使用先前已通过身份验证的 `LAContext ` ,则操作直接成功而不要求用户进行身份验证;
|
||||
- 如果是使用先前未经过身份验证的 `LAContext ` ,则系统会尝试在该 `LAContext ` 上进行身份验证,如果成功就可以在后续的钥匙串操作中重用。
|
||||
|
||||
> 更多可见官方的: [accessing_keychain_items_with_face_id_or_touch_id](https://developer.apple.com/documentation/localauthentication/accessing_keychain_items_with_face_id_or_touch_id/)
|
||||
|
||||
可以看到, iOS 上都只需要简单地配置就行了,因为系统层面也不会给你多余的能力。
|
||||
|
||||
> ⚠️提示,如果你有需要屏蔽 iOS 在生物验证失败之后,不展示输入密码的选项,可以配置 `LAContext ` 的 `context.localizedFallbackTitle=""` 来实现。
|
||||
|
||||
## 三、最后
|
||||
|
||||
虽然本篇从头到位并没有教你如何使用 Android 或者 iOS 的生物认证,但是作为汇总资料,本篇基本覆盖了 Android 或者 iOS 生物认证相关的基本概念和问题,相信本篇将会特别适合正在调研生物认证相关开发的小伙伴。
|
||||
|
||||
最后,还是惯例,如果对于这方便你有什么问题或者建议,欢迎留言评论交流。
|
|
@ -1,94 +0,0 @@
|
|||
# Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑
|
||||
|
||||
|
||||
Flutter 在 iOS 上的编译问题相信大家多多少少遇到过,不知道大家在搜索这方便的问题时,得到的答案是不是*让你 clean 或者 install 多几次*,很多时候就算解决完问题,也是处于薛定谔的状态,所以**本篇也简单记录下 Flutter 开发中,OC 混编 Swift 遭遇动态库和静态库的问题**,希望对“蒙圈”中的你有点帮助。
|
||||
|
||||
![image-20220422091858815](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image1)
|
||||
|
||||
|
||||
|
||||
首先,当我在一个 OC 项目里接入一个 Swift 插件,可能会遇到什么问题?
|
||||
|
||||
如下图所示,**如果你是一个比较老的 Flutter 项目,那可能会出现 swift 插件出现 not found 的问题**。
|
||||
|
||||
![image-20220422093205569](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image2)
|
||||
|
||||
针对这个问题,一般都是建议在 Podfile 文件下添加 `use_frameworks!` ,有时候还会建议添加 `use_modular_headers!` ,那这两个标记位的作用是什么?
|
||||
|
||||
```shell
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
```
|
||||
|
||||
> 我们知道 Podfile 的作用是处理 CocoaPads ,而 `use_frameworks!`告诉 CocoaPods 你想使用 Framework 而不是静态库,而默认由于 Swift 不支持静态库,因此有一开始 Swift 必须使用 Framework 的限制。
|
||||
|
||||
静态库和 Framework 的区别在于:
|
||||
|
||||
- *.a 的静态库类似于编译好的机械代码,源代码和库代码都被整合到单个可执行文件中,所以它会和设备架构绑定,并且不包含资源文件比如图片;
|
||||
- Framework 支持将动态库、头文件和资源文件封装到一起的一种格式,其中动态库的简单理解是:不会像静态库一样被整合到一起,而是在运行或者运行时动态链接;
|
||||
|
||||
另外一个配置 `use_modular_headers!` ,它主要是将 pods 转为 Modular,因为 Modular 是可以直接在 Swift中 import ,所以不需要再经过 bridging-header 的桥接。
|
||||
|
||||
> 但是开启 `use_modular_headers!` 之后,会使用更严格的 header 搜索路径,开启后 pod 会启用更严格的搜索路径和生成模块映射,历史项目可能会出现重复引用等问题,因为在一些老项目里 CocoaPods 是利用Header Search Paths 来完成引入编译,当然使用 `use_modular_headers!`可以提高加载性能和减少体积。
|
||||
|
||||
继续回到问题上,我们在添加完 `use_frameworks!` 之后,有一定几率中奖各种 *Undefined symbol* 的错误问题,这时候不要慌,因为这是 Swfit 里有静态库导致。
|
||||
|
||||
![image-20220422103410759](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image3)
|
||||
|
||||
很明显 Swift 不支持静态库的行为不科学,所以从 Xcode 9 开始 Swift 就开始支持静态库,而 CocoaPods 1.9.0 开始,引入了 **`use_frameworks! :linkage => :static`** 来生支持有静态库和 Framework 的情况。
|
||||
|
||||
所以修改 use_frameworks 配置,增加 static 之后可以看到 *Undefined symbol* 的错误都消失了,但是运行之后,可能会喜提新的问题: *non-modular header* 。
|
||||
|
||||
![image-20220422104501881](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image4)
|
||||
|
||||
如果你去搜索答案,有很多答案会告诉你如下图所示,通过 `Allow Non-modular Includes in Framework Modules` 设置为 `true` 就可以解决问题,**但是很明显这并不是正解,它更多适用于临时的紧急状体下**。
|
||||
|
||||
![image-20220422105705987](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image5)
|
||||
|
||||
当然,你也可以在出现问题的插件的 `.podspec` 下单独配置 ALLOW ,效果相同,更轻量级,但是这也只是临时解决方案。
|
||||
|
||||
```shell
|
||||
s.user_target_xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' }
|
||||
```
|
||||
|
||||
为什么说这种方式不靠谱,因为你不知道官方会什么时候删除这种允许,当然这个问题网友提供的解决方案其实千奇百怪:
|
||||
|
||||
- 如果是 App 使用 **dynamic framework** 里的 **header** 导致错误,可以使用 `#import "MyFile.h"` 而不是 `#import <MyFramework/MyFile.h>` ;
|
||||
- 将`#import`语句移到 `.m`(而不是将其放在`.h`头文件中), 这样它就不会有包含 *non-modular header* 的问题,例如: https://github.com/AFNetworking/AFNetworking/pull/2206/files ;
|
||||
- 重命名 header ,不要让 header 和模块名一样,变为 `#import <FrameworkName/Header.h>`
|
||||
- 在 build setting 配置 OTHER_SWIFT_FLAGS -Xcc -Wno-error=non-modular-include-in-framework-module 解决 Swift 的问题;
|
||||
|
||||
**有可能它们都能解决你的问题,但是为什么呢?下次遇到这些问题要选哪个解决?**
|
||||
|
||||
回归到我们的问题,其实我的问题关键是:**不能在 Framework Module 中使用非 Modular 的 Header**,也就问题是在 Framework Module 中加载了非当前 Module 的头文件,而由于 Header 是对外 public ,比如配置到了 `s.public_header_files` ,就会导致非 Modular 的 Header 也出现对外暴露的风险,所以我这边的解放方式也很简单:
|
||||
|
||||
**在 `s.public_header_files` 里只放需要公开的 *Plugin.h ,使用了非 Modular 的 Header 不对外 public,从而符合规范达到编译成功**。
|
||||
|
||||
所以这里面的核心是:不要在 Umbrella Header File 中引用不需要对外公开的 OC 头文件去作为子 module ,这也解释了为什么上面讲出问题的 `#import`语句移到 `.m` 就解决问题的逻辑。
|
||||
|
||||
> 例如有时候你还会引用一些系统的 C Module ,其实在 Framework Module 化过程中也会有类似的问题。
|
||||
|
||||
所以知道了为什么和怎么解决,就不会只是粗暴通过 LLVM 的配置来设置 `Allow Non-modular Includes in Framework Modules` 去解决薛定谔的问题。
|
||||
|
||||
另外你可能还有用到的,比如模拟器编译提示 unsupport arm64、 BITCODE 失败,SWIFT_VERSION 版本冲突等等:
|
||||
|
||||
```shell
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
# building for iOS Simulator, but linking in an object file built for iOS, for architecture ‘arm64’
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||
#不支持 BITCODE
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
#解决swift模块问题
|
||||
config.build_settings['SWIFT_VERSION'] = '5.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
当然,最后一句话:**珍爱头发,远离 Swift 混编**。
|
139
Flutter-CCK.md
139
Flutter-CCK.md
|
@ -1,139 +0,0 @@
|
|||
# 一文快速带你了解 KMM 、 Compose 和 Flutter 的现状
|
||||
|
||||
又到了喜闻乐见的环节,**本篇主要是科普 KMM 、 Compose 和 Flutter 的最新现状**,对于 Compose 和 Flutter 大家可能并不陌生,但是对于 KMM 也许会存在疑惑,KMM 全称 Kotlin Multiplatform Mobile ,故名思义它是用 Kotlin 实现的跨平台框架,那为什么今天突然会聊到它?
|
||||
|
||||
起因如下图所示,今天突然有群友提及了 KMM ,并且用了“变天”的词汇,顿时就勾起了我的兴起,因为 KMM 这些年来一直“不温不火”,可以说很多使用 Kotlin 开发的 “Androider” 对它都很陌生,难道最近它又有了什么突破性的进展?
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221027_M1/image1.png) | ![](http://img.cdn.guoshuyu.cn/20221027_M1/image2.png) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ |
|
||||
|
||||
而在求证一番之后,原来起因来自 10 月初 **Android 官方宣布 [ Jetpack 开始要支持 KMM](https://android-developers.googleblog.com/2022/10/announcing-experimental-preview-of-jetpack-multiplatform-libraries.html)** 了,目前 [Collections](https://developer.android.com/jetpack/androidx/releases/collection) 和 [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) 已经可以通过依赖 `-dev01` 版本在多平台上使用,同时 **KMM 进入 Beta 版本阶段**。
|
||||
|
||||
**所以目前 KMM 变不了天,至少它还处于 Beta 阶段,但是 Jetpack 开始支持 KMM 是个很好的消息,这意味着 KMM 的社区支持有了官方保证**。
|
||||
|
||||
> 好了,介绍完起因,接下来开始进入今天的主题,什么是 KMM 、 Compose 和 Flutter。
|
||||
|
||||
|
||||
|
||||
# KMM
|
||||
|
||||
Kotlin Multiplatform Mobile – KMM 是基于 Kotlin 并应用在 iOS 和 Android 的一种跨平台技术,它的特点是结合了跨平台和原生开发协同开发的模式,如下图所示,简单的理解就是:**从纯原生开发变成了 KMM + 原生 UI 开发**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image3.png)
|
||||
|
||||
**使用 KMM 可以把你的业务逻辑和基建部分的能力跨平台化**,例如网络请求、数据存储,状态上报等模块通过 KMM 实现 Android 和 iOS 通用,例如前面介绍的 DataStore 就可以在 iOS 上支持使用。
|
||||
|
||||
> 在官方的介绍里 KMM 的早期使用者有百度、Netflix、VMWare、Philips 等,目前收到的反馈都挺不错,而 Beta 版本也意味着现在 KMM 已经具备了使用的基础。
|
||||
|
||||
*那你可能会好奇,KMM 支持 Web 吗*?
|
||||
|
||||
聊到这个话题就很有趣,从我的角度上看,我会说 Kotlin Mutiplatform 支持,但是 KMM 不支持。
|
||||
|
||||
如果你安装过 KMM 插件和创建过 KMM 项目,你会看到 KMM 不管是从 logo 还是项目创建都只有 Android 和 iOS ,但是,Kotlin Mutiplatform 是支持 Web 的,通过 Kotlin JS 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221027_M1/image4.png) | ![image-20221027153602062](http://img.cdn.guoshuyu.cn/20221027_M1/image5.png) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
如果接触 Kotlin Mutiplatform 比较早,那你那么可能还听说过 KMP ,KN 之类的缩写,那它们和 KMM 又是什么关系?简单来说:
|
||||
|
||||
- KMP 一般指的就是 Kotlin Mutiplatform ,我依稀记得 KMP 这个概念是在 Kotlin 1.2 的时候被提出,可以将Kotlin 运行到特定平台的 JVM 和 JS 代码上
|
||||
- KN 一般指的是 Kotlin Native ,KN 属于是将 Kotlin 编译为 Native 二进制文件的技术,甚至可以在没有虚拟机的情况下运行,例如 KMM 上的 iOS 就是使用了 KN 的能力,
|
||||
- KMM 是利用了 JVM 和 KN 能力实现的针对 Android 和 iOS 平台的 Kotlin 框架:Android(Kotlin/JVM)和 iOS (Kotlin/Naitve)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image6.png)
|
||||
|
||||
另外还有 Kotlin JS 用于 Web 平台,**所以 KMP 可以看作是大集合,而 KMM 是其中针对 Android 和 iOS 的支持,另外通过 Kotlin Native 和 Kotlin JS 也可以支持拓展到 PC 端和 Web 端**。
|
||||
|
||||
那么到这里你应该理解:**KMM 主要是用来写跨平台逻辑,涉及到 UI 部分你还是需要通过原生实现**,如果你从另外一个角度看,用 KMM 对于 Android 开发来说几乎等于白送的能力,因为它只需要 Kotlin。
|
||||
|
||||
> 至少 Compose 你还需要适应下响应式开发模式。
|
||||
|
||||
*那或者有人就问:那 KMM 这也的意义何在*?
|
||||
|
||||
事实上还真有,**KMM 在 App 的基建上会很实用,比如做数据上报,崩溃统计,数据分析等等**,纯逻辑的跨平台不影响 UI 部分,目前也是在这些场景上 KMM 应用较多。
|
||||
|
||||
> 另外还有人问我,KMM 可以用 Java 开发吗? 嗯,这是个好问题,下次不要再问了。
|
||||
|
||||
当然,KMM 也存在一些局限,比如使用 ViewModel 和协程如何在 iOS 上运行的问题,不过社区针对这部分也有一些第三方支持,所以对于 KMM 的未来还是值得期待。
|
||||
|
||||
# Compose
|
||||
|
||||
Compose 相信大家不会陌生,**其实 Compose 也可以分两部分看待, Jetpack Compose 和 Compose Multiplatform**:
|
||||
|
||||
- 由 Android 官方维护的 Jetpack Compose
|
||||
- 由 JetBrains 维护的 [compose-jb ](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FJetBrains%2Fcompose-jb)实现的 Compose Multiplatform
|
||||
|
||||
**如果说 KMM 时用于实现跨平台的业务逻辑,那么 Compose Multiplatform 就是专注于跨平台 UI 上的支持**,那 KMM 和 Compose Multiplatform 是什么关系呢?
|
||||
|
||||
从项目角度看, compose-jb 和 KMM 其实没有关系,因为 KMM 还在 beta ,但是 Compose Multiplatform 正式已经发布接近一年的时间。
|
||||
|
||||
> 但是你要说完全没关系显然是不可能,毕竟 Kotlin Native 和 Kotlin JS 的能力其实在 Compose Multiplatform 里很重要。
|
||||
|
||||
当然,如下图所示,Compose Multiplatform 在跨平台开发体验上还是有所区别,**Compose 目前是通过多个模块不同实现来支持多平台,所以目前 Jetpack Compose 和 Compose Multiplatform 有一些“割裂”**,特别是在 Web 端,想要达到 Flutter 一样共享代码的比例还需要继续努力。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image7.png)
|
||||
|
||||
> PS :图比较老,iOS 其实目前已经进入[实验阶段](https://github.com/JetBrains/compose-jb/issues/2397#issuecomment-1277536570) ,[` androidx.compose.ui.main.defaultUIKitMain` ](https://github.com/JetBrains/compose-jb/blob/master/experimental/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt) 相关的支持距离正式发布可以期待。
|
||||
|
||||
另外 Compose Multiplatform 还有的问题就是缺少插件社区,这其实是跨平台领域必不可少的配置:**前端有 npm 、Flutter 有 pub,你可以通过它们的中央官网搜索你想要的库,查看它们的热度,版本,兼容和使用量等等信息,设置官方认证和安全保障,但是 Maven 时代在这方面一直很弱**。
|
||||
|
||||
另一方面 Compose 的优势也很明显:
|
||||
|
||||
- Kotlin 生态
|
||||
- Android 开发友好
|
||||
- 打包体积增长不大,代码压缩比例高
|
||||
- 性能不错,compose-android 和 compose-desktop 都使用 Skia
|
||||
|
||||
**而随着 Jetpack 开始支持 KMM ,那么 Compose Multiplatform 的社区支持力度将得到进一步提升,因为变相 Compose Multiplatform 也可以支持 Jetpack** 。
|
||||
|
||||
至于前面所说的“割裂”问题,目前可以看到官方也在有序推进,其中就有 desktop 的部分代码已经挪到了androidx 上,从这里看或者统一的 Compose lib 并不遥远。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image8.png)
|
||||
|
||||
|
||||
|
||||
> PS: JetBrains 目前就已经将 Toolbox 应用通过 Compose Multiplatform 实现并且发布使用。
|
||||
|
||||
# Flutter
|
||||
|
||||
常看我文章的应该对 Flutter 更不陌生,现在 Flutter 已经是 3.3 的版本,Flutter 的特点就是跨平台,因为它并没有自己的平台,同时它也是 single codebase 的跨平台实现。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image9.png)
|
||||
|
||||
关于 Flutter 和其他框架的对比或者使用数据这里就不多赘述,因为这方便之前我已经分享过很多,感兴趣的可以参考下方链接,这里介绍一些其他比较有意思的话题。
|
||||
|
||||
> - [Flutter VS Other 量化对比](https://juejin.cn/post/7084533408986054669)
|
||||
>
|
||||
> - [国内大厂应用在移动端 Flutter 框架使用分析](https://juejin.cn/post/7012382656578977806)
|
||||
>
|
||||
> - [国内大厂在移动端跨平台的框架接入分析](https://juejin.cn/post/6844904177949212680)
|
||||
|
||||
**在 Jetbrains 的开源项目里有一个叫 [skiko ](https://github.com/JetBrains/skiko) 的项目**,Skiko(Kotlin 的 Skia 的缩写)是一个图形库,它支持 Kotlin/JVM 、Kotlin/JS 、Kotlin/Native 等相关实现,目前支持有:
|
||||
|
||||
- Kotlin/JVM - Linux、Windows、macOS、Android
|
||||
- Kotlin/JS - web
|
||||
- Kotlin/Native - iOS 、macOS
|
||||
|
||||
如果从这个角度看 Compose Multiplatform 未来的方向会和 Flutter 很像,甚至因为 Flutter 走过更多的坑,所以 Compose Multiplatform 在对接 Skia 上可以有更多的参考。
|
||||
|
||||
> 其实未来 Linux、Windows 等平台也完全可以脱离 JVM 通过 Kotlin/Native + Skiko 实现支持,只是维护成本会变高。
|
||||
|
||||
而 **Flutter 在自建渲染引擎上其实已经越来越激进,因为直接使用 Skia 已经无法满足日益增长的 Bug 和性能极限,所以官方开始了自研[渲染引擎Impeller](https://mp.weixin.qq.com/s/GptJbPXPediNRc4KvZzr6g)** 。
|
||||
|
||||
因为 Flutter 团队现在出现问题每次都要和 Skia 团队沟通,然后等跟进,这样的节奏太慢了,从官方的更新日志上就可以看出目前 Flutter 的迭代速度依然很夸张。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221027_M1/image10.png)
|
||||
|
||||
所以**这次自研的 Impeller 本质上是为了解决 Skia 需要运行时遇到的问题, Impeller 可以直接在编译器就完成 GLSL 和 MSL ,不需要 SKSL 从而提高了性能和运行时的稳定性** ,目前优先在 iOS 平台上开始支持 ,配合 Metal 做优化,后续如果没问题也会同步支持 Android 和 Vulkan 。
|
||||
|
||||
> 从这个角度猜测,Flutter 在 Skia 遇到的问题 Compose Multiplatform 也很可能会遇上,而如果后续 Impeller 项目进展顺利,那它或者并不会局限在 Flutter ,也许也可以拓展支持到 Compose Multiplatform上。
|
||||
|
||||
其实自研发引擎并不奇怪,随着项目的发展和深入,很多底层问题没办法快速推进就会反推自研,例如 [Hermes 在 RN 0.7 成为默认 Engine](https://juejin.cn/post/7140474062211383333) 也是类似问题的体现,**自研底层属于是一个负责任的开源团队的必经之路**。
|
||||
|
||||
# 最后
|
||||
|
||||
今天这篇文章的内容更多的科普性质而非技术行,主要是针对目前 KMM 、Compose 和 Flutter 的现状做一个陈述,其实很多时候它们之间并不冲突,但是作为开发者很经常就像开头一样,用“对立”的角度来看 A 火了 B 就要挂,这种心态大可不必。
|
||||
|
||||
另外,**我更喜欢“百花齐放”的氛围,当然你也可以万花丛中只取一朵,所以不必过于焦虑,需要什么就用什么就可以**,技术服务于业务,就像我接触到的很多开发,他们需要使用什么技术并不是自己能决定的。
|
||||
|
||||
> 就比如前面那位问我 “KMM 上可以用 Java” 的那位兄弟,他是因为公司 leader 觉得 Kotlin 不成熟而不给用在 Android 上,嗯,他的 Leader 是一位后端开发。
|
209
Flutter-Chat2.md
209
Flutter-Chat2.md
|
@ -1,209 +0,0 @@
|
|||
聊天列表是一个很扣细节的场景,在之前的 [《Flutter 实现完美的双向聊天列表效果,滑动列表的知识点》](https://juejin.cn/post/7029517821004480549) 里,通过 `CustomScrollView` 和配置它的 `center` 从而解决了数据更新时的列表跳动问题,但是这时候又有网友提出了新的问题:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image1)
|
||||
|
||||
如下动图所示,可以看到虽然列表在添加新数据后虽然没有发生跳动,但是在列表数据长度足够的情况下,顶部会有一篇空白。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image2)
|
||||
|
||||
|
||||
如下代码所示,这个问题的起因正是在解决跳动问题而增加的 `center` ,因为列表是 `reverse` ,并且红色的 `SliverList` 长度只有 3 条,高度不够导致顶部留空白。
|
||||
|
||||
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
controller: scroller,
|
||||
reverse: true,
|
||||
center: centerKey,
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
var item = newData[index];
|
||||
if (item.type == "Right")
|
||||
return renderRightItem(item);
|
||||
else
|
||||
return renderLeftItem(item);
|
||||
},
|
||||
childCount: newData.length,
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.zero,
|
||||
key: centerKey,
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
var item = loadMoreData[index];
|
||||
if (item.type == "Right")
|
||||
return renderRightItem(item);
|
||||
else
|
||||
return renderLeftItem(item);
|
||||
},
|
||||
childCount: loadMoreData.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
```
|
||||
|
||||
如下图结合图片理解更形象:
|
||||
|
||||
- `center` 其实就是列表的起始锚点,我们把锚点给了 `SliverPadding` ,而因为列表是 `reverse`,所以起始位置是在屏幕下方;
|
||||
- 红色的 old 数据 `SliverList` ,在代码里是处于 `center` 的下方,而因为 `reverse` 所以它实际就是黄色的部分;
|
||||
- 所以虽然绿色的 `SliverList` 虽然新增了数据,但是从 `center` 往上的高度还是不够,所以就出现了黄色 `SliverList` 顶部空白的问题;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image3)
|
||||
|
||||
|
||||
结合这个问题,这里可以发现关键的点就在于 `reverse` ,而对比微信和QQ的聊天列表需求,在没有数据时,消息数据应该是从顶部开始,所以这时候就需要我们调整列表实现,参考微信/QQ 的实现模式。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image4)
|
||||
|
||||
如下代码所以,这里针对新交互场景做了优化调整:
|
||||
|
||||
- 去除 `CustomScrollView` 的 `reverse` ;
|
||||
- 对调两个 `SliverList` 的位置,把加载 old 数据的 `SliverList` 放到 `center` 的前面;
|
||||
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
controller: scroller,
|
||||
center: centerKey,
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
var item = loadMoreData[index];
|
||||
if (item.type == "Right")
|
||||
return renderRightItem(item);
|
||||
else
|
||||
return renderLeftItem(item);
|
||||
},
|
||||
childCount: loadMoreData.length,
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.zero,
|
||||
key: centerKey,
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
var item = newData[index];
|
||||
if (item.type == "Right")
|
||||
return renderRightItem(item);
|
||||
else
|
||||
return renderLeftItem(item);
|
||||
},
|
||||
childCount: newData.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
是不是很简单,就这?运行后也如下图所示,可以看到运行后的代码不会再有空白的情况,也没有新增数据跳动的情况,双向滑动也正常,那你知道为什么吗?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image5)
|
||||
|
||||
|
||||
如下图所示,调整后从结构上变成了右边的逻辑:
|
||||
|
||||
- 数据起始锚点在页面顶部,所以不会存在顶部留空问题;
|
||||
- 在 `center` 下面的 `SliverList` 按照正向排序正常显示,用于显示新数据;
|
||||
- 在 `center` 上面的 `SliverList` 列表会被变成以 `center` 为起点反向顺序显示,用于加载旧数据;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image6)
|
||||
|
||||
当然,这里有一点需要注意的局就是:**起始进来时加载的第一页数据应该是用绿色的正向 `SliverList` ,因为起始点在顶部,如果不用下面绿色的正向 `SliverList` ,就会导致第一次数据看不到的情况**。
|
||||
|
||||
这时候就有人可能会说,如果是下图所示场景,只加载旧数据,不加载新数据,那不就出现底部留空了吗?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image7)
|
||||
|
||||
是的,**我们其实是把顶部留空的问题转移到了底部,但是这个问题在实际业务场景是不成立**,进入聊天列表首先就需要先加载满一页的数据,所以:
|
||||
|
||||
- 如果 old 数据本来就不够,例如例子里只有3条,那也就不会有加载更多 old 数据的场景,所以不会产生滑动;
|
||||
- 如果 old 数据足够,那默认就足以撑满列表;
|
||||
|
||||
而随着 new 数据的增加,页面也会被填满从而可以正常滑动并且充满,所以从这个实现上看会更加合理。
|
||||
|
||||
那有人可能会说,就这?还有什么可以优化的小技巧? 比如增加**判断列表是否处于底部,决定在接受到新数据时是否滑动到最新消息。**
|
||||
|
||||
|
||||
实现这个优化也很简单,首先我们可以嵌套一个 `NotificationListener` , 在这里我们主要是获取 `notification.metrics.extentAfter` 这个参数。
|
||||
|
||||
```dart
|
||||
NotificationListener(
|
||||
onNotification: (notification) {
|
||||
if (notification is ScrollNotification) {
|
||||
if (notification.metrics is PageMetrics) {
|
||||
return false;
|
||||
}
|
||||
if (notification.metrics is FixedScrollMetrics) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.left ||
|
||||
notification.metrics.axisDirection == AxisDirection.right) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
///取到这个值
|
||||
extentAfter = notification.metrics.extentAfter;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
> 这里的 `if` 判断,只是为了规避其他控件的影响,比如列表里的 `PageView` 或者 `TextFiled` 的影响。
|
||||
|
||||
那 `extentAfter` 参数的作用是什么? 事实上在 `FixedScrollMetrics` 里有 `extentBefore` 、 `extentInside` 和 `extentAfter` 三个参数,它们的关系类似下图所示:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image8)
|
||||
|
||||
一般情况下:
|
||||
|
||||
- `extentInside` 就是视图窗口大小;
|
||||
- `extentBefore` 就是前面还可以滑动距离;
|
||||
- `extentAfter` 就是后面还可以滑动距离;
|
||||
|
||||
**所以我们只需要判断 `extentAfter` 是否为 0 ,就可以判断列表是不是处于底部** ,从而针对场景首先不同的业务逻辑,例如下图所示,针对列表是否处于底部,在接收到新数据时是直接跳到最新数据,还是弹出提示用让用户点击跳转。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Chat2/image9)
|
||||
|
||||
```dart
|
||||
if (extentAfter == 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text("你目前位于最底部,自动跳转新消息item"),
|
||||
duration: Duration(milliseconds: 1000),
|
||||
));
|
||||
Future.delayed(Duration(milliseconds: 200), () {
|
||||
scroller.jumpTo(scroller.position.maxScrollExtent);
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: InkWell(
|
||||
onTap: () {
|
||||
scroller.jumpTo(scroller.position.maxScrollExtent);
|
||||
},
|
||||
child:Text("点击我自动跳转新消息item")
|
||||
),
|
||||
duration: Duration(milliseconds: 1000),
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
所以从聊天列表的场景上看,实现一个聊天列表并不难,但是需要优化的细节可能会很多,如果你在这方面还有什么问题,欢迎评论交流。
|
||||
|
||||
|
||||
> 实例代码可见:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/chat_list_scroll_demo_page_2.dart
|
205
Flutter-DWN.md
205
Flutter-DWN.md
|
@ -1,205 +0,0 @@
|
|||
# 掘金x得物公开课 - Flutter 3.0下的混合开发演进
|
||||
|
||||
hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 [Flutter GDE](https://juejin.cn/post/7102242694755254279),借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开发的技术演进。
|
||||
|
||||
为什么混合开发在 Flutter 里是特殊的存在?因为它渲染的控件是通过 Skia 直接和 GPU 交互,也就是说 Flutter 控件和平台无关,甚至连 UI 绘制线程都和原生平台 UI 线程是相互独立,**所以甚至于 Flutter 在诞生之初都不支持和原生平台的控件进行混合开发,也就是不支持 `WebView` ,这就成了当时最大的缺陷之一** 。
|
||||
|
||||
其实从渲染的角度看 Flutter 更像是一个 2D 游戏引擎,事实上 Flutter 在这次 Google I/O 也分享了基于 [Flutter 的游戏开发 ToolKit 和第三方工具包 Flame](https://juejin.cn/post/7103284735010406407) ,如图所示就是本次 Google I/O 发布的 Pinball 小游戏,所以从这些角度上看都可以看出 Flutter 在混合开发的特殊性。
|
||||
|
||||
> **如果说的更形象简单一点,那就是如何把原生控件渲染到 `WebView` 里**。
|
||||
|
||||
|
||||
|
||||
![TT](http://img.cdn.guoshuyu.cn/20220626_DWN/image2.gif)
|
||||
|
||||
# 最初的社区支持
|
||||
|
||||
不支持 `WebView` 在最初可以说是 Flutter 最大的痛点之一,所以在这样窘迫的情况下,社区里涌现出一些临时的解决方法,比如 `flutter_webview_plugin` 。
|
||||
|
||||
类似 `flutter_webview_plugin` 的出现,解决了当时大部分时候 App 里打开一个网页的简单需求,如下图所示,它的思路就是:
|
||||
|
||||
> 在 Flutter 层面放一个占位控件提供大小,然后原生层在同样的位置把 ` WebView` 添加进去,从而达到看起来把 ` WebView` 集成进去的效果,**这个思路在后续也一直被沿用**。
|
||||
|
||||
![image-20220625170833702](http://img.cdn.guoshuyu.cn/20220626_DWN/image3.png)
|
||||
|
||||
**这样的实现方式无疑成本最低速度最快,但是也带来了很多的局限性**。
|
||||
|
||||
相信大家也能想到,**因为 Flutter 的所有控件都是渲染一个 `FlutterView` 上,也就是从原生的角度其实是一个单页面的效果**,所以这种脱离 Flutter 渲染树的添加控件的方法,无疑是没办法和 Flutter 融合到一起,举个例子:
|
||||
|
||||
- 如图一所示,从 Flutter 页面跳到 Native 页面的时候,打开动画无法同步,因为 `AppBar` 是 Flutter 的,而 Native 是原生层,它们不在同一个渲染树内,所以无法实现同步的动画效果
|
||||
- 如图二所示,比如在打开 Native 页面之后,通过 `Appbar` 再打开一个黄色的 Bottm Sheet ,可以看到此时黄色的 Bottm Sheet 打开了,但是却被 Native 遮挡住(Demo 里给 Native 设置了透明色),因为 Flutter 的 Bottm Sheet 是被渲染在 `FlutterView` 里面,而 Native UI 把 `FlutterView` 挡住了,所以新的 `Flutter UI` 自然也被遮挡
|
||||
- 如图三所示,当我们通过 reload 重刷 Flutter UI 之后,可以看到 Flutter 得 UI 都被重置了,但是此时 Native UI 还在,因为此时已经没有返回按键之类的无法关闭,这也是这种集成方式一不小心就影响开发的问题
|
||||
- 如图四通过 iOS 上的 debug 图层,我们可以更形象地看到这种方式的实现逻辑和堆叠效果
|
||||
|
||||
| 动画不同步 | 页面被挡 | reload 之后 | iOS |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| ![11111111](http://img.cdn.guoshuyu.cn/20220626_DWN/image4.gif) | ![222222222](http://img.cdn.guoshuyu.cn/20220626_DWN/image5.gif) | ![333333](http://img.cdn.guoshuyu.cn/20220626_DWN/image6.gif) | ![image-20220616142126589](http://img.cdn.guoshuyu.cn/20220626_DWN/image7.png) |
|
||||
|
||||
# PlatformView
|
||||
|
||||
随着 Flutter 的发展,官方支持混合开发势在必行,所以第一代 `PlatformView` 的支持还是诞生了,但是由于 Android 和 iOS 平台特性的不同,最初Android 的 `AndroidView` 和 iOS 的 `UIKitView` 实现逻辑相差甚远,**以至于后面 Flutter 的 `PlatformView` 的每次大调整都是围绕于 Android 在做优化** 。
|
||||
|
||||
### Android
|
||||
|
||||
最初 Flutter 在 Android 上对 `PlatformView` 的支持是通过 `VirtualDisplay` 实现,`VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,`VirtualDisplay` 一般在副屏显示或者录屏场景下会用到,而在 Flutter 里 `VirtualDisplay` 会将虚拟显示区域的内容渲染在一个内存 `Surface`上。
|
||||
|
||||
**在 Flutter 中通过将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中 ,然后通过 textureId 在 `VirtualDisplay` 对应的内存中提取绘制的纹理**, 简单看实现逻辑如下图所示:
|
||||
|
||||
![image-20220626151538054](http://img.cdn.guoshuyu.cn/20220626_DWN/image8.png)
|
||||
|
||||
> 这里其实也是类似于最初社区支持的模式:通过在 Dart 层提供一个 `AndroidView` ,从而获取到控件所需的大小,位置等参数,当然这里多了一个 `textureId` ,这个 id 主要是提交给 Flutter Engine ,通过 id Flutter 就可以在渲染时将画面从内存里提出出来。
|
||||
|
||||
### iOS
|
||||
|
||||
在 iOS 平台上就不使用类似 `VirtualDisplay` 的方法,而是**通过将 Flutter UI 分为两个透明纹理来完成组合**,这种方式无疑更符合 Flutter 社区的理念,这样的好处是:
|
||||
|
||||
> 需要在 `PlatformView` 下方呈现的 Flutter UI 可以被绘制到其下方的纹理;而需要在 `PlatformView` 上方呈现的 Flutter UI 可以被绘制到其上方的纹理, 它们只需要在最后组合起来就可以了。
|
||||
|
||||
是不是有点抽象?
|
||||
|
||||
简单看下面这张图,其实就是通过在 `NativeView` 的不同层级设置不同的透明图层,然后把不同位置的控件渲染到不同图层,最终达到组合起来的效果。
|
||||
|
||||
![image-20220626151526444](http://img.cdn.guoshuyu.cn/20220626_DWN/image9.png)
|
||||
|
||||
那明明这种方法更好,为什么 Android 不一开始也这样实现呢?
|
||||
|
||||
因为当时在实现思路上, `VirtualDisplay` 的实现模式并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:*当 iOS 视图向下移动 `2px` 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 `2px`*。
|
||||
|
||||
但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。**如果强行以这种方式在 Android 上使用,最终将产生很多如 `AndroidView` 与 Flutter UI 不同步的问题**。
|
||||
|
||||
### 问题
|
||||
|
||||
事实上 `VirtualDisplay` 的实现方式也带来和很多问题,简单说两个大家最直观的体会:
|
||||
|
||||
#### 触摸事件
|
||||
|
||||
因为控件是被渲染在内存里,虽然**你在 UI 上看到它就在那里,但是事实上它并不在那里**,你点击到的是 `FlutterView `,所以**用户产生的触摸事件是直接发送到 `FlutterView`**。
|
||||
|
||||
所以触摸事件需要在 `FlutterView` 到 Dart ,再从 Dart 转发到原生,然后如果原生不处理又要转发回 Flutter ,如果中间还存在其他派生视图,事件就很容易出现丢失和无法响应,而这个过程对于 `FlutterView` 来说,在原生层它只有一个 View 。
|
||||
|
||||
所以 Android 的 `MotionEvent` 在转化到 Flutter 过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。
|
||||
|
||||
#### 文字输入
|
||||
|
||||
一般情况下 **`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的内存位置会始终被认为是 `unfocused` 的状态**。
|
||||
|
||||
> `InputConnections` 在 `unfocused` 的 View 中通常是会被丢弃。
|
||||
|
||||
所以 **Flutter 重写了 `checkInputConnectionProxy` 方法,这样 Android 会认为 `FlutterView` 是作为 `AndroidView` 和输入法编辑器(IME)的代理**,这样 Android 就可以从 `FlutterView` 中获取到 `InputConnections` 然后作用于 `AndroidView` 上面。
|
||||
|
||||
> 在 Android Q 开始又因为非全局的 `InputMethodManager` 需要新的兼容
|
||||
|
||||
当然还有诸如性能等其他问题,但是至少先有了支持,有了开始才会有后续的进阶,在 Flutter 3.0 之前, `VirtualDisplay` 一直默默在 `PlatformView` 的背后耕耘。
|
||||
|
||||
# HybridComposition
|
||||
|
||||
时间来到 Flutter 1.2,Hybrid Composition 是在 Flutter 1.2 时发布的 Android 混合开发实现,它使用了类似 iOS 的实现思路,提供了 Flutter 在 Android 上的另外一种 `PlatformView` 的实现。
|
||||
|
||||
如下图是在 Dart 层使用 `VirtualDisplay` 切换到 `HybridComposition` 模式的区别,最直观的感受应该是需要写的 Dart 代码变多了。
|
||||
|
||||
![111111](http://img.cdn.guoshuyu.cn/20220626_DWN/image10.png)
|
||||
|
||||
但是其实 `HybridComposition` 的实现逻辑是变简单了: **`PlatformView` 是通过 `FlutterMutatorView` 把原生控件 `addView` 到 `FlutterView` 上,然后再通过 `FlutterImageView` 的能力去实现图层的混合**。
|
||||
|
||||
> 又懵了?不怕,马上你就懂了
|
||||
|
||||
简单来说就是 `HybridComposition` 模式会直接把原生控件通过 `addView` 添加到 `FlutterView` 上 。**这时候大家可能会说,咦~这不是和最初的实现一样吗?怎么逻辑又回去了** ?
|
||||
|
||||
> 其实确实是社区的进阶版实现,Flutter 直接通过原生的 `addView` 方法将 `PlatformView` 添加到 `FlutterView` 里,而当你还需要在 `PlatformView` 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 `FlutterImageView` 来承载这个 Widget 的纹理。
|
||||
|
||||
举一个简单的例子,如下图所示,一个原生的 `TextView` 被通过 `HybridComposition` 模式接入到 Flutter 里(`NativeView`),而在 Android 的显示布局边界和 Layout Inspector 上可以清晰看到: **灰色 `TextView` 通过 `FlutterMutatorView` 被添加到 `FlutterView` 上被直接显示出来** 。
|
||||
|
||||
![image-20220618152055492](http://img.cdn.guoshuyu.cn/20220626_DWN/image11.png)
|
||||
|
||||
**所以在 `HybridComposition` 里 `TextView` 是直接在原生代码上被 add 到 `FlutterView` 上,而不是提取纹理**。
|
||||
|
||||
那如果我们看一个复杂一点的案例,如下图所示,其中蓝色的文本是原生的 `TextView` ,红色的文本是 Flutter 的 `Text` 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:
|
||||
|
||||
- 两个蓝色的 `TextView` 是通过 `FlutterMutatorView` 被添加在 `FlutterView` 之上,并且把没有背景色的红色 RE 遮挡住了
|
||||
- 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 `TextView` 之上,所以这时候多一个 `FlutterImageView` ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。
|
||||
|
||||
![image-20220616165047353](http://img.cdn.guoshuyu.cn/20220626_DWN/image12.png)
|
||||
|
||||
**可以看到 `Hybrid Composition` 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生角度看它就是原生层面的物理堆叠,需要都一个层级就多加一个 `FlutterImageView` ,同一个层级的 Flutter 控件共享一个 `FlutterImageView`** 。
|
||||
|
||||
当然,在 `HybridComposition` 里 `FlutterImageView` 也是一个很有故事的对象,由于篇幅原因这里就不详细展开,这里大家可以简单看这张图感受下,也就是在有 `PlatformView` 和没有 `PlatformView` 是,Flutter 的渲染会有一个转化的过程,而**在这个变化过程,在 Flutter 3.0 之前可以通过 ` PlatformViewsService.synchronizeToNativeViewHierarchy(false);` 取消**。
|
||||
|
||||
![image-20220618153757996](http://img.cdn.guoshuyu.cn/20220626_DWN/image13.png)
|
||||
|
||||
最后,Hybrid Composition 也不少问题,比如上面的转化就是为了解决动画同步问题,当然这个行为也会产生一些性能开销,例如:
|
||||
|
||||
> 在 Android 10 之前, *Hybrid Composition* 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 *Hybrid Composition* 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 *Hybrid Composition* 的 `PlatformView` ,就可能会变卡顿甚至闪烁。
|
||||
|
||||
其他还有线程同步,闪烁等问题,由于篇幅就不详细展开,如果感兴趣的可以详细看我之前发布过的 [《Flutter 深入探索混合开发的技术演进》](https://juejin.cn/post/7093858055439253534) 。
|
||||
|
||||
|
||||
|
||||
# TextureLayer
|
||||
|
||||
随着 Flutter 3.0 的发布,第一代 `PlatformView` 的实现 `VirtualDisplay` 被新的 `TextureLayer` 所替代,如下图所示,简单对比 `VirtualDisplay` 和 `TextureLayer` 的实现差异,**可以看到主要还是在于原生控件纹理的提取方式上**。
|
||||
|
||||
|
||||
|
||||
![image-20220618154327890](http://img.cdn.guoshuyu.cn/20220626_DWN/image14.png)
|
||||
|
||||
从上图我们可以得知:
|
||||
|
||||
- 从 `VirtualDisplay` 到 `TextureLayer` , **Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑**;
|
||||
- 以前 Flutter 中会将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到;**现在 `AndroidView` 需要的内容,会通过 View 的 `draw` 方法被绘制到 `SurfaceTexture` 里,然后同样通过 `TextureId` 获取绘制在内存的纹理** ;
|
||||
|
||||
是不是又有点蒙?简单说就是不需要绘制到副屏里,现在直接通过 override `View` 的 `draw` 方法就可以了。
|
||||
|
||||
在 *TextureLayer* 的实现里,同样是需要**把控件添加到一个 `PlatformViewWrapper` 的原生布局控件里,但是这个控件通过 override 了 `View` 的 `draw` 方法,把原本的 Canvas 替换成 `SurfaceTexture` 在内存的 Canvas ,所以 `PlatformViewWrapper` 的 child 会把控件绘制到内存的 `SurfaceTexture` 上。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220626_DWN/image15.png)
|
||||
|
||||
举个例子,还是之前的代码,如下图所示,这时候通过 *TextureLayer* 模式运行之后,通过 Layout Inspector 的 3D 图层可以看到,两个原生的 `TextView` 通过 `PlatformViewWrapper` 被添加到 ` FlutterView` 上。
|
||||
|
||||
但是不同的是,**在 3D 图层里看不到 `TextView` 的内容,因为绘制 `TextView` 的 Canvas 被替换了**,所以 `TextView` 的内容被绘制到内存的 Surface 上,最终会在渲染时同步 Flutter Engine 里。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220626_DWN/image16.png)
|
||||
|
||||
看到这里,你可能也发现了,这时候因为有 `PlatformViewWrapper` 的存在,点击会被 `PlatformViewWrapper` 内部拦截,从而也解决了触摸的问题, 而这里刚好有人提了一个问题,如下图所示:
|
||||
|
||||
> "从图 1 Layout Inspector 看, `PlatformWrapperView` 是在 `FlutterSurfaceView` 上方,为什么如图 2 所示,点击 Flutter button 却可以不触发 native button的点击效果?"。
|
||||
|
||||
| 图1 | 图2 |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| ![image.png](http://img.cdn.guoshuyu.cn/20220626_DWN/image17) | ![img](http://img.cdn.guoshuyu.cn/20220626_DWN/image18) |
|
||||
|
||||
思考一下,因为最直观的感受:**点击不都是被 `PlatformViewWrapper` 拦截了吗?明明 `PlatformViewWrapper` 是在 `FlutterSurfaceView` 之上,为什么 `FlutterSurfaceView` 里的 FlutterButton 还能被点击到**?
|
||||
|
||||
这里简单解释一下:
|
||||
|
||||
- 1、首先那个 Button 并不是真的被摆放在那里,而是通过 `PlatformViewWrapper` 的 `super.draw`绘制到 surface 上的,所以在那里的是 `PlatformViewWrapper` ,而不是 Button ,**Button 的内容已经变成纹理去到了 `FlutterSurfaceView` 里面**。
|
||||
- 2、 **`PlatformViewWrapper` 里重写了 `onInterceptTouchEvent` 做了拦截**,`onInterceptTouchEvent` 这个事件是从父控件开始往子控件传,因为拦截了所以不会让 Button 直接响应,然后在 `PlatformViewWrapper` 的 `onTouchEvent` 响应里是做了点击区域的分发,响应会分发到了 `AndroidTouchProcessor` 之后,会打包发到 `_unpackPointerDataPacket` 进入 Dart
|
||||
- 3、 在 Dart 层的点击区域,如果没有 Flutter 控件响应,会是 `_PlatformViewGestureRecognizer`-> `updateGestureRecognizers` -> `dispatchPointerEvent` -> `sendMotionEvent` 又发送回原生层
|
||||
- 4、回到原生 `PlatformViewsController` 的 `createForTextureLayer` 里的 `onTouch` ,执行 `view.dispatchTouchEvent(event);`
|
||||
|
||||
![image-20220625171101069](http://img.cdn.guoshuyu.cn/20220626_DWN/image19.png)
|
||||
|
||||
总结起来就是:**`PlatfromViewWrapper` 拦截了 Event ,通过 Dart 做二次分发响应,从而实现不同的事件响应 ** ,它和 VirtualDisplay 的不同是, VirtualDisplay 的事件响应都是在 `FlutterView` 上,但是TextureLayout 模式,是有独立的原生 `PlatfromViewWrapper` 控件来开始,所以区域效果和一致性会更好。
|
||||
|
||||
### 问题
|
||||
|
||||
最后这里还需要提个醒,如果你之前使用的插件使用的是 `HybirdComposition ` ,但是没做兼容,也就是使用的还是 `PlatformViewsService.initSurfaceAndroidView` 的话,它也会切换成 `TextureLayer` 的逻辑,**所以你需要切换为 `PlatformViewsService.initExpensiveAndroidView` ,才能继续使用原本 `HybirdComposition ` 的效果**。
|
||||
|
||||
> ⚠️我也比较奇怪为什么 Flutter 3.0 没有提及 Android 这个 breaking change ,因为对于开发来说其实是无感的,不小心就掉坑里。
|
||||
|
||||
那你说为什么还要 `HybirdComposition ` ?
|
||||
|
||||
前面我们说过, `TextureLayer` 是通过在 `super.draw` 替换 Canvas 的方法去实现绘制,但是它替换不了 `Surface` 里的一些 Canvas ,所以比如一些需要 `SurfaceView` 、`TextureView` 或者有自己内部特殊 `Canvas` 的场景,你还是需要 `HybirdComposition ` ,只不过可能会和官方新的 API 名字一样,它 Expensive 。
|
||||
|
||||
Expensive 是因为在 Flutter 3.0 正式版开始,`FlutterView` 在使用 `HybirdComposition ` 时一定会 converted to `FlutterImageView` ,这也是 Flutter 3.0 下一个需要注意的点。
|
||||
|
||||
![image-20220616170253242](http://img.cdn.guoshuyu.cn/20220626_DWN/image20.png)
|
||||
|
||||
> 更多内容可见 [《Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer》](https://juejin.cn/post/7098275267818291236)
|
||||
|
||||
![image-20220625164049356](http://img.cdn.guoshuyu.cn/20220626_DWN/image21.png)
|
||||
|
||||
# 最后
|
||||
|
||||
最后做个总结,可以看到 Flutter 为了混合开发做了很多的努力,特别是在 Android 上,也是因为历史埋坑的原因,由于时间关系这里没办法都详细介绍,但是相信本次之后大家对 Flutter 的 `PlatformView` 实现都有了全面的了解,这对大家在未来使用 Flutter 也会有很好的帮助,如果你还有什么问题,欢迎交流。
|
||||
|
||||
![image-20220626151444011](http://img.cdn.guoshuyu.cn/20220626_DWN/image22.png)
|
661
Flutter-DWW.md
661
Flutter-DWW.md
|
@ -1,661 +0,0 @@
|
|||
# Flutter 深入探索混合开发的技术演进
|
||||
|
||||
关于 Flutter 混合 `PlatformView` 的实现已经介绍过两次,随着 5 月份谷歌 IO 的接近,新的 `PlatformView` 实现应该也会随之而来,本次就从头到尾来一个详细的关于 `PlatformView` 的演进总结。
|
||||
|
||||
> Flutter 作为新一代的跨平台框架,通过自定义渲染引擎的创新大大提高了跨平台的性能和一致性,但也正是因为这点, 相比之下 Flutter 在混合开发时对于原生控件的支持成本更高。
|
||||
|
||||
## Flutter 混合开发的难点
|
||||
|
||||
首先 Flutter 在混合开发中最大的难点就在于它独立的渲染引擎,举一个不是很恰当的例子:
|
||||
|
||||
> Flutter 里混合开发类似与把原生控件渲染到 `WebView ` 里。
|
||||
|
||||
大致上在 Flutter 里混合开发的感觉就是这样,因为 **Flutter UI 不会转换为原生控件,而是由 Flutter Engine 使用 Skia 直接渲染在 `Surface` 上**。
|
||||
|
||||
所以 Flutter 在最早出来时并不支持 `WebView` 或 `MapView` 这些常用的控件,这也导致了当时 Flutter 一度的风评不大好,所以衍生出了第一代非官方的混合开发支持,例如: `flutter_webview_plugin `。
|
||||
|
||||
**在官方 `WebView` 控件支持出来之前** ,第三方是直接在 `FlutterView` 上覆盖了一个新的原生控件,利用 Dart 中的占位控件来**传递位置和大小**。
|
||||
|
||||
> Flutter 里几乎所有渲染都是渲染到 `FlutterView` 这样一个单页面上,所以直接覆盖一个新的原生 `WebView` 只能说缓解燃眉之急。
|
||||
|
||||
如下图,在 Flutter 端 `push` 出来一个 **设定好位置和大小** 的 `SingleChildRenderObjectWidget` ,从而得到需要显示的大小和位置,将这些信息通过 `MethodChannel` 传递到原生层,在原生层 `addContentView` 一个指定大小和位置的 `WebView` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image1.png)
|
||||
|
||||
这样看起来就像是在 Flutter 中添加了 `WebView` ,但实际这样的操作只能说是“救急”,**因为这样的行为脱离了 Flutter 的渲染树**,其中一个问题就是:
|
||||
|
||||
> 当你跳转 Flutter 其他页面的时候会被当前原生的 `WebView` 挡住;并且打开页面的动画时`Appbar` 和 `WebView` 难以保持一致,因为 `Appbar` 和 `WebView` 是出于两个动画体系和渲染体系。
|
||||
|
||||
就比如打开了新的 Flutter UI 2 页面,但是由于它还是在 `FlutterView` 内,所以它会被 `WebView` 所遮挡。
|
||||
|
||||
![image-20220307175357032](http://img.cdn.guoshuyu.cn/20220309_DWZB/image2.png)
|
||||
|
||||
但是这个“占位”显示的思路,也起到了一定的作用,在后续 Flutter 支持原生 `PlatformView` 上起到了带头的作用。
|
||||
|
||||
## Flutter 初步支持原生控件
|
||||
|
||||
为了让 Flutter 真正走向大众化,官方开始推出了官方基于 `PlatformView` 的系列实现,比如: `webview_flutter` ,而这个实现 “缝缝补补” 也被沿用至今,成了 Flutter 接入原生的方式 之一。
|
||||
|
||||
### Android
|
||||
|
||||
在 `PlatformView` 的整个实现中 Android 坑一直是最多的,因为一开始 Android 上主要是通过 `AndroidView` 做完成这项工作,而它的 *Virtual Displays* 实现其实并不友好。
|
||||
|
||||
**在 Flutter 中会将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中 ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到**。
|
||||
|
||||
> `VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,一般在副屏显示或者录屏场景下会用到,在 `VirtualDisplay` 里会将虚拟显示区域的内容渲染在一个 `Surface` 上。
|
||||
|
||||
![image-20220307180859494](http://img.cdn.guoshuyu.cn/20220309_DWZB/image3.png)
|
||||
|
||||
如上图所示,**简单来说就是原生控件的内容被绘制到内存里,然后 Flutter Engine 通过相对应的 `textureId` 就可以获取到控件的渲染数据并显示出来**,这个过程 `AndroidView` 这个占位控件提供了 size、offset 等位置和大小参数。
|
||||
|
||||
通过从 `VirtualDisplay` 获取纹理,并将其和 Flutter 原有的 UI 渲染树混合,使得 Flutter 可以在自己的 Flutter Widget tree 中以图形方式插入 Android 原生控件。
|
||||
|
||||
### iOS
|
||||
|
||||
在 iOS 平台上就不使用类似 `VirtualDisplay` 的方法,而是**通过将 Flutter UI 分为两个透明纹理来完成组合:一个在 iOS 平台视图之下,一个在其上面**。
|
||||
|
||||
所以这样的好处就是:
|
||||
|
||||
- 需要在 “iOS平台” 视图下方呈现的Flutter UI,最终会被绘制到其下方的纹理上;
|
||||
- 而需要在 “平台” 上方呈现的 Flutter UI,最终会被绘制在其上方的纹理;
|
||||
|
||||
iOS 上**它们只需要在最后组合起来就可以了**,通常这种方法更好,因为这意味着 Native View 可以直接添加到 Flutter 的 UI 层次结构中,但是可惜一开始 Android 平台并不支持这种模式。
|
||||
|
||||
### 问题
|
||||
|
||||
尽管前面可以使用 `VirtualDisplay` 将 Android 控件嵌入到 Flutter UI 中 ,但这种 `VirtualDisplay` 这种介入还有其他麻烦的问题需要处理。
|
||||
|
||||
#### 触摸事件
|
||||
|
||||
**默认情况下, `PlatformViews` 是没办法接收触摸事件**,因为 `AndroidView` 其实是被渲染在 `VirtualDisplay` 中 ,而每当用户点击看到的 `"AndroidView"` 时,其实他们就真正”点击的是正在渲染的 `Flutter` 纹理 ,**用户产生的触摸事件是直接发送到 Flutter View 中,而不是他们实际点击的 `AndroidView`**。
|
||||
|
||||
所以 `AndroidView` 使用 Flutter Framework 中检测用户的触摸是否在需要的特殊处理的区域内:
|
||||
|
||||
> 当触摸成功时会向 Android embedding 发送一条消息,其中包含 touch 事件的详细信息。
|
||||
|
||||
这就变成有些本末倒置,触摸事件从原生-Flutter-原生,中间的转化导致某些信息被丢失,也导致了响应的延迟。
|
||||
|
||||
#### 文字输入
|
||||
|
||||
**`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的位置会始终被认为是 `unfocused` 的状态**。
|
||||
|
||||
所以需要做一套代理来处理 `InputConnections` 做输入,甚至这个行为在 `WebView` 上更复杂,因为 `WebView` 具有自己内部的逻辑来创建和设置输入连接,而这些输入连接并没有完全遵循 Android 的协议。
|
||||
|
||||
### 同步问题
|
||||
|
||||
另外还需要处理各种同步问题,比如官方就创建了一个 `SurfaceTextureWrapper` 用于处理同步的问题。
|
||||
|
||||
因为当承载 `AndroidView` 的 `SurfaceTexture` 被释放时,由于 `SurfaceTexture.release` 是在 platform 线程被调用,而 `attachToGLContext `是在 raster 线程被调用,不同线程调用时可能导致:**当 `attachToGLContext `被调用时 texture 已经被释放了,所以需要 `SurfaceTextureWrapper` 用于实现 Java 里同步锁的效果**。
|
||||
|
||||
|
||||
|
||||
## Flutter Hybrid Composition
|
||||
|
||||
|
||||
|
||||
所以经历了 *Virtual Display* 的折磨之后,官方终于在后续推出了更为合理的实现。
|
||||
|
||||
### 实现逻辑
|
||||
|
||||
*hybrid composition* 的出现给 Flutter 提供了一种新的混合思路,**那就是直接把原生控件添加到 Flutter 里一起组合渲染**。
|
||||
|
||||
首先简单介绍下使用,比起 *Virtual Display* 直接使用 `AndroidView` ,*hybrid composition* 相对会复杂一点点,dart 里使用到 `PlatformViewLink` 、`AndroidViewSurface` 、 `PlatformViewsService` 这三个对象。
|
||||
|
||||
正常在 dart 层面,使用 *hybrid composition* 接入原生控件:
|
||||
|
||||
- 通过 `PlatformViewLink` 的 `viewType` 注册了一个和原生层对应的注册名称,这和之前的 `PlatformView` 注册一样;
|
||||
- 然后在 `surfaceFactory` 返回一个 `AndroidViewSurface` 用于处理绘制和接收触摸事件,同时也是一个类似占位的作用;
|
||||
- 最后在 `onCreatePlatformView` 方法使用 `PlatformViewsService` 初始化 `AndroidViewSurface` 和初始化所需要的参数,同时通过 Engine 去触发原生层的显示。
|
||||
|
||||
```dart
|
||||
Widget build(BuildContext context) {
|
||||
// This is used in the platform side to register the view.
|
||||
final String viewType = 'hybrid-view-type';
|
||||
// Pass parameters to the platform side.
|
||||
final Map<String, dynamic> creationParams = <String, dynamic>{};
|
||||
|
||||
return PlatformViewLink(
|
||||
viewType: viewType,
|
||||
surfaceFactory:
|
||||
(BuildContext context, PlatformViewController controller) {
|
||||
return AndroidViewSurface(
|
||||
controller: controller,
|
||||
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
|
||||
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
||||
);
|
||||
},
|
||||
onCreatePlatformView: (PlatformViewCreationParams params) {
|
||||
return PlatformViewsService.initSurfaceAndroidView(
|
||||
id: params.id,
|
||||
viewType: viewType,
|
||||
layoutDirection: TextDirection.ltr,
|
||||
creationParams: creationParams,
|
||||
creationParamsCodec: StandardMessageCodec(),
|
||||
)
|
||||
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
|
||||
..create();
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
看起来好像是把一个 `AndroidView` 完成的事情变得相对复杂了,但是其实 *hybrid composition* 的实现相比其实更好理解。
|
||||
|
||||
使用 *hybrid composition* 之后, **`PlatformView` 就直接通过 `FlutterMutatorView`(一个特殊的 `FrameLayout`) 把原生控件 `addView` 到 `FlutterView`上,然后再通过 `FlutterImageView` 的能力去实现多图层的混合**。
|
||||
|
||||
不理解吗?没事,我们后面会详细介绍,先简单解释就是:
|
||||
|
||||
> Flutter 只直接通过原生的 `addView` 方法将 `PlatformView` 添加到 `FlutterView` ,这就不需要什么 `surface `渲染再去获取的开销,而当你还需要再 `PlatformView` 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 `FlutterImageView` 来承载这个 Flutter Widget 。
|
||||
|
||||
![image-20220308173844917](http://img.cdn.guoshuyu.cn/20220309_DWZB/image4.png)
|
||||
|
||||
|
||||
|
||||
### 深入例子详解
|
||||
|
||||
|
||||
|
||||
接下来让我们从实际例子去理解 *Hybrid Composition* ,结合 Andriod Studio 的 Layout Inspector,并开启手机的绘制边界来看会更直观。
|
||||
|
||||
如下代码所示,一般情况下我们运行之后会看到一片黑色,因为这时候 `FlutterView` 只有一个 `FlutterSurfaceView` 的子控件存在,此时虽然我们画面上是有一个 Flutter 的红色 `RE` 文本控件 ,不过因为是由 Flutter 直接在 Surface 直接绘制,所以 Layout Inspector 看不到只显示黑色。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image5.png)
|
||||
|
||||
此时我们添加一个通过 *Hybrid Composition* 实现一个原生的 `TextView` 控件,通过 `PlatformView` 在 Flutter 上渲染出一个灰色 `RE` 文本。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.6, -0.6),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
![image-20220308110032575](http://img.cdn.guoshuyu.cn/20220309_DWZB/image6.png)
|
||||
|
||||
可以看到,如上图所示,在我们的显示布局边界上可以清晰看到它的信息:
|
||||
|
||||
> `TextView` 通过 `FlutterMutatorView` 被添加到 `FlutterView` 上被直接显示出来。
|
||||
|
||||
**所以 `TextView` 是直接在原生代码上被 add 到 `FlutterView` 上,而不是提取纹理**,另外可以看到,左侧栏里多了一个 `FlutterImageView` ,并且之前看不到的 Flutter 控件红色 `RE` 文本也出现了,背景也变成了 Flutter 上的白色。
|
||||
|
||||
我们先暂时忽略新出现的 `FlutterImageView` 和 Flutter UI 能够出现在 Layout Inspector 的原因,留到后面再来分析,此时我们再新增加以一个红色的 Flutter `RE` 控件到` Stack` 里,位于 `PlatformView` 的灰色 `RE` 下。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.4, -0.4),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(-0.6, -0.6),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image7.png)
|
||||
|
||||
如上图所示,可以看到布局和渲染效果正常,Flutter 的红色 `RE ` 被上面的 `PlatformView` 灰色 `RE` 遮挡了部分,这是符合代码的渲染效果。
|
||||
|
||||
如果这时候我们把新增加的第二个红色 `RE` 放到灰色 `PlatformView` 灰色 `RE` 上,会发生什么情况?
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.6, -0.6),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(-0.4, -0.4),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
![image-20220308110325717](http://img.cdn.guoshuyu.cn/20220309_DWZB/image8.png)
|
||||
|
||||
可以看到红色的 `RE` 成功被渲染到灰色 `RE` 上 ,而之所以能够渲染上去的原因,是因为这个和 `PlatformView` 有交集的 `Text` ,被渲染到一个新增的 `FlutterImageView` 控件上, 也就是 Flutter 判断了此时新红色 `RE` 文本需要渲染到 `PlatformView` 上,所以添加了一个 `FlutterImageView` 用于承载这部分渲染内容。
|
||||
|
||||
如果这时候挪动第二个红色的 `RE` 让它和 `PlatformView` 没有交集,但是还是在 `Stack` 里位于 `PlatformView` 之上会如何?
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.6, -0.6),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(-0.8, -0.8),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
![image-20220308110754283](http://img.cdn.guoshuyu.cn/20220309_DWZB/image9.png)
|
||||
|
||||
可以看到虽然 `FlutterImageView` 没了,第二个红色的 `RE` 也回到了默认的 Surface上,所以这就是 *Hybrid Composition* 混合原生控件的最基础设计理念:
|
||||
|
||||
- **直接把原生控件添加到 `FlutterView` 之上**;
|
||||
- **原生和 Flutter 控件混合堆叠时,用新的 `FlutterImageView` 来实现层级覆盖;**
|
||||
- **如果没有交集就不需要新的 `FlutterImageView`;**
|
||||
|
||||
|
||||
|
||||
关于 `FlutterImageView` 后面再展开,我们继续这个例子,让两个 Flutter 的红色 `RE` 都渲染到 `PlatformView` 的灰色的`RE `上会是什么情况?
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(0.6, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0.6, 0),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
![image-20220308181045237](http://img.cdn.guoshuyu.cn/20220309_DWZB/image10.png)
|
||||
|
||||
如上图所示,可以看到两个红色的 Flutter `RE` 控件共享了一个 `FlutterImageView` ,这里可以得到一个新的结论:**和 `PlatformView` 有交集的同层级 Flutter 控件会同享同一个 `FlutterImageView` 。 **
|
||||
|
||||
我们继续调整示例,如下代码我们新增多一个 `PlatformView` 的灰色 `RE` 控件,然后调整位置,但是 Flutter 控件都在一个层级上,运行之后可以看到,只要 Flutter 控件都在同一个层级,就同享同一个 `FlutterImageView` 。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.2, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0.2, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0, -0.1),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0, 0.2),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
![image-20220308181925410](http://img.cdn.guoshuyu.cn/20220309_DWZB/image11.png)
|
||||
|
||||
|
||||
|
||||
但是如果不在一个层级呢?我们调整两个灰色 `RE` 的位置,让 `PlatformView` 的灰色 `RE` 和 Flutter 的红色 `RE` 交替出现。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.2, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0, -0.1),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0.2, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0, 0.2),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
![image-20220308182349443](http://img.cdn.guoshuyu.cn/20220309_DWZB/image12.png)
|
||||
|
||||
可以看到,两个红色的 Flutter `RE` 控件都单独被渲染都一个 `FlutterImageView` 上,所以我们有新的结论:**和 `PlatformView` 有交集的 Flutter 控件如果在不同层级,就需要不同的 `FlutterImageView` 来承载。**
|
||||
|
||||
所以一般在使用 `PlatformView` 的场景上,不建议有过多的层级堆叠或者过于复杂的 UI 场景。
|
||||
|
||||
|
||||
|
||||
接着我们继续测试,还记得前面说过 *Virtual Display* 上关于触摸事件的问题,所以此时我们直接给 `PlatformView` 的 灰色 `RE` 在原生层添加点击事件弹出 Toast 测试。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.7, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0.8, 0),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
color: Colors.amber,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
![image-20220308112613726](http://img.cdn.guoshuyu.cn/20220309_DWZB/image13.png)
|
||||
|
||||
|
||||
|
||||
可以看到运行后点击能够正常弹出 Toast ,所以对于 `PlatformView` 来说本身的点击和触摸是可以正常保留,然后我们调整下红色大 `RE` 和灰色 `RE` 让他们产生交集,同时给红色的大 `RE` 也添加点击事件,弹出 `SnackBar` 。
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment(-0.3, 0),
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: NativeView(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment(0.8, 0),
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 50, color: Colors.red),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: new Text("Re Click")));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.amber,
|
||||
child: new Text(
|
||||
"RE",
|
||||
style: TextStyle(fontSize: 100, color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image14.png)
|
||||
|
||||
运行之后可以看到,点击没有被覆盖的灰色部分,还是可以弹出 Toast ,点击红色 `RE` 和灰色 `RE` 的交集处,可以正常弹出 `SnackBar`。
|
||||
|
||||
所以可以看到 ***Hybrid Composition* 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生 角度,它就是原生层面的物理堆叠**。
|
||||
|
||||
现在大家应该大致对于 *Hybrid Composition* 有了一定理解,那回到前面那个一开始 Layout InSpector 黑屏 ,后来又能渲染出界面的原因,这就和首次添加 Hybrid Composition 时多出来的 `FlutterImageView` 有关系。
|
||||
|
||||
|
||||
|
||||
如下图所示,可以看到此时原生的灰色 `RE` 和 Flutter 的红色 `RE` 是没有交集的,为什么会多出来一个 `FlutterImageView` 呢?
|
||||
|
||||
![image-20220309093557122](http://img.cdn.guoshuyu.cn/20220309_DWZB/image15.png)
|
||||
|
||||
|
||||
|
||||
这就需要说到 `flutterView.convertToImageView()` 这个方法。
|
||||
|
||||
在 Flutter 渲染 *Hybrid Composition* 的 `PlatformView` 时,会有一个 `flutterView.convertToImageView()` 的操作,这个操作是:**把默认的 `FlutterSurfaceView` 渲染切换到 `FlutterImageView` 上** ,所以此时会有一个 新增的 `FlutterImageView` 出现在 `FlutterSurfaceView` 之上。
|
||||
|
||||
> 为什么需要 `FlutterImageView` ?那就要先理解下 `FlutterImageView` 是如何工作的,因为在前面我们说过,和 `PlatformView` 有交集的时候 Flutter 的控件也会被渲染到 `FlutterImageView` 上。
|
||||
|
||||
`FlutterImageView` 本身是一个原生的 Android View 控件,它的内部有几个关键对象:
|
||||
|
||||
- `imageReader` :提供一个 surface ,并且能够直接访问到 surface 里的图像数据;
|
||||
- `flutterRenderer` : 外部传入的 Flutter 渲染类,这里用于切换/提供 Flutter Engine 里的渲染所需 surface ;
|
||||
- `currentImage` : 从 `imageReader` 里提取出来的 `Image` 画面;
|
||||
- `currentBitmap` :将 `Image` 转为 `Bitmap` ,用于 `onDraw` 时绘制;
|
||||
|
||||
|
||||
|
||||
所以简单地说 `FlutterImageView` 工作机制就是:**通过 `imageReader` 提供 surface 给 Engine 渲染,然后把 `imageReader` 里的画面提取出来,渲染到 `FlutterImageView` 的 `onDraw` 上**。
|
||||
|
||||
|
||||
|
||||
所以回归到前面的 `flutterView.convertToImageView()` ,在 Flutter 渲染 *Hybrid Composition* 的 `PlatformView` 时,会先把自己也变成了一个 `FlutterImageView` ,然后进入新的渲染流程:
|
||||
|
||||
- Flutter 在 `onEndFrame` 时,也就是每帧结束时,会判断当前界面是否还有 `PlatformView `,如果没有就会切换会默认的 `FlutterSurfaceView` ;
|
||||
- 如果还存在 `PlatformView ` ,就会调用 `acquireLatestImage` 去获取当前 `imageReader` 里的画面,得到新的 `currentBitmap` ,然后触发 `invalidate` 。
|
||||
- `invalidate` 会导致 `FlutterSurfaceView` 执行 `onDraw` ,从而把 `currentBitmap` 里的内容绘制出来。
|
||||
|
||||
![image-20220308171209135](http://img.cdn.guoshuyu.cn/20220309_DWZB/image16.png)
|
||||
|
||||
|
||||
|
||||
所以我们搞清楚了 `FlutterImageView` 的作用,也搞清楚了为什么有了 *Hybrid Composition* 的 `PlatformView ` 之后,在 Android Studio 的 Layout Inspector 里可以看到 Flutter 控件的原因:
|
||||
|
||||
|
||||
|
||||
> **因为有 *Hybrid Composition* 之后, `FlutterSurfaceView` 变成了 `FlutterImageView` ,而 `FlutterImageView` 绘制是通过 `onDraw` ,所以可以在 Layout Inspector 里出现。**
|
||||
|
||||
|
||||
|
||||
那为什么会有把 `FlutterSurfaceView` 变成了 `FlutterImageView` 这样的操作?**原因其实是为了更好的动画同步和渲染效果**。
|
||||
|
||||
因为前面说过,*Hybrid Composition* 是直接把添加到 `FlutterView` 上面,所以走的还是原生的渲染流程和时机,而这时候通过把 `FlutterSurfaceView` 变成了 `FlutterImageView` ,也就是把 Flutter 控件渲染也同步到原生的 `OnDraw` 上,这样对于画面同步会更好。
|
||||
|
||||
那有人就要说了,我就不喜欢 `FlutterImageView` 的实现,有没有办法不在使用 *Hybrid Composition* 时把 `FlutterSurfaceView` 变成了 `FlutterImageView` 呢?
|
||||
|
||||
有的,官方在 `PlatformViewsService` 内提供了对应的设置支持:
|
||||
|
||||
```dart
|
||||
PlatformViewsService.synchronizeToNativeViewHierarchy(false);
|
||||
```
|
||||
|
||||
在设置为 false 之后,可以看到只有 *Hybrid Composition* 的 `PlatformView` 的内容才能在 Layout Inspector 上看到,而 `FlutterSurfaceView` 看起来就是黑色空白。
|
||||
|
||||
![image-20220308151751781](http://img.cdn.guoshuyu.cn/20220309_DWZB/image17.png)
|
||||
|
||||
![image-20220308151856383](http://img.cdn.guoshuyu.cn/20220309_DWZB/image18.png)
|
||||
|
||||
|
||||
|
||||
### 问题
|
||||
|
||||
那 *Hybrid Composition* 就是完美吗? 肯定不是,事实上 *Hybrid Composition* 也有很多小问题,其中就比如性能问题。
|
||||
|
||||
例如在不使用 *Hybrid Composition* 的情况下,Flutter App 中 UI 是在特定的光栅线程运行,所以 Flutter 上 App 本身的主线程很少受到阻塞。
|
||||
|
||||
**但是在 *Hybrid Composition* 下,Flutter UI 会由平台的 `onDraw` 绘制,这可能会导致一定程度上需要消耗平台性能和占用通信的开销**。
|
||||
|
||||
例如在 Android 10 之前, *Hybrid Composition* 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 *Hybrid Composition* 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 *Hybrid Composition* 的 `PlatformView` 。
|
||||
|
||||
> 具体体现在 ImageReader 创建时,大于 29 的可以使用 `HardwareBuffer` ,而`HardwareBuffer` 允许在不同的应用程序进程之间共享缓冲区,通过 `HardwareBuffers` 可以映射各种硬件系统的可访问 memory,例如 GPU。
|
||||
|
||||
![image-20220309153648439](http://img.cdn.guoshuyu.cn/20220309_DWZB/image19.png)
|
||||
|
||||
**所以如果当 Flutter 出现动画卡顿时,或者你就应该考虑使用 *Virtual Display* 或者禁止 `FlutterSurfaceView` 变成了 `FlutterImageView`**。
|
||||
|
||||
> 事实上 *Virtual Display* 的性能也不好,因为它的每个像素都需要通过额外的中间图形缓冲区。
|
||||
|
||||
|
||||
|
||||
## 未来变化
|
||||
|
||||
在目前 master 的 [#31198](https://github.com/flutter/engine/pull/31198) 这个合并上,提出了新的实现方式用于替代现有的 *Virtual Display* 。
|
||||
|
||||
这个还未发布到正式本的调整上, *Hybrid Composition* 基本没有变化,主要是调整了一些命名,主要逻辑还是在于 `createForTextureLayer` ,目前还无法保证它后续的进展,目前还有 一部分进度在 [#97628](https://github.com/flutter/flutter/pull/97628) ,所以先简单介绍下它的情况。
|
||||
|
||||
在这个新的实现上,*Virtual Display* 的逻辑变成了 `PlatformViewWrapper ` , `PlatformViewWrapper ` 本身是一个 `FrameLayout` ,同样是 `flutterView.addView(); ` ,基本逻辑和 *Hybrid Composition* 很像,只不过现在添加的是 `PlatformViewWrapper ` 。
|
||||
|
||||
![image-20220309163231386](http://img.cdn.guoshuyu.cn/20220309_DWZB/image20.png)
|
||||
|
||||
|
||||
|
||||
在这里 *Virtual Display* 没有了,原本 *Virtual Display* 创建的 Surface 被设置到 `PlatformViewWrapper ` 里面。
|
||||
|
||||
简单介绍下:**在 `PlatformViewWrapper ` 里,会通过 `surface.lockHardwareCanvas();` 获取到当前 ` Surface` 的 `Canvas` ,并且通过 `draw(surfaceCanvas)` 传递给了 `child `**。
|
||||
|
||||
所以 `child ` 的 UI 就被绘制到传入的 ` Surface` 上,而 Flutter Engine 根据 ` Surface` 的 id 又可以获取到对应的数据,通过一个不可视的 `PlatformViewWrapper` 完成了绘制切换而无需使用 `VirtualDisplay` 。
|
||||
|
||||
当然,目前在测试中接收到的反馈里有还不如以前的性能好,所以后续会如何调整还是需要看测试结果。
|
||||
|
||||
|
||||
|
||||
> PS ,如果这个修改正式发布,可能 Flutter 的 Android miniSDK 版本就需要到 23 起步了。因为 `lockHardwareCanvas()` 需要 23 起,而不用兼容更低平台的原因是 `lockCanvas()` 属于 CPU copy ,性能上会慢很多
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/WechatIMG230.jpeg)
|
|
@ -1,488 +0,0 @@
|
|||
> hello 大家好,我是《Flutter开发实战详解》的作者郭树煜,看标题就知道今天我要给大家分享的是 Flutter 相关的主题,分享内容是也比较直接简单,就是关于 **Flutter 布局相关的知识点**。
|
||||
|
||||
相信大家可能都听说过或者用过 Flutter ,对这部分内容可能有一定了解,但是正如标题所示,本次的主题是带你了解不一样的 Flutter ,**或者说经常性被萌新忽略的东西** ,所以这次将通过不一样的角度,带你看看 Flutter 的尺寸布局有趣的地方。
|
||||
|
||||
|
||||
## 一、开始之前
|
||||
|
||||
在聊 Flutter 的布局之前,*首先大家觉得 Flutter 是什么?*
|
||||
|
||||
**Flutter 其实主要是跨平台的 UI 框架,它核心能力是解决 UI 的跨平台**,和别的跨平台框架不一样的地方在于:**它在性能接近原生的同时,做到了控件和平台无关的实现**。
|
||||
|
||||
|
||||
但如果大家用过 Flutter ,应该知道 Flutter 里的我们写的界面都是通过 `Widget` 完成,并且可能会看起来嵌套得很多层,为什么呢?
|
||||
|
||||
这里就要先简单说一下 Flutter 的一些基础信息,**在 Flutter 里有 `Widget` 、 `Element`、 `RenderObject` 、 `Layer` 等关键的核心设定**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image1)
|
||||
|
||||
其中我们最常写的 **`Widget` 并不是真正的 View 实例**,它需要转化为对应的 `RenderObject ` 才能绘制,而 `Element` 是 `Widget` 和 `RenderObject` 关键的中间实例,我们日常 Flutter 开发里用到的 **`BuildContext` 就是 `Element` 的抽象对象**。
|
||||
|
||||
> 也就是大致 `Widget` -> `Element` -> `RenderObject` 这样的过程。
|
||||
|
||||
**所以在 Flutter 里 `Widget` 代码只是“配置文件”的作用,真正工作的实例是它内部对应的 `Element` 和 `RenderObject` 实体**。
|
||||
|
||||
这也是 `Widget` 为什么可以是不可变的原因,它可以在使用时的被频繁构建,因为它不是真正干活的,**`Widget` 承载的是 `RenderObject` 里绘制时需要的各种状态信息**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image2)
|
||||
|
||||
|
||||
这里举个简单例子,如图代码所示,我们定义了一个 text 的 Widget,然后分别在 4 个地方添加,并成功运行,如果是一个真正的 View ,是不可以同时在 4 个地方被加载。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image3)
|
||||
|
||||
|
||||
通过这个例子可以看到 `Widget` 并不是真正干活的,而主要负责绘制和布局的逻辑都在 `RenderObject` 。 **因为布局和绘制的主要逻辑都在 `RenderObject` ,所以今天我们主要的内容也是在 `RenderObject`**。
|
||||
|
||||
在 Flutter 里 `RenderObject` 作为绘制和布局的实体,主要可以分为两大子类:`RenderBox` 和 `RenderSliver` ,其中 `RenderSliver` 主要是在可滑动列表这种场景中使用,所以本次我们主要讨论的是 `RenderBox` 这种布局场景。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image4)
|
||||
|
||||
## 二、Flutter 的布局
|
||||
|
||||
|
||||
**一般情况 Flutter 里的大小布局是从上往下传递 `Constraints` ,从下往上返回 `Size` 这样的流程**。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image5)
|
||||
|
||||
简单理解这句话就是:父容器根据布局需要往下传递一个约束信息,而最子容器会根据自己的状态返回一个明确的大小,如果自己没有就继续往下的 child 递归。
|
||||
|
||||
|
||||
> 更粗旷一些说就是:从上往下传递约束,传入的约束一般是有 `minHeight`、 `maxHeight` 、 `minWidth` 和 `maxWidth` 等等,但是从下往上返回的 size 时,就会是一个固定 `width` 和 `height` 尺寸。
|
||||
|
||||
而对于 Flutter ,**布局的逻辑主要在对应 `RenderObject` 的 `performLayout`**。
|
||||
|
||||
> 所以一般如果对于 `Widget` 的布局感兴趣或者有疑惑,就可以先找到这个 `Widget` 的 `RednerObject` ,看这个 `RednerObject` 的 `performLayout` 逻辑是怎么实现。
|
||||
|
||||
在 Flutter 最常用的就是应是 `Container` 了, `Container` 作为 Flutter 里最常用的抽象配置模版,它在宽高布局这一块用的是 `ConstrainedBox`,而不管是 `ConstrainedBox` 还是 `SizedBox`, 他们对应的 `RenderObject` 都是 `RenderConstrainedBox`。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image6)
|
||||
|
||||
**所以我们就以 `RenderConstrainedBox` 相关的例子来举例**,看看 `ConstrainedBox` 是如何大小布局。
|
||||
|
||||
### 2.1、ConstrainedBox 的约束布局
|
||||
|
||||
如下代码所示,可以看到 `ColoredBox` 没有指定大小,但是运行后 `ColoredBox` 得到的是一个 100 x 100 的红色正方形, 因为它的父级 `ConstrainedBox` 往下传递的是 100 x 100 大小的 `ConstrainedBox` 约束。
|
||||
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image7)
|
||||
|
||||
|
||||
那如果这时候,把 `min` 的宽高改为 10 会发生什么事?
|
||||
|
||||
可以看到此时 `ColoredBox` 的大小变成和 `min` 的宽高一样大,为什么呢?
|
||||
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image8)
|
||||
|
||||
首先 `ColoredBox` 并没有实现自己的 `performLayout`,而是通过继承了 `RenderProxyBox` 默认的逻辑来实现,这种情况在 Flutter 里比较常见,可以看到默认 `RenderProxyBox` 下:
|
||||
|
||||
- **在没有 child 的时候,用的是 `constraints.smallest`** ,也就是传递下来约束的最小值宽高;
|
||||
- 在有 child 的时候使用 child 的大小;
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image9)
|
||||
|
||||
所以我们知道了,当控件没有实现自定义的 `performLayout` 时,并且没有 child 时,它很可能就是跟着父级约束的 smallest 走。
|
||||
|
||||
继续测试,如果这时候给 `ColoredBox` 增加一个 80 的 child ,可以看到红色框变了,变成了 `ColoredBox` 的 child 的大小 80 而不是 smallest,因为这时候 `ColoredBox` 有了 child, 用的是 child 的大小。
|
||||
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image10)
|
||||
|
||||
|
||||
那如果我把 `ColoredBox` 的 child 修改为 150 的大小呢?
|
||||
|
||||
可以看到运行后红色方块还是 100 的大小,并没有变成 150。
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: 150,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image11)
|
||||
|
||||
这是为什么呢?
|
||||
|
||||
我们通过 Flutter 的调试工具看,可以看到我们虽然给 `SizedBox` 配置了 150 的参数,但是实际 `RenderConstrainedBox` 最终渲染时输出是 100 。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image12)
|
||||
|
||||
这里有两点:
|
||||
|
||||
- 第一就是 `Widget` 仅仅是作为配置信息,我们配置的宽高是 150 ,而实际 `RenderObject` 输出的是 100 ,所以我们写的并不是真实的 `View`, 真正的布局效果还是要看 `RenderObject` 的脸色;
|
||||
|
||||
- 从 `SizedBox` 的 `RenderConstrainedBox` 看, 它的 `performLayout` 的实现在没有 child时, 150 的大小会被 `enforce` 成 parent 的 100
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image13)
|
||||
|
||||
对应 `enforce` 内部是通过 `clamp` 这个 API 完成, `enforce` 执行效果等同于 `150.clamp(10, 100)`,所以会得到 100 的结果。
|
||||
|
||||
> `clamp` 便是如果数据时在区间内就返回该数值,否则返回离其最近的边界值。
|
||||
|
||||
**所以通过 enforce `RenderConstrainedBox` 不会超出父容器的大小。**
|
||||
|
||||
那么为了实验,我们接下来把 `SizeBox` 换成 `ConstrainedBox` ,并且调整为约束为 10 - 150 的大小。
|
||||
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 150, minHeight: 10, maxWidth: 150, minWidth: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
可以看到红色正方形又变成了 10 的大小,为什么呢?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image14)
|
||||
|
||||
通过源码可以看到:
|
||||
|
||||
- 首先 `enforce` 执行是 `150.clamp(10, 100)` 和 `10.clamp(10, 100)` ,等到的自然就是 `10-100`;
|
||||
- 之后再到 `constrain` 里 0.clamp(10, 100),所以输出的是 10 这个最小值;
|
||||
|
||||
> 先前是 100.clamp(10, 100) 自然就是 100 的大小,而现在是 0.clamp(10, 100) ,自然就成了 10 。
|
||||
|
||||
从上面的例子,可以看到父布局约束影响 child 的大小的过程,甚至是变相局限住了 child 的大小返回,但是这都是在 `child.layout` 之后取得的大小。
|
||||
|
||||
**那如果想要在 child.layout 之前就获取到 child 的大小呢?也就是 child 布局之前就获取到 child 的大小?**
|
||||
|
||||
可以这样吗?当然可以!一般在官方的 RenderBox 都会有这四个方法:
|
||||
|
||||
- `computeMaxIntrinsicWidth`
|
||||
- `computeMinIntrinsicWidth`
|
||||
- `computeMaxIntrinsicHeight`
|
||||
- `computeMinIntrinsicHeight`
|
||||
|
||||
为什么说一般呢?
|
||||
|
||||
因为你不写一般也不报错,并且这四个方法其实一般很少被调用,**官方对它的描述是开销昂贵**,并且我们调用时也不是直接调用它,而是通过对应的 get 方法:
|
||||
|
||||
- `getMaxIntrinsicWidth`
|
||||
- `getMinIntrinsicWidth`
|
||||
- `getMaxIntrinsicHeight`
|
||||
- `getMinIntrinsicHeight`
|
||||
|
||||
在默认规范里,一般你只能 override `compute` 开头的 API 去实现需要的逻辑,然后调用只能通过 get 对应的方法去调用,最后会执行到 `compute` 开头的 API ,它们之间时一一对应的。
|
||||
|
||||
> 也就是通过 `getMinIntrinsicWidth` 来调用,比如:`child.getMinIntrinsicWidth` 最终调用到 `computeMinIntrinsicWidth`。
|
||||
|
||||
|
||||
看到这里大家有没想过: **RenderBox 如何拿到 child ?child 如何从 Widget 变成 RenderObject?**
|
||||
|
||||
这里就是 Element 起到的作用,当 `Widget` 被加载时:
|
||||
|
||||
- 就会调用 `inflateWidget` 去创建它的 `Element`,然后通过 `mount` 用 `createRenderObject` 创建出它的 `RenderObject`;
|
||||
- 之后再执行 `attachRenderObject `, 这时候这个 child 会通过 `_findAncestorRenderObjectElement` 去找到它的 parent ,也就是离他最近的一个 `RenderObjectElment`;
|
||||
- 最后执行 parent 的 `insertRenderObjectChild` ,这时 child 就被插入进去 `RenderObject`,在 `RenderObject` 里就可以获取到 `Widget`;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image15)
|
||||
|
||||
|
||||
也就是 child 在 `Element` 里被加载后,创建出对应的 `RenderObject` ,并且找到自己的 parent 然后将自己加入进去。
|
||||
|
||||
|
||||
> Flutter 既然有具备 `RenderObject` 的 `Element` ,那同样也就有没有 `RenderObject` 的 `Element` ,比如 `ComponentElement` ,也就是我们常用的 `StatelessWidget` 等。
|
||||
|
||||
**这里可以看到 Element 得连接作用**。
|
||||
|
||||
## 三、多个 Child 的布局
|
||||
|
||||
前面介绍了单个 Child 的布局,这里简单介绍下多个 Child 主要有什么不同。
|
||||
|
||||
其实多个 Child 和单个一样,都会是从上往下传递 `Constraints` ,从下往上返回 `Size` 这样的流程。
|
||||
|
||||
比如下图,这是我们前面看到的例子,这里使用了 `Column` 控件对多个 `Text` 进行布局。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image16)
|
||||
|
||||
而其实 `Column` 和 `Row` 都是 `Flex` 的子类,我们按照思路去看 `RenderFlex` 的实现,就可以看到,对于多个 Child 的布局主要有这么几个关键点:
|
||||
|
||||
- `MultiChildRenderObjectWidget`;
|
||||
- `MultiChildRenderObjectElement`;
|
||||
- `ParentData`;
|
||||
|
||||
`Widget` 和 `Element` 的逻辑我们这里暂时不深入展开,主要讲解不同的就是在 `RenderBox` 的 `ParentData`。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image17)
|
||||
|
||||
如上图所示,基本上所有 Multi Child 的实现都有自己特有的 `ParentData` ,并且他们还不是直接继承 `ParentData`, 而是继承他们的子类 `ContainterBoxParentData`。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image18)
|
||||
|
||||
|
||||
如图所示,他们的作用就是:
|
||||
|
||||
- `BoxParentData` 具备 `Offset` 参数,是用来觉得 Child 在控件的位置;
|
||||
- `ContainterBoxParentData` 带有两个 `Sibling` 参数,主要是 `RenderBox` 里访问 children 就是通过这个双链表的方式访问的;
|
||||
- `FlexParentData` 就是当前 `RenderFlex` 布局所需的参数;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image19)
|
||||
|
||||
|
||||
可以看到这就是 `RenderFlex` 布局时关键的参数所在,我们添加的 children `Widget`,在经过 `Element` 加载后,在前面说过的 `insert` 步骤会从一个 `List<Widget>` 变成通过 `ParentData` 的两个 `Sibling` 参数连接在一起的双向链表,访问时就是通过它进行访问的。
|
||||
|
||||
**所以在 children 布局时,我们通过对应的 `ParentData` 子类返回 child,然后通过给 `ParentData` 配置 `Offset` 来决定 child 的位置**。
|
||||
|
||||
|
||||
> 官方提供了更方便的自定义布局 `CustomMultiChildLayout` ,不需要你一步一步实现,比如常用的默认页面脚手架 `Scaffold` 就是用它实现。
|
||||
|
||||
|
||||
## 四、有趣的知识点
|
||||
|
||||
既然聊到这个,我们在深入聊聊一些有趣的知识点,比如前面代码里的一直出现的 Scaffold ,这个是我们 Flutter 开发里最常用到的页面脚手架,也是一个页面布局的开始。
|
||||
|
||||
如果这时候把 `Scaffold` 给去掉,运行最初的代码,可以看到整个屏幕都红了,也即是 `ConstrainedBox` 铺满了整个屏幕。
|
||||
|
||||
|
||||
```dart
|
||||
MaterialApp(
|
||||
title: 'GSY Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image20)
|
||||
|
||||
为什么呢?
|
||||
|
||||
我们通过 Flutter 的调试工具可以看到,此时上级给你的约束就是屏幕大小,没有区间,而 `enforce` 等于 `10.clamp(392.72, 392.72)`
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image21)
|
||||
|
||||
看到了没有,你没得选,`clamp(392.72, 392.72)` 也就是强行都变成了屏幕的宽度。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image22)
|
||||
|
||||
|
||||
那如果这时候,我们加了一个 `Center` 控件呢?
|
||||
|
||||
可以看到约束大小又有了!
|
||||
|
||||
```dart
|
||||
MaterialApp(
|
||||
title: 'GSY Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: Center(
|
||||
child:ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
可以看到约束变成了 `0-392.72` 的约束,也就是 `10.clamp(0, 392.72)`
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image23)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image24)
|
||||
|
||||
为什么呢?
|
||||
|
||||
因为 `Center` 的 `RenderObject` 是 `RenderPositionedBox` ,**它在布局的时候会有一个 `constraints.loosen()` 的操作**,这也是为什么你有时候加多一个 `Center` 布局就突然生效的原因,因为 `loosen` 就成了 0-392.72 的约束。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image25)
|
||||
|
||||
|
||||
```dart
|
||||
BoxConstraints loosen() {
|
||||
assert(debugAssertIsValid());
|
||||
return BoxConstraints(
|
||||
minWidth: 0.0,
|
||||
maxWidth: maxWidth,
|
||||
minHeight: 0.0,
|
||||
maxHeight: maxHeight,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
如果不加 `Center`,像之前用的 `Scaffold` 为什么也能让 `BoxConstraints` 生效呢?
|
||||
|
||||
> 因为会出现虽然位置不对,所以这里调成了 100 比较好看到。
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
|
||||
child: ColoredBox(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image26)
|
||||
|
||||
|
||||
这其实是因为 `Scaffold` 的实现是一个叫 `CustomMultiChildLayout` 的控件。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image27)
|
||||
|
||||
|
||||
**`Scaffold` 内的 `CustomMultiChildLayout` 布局时,对 `body` 使用了一个叫 `_BodyBoxConstraints` 的 `Constraints` 子类,这个类默认下所有 min 都是 0**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image28)
|
||||
|
||||
所以对于 body 下的 child 而言,都会有 0 的 min 约束信息存在。
|
||||
|
||||
> 所以 10.clamp(0, 392.72) 可以生效。
|
||||
|
||||
**那可能还会有人就疑惑, child 返回的 size 是在哪里使用?**
|
||||
|
||||
答案肯定是在 `paint` 的时候了使用,那这个 `Offset` 又是什么?
|
||||
|
||||
|
||||
举个例子,我们看之前用过的 `Center` 里面,它会在 `paintChild` 的时候,会添加 `Offset` 信息,所以 child 就会在绘制的时候有偏移,从而绘制到准确的地方。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image29)
|
||||
|
||||
|
||||
所以最终如下图所示,**`ColoredBox` 在绘制 Rect 时,通过 `Offset` (决定位置) 和 `Size`(决定大小),而至绘制出对应位置的红色方框**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image30)
|
||||
|
||||
那如果我画的时候不遵循这个 `Offset` 呢?
|
||||
|
||||
这里我们可以通过一个简单的例子,直接用 `CustomPaint` 画一个 Demo。
|
||||
|
||||
```dart
|
||||
new Container(
|
||||
height: 200,
|
||||
width: 200,
|
||||
color: Colors.greenAccent,
|
||||
child: CustomPaint(
|
||||
///直接使用值做动画
|
||||
foregroundPainter: _AnimationPainter(animation1),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
可以看到,虽然 CustomPaint 是在 200 x 200 的大小下,但是动画绘制的圆可以很直接的超出这个大小。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image31)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image32)
|
||||
|
||||
**所以可以看到 Flutter 本质是一块画板,通过各种 `Layer` 分层,在每个 `Layer` 上又根据约定好的 `Size` 和 `Offset` 绘制控件**。
|
||||
|
||||
> Layer 就是一群 `RenderObject` 的集合。
|
||||
|
||||
其实只要你拿到这个 `Layer` 上的 `Canvas` ,就可以会知道这个 `Layer` 上的任意位置,当然一般情况下为了正确布局绘制,还是要遵循这个规则的。
|
||||
|
||||
> 常见的每个 `Route` 就是一个独立的 `Layer` 。
|
||||
|
||||
### 总结
|
||||
|
||||
最后做个总结:
|
||||
|
||||
- `Widget` 只是配置文件,它不可变,每次改变都会重构,它并不是真正的 `View `;
|
||||
- 布局逻辑主要在 `RenderBox` 子类的 `performLayout`,并且可以提前获取 `child.size` ;
|
||||
- `Element` 的连接作用,`Widget` 被首次加载会创建 `Element` 和 `RenderObject` ,并连接到一起;
|
||||
- 多 `child` 布局里是通过 `ContainerBoxParentData` 来访问多个 child;
|
||||
- 约束布局时 `smallest` 和有没有 0 值(区间最小值)会影响约束的效果;
|
||||
- 控件绘制时遵循对应的 `Size` 和 `Offset` ,也可以超出 `Size` 绘制,具体看所在 `Layer` 的 `Canvas` ;
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image33)
|
|
@ -1,205 +0,0 @@
|
|||
# Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状
|
||||
|
||||
Hello,大家好,我是《Flutter开发实战详解》的作者,Github GSY 系列项目的负责人郭树煜,本次 Google I/O Extended 我主要是给大家回顾一下本次 I/O 大会关于 Flutter 的一些亮点。
|
||||
|
||||
> 其实本次 I/O 大会对我来说也有特别的意义,因为本次 I/O 大会之后,**我参加了 Dart/Flutter GDE 的最后一轮面试,有幸顺利通过了**,这对于我个人来说也是一个里程碑。 - [《从台下到台上,我成为 GDE(谷歌开发者专家) 的经验分享》](https://juejin.cn/post/7102242694755254279)
|
||||
|
||||
## 游戏
|
||||
|
||||
如果要说本次 I/O 里 Flutter 有什么亮点,那其中之一必定就是官方的 Flutter 小游戏 [pinball](https://pinball.flutter.dev/#/) 。
|
||||
|
||||
![image-20220525145827978](http://img.cdn.guoshuyu.cn/20220528_未命名/image1.png)
|
||||
|
||||
其实这不是第一次 Flutter 和游戏领域有交集,例如:
|
||||
|
||||
- Unity 就有 Flutter 相关的 [UIWidgets](https://github.com/Unity-Technologies/com.unity.uiwidgets) ,它是 Unity 编辑器的一个插件包,可帮助开发人员通过 Unity 引擎来创建、调试和部署高效的跨平台应用;
|
||||
- 腾讯的 PUBG 吃鸡游戏,其中一些游戏的非游戏 UI 已经开始转向 Flutter ;
|
||||
|
||||
因为 Flutter 拥有平台无关的渲染引擎 Skia ,而 Skia 的 2D 渲染能力从手机端、Web 端到 PC 端的支持,经过这么多年的发展已经很成熟,**所以在一定程度上,Flutter 本身就是一个 2D 版的“游戏引擎”** 。
|
||||
|
||||
**Flutter 其实一直有针对游戏引擎有一个关于游戏的 [Toolkit](https://flutter.dev/games)** ,一般情况下我们可以把游戏分为两类:
|
||||
|
||||
- 射击游戏、赛车游戏等的动作游戏;
|
||||
- 棋盘游戏、卡牌游戏、拼图、策略游戏等休闲游戏;
|
||||
|
||||
而其实上述这些休闲游戏和 App 十分接近,所以从场景上,它挺更适合使用 Flutter 来进行开发。
|
||||
|
||||
甚至在官方的 ToolKit 里,还包含了如`google_mobile_ads`, `in_app_purchase`, `audioplayers`, `crashlytics`, 和`games_services` 等工具包,**提前为广告和应用内购进行和内置集成支持**。
|
||||
|
||||
![image-20220525112604487](http://img.cdn.guoshuyu.cn/20220528_未命名/image2.png)
|
||||
|
||||
当然,如果你需要实现更复杂的游戏场景,例如 [pinball](https://pinball.flutter.dev/#/) 这样的游戏效果,那么你可能就需要第三方的 [Flame ](https://pub.dev/packages/flame)包来完成,这里 GIF 有些掉帧,但是实际使用过程中,如果我不说,你不会发现这是一个 Flutter Web 写的游戏。
|
||||
|
||||
![TT](http://img.cdn.guoshuyu.cn/20220528_未命名/image3.gif)
|
||||
|
||||
**Pinball 本身是基于 Flame SDK ,通过 Flutter 和 Firebase 开发的一个具备完成功能的弹珠游戏**。
|
||||
|
||||
**其中 Flame 提供了各类游戏相关的开箱即用功能,例如动画、物理、碰撞检测等**,同时 Flame 还可以利用了 Flutter framework 的基础内容,所以如果你是 Flutter 的开发者,那么其实你已经具备使用 Flame 构建游戏所需的基础。
|
||||
|
||||
> 其实 Flame 仓库创建于在 2017,并且此之前也有一些使用 Flame 开发的样例子,只是这次 I/O 官方通过 Pinball 游戏,给 Flame 做了一些背书。
|
||||
|
||||
在官方的例子就提供了游戏里关于 Camera 的相关示例,在点击屏幕时会添加一个比萨,摄像头会跟随移动,另外在这个例子中还有一些多米诺牌排列在一起,在它会和比萨产生碰撞,从而使瓷砖倾斜,并且引起一些列的物理连锁反应。
|
||||
|
||||
![rrr](http://img.cdn.guoshuyu.cn/20220528_未命名/image4.gif)
|
||||
|
||||
```dart
|
||||
class CameraExample extends DominoExample {
|
||||
static const String description = '''
|
||||
This example showcases the possibility to follow BodyComponents with the
|
||||
camera. When the screen is tapped a pizza is added, which the camera will
|
||||
follow. Other than that it is the same as the domino example.
|
||||
''';
|
||||
|
||||
@override
|
||||
void onTapDown(TapDownInfo details) {
|
||||
final position = details.eventPosition.game;
|
||||
final pizza = Pizza(position);
|
||||
add(pizza);
|
||||
pizza.mounted.whenComplete(() => camera.followBodyComponent(pizza));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
另外,其实在 2020 年也有一些开发者使用Flutter&Flame在游戏上进行实践,例如掘金上的 [吉哈达](https://juejin.cn/post/6857049079000760334) 在 2020 年就发布过基于 Flame 的坦克大战游戏,本身也是一个比较完整的开源小游戏。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220528_未命名/image5.webp)
|
||||
|
||||
回到 Pinball ,如果你去看 Pinball 游戏的代码,你就会发现它使用的是 Flutter Web 里的 CanvasKit 作为渲染,也就是通过 WebAssembly + Skia 实现的绘制。
|
||||
|
||||
![image-20220525101651021](http://img.cdn.guoshuyu.cn/20220528_未命名/image6.png)
|
||||
|
||||
了解过 Flutter 的同学可能知道,Flutter Web 默认在 PC 使用 CanvasKit 渲染 UI ,而在手机端默认会使用 Html 来绘制 UI ,但是如果你使用了 Flame ,那么在手机端也会是 CanvasKit ,**因为从设计上考虑,只有 CanvasKit 更符合游戏的设计思想和保持运行效果的一致性**。
|
||||
|
||||
![image-20220525102914789](http://img.cdn.guoshuyu.cn/20220528_未命名/image7.png)
|
||||
|
||||
当然,这也带来了加载太慢的问题,可以看到打开 pinball 大概花费了 3.6 min,这确实是 Flutter Web 在 CanvasKit 下的通病之一。
|
||||
|
||||
而 Flutter 开发游戏和在传统 App 中不同的点主要在:
|
||||
|
||||
- 一般传统 App 通常屏幕在视觉上是静态的,直到有来自用户的事件或交互才会发生变化;
|
||||
- 对于游戏这一情况正好相反——UI 需要不断更新,游戏状态会不断发生变化;
|
||||
|
||||
所以 在 I/O Pinball 中,游戏通过 loop 循环对球在赛场上的位置和状态做出反应,例如球与物体发生碰撞或球脱离比赛,从而做出相应。
|
||||
|
||||
```dart
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt); final direction = -parent.body.linearVelocity.normalized();
|
||||
angle = math.atan2(direction.x, -direction.y);
|
||||
size = (_textureSize / 45) *
|
||||
parent.body.fixtures.first.shape.radius;
|
||||
}
|
||||
```
|
||||
|
||||
另外还有,在构建 I/O Pinball 下,可以看到界面是有明显的类 3D 效果,那如何仅使用 2D 元素创建 3D 效果?
|
||||
|
||||
![image-20220525152706873](http://img.cdn.guoshuyu.cn/20220528_未命名/image8.png)
|
||||
|
||||
**其实就是通过对组件进行排序和堆叠资源的层级,以此来以确定它们在屏幕上的呈现位置**,例如当球在斜坡上发射时,球的所在的层级顺序增加,因此它看起来在斜坡的顶部。
|
||||
|
||||
```dart
|
||||
/// Scales the ball's body and sprite according to its position on the board.
|
||||
class BallScalingBehavior extends Component with ParentIsA<Ball> {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final boardHeight = BoardDimensions.bounds.height;
|
||||
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; final standardizedYPosition = parent.body.position.y + (boardHeight / 2);
|
||||
final scaleFactor = maxShrinkValue +
|
||||
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;final ballSprite = parent.descendants().whereType<SpriteComponent>();
|
||||
if (ballSprite.isNotEmpty) {
|
||||
ballSprite.single.scale.setValues(
|
||||
scaleFactor,
|
||||
scaleFactor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
另外弹球游戏场上有一些元素,如 Android、Dash、Sparky 和 Chrome Dino,它们都是有动画效果。
|
||||
|
||||
对于这些使用的是 sprite sheets,它包含在带有 `SpriteAnimationComponent` ,对于每个元素都有一个文件,其中包含不同方向的图像、文件中的帧数以及帧之间的时间。
|
||||
|
||||
使用这些数据,`SpriteAnimationComponent` 在 Flame 内将所有图像循环编译在一起,从而使元素看起来具有动画效果。
|
||||
|
||||
![image-20220525115124190](http://img.cdn.guoshuyu.cn/20220528_未命名/image9.png)
|
||||
|
||||
```dart
|
||||
final spriteSheet = gameRef.images.fromCache(
|
||||
Assets.images.android.spaceship.animatronic.keyName,
|
||||
);const amountPerRow = 18;
|
||||
const amountPerColumn = 4;
|
||||
final textureSize = Vector2(
|
||||
spriteSheet.width / amountPerRow,
|
||||
spriteSheet.height / amountPerColumn,
|
||||
);
|
||||
size = textureSize / 10;animation = SpriteAnimation.fromFrameData(
|
||||
spriteSheet,
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: amountPerRow * amountPerColumn,
|
||||
amountPerRow: amountPerRow,
|
||||
stepTime: 1 / 24,
|
||||
textureSize: textureSize,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
最后 Flame 代码库还附带一个组件沙箱,类似于 UI 组件库,可以在开发游戏时,这是一个有用的工具,因为它允许开发者单独开发游戏组件,并确保它们在将它们集成到游戏中之前的外观和行为符合预期。
|
||||
|
||||
![1*zAjKICKgCTiEiiMTou9MJQ](http://img.cdn.guoshuyu.cn/20220528_未命名/image10.gif)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 全平台
|
||||
|
||||
Flutter 3.0 另外一个重点就是**增加了对 macOS 和 Linux 应用程序的稳定支持,这是 Flutter 的一个里程碑,现在借助 Flutter 3.0,开发者可以通过一个代码库为六个平台构建应用**。
|
||||
|
||||
![image-20220525115916985](http://img.cdn.guoshuyu.cn/20220528_未命名/image11.png)
|
||||
|
||||
|
||||
|
||||
自此 Flutter 终于全平台 stable 支持了,这种支持不是说添加对应平台的UI 渲染致支持就可以:**它包括新的输入和交互模型、编译和构建支持、accessibility 和国际化以及特定于平台的集成等等,Flutter 团队的目标是让开发者能够灵活地利用底层操作系统,同时根据开发者的选择尽可能多的共享 UI 和逻辑**。
|
||||
|
||||
> 例如在 macOS 上,现在支持 Intel 和 Apple Silicon,提供 [Universal Binary](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fapple-silicon%2Fbuilding-a-universal-macos-binary) 支持,允许应用打包支持两种架构上的可执行文件,Flutter 利用了 [Dart 对 Apple 芯片的支持](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fdartlang%2Fannouncing-dart-2-14-b48b9bb2fb67) 在基于 M1 的设备上更快地编译并支持 macOS 应用程序的 [Universal Binary](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fapple-silicon%2Fbuilding-a-universal-macos-binary) 文件。
|
||||
|
||||
本次 I/O 官方就提供了一个 Flutter 合作伙伴的案例:[Superlist](https://link.juejin.cn/?target=https%3A%2F%2Fsuperlist.com%2F) ,它是 Flutter 如何实现 Desktop 应用的一个很好的例子,它在 I/O 当天发布了测试版。
|
||||
|
||||
![RR](http://img.cdn.guoshuyu.cn/20220528_未命名/image12.gif)
|
||||
|
||||
Superlist 将列表、任务和自由格式内容,组合成全新的待办事项列表和个人计划,提供协作能力,同时 Superlist 也是开源项目 [super_editor](https://github.com/superlistapp/super_editor) 的维护组织,所以社区的支持其实对于 Flutter 来说很重要。
|
||||
|
||||
**每个 Flutter 正式版的发布都包含了大量来自社区的 PR ,例如本次 Flutter 3.0 版本发布就合并了 5248 个 PR**。
|
||||
|
||||
**当然,本次在 PC 端还有做了一定的取舍:放弃 Windows 7/8**。
|
||||
|
||||
在 Flutter 3.0 中推荐将 Windows 的版本提升到 Windows 10,虽然目前 Flutter 团队不会阻止在旧版本(Windows 7、Windows 8、Windows 8.1)上进行开发,但 [Microsoft 不再支持](https://link.juejin.cn/?target=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Flifecycle%2Ffaq%2Fwindows) 这些版本,虽然 Flutter 团队将继续为旧版本提供“尽力而为”的支持,但还是鼓励开发者升级。
|
||||
|
||||
> **注意**:目前还会继续为在 Windows 7 和 Windows 8 上能够正常*运行* Flutter 提供支持;此更改仅影响开发环境。
|
||||
|
||||
另外,Flutter 在 PC 领域虽然目前不像 App 端那么丰富,但是社区也涌向了一批优质的第三方支持,例如 [leanflutter.org](https://github.com/leanflutter) 目前发布了很多关于 PC 端相关的内容,大家可以在 pub 或者 github 看到相关的内容,其中比如
|
||||
|
||||
- **window_manger 就在 PC 领域备受关注**,它本身是用于调整窗口的桌面应用的大小和位置,支持 macOS、Linux、WIndows等平台,所以这个包在桌面端领域就相当实用;
|
||||
- flutter_distributor 可以帮助你在多个平台上实现自动构建和定制化的发布
|
||||
|
||||
![image-20220528215030023](http://img.cdn.guoshuyu.cn/20220528_未命名/image13.png)
|
||||
|
||||
**类似 leanflutter 等作者已经在 Pub 发布了很多关于 PC 端能力拓展的插件**,所以大家对于 PC 端支持的忧虑可以开始放下,尝试一些 Flutter 的 PC 端开发。
|
||||
|
||||
> **注意是 leanflutter 不是 learnflutter**。
|
||||
|
||||
最后,目前 Flutter PC 端在国内也开始被越来越多的大厂所接纳,比如知名的钉钉、字节、企业微信都在 Flutter PC 端进行投入开发,它们的投入使用也可以反向推动 Flutter PC 端的健康成长。![image-20220525135033045](http://img.cdn.guoshuyu.cn/20220528_未命名/image14.png)
|
||||
|
||||
就比如官方的 2022 roadmap 提到:**无论一个 SDK 有多么优秀,如果只有少数人在使用它,它都不能反映出它的价值; 而如果 SDK 很普通但是却被大量开发人员使用,它也会有一个健康和有价值的框架,使用这个框架的人才能真正从社区和框架中受益**。
|
||||
|
||||
![image-20220528215500266](http://img.cdn.guoshuyu.cn/20220528_未命名/image15.png)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
dff
|
439
Flutter-FF.md
439
Flutter-FF.md
|
@ -1,439 +0,0 @@
|
|||
# Flutter Festival | 2022 年 Flutter 适合我吗?Flutter VS Other 量化对比
|
||||
|
||||
|
||||
Hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 系列开源项目的负责人郭树煜,比如 [gsy_github_app_flutter](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FCarGuo%2Fgsy_github_app_flutter) 、GSYVideoPlayer 等的项目 。
|
||||
|
||||
> 看到这个题目大家应该知道,今天这个主题并不是纯粹的技术内容分享,可以说还有点吃力不讨好,其实我很少分享这类主题,不过最近觉得有必要做这么一个算是科普向的内容吧。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image1)
|
||||
|
||||
|
||||
|
||||
## Flutter 的现状
|
||||
|
||||
我是在 2017 年左右接触的 Flutter ,说来起来有趣,那时候因为我需要做一场关于跨平台技术的内部分享,主要目的是给公司其他事业部推 React Native 框架,好巧不巧地那时候刚好看到 Flutter ,就被我当作凑数的“添头”给加到分享里,自此我就开始了和 Flutter 之间的故事。
|
||||
|
||||
回到正题,Flutter 开源至今其实已经将近 7 年的时间,如今在 2022 年看来,**Flutter 已经是不再是以前小众的跨平台框架了**。
|
||||
|
||||
![image-20220222115737486](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image2)
|
||||
|
||||
如图所示,截止我 2 月份截图时,可以看到如今的 Flutter 已经有高达 `137k` 的 star , `10k+` Open 和 `50k+` Closed 的 issue 也足以说明 Flutter 社区和用户的活跃度。
|
||||
|
||||
**从官方公布的数据上, Flutter 已经基本超过其他跨平台框架,成为最受欢迎的移动端跨平台开发工具,截至 2022 年 2 月,有近 50 万个应用程序使用了 Flutter**。
|
||||
|
||||
如图所示,去年下半旬的数据调查中,**Flutter 也成为了排名第一的“被使用”和“被喜爱”的跨平台框架**,可以看到 Flutter 在 2019 到 2022 有了很明显的增长,有接近 42% 的跨平台开发者会使用 Flutter。
|
||||
|
||||
|
||||
![image-20220222115623672](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image3)
|
||||
|
||||
![image-20220222115549701](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image4)
|
||||
|
||||
|
||||
|
||||
其实在去年和前年,我也做过一些简单的统计:
|
||||
|
||||
- 2020 年 `52` 个样本中有 `19` 个 App 里出现了 Flutter;
|
||||
- 2021 年 `46` 个样本中有 `24` 个 App 里出现了 Flutter;
|
||||
|
||||
本次基于 2022 年 2 月 22 号,在对比了 **57 款**常用 App 之后得到的数据:
|
||||
|
||||
| Flutter | React Native | Weex | 没有使用跨平台 |
|
||||
| :----------------------------------------------------------- | :----------------------------------------------------------- | :------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| 27 | 24 | 5 | 13 |
|
||||
| 链家、转转、掘金、**中国大学 MOOC**、同花顺、饿了么、凤凰新闻、微信、微视、哔哩哔哩漫画、腾讯课堂、企业微信、学习强国、闲鱼、携程旅行、腾讯会议、**微博**、贝壳找房、百度网盘、 WPS Office、唯品会、**美团众包**、**美团外卖商家版**、**UC**、QQ(libmxflutter),**小米运动**、**优酷视频** | 美团 、**美团众包** 、**美团外卖商家版** 、美团外卖、爱奇艺、**中国大学 MOOC** 、脉脉 、小红书、安居客、得物、58同城、微信读书、汽车之家、飞书、喜马拉雅、去哪儿旅行、菜鸟、京东、快手、携程、**米家**、**UC**、**小米运动**、**优酷视频** | **UC** 、**闲鱼** 、 **微博** 、**米家、优酷视频** | QQ音乐、Boss直聘、今日头条、流利说、知乎、腾讯新闻、财经社、酷狗音乐、拼多多、抖音、起点、什么值得买、百度地图 |
|
||||
|
||||
> 这些数据来源于 Android 的 Apk ,以是否存在` libflutter.so` 、`libreactnativejni.so` 和 `libweexcore.so` 等动态库为依据,如果项目使用了插件化下发可能会被忽略。
|
||||
|
||||
可以看到 Flutter 和 React Native 的出现都接近 50%,而 Weex 的占有率已经很低,**另外在这个小样本下,可以看到现在大多数 App 或多或少都可能带有一些跨平台框架的趋势**。
|
||||
|
||||
> 同时,加粗部分的 App 因为业务需要, 在应用内使用了不止一种的跨平台框架,比如UC、闲鱼等。
|
||||
|
||||
而在官方去年的 Q4 数据调查里,*在过去 6 个月中,分别**有 72% 和 91% 的开发者使用 Flutter 为 iOS 和 Android 开发 App*** 。
|
||||
|
||||
![0*TERDonM4zc_kafRm](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image5)
|
||||
|
||||
|
||||
|
||||
再看一份数据,是 Dart 的第三方插件托管平台 pub.dev 上的数据,基于 2022-02-22 的数据:
|
||||
|
||||
| All | 23495 packages |
|
||||
| ----------- | -------------- |
|
||||
| Flutter | 21714 packages |
|
||||
| Android/iOS | 20352 packages |
|
||||
| Web | 12584 packages |
|
||||
| PC | 14314 packages |
|
||||
| Null safety | 12615 packages |
|
||||
|
||||
目前大概有 2.3 万个公开的第三方支持包托管在 pub 上,其中支持 Flutter 的有 2.1 万个,可以看出 Dart 语言的用户基本都是来源于 Flutter 。
|
||||
|
||||
另外从数据上看大部分的库都支持 Android 和 iOS ,而对于 Web 和 PC 的支持接近60% ,而比较意外的是,目前支持 Null safety 的包也就接近60%,也就是还有 40% 多的包还停留在较老的版本上。
|
||||
|
||||
而在官方的 Q4 调查里可以看到,**使用 Flutter 作为主要工作的比例在逐步提高**。
|
||||
|
||||
![0_Zw_zyVq5CfP7Y09o](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image6)
|
||||
|
||||
最后在聊一聊 Flutter 官方对于 Flutter 一直坚持一个理念:
|
||||
|
||||
**一个 `SDK` 再优秀,如果只有少部分人在使用,那它也不能体现价值;而一个 `SDK` 即使平庸,但是有大量开发者使用,那也会拥有一个健康繁荣的生态氛围,这样使用框架的人才能从中受益**。
|
||||
|
||||
> 补充一句,你知道调查里大家最不满意的 Flutter 的是哪个方面吗?
|
||||
>
|
||||
> **是文本编辑**!Q4调查里,对文本编辑功能的满意度从 82.3%(单行)和 82.2%(过滤和格式化)下降到 69.6%(多行)和 66.6%(富文本编辑器),目前多编辑体验和输入富文本支持上,确实不是特别友好。
|
||||
|
||||
## Flutter VS Other
|
||||
|
||||
聊完 Flutter 的现状,我们继续讨论 Flutter 和其他框架的一些直观对比。
|
||||
|
||||
### 实现原理
|
||||
|
||||
这部分内容其实分享过很多次,简单说一下,首先对比它们的实现原理,如下图所示,可以看到:
|
||||
|
||||
- 对于原生 Android 或者 Compose 而言,是**原生代码经过 skia 最后到 GPU 完成渲染绘制**,Android 原生系统本身自带了 skia;
|
||||
|
||||
- 对于 Flutter 而言,**Dart 代码里的控件经过 skia 最后到 GPU 完成渲染绘制**,这里在 Andriod 上使用的系统的 skia ,而在 iOS 上使用的是打包到项目里的 skia ;
|
||||
|
||||
- 对于 ReactNative/Weex 等类似的项目,它们是**运行在各自的 JS 引擎里面,最后通过映射为原生的控件,利用原生的渲染能力进行渲染**;
|
||||
|
||||
- 对于 uni-app 等这类 Hybird 的跨平台框架,使用的主要就是 **WebView 的渲染能力**;(不讨论开启weex情况)
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image7)
|
||||
|
||||
|
||||
|
||||
首先看到,从理论上来说, **Flutter 在实现上是最接近原生,因为从实现路径上基本是一致的,而 RN/Weex 相对会差一些,而 uni-app 通过 WebView 的渲染会是最末**。
|
||||
|
||||
但是对于性能问题,**事实上很多时候性能门槛不在于框架,而在于开发者**,我见过用 Cordova 开发的 App 性能和体验都调教得很不错,我记得有一次大会分享和支付宝的大佬聊过,支付宝也使用了很多 H5 的 Hybird 技术,得益于 UC 的自研内核,在性能体验上一直还挺不错。
|
||||
|
||||
### 构建大小
|
||||
|
||||
接着我们对比应用构建的大小,这里主要对比 Android ,因为 iOS 上应用的大小似乎越来越没人在意,比如 QQ 这个极端的例子:
|
||||
|
||||
![image-20220225113714959](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image8)
|
||||
|
||||
|
||||
|
||||
回到问题上,关于应用大小问题,之前恰好看到有多人说过:
|
||||
|
||||
> “Compose 上 Kotlin/JVM 为 JVM 和 Android 平台生成 jar/aar 文件、通过 Kotlin/Native 为 IOS 平台生成 framework 文件、通过 Kotlin/JS 为 Web 平台生成 JavaScript 文件,最终调用的还是原生 API,这使得采用 Compose Multiplatform 不会导致性能损耗,且不会像 Flutter 那样明显增大应用体积。”
|
||||
|
||||
是的,从实现上看 Flutter 在实现上确实应该比 Compose 占据更多体积,但是真实情况是怎么样呢?
|
||||
|
||||
首先我们创建几个空项目,然后打包时只保留 `arm64-v8a` 相关的动态库,因为一般情况下上架也只会保留其中一种 so 库。
|
||||
|
||||
在我们不写任何代码的情况下,构建出 Android 的 Release 包,得到如下结果:
|
||||
|
||||
- Flutter
|
||||
|
||||
![l37e90013d3143b59b2fedac8175846c2-s-mab43156a06c705c0e724893593dff285](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image9)
|
||||
|
||||
- React Native
|
||||
|
||||
![l86641a7d82e2feff1f984855ecbd562c-s-mdb88514a3334653b9e61c27c51634605](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image10)
|
||||
|
||||
- Compose
|
||||
|
||||
![l6125476d649868bb69a29a009574a232-s-mf07265732fba4b70ab8330b8014db858](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image11)
|
||||
|
||||
- 原生 Android
|
||||
|
||||
![l64e110c95184cd1d58dc061c7a37337f-s-m7042d4089e4705f94ae59f9477189827](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image12)
|
||||
|
||||
|
||||
|
||||
可以看到 :
|
||||
|
||||
- React Native 的空包最大,主要体积来自于其内部的各种动态库,比如 JSCore ;
|
||||
- Flutter 次之,主要体积来也是自于其内部的动态库,比如 Flutter 的 framework;
|
||||
- Compose 的体积和原生相当接近,主要内容来自于 classes 文件;当然这里没有混淆和压缩,混淆和压缩后可以小很多;
|
||||
|
||||
|
||||
|
||||
从结果上看空项目下确实是 Flutter 比 Compose 所占据的体积更大,但是这里有一点需要注意的是:
|
||||
|
||||
- 单纯 Flutter 开发下,主要的应用体积会来自 `libapp.so` ,这部分代码是经过 AOT 编译后的 Native 二进制代码;
|
||||
- 而 Compose 的体积增长主要来自于 classes 文件,这部分的代码增长需要通过混淆等来压缩;
|
||||
|
||||
额外提一点,**大家可能会好奇 Compose 编译后是怎么完成布局渲染**?
|
||||
|
||||
这里简单介绍下,**Compose 里的控件和原生控件并不是一个体系**,大家如果去看编译后的内容,就会发现例如 `BOX` 这样的控件在编译后是通过 `ComposerKt` 和 `BoxKt` 等的 framework 实现来完成的布局与绘制。
|
||||
|
||||
![image-20220223163739226](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image13)
|
||||
|
||||
所以 **Compose 编译后不是转化为原生的 Android 上的 View 去显示**,而是依赖于平台的 `Canvas` ,在这点上和 Flutter 有点相似,简单地说可以理解为 Compose 是全新的一套 View 。
|
||||
|
||||
> 另外友情提示:虽然是全新的 View ,但是 `Compose` 的组件在 Android 上是可以显示了布局边界。
|
||||
|
||||
回顾到体积的问题上,因为我恰好开源有一些列 GSY 项目,它们实现的业务逻辑十分相似,所以都打包成 Release 模式之后,我们对比它们的体积大小:
|
||||
|
||||
- Flutter
|
||||
|
||||
![ld53439aaeaa21568253c98480767caee-s-m1c224ff23fb985b1bded376f0cceebdc](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image14)
|
||||
|
||||
- React Native
|
||||
|
||||
![lca0d0a439e3b9d18d0195521fad90c14-s-m7fdc781bd60e584b0c161115fa824f43](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image15)
|
||||
|
||||
- 原生 Android
|
||||
|
||||
![le2f6c0258c501a6fdae93b47deff024c-s-mc5ea88d982f7d79299b1b0391b7e95ab](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image16)
|
||||
|
||||
|
||||
|
||||
因为我目前还没有 Compose 的项目,所以这里以原生作为对比,可以看到:
|
||||
|
||||
- Flutter 项目从空的 5.7 M 变成了 9.8M ,增长了 4.1 M 的大小;
|
||||
- React Native 项目从 9.4 M 变成了 12.7M,增长了 3.4 M 的大小;
|
||||
- 原生项目从 3.2 M 变成了 9.3 M ,增长了 6.1 M 的大小;
|
||||
|
||||
虽然不精准,但是可以看到在大致相同的业务场景下, **Flutter 和原生项目的总大小反而相差不大,而原生项目的增加其实比 Flutter 更显著一些**。
|
||||
|
||||
但是这里的前提是原生不开启压缩和混淆,如果开启压缩和混淆之后,如下图所示可以看到体积发生了变化,体积从 9.3M 变成了 6.4 M ,所以大致上可以看出,**在开启混淆和压缩之后,原生 App 体积增长和 Flutter 差异不会太大**。
|
||||
|
||||
![image-20220223172138586](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image17)
|
||||
|
||||
|
||||
|
||||
另外,这里我找了一个网上的纯 Compose 做了测试,在开启混淆和压缩后,Compose 体积的大小变化就十分显著:**从 9.6 M 变成了 2.4 M ,这得益于 Compose 里的代码基本都是可以被混淆的**。
|
||||
|
||||
![image-20220223170138121](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image18)
|
||||
|
||||
![image-20220223170242884](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image19)
|
||||
|
||||
|
||||
|
||||
所以得到结论:
|
||||
|
||||
- **在开启了压缩之后的 Compose ,体积确确实实会比 Flutter 更小更有优势,这里的优势来源于 classes 的压缩效率**
|
||||
- **React Native 的体积一般情况下都会比 Flutter 更大,同理 Weex 也类似;**
|
||||
|
||||
|
||||
|
||||
当然这个也不是绝对的,体积大小有时候也和开发者的习惯有关系,比如某天我就在群里刚好看到,某个 App 的 Flutter 业务动态库居然可以高达 77.4 M 。
|
||||
|
||||
|
||||
|
||||
![lb47319abc6776b6ac76d45775ecfa7e8-s-m0f2c227b7d605d6930401a084bd16170](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image20)
|
||||
|
||||
|
||||
|
||||
这是什么概念?一般情况下 10M - 15M 就是普通中小型 App 的 Flutter 动态库大小 ,而 大型 APP 一般也会控制在 20M - 35M 之间,就算是很大的体积了,例如 UC 也就是 35 M 、企业微信 28.9M 的水平。
|
||||
|
||||
|
||||
|
||||
![lf6f59898fbe0c6b11639e81c92796155-s-m5f015f612b387f867b36de285a780d88](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image21)
|
||||
|
||||
|
||||
|
||||
![le0e42c9311522a26d2a83afda916b3db-s-m1548598cff0bb0080caf643514588d90](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image22)
|
||||
|
||||
|
||||
|
||||
所以体积大小上,更多是开发者的主观控制,也和你是否开启混淆和压缩有关系,主要介绍这个是让大家对不同项目的打包产物有个直观的认识,从而对选择哪种开发框架提供一个判断的依据。
|
||||
|
||||
### 构建过程
|
||||
|
||||
接下来聊聊构建过程,为什么聊这个,因为对于新手来说,构建过程的问题是一个很容易放弃的过程。
|
||||
|
||||
如下图所示就是非原生开发在运行 Flutter 时经常可以遇到的问题:
|
||||
|
||||
![l9d17062e9171bfc73b423f52f22bae27-s-m908dc799447e36c1a7c8ca7275416992](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image23)
|
||||
|
||||
|
||||
|
||||
如果你看到运行后一直停留在` assembleDebug` 阶段没有进入下一步,那这时候其实是 Android 在通过网络下载一些环境依赖,比如 Gradle SDK、 aar 库等这些运行所需的包,而这个过程通过 `flutter run` 或者 idea 运行是看不出来进度的,你只有进入 `andorid/` 目录下执行 `./gradlew assembleDebug` 就可以看到类似的进度:
|
||||
|
||||
![l0d886e19d6ba2c3eca279cea12c62628-s-me77c94c2fa3e28791919d0d3153efc9f](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image24)
|
||||
|
||||
例如在 Flutter 官方 Q4 的调查里,在发布应用程序时,需要处理 Xcode (iOS) 和 Gradle (Android) 是最常见的问题,为什么说这个? 首先这里可以看出一点,**对原生平台的不熟悉会是使用跨平台开发的一个痛点**。
|
||||
|
||||
![0_3MXILeNbFbIFagLu](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image25)
|
||||
|
||||
![0_qtLcSyZO68tuY6Gk](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image26)
|
||||
|
||||
当然,在对比所有跨平台开发的这个环节里, Flutter 虽然不能说是最好,但是 React Native 绝对是最拉胯的,因为不管是 Weex 还是 React Native , node_module 黑洞一直都是头痛的问题:
|
||||
|
||||
![image-20220223173738326](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image27)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
举个例子,React Native 项目的 node_module 黑洞,经常导致了它在环境安装和运行上会给你“惊喜”,各种丰富的插件和工具,在实用的同时又成了臃肿的坑,比如这是我前段时间久违需要处理一个 React Native 项目时遇到的问题:
|
||||
|
||||
![l44f7689357e4deb77b7c5019177f3442-s-m2fc075a1dd990c3aaabc19acb201f279](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image28)
|
||||
|
||||
![lc29a1742b1876eea0deee1c895d05a1a-s-m1d64d4caf0ea69d9a76e350e370f46de](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image29)
|
||||
|
||||
**依赖中的依赖,各种库的版本所需的 node 环境不同,需要我从中平衡出一个合适的版本**。当然这不是最麻烦的,最麻烦的是在电脑 A 上运行成功之后,在 B 电脑 npm 之后发现无法运行的问题,相信这是每个 React Native 开发的必修课。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image30)
|
||||
|
||||
> 从前端开发角度,比如扁平化依赖,当然扁平化依赖的展开后依赖深度就变成了数量很可观的文件目录,依赖结构变得就不直观了,当然现在的 npm ,pnpm 工具都有了新的优化,
|
||||
|
||||
相反 Flutter 在这方面就轻量很多,目前 Dart 的 pub 包层级很浅,路径相对清晰,这也是我觉得在这方面 Flutter 基本上比 React Native 更舒服的原因,**所以在原生环境依赖复杂度一致的情况下,Flutter 确实比 RN 更容易进入 hello world** 。
|
||||
|
||||
|
||||
|
||||
### Flutter & Compose
|
||||
|
||||
最后聊聊 Flutter 和 Compose 之间的对比。
|
||||
|
||||
相信大家对于 Flutter 和 React Native 之间的对比看得多了,因为 React Native 发布至今已经很久了,并且 Flutter 和 React Native 之间是不同公司在维护 ,而**对于 Flutter 和 Compose ,它们都是谷歌开源的项目,并且都在支持多平台,那它们之间有什么不同?应该如何选择?**
|
||||
|
||||
|
||||
|
||||
首先提一个题外话:**前端有 npm 、Flutter 有 pub 、iOS 有 cocoaPods,你可以通过它们的官网搜索你想要的库,查看它们的热度,版本,兼容和使用量等等信息,但是 Android 呢?**
|
||||
|
||||
Android 的 Gradle 是不是缺少了这样一个便捷的存在,以至于我们只能在 Github 通过关键字去检索,而这个影响其实也渗透到 Compose 里,这对 Compose 在跨平台发展上是一个问题。
|
||||
|
||||
首先谷歌官方的定义,**Compose 是 Android 的现代原生界面工具包,而且正如前面我们介绍的,它是一套全新的 UI ,所以 Compose 是有自己的平台,也就是 Android,那是它的主场**。
|
||||
|
||||
> 从可以看官方的 [路线图]( https://developer.android.google.cn/jetpack/androidx/compose-roadmap) 可以看出来, 谷歌对 Compose 的经历主要都是集中在 Android 原生平台,而 Compose Multiplatform 是由 JetBrains 维护的 [compose-jb ](https://github.com/JetBrains/compose-jb ) 来实现。
|
||||
|
||||
**Flutter 没有自己的平台** ,它是一个跨多平台的 UI 框架,它出生就是为了多平台而生,从目前支持的 Android、iOS、Web 、Window 都发布了正式版支持,而 Linux 和 MacOS 估计也不远了。
|
||||
|
||||
所以这是它们直接最大的区别之一:**Compose 是谷歌为 Android 设计的全新 UI 框架,并且 JetBrains 把它拓展到支持跨平台,而 Flutter 主要就是为了跨平台而生** 。
|
||||
|
||||
虽然都支持跨平台,但是二者之间也是有很大差异,如图所示是它们实现上的不同:
|
||||
|
||||
![image-20220223174643400](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image31)
|
||||
|
||||
|
||||
|
||||
在实现上的差异是: **Flutter 对外是通过一套官方的 Framework 来支持多平台,而 Compose 目前是通过多个模块不同实现来支持多平台**。
|
||||
|
||||
Flutter 不用说,就是通过编译时不同的命令去生成不同平台的代码,这期间统一有 Flutter framework 来完成输出,而目前 Compose 在 Web 、Desktop 和 Mobile 上的实现逻辑是并不一定能通用的,特别是 Web。
|
||||
|
||||
> Compose 目前在 iOS 还没有正式的支持,虽然可以通过一些方式支持,但是还不是特别方便,而在 Web 上 Compose 需要使用和导入的包也是具备特殊化,反而是 Mobile 和 Desktop 之间反而是能共用 `compose-ui` 的内容。
|
||||
|
||||
举个例子,在 [compose-jb ](https://github.com/JetBrains/compose-jb ) 里 对 Web 的支持代码如下,可以看到导入的和使用的控件都具备它自己的特殊性。
|
||||
|
||||
```kotlin
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.jetbrains.compose.web.css.*
|
||||
|
||||
fun main() {
|
||||
var count: Int by mutableStateOf(0)
|
||||
|
||||
renderComposable(rootElementId = "root") {
|
||||
Div({ style { padding(25.px) } }) {
|
||||
Button(attrs = {
|
||||
onClick { count -= 1 }
|
||||
}) {
|
||||
Text("-")
|
||||
}
|
||||
|
||||
Span({ style { padding(15.px) } }) {
|
||||
Text("$count")
|
||||
}
|
||||
|
||||
Button(attrs = {
|
||||
onClick { count += 1 }
|
||||
}) {
|
||||
Text("+")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
所以对于 Compose 来说,更多像是:你学会了这个框架,然后就具备了写 Web 和 Desktop 的能力;而对于 Flutter 来说它在跨平台的体验会更好。
|
||||
|
||||
所以从我理解上是:
|
||||
|
||||
- **Compose 是 Jetpack 系列的全新 UI 库**,主要是被应用到 Android 界面开发,它就是为了重新定义 Android 上 UI 的编写方式, 所以你也可以选择不用,用不用都能开发 Android 的 UI,**但是如果你继续在Android 上深耕,那么你最好还是要学会**。
|
||||
- **Flutter 的未来在于多平台,更稳定可靠的多平台 UI 框架。如果你的路线方向不是大前端或者多端开发者,那你可以不会也没关系。**
|
||||
|
||||
而从使用这角度,不管你是先学会 Compose 还是先学会 Flutter,对于你掌握另外一项技能都有帮助,相当于学会一种就等于学会另一种的 70% :
|
||||
|
||||
- **如果你是原生开发,还没接触过 Flutter , 那先去学 Compose** ,这对你的 Android 生涯更有帮助,然后再学 Flutter 也不难。
|
||||
- **如果你已经在使用或者学习 Flutter ,那么请继续深造**,不必因为担心 Compose 而停滞不前,当你掌握了 Flutter 后其实离 Compose 也不远了。
|
||||
|
||||
> 对比了 Flutter 和 Conpose 的很多设计理念和源码,他们在实现上的相似度很高。
|
||||
|
||||
当然,**跨平台之所以是跨平台,首先就是要有对应原生平台的存在,** 很多原生平台的问题都需要回归到平台去解决,那些喜欢吹 xxx 制霸原生要凉的节奏,仅仅是因为“你的焦虑会成为它们的利润”,没有了平台还要跨平台干嘛?
|
||||
|
||||
|
||||
|
||||
## 一些见解
|
||||
|
||||
最后简单聊聊我的一些见解。
|
||||
|
||||
### 跨平台的底层逻辑
|
||||
|
||||
在 Flutter 之前,移动端跨平台的底层逻辑无非两种:
|
||||
|
||||
- 一种是靠 WebView 跨平台;
|
||||
- 一种是靠代理原生控件跨平台;
|
||||
|
||||
所以早期的移动端跨平台控件一开始就 Cordova 、Ionic 等这些框架,它们的目的就是将前端 H5 的能力拓展到 App 端,让前端开发能力也可以方便开发 Android 和 iOS 应用,那时候的口号我记得是:**write Once, run everywhere** 。
|
||||
|
||||
后来,得益于 React 的盛行,React Native 开辟了新的逻辑:用前端的方式去写原生 App ,通过把 JS 控件转化为原生控件进行渲染,让移动端跨平台的性能脱离了 WebView 的限制,性能得到了提升,而 React Native 强调的是 **learn once, write everywhere** ,也就是你学会了 React ,可以开发网页,也可以开发 App 。
|
||||
|
||||
而到了 Flutter ,它直接摆脱了平台控件的依赖,它自己产出了一套平台无关的控件,通过 GPU 直接渲染出来,这样做的成本无疑是最高的,但是所带来的“解耦”和“所见即所得”无疑是最好的,而 Flutter 的口号是 **Build apps for any screen** 。
|
||||
|
||||
**但是如果是放到真实应用场景上,不是说 Flutter 就是最优解,而是需要衡量你的业务场景来选择合适你的框架** , 例如:
|
||||
|
||||
- 如果你的业务场景是多框架混合开发,那 Flutter 明显不占据优势;
|
||||
- 如果你的场景是需要很强的文本编辑和富文本场景,那 Flutter 明显不占据优势;
|
||||
- 如果你的 KPI 对内存占用特别敏感,那 Flutter 也不是特别占据优势;
|
||||
- 如果你需要热更新,那 Flutter 也并不占据优势;
|
||||
|
||||
### 热更新
|
||||
|
||||
既然说到热更新,就简单介绍下热更新的问题。首先 Flutter 官方并不支持热更新,不像 React Native 一样有着十分成熟且通用的 `code-push` 框架。
|
||||
|
||||
> 为什么呢?首先 React Native 写的 JS 代码是属于纯脚本文本,就算打包成 bundle 文件它也是纯文本格式,所以通过 `code-push` 下发一个文本 bundle 并不违规,同时 `code-push` 也没办法下发打包后的原生平台代码,因为那不合规。
|
||||
|
||||
Flutter 打包后的 AOT 代码属于可执行二进制文件,如果通过热更新逻辑直接下发它,那无疑是违法了苹果 App store 和 Google Play 的政策,那 Flutter 能不能热更新呢?
|
||||
|
||||
答案是可以的,鉴于国内对热更新的“必须性”,也诞生了许多第三方框架,例如:
|
||||
|
||||
> MxFlutter(腾信) 、Fair (58 同城) 、 liteApp (企业微信)、Flap (MTFlutter 美团)、flutter_code_push (chimera) 等等。
|
||||
|
||||
它们都不是直接下发编译后的二进制代码,例如:
|
||||
|
||||
- MxFlutter 是用 js/ts 写控件来下发更新;
|
||||
- liteApp 是通过 vue 模版来输入;
|
||||
- Flap 是对 Dart 的 DSL 和编码过程做处理下发;
|
||||
|
||||
这些做法都需要为了热更新去做一些牺牲,所以本质上 Flutter 在热更新这个问题一直“不友好”。
|
||||
|
||||
> 当然,如果不上架 Google Play ,那么 Android 热更新 so 动态库本来就不是什么门槛,所以如果你其实可以在 Android 上粗暴地使用已有的插件化方案解决。
|
||||
|
||||
### 多平台
|
||||
|
||||
最后说一些 Flutter 的多平台,还记得前面说的 **Build apps for any screen** 吗?Flutter 不也是 *write Once, run everywhere* 吗?官方不就是支持一套代码直接打包 Android、iOS、Web、Window、MacOS、Linux 这些平台吗?
|
||||
|
||||
**从我的经验出发,我想说 *write Once, run everywhere* 很美好,但是不现实**。 Flutter 确实可以一套代码直接运行到所有平台,但是就目前的体验而言,一套代码去适配所有平台的成本远远高于它所带来的便捷。
|
||||
|
||||
先说 Web ,Web 平台在几个平台里最特殊,因为它本身就需要适配 Mobile 端和 PC 端的操作逻辑,而目前Flutter Web :
|
||||
|
||||
- 在 Mobile 端使用的是 `HtmlCanvas` ,也就是转化为 Web 端的“原生”控件进行渲染,这就带来了耦合和 API 适配的难度;
|
||||
- 在 PC 端 Flutter 可以使用 `CanvasKit` 来进行绘制,但是它使用 `wasm` 技术目前相对“激进” ,实际无论在体积、SEO、兼容性上都存在问题;
|
||||
|
||||
**所以 Flutter Web 目前还不好用,那它发布的稳定版本意义在哪里? 就在于你的代码支持打包成 Web!**
|
||||
|
||||
当你在构建完关于 Android 和 iOS 的应用后,你可以把 App 的一些 UI 和业务快速构建出 Web 页面,这就是它的价值所以,**你完全不需要从 0 开始去实现这部分以后的内容**,在“又不是不能用”的前提下。
|
||||
|
||||
|
||||
|
||||
> 目前比如阿里卖家、美团外卖商家课堂等等项目使用了 Flutter Web
|
||||
|
||||
|
||||
|
||||
再说 PC 端,PC 端本身的应用逻辑就和手机差异化很大:鼠标、键盘、可编窗口大小、横屏、滚动等这些方面,其实很难直接可以一套代码兼容,在我的理解更多是在 Android 和 iOS 上的一些控件、动画、UI、列表、业务逻辑等,可以在需要的时候直接在 PC 端上使用。**如果真的需要比较好的体验,个人建议还是至少把 PC 和 Mobile 分开两个业务项目实现**。
|
||||
|
||||
那如果真的要一套代码,有什么好的支持吗 ?也是有的,例如: `responsive_framework` 。
|
||||
|
||||
|
||||
|
||||
![image21](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image32)
|
||||
|
||||
![image22](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image33)
|
|
@ -1,198 +0,0 @@
|
|||
# 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 `<div>`中** ,当以这种方式嵌入时,Flutter 就变成了一个 Web 组件与 Web DOM 完全集成,甚至可以使用 CSS 来设置父 Flutter 对象的样式。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image24.gif)
|
||||
|
||||
> 例如将 Flutter 嵌入到基于 HTML 的网页中,然后使用 CSS 旋转效果,并且在旋转时 Flutter 内容仍可以交互。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image25.png)
|
||||
|
||||
同时 Dart 3 还对 Pub 上的 [js 包](http://pub.dev/packages/js)进行了一些重大更改,从而实现 **JavaScript 和 Dart 之间可以直接调用**,如里使用 `@JSExport` 属性注释 Dart 代码中的任何函数,然后从 JS 代码中调用它。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image26.png)
|
||||
|
||||
除此之外 Flutter Web 也有一系列的优化计划,其中针对体积大小的优化是最重要的指标之一。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image27.png)
|
||||
|
||||
从官方提供的数据下看,未来 Flutter Web 的加载速度将会不断提升。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image28.png)
|
||||
|
||||
最后,现在 Flutter 支持在 Web 上的使用 Pixel shaders ,从而实现各种炫酷的视觉效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image29.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image30.gif)
|
||||
|
||||
|
||||
|
||||
# Flutter 新闻工具包
|
||||
|
||||
本次还有一个有意思但是对国内比较鸡肋的介绍: [Flutter News Toolkit](https://github.com/flutter/news_toolkit),一个用来加速新闻应用开发的免费 Flutter 应用模板。
|
||||
|
||||
这是 Flutter 团队和 [GNI](https://newsinitiative.withgoogle.com/) 合作的项目,官方宣称与 iOS 和 Android 上的传统双端开发相比,在该领域使用 FNT 可以节省高达 80% 的时间。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image31.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image32.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
|
||||
|
||||
# 使用 Wonderous 适应大屏幕
|
||||
|
||||
Wonderous 早在去年 9 月份官方就[推荐过一次](https://mp.weixin.qq.com/s/cAwU2RmG-VtTBjPLweoobg) ,这一次主要是介绍了 Wonderous 的下一个版本,**增加了对可折叠设备、平板电脑和平板电脑横向的支持**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image33.png)
|
||||
|
||||
> 此次迭代同时测试了 Flutter 对不同设备格式的适配能力, 具体可见:https://github.com/gskinnerTeam/flutter-wonderous-app
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image34.png)
|
||||
|
||||
|
||||
|
||||
# Impeller
|
||||
|
||||
**随着 3.7 的发布,Impeller 现在已经可以在 iOS 上进行预览**。
|
||||
|
||||
Impeller 针对 Flutter 进行了优化,提供了更大的灵活性和对图形管道的控制支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image35.png)
|
||||
|
||||
例如使用预编译着色器,减少运行时由着色器编译引起的丢帧,利用 Metal 和 Vulkan 中的原始支持等等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image36.png)
|
||||
|
||||
除了让 UI 更流畅,Impeller 还可以在某些极端情况下显着提高性能,比如大会介绍的一个例子:
|
||||
|
||||
> 左边是默认渲染器,右边是 Impeller,可以看到滚动是左侧因为性能问题导致帧速率为 7-10 fps,而右侧 Impeller 可以稳定在 60 fps 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image37.png)
|
||||
|
||||
|
||||
|
||||
# 3D
|
||||
|
||||
**本次最后一个亮点就是 Flutter 未来将正式支持 3D 渲染**,同时也代表着 Flutter 在游戏领域的更进一步。
|
||||
|
||||
> 其实从去年的 I/O 也好,还有本次 [Flutter Forward](https://flutter.dev/events/flutter-forward) 提前预热的相关内容,可以看到 Flutter 进军游戏领域一直没有停歇。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image38.png)
|
||||
|
||||
在本次演示中,除了支持 3D 渲染之外,还支持对 3D 文件资源进行 hotload 、添加动画支持。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230126_FF/image39.png) | ![](http://img.cdn.guoshuyu.cn/20230126_FF/image40.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
可以看到,在演示中多个 3D 模型同时渲染动画的情况下,画面依然可以流畅运行,这绝对是本次 Flutter Forward 最让人期待的特性。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image41.gif)
|
||||
|
||||
最后官方还演示了在低端 iPhone 上的 3d 游戏场景(有指纹解锁的老 iPhone ),可以看到画面还是相当流畅。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230126_FF/image42.gif)
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
**看完之后你是不是蠢蠢欲动?但是这里面绝大多的都还只是开发中,可能会在未来可能还会有其他变动**,而本次 Flutter Forward 展示它们的目的,相信也是官方想让大家更直观地了解 Flutter 未来的方向。
|
||||
|
||||
最后总结一下,本次 Flutter Forward 主要的核心内容有:
|
||||
|
||||
- Impeller
|
||||
- 3D 支持
|
||||
- add-to-web 支持
|
||||
- Dart 3
|
||||
|
||||
让我们期待未来 Flutter 的更新能让这些 Feature 都能用上吧,在没有坑的情况下~
|
|
@ -1,544 +0,0 @@
|
|||
本篇将带你深入理解 Flutter 开发过程中关于字体和文本渲染的“冷”知识,帮助你理解和增加关于 Flutter 中字体绘制的“无用”知识点。
|
||||
|
||||
> 毕竟此类相关的内容太少了
|
||||
|
||||
首先从一个简单的文本显示开始,如下代码所示,运行后可以看到界面内出现了一个 **H** 字母,它的 `fontSize` 是 **100**,`Text` 被放在一个高度为 **200** 的 `Container` 中,然后如果这时候有人问你:**`Text` 显示 **H** 字母需要占据多大的高度,你知道吗?**
|
||||
|
||||
|
||||
```
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: new Text(
|
||||
"H",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image1)
|
||||
|
||||
### 一、TextStyle
|
||||
|
||||
如下代码所示,为了解答这个问题,首先我们给 `Text` 所在的 `Container` 增加了一个蓝色背景,并增加一个 `100 * 100` 大小的红色小方块做对比。
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
child: new Text(
|
||||
"H",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
结果如下图所示,可以看到 **H** 字母的上下有着一定的 `padding` 区域,蓝色`Container` 的大小明显超过了 **100** ,但是黑色的 **H** 字母本身并没有超过红色小方块,那蓝色区域的高度是不是 `Text` 的高度,它的大小又是如何组成的呢?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image2)
|
||||
|
||||
**事实上,前面的蓝色区域是字体的行高,也就是 line height** ,关于这个行高,首先需要解释的就是 `TextStyle` 中的 `height` 参数。
|
||||
|
||||
默认情况下 `height` 参数是 `null`,当我们把它设置为 **`1`** 之后,如下图所示,可以看到蓝色区域的高度和红色小方块对齐,变成了 **100** 的高度,也就是行高变成了 **100** ,而 **H** 字母完整的显示在蓝色区域内。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image3)
|
||||
|
||||
|
||||
那 `height` 是什么呢?根据文档可知,首先 `TextStyle` 中的 `height` 参数值在设置后,其效果值是 `fontSize` 的倍数:
|
||||
|
||||
- 当 `height` 为空时,行高默认是使用字体的**量度**(这个**量度**后面会有解释);
|
||||
- 当 `height` 不是空时,行高为 `height` * `fontSize` 的大小;
|
||||
|
||||
如下图所示,蓝色区域和红色区域的对比就是 `height` 为 `null` 和 `1` 的对比高度。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image4)
|
||||
|
||||
|
||||
另外上图的 `BaseLine` 也解释了:为什么 `fontSize` 为 100 的 **H** 字母,不是充满高度为 100 的蓝色区域。
|
||||
|
||||
根据上图的示意效果,在 `height` 为 1 的红色区域内,**H** 字母也应该是显示在基线之上,而基线的底部区域是为了如 g 和 j 等字母预留,所以如下图所示,在 `Text` 内加入 g 字母并打开 Flutter 调试的文本基线显示,由 Flutter 渲染的绿色基线也可以看到符合我们预期的效果。
|
||||
|
||||
> 忘记截图由 g 的了,脑补吧。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image5)
|
||||
|
||||
接着如下代码所示,当我们把 `height` 设置为 **`2`** ,并且把上层的高度为 **200** 的 `Container` 添加一个紫色背景,结果如下图所示,可以看到蓝色块刚好充满紫色方块,因为 `fontSize` 为 **100** 的文本在 **x2** 之后恰好高度就是 **200**。
|
||||
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
color: Colors.purple,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
child: new Text(
|
||||
"Hg",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
height: 2,
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image6)
|
||||
|
||||
> 不过这里的 `Hg` 是往下偏移的,为什么这样偏移在后面会介绍,还会有新的对比。
|
||||
|
||||
最后如下图所示,是官方提供的在不同 `TextStyle` 的 `height` 参数下, `Text` 所占高度的对比情况。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image7)
|
||||
|
||||
### 二、StrutStyle
|
||||
|
||||
那再回顾下前面所说的默认字体的**量度**,这个默认字体的**量度**又是如何组成的呢?这就不得不说到 `StrutStyle` 。
|
||||
|
||||
如下代码所示,在之前的代码中添加 `StrutStyle` :
|
||||
|
||||
- 设置了 `forceStrutHeight` 为 true ,这是因为只有 `forceStrutHeight` 才能强制重置 `Text` 的 `height` 属性;
|
||||
- 设置了`StrutStyle` 的 `height` 设置为 **`1`** ,这样 `TextStyle` 中的 `height` 等于 **`2`** 就没有了效果。
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
color: Colors.purple,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
child: new Text(
|
||||
"Hg",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
height: 2,
|
||||
),
|
||||
strutStyle: StrutStyle(
|
||||
forceStrutHeight: true,
|
||||
fontSize: 100,
|
||||
height: 1
|
||||
),
|
||||
|
||||
),
|
||||
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
效果如下图所示,虽然 `TextStyle` 的 `height` 是 **`2`** ,但是显示出现是以 `StrutStyle` 中 `height` 为 **`1`** 的效果为准。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image8)
|
||||
|
||||
|
||||
然后查看文档对于 `StrutStyle` 中 `height` 的描述,可以看到:`height` 的效果依然是 `fontSize` 的倍数,但是不同的是这里的对 `fontSize` 进行了补充说明 : `ascent + descent = fontSize`,其中:
|
||||
|
||||
- `ascent` 代表的是基线上方部分;
|
||||
- `descent` 代表的是基线的半部分
|
||||
|
||||
- 其组合效果如下图所示:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image9)
|
||||
|
||||
> Flutter 中 `ascent` 和 `descent` 是不能用代码单独设置。
|
||||
|
||||
除此之外,**`StrutStyle` 的 `fontSize` 和 `TextStyle` 的 `fontSize` 作用并不一样**:当我们把 `StrutStyle` 的 `fontSize` 设置为 **50** ,而 `TextStyle` 的 `fontSize` 依然是 **100** 时,如下图所示,可以看到黑色的字体大小没有发生变化,而蓝色部分的大小变为了 **50** 的大小。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image10)
|
||||
|
||||
有人就要说那 `StrutStyle` 这样的 `fontSize` 有什么用?
|
||||
|
||||
这时候,如果在上面条件不变的情况下,把 `Text` 中的文本变成 `"Hg\nHg"` 这样的两行文本,可以看到换行后的文本重叠在了一起,**所以 `StrutStyle`的 `fontSize` 也是会影响行高**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image11)
|
||||
|
||||
另外,在 `StrutStyle` 中还有另外一个参数也会影响行高,那就是 `leading` 。
|
||||
|
||||
如下图所示,加上了 `leading` 后才是 Flutter 中对字体行高完全的控制组合,`leading` 默认为 `null` ,同时它的效果也是 `fontSize` 的倍数,并且分布是上下均分。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image12)
|
||||
|
||||
|
||||
所以如下代码所示,当 `StrutStyle` 的 `fontSize` 为 **100** ,`height` 为 1,`leading` 为 1 时,可以看到 `leading` 的大小让蓝色区域变为了 **200**,从而 和紫色区域高度又重叠了,不同的对比之前的 `Hg` 在这次充满显示是居中。
|
||||
|
||||
```
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
color: Colors.purple,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
child: new Text(
|
||||
"Hg",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
height: 2,
|
||||
),
|
||||
strutStyle: StrutStyle(
|
||||
forceStrutHeight: true,
|
||||
fontSize: 100,
|
||||
height: 1,
|
||||
leading: 1
|
||||
),
|
||||
|
||||
),
|
||||
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 因为 `leading` 是上下均分的,而 `height` 是根据 `ascent` 和 `descent` 的部分放大,明显 `ascent` 比 `descent` 大得多,所以前面的 `TextStyle` 的 `height` 为 2 时,充满后整体往下偏移。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image13)
|
||||
|
||||
|
||||
### 三、backgroundColor
|
||||
|
||||
那么到这里应该对于 Flutter 中关于文本大小、度量和行高等有了基本的认知,接着再介绍一个属性:`TextStyle` 的 `backgroundColor` 。
|
||||
|
||||
> 介绍这个属性是为了和前面的内容产生一个对比,并且解除一些误解。
|
||||
|
||||
如下代码所示,可以看到 `StrutStyle` 的 `fontSize` 为 **100** ,`height` 为 **`1`**,按照前面的介绍,蓝色的区域大小应该是和红色小方块一样大。
|
||||
|
||||
然后我们设置了 `TextStyle` 的 `backgroundColor` 为具有透明度的绿色,结果如下图所示,可以看到 `backgroundColor` 的区域超过了 `StrutStyle`,显示为**默认情况下字体的度量**。
|
||||
|
||||
```
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
color: Colors.purple,
|
||||
alignment: Alignment.center,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
child: new Text(
|
||||
"Hg",
|
||||
style: TextStyle(
|
||||
fontSize: 100,
|
||||
backgroundColor: Colors.green.withAlpha(180)
|
||||
),
|
||||
strutStyle: StrutStyle(
|
||||
forceStrutHeight: true,
|
||||
fontSize: 100,
|
||||
height: 1,
|
||||
),
|
||||
|
||||
),
|
||||
|
||||
),
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
color: Colors.red,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image14)
|
||||
|
||||
这是不是很有意思,事实上也可以反应出,字体的度量其实一直都是默认的 `ascent + descent = fontSize`,我们可以改变 `TextStyle` 的 `height` 或者 `StrutStyle` 来改变行高效果,但是本质上的 `fontSize` 其实并没有变。
|
||||
|
||||
如果把输入内容换成 `"H\ng"` ,如下图所示可以看到更有意思的效果。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image15)
|
||||
|
||||
### 四、TextBaseline
|
||||
|
||||
最后再介绍一个属性 :`TextStyle` 的 `TextBaseline`,因为这个属性一直让人产生“误解”。
|
||||
|
||||
关于 `TextBaseline` 有两个属性,分别是 `alphabetic` 和 ` ideographic` ,为了更方便解释他们的效果,如下代码所示,我们通过 `CustomPaint` 把不同的基线位置绘制出来。
|
||||
|
||||
```
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: 400,
|
||||
color: Colors.purple,
|
||||
child: CustomPaint(
|
||||
painter: Text2Painter(),
|
||||
),
|
||||
)
|
||||
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class Text2Painter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
var baseLine = TextBaseline.alphabetic;
|
||||
//var baseLine = TextBaseline.ideographic;
|
||||
|
||||
final textStyle =
|
||||
TextStyle(color: Colors.white, fontSize: 100, textBaseline: baseLine);
|
||||
final textSpan = TextSpan(
|
||||
text: 'My文字',
|
||||
style: textStyle,
|
||||
);
|
||||
final textPainter = TextPainter(
|
||||
text: textSpan,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(
|
||||
minWidth: 0,
|
||||
maxWidth: size.width,
|
||||
);
|
||||
|
||||
final left = 0.0;
|
||||
final top = 0.0;
|
||||
final right = textPainter.width;
|
||||
final bottom = textPainter.height;
|
||||
final rect = Rect.fromLTRB(left, top, right, bottom);
|
||||
final paint = Paint()
|
||||
..color = Colors.red
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
canvas.drawRect(rect, paint);
|
||||
|
||||
// draw the baseline
|
||||
final distanceToBaseline =
|
||||
textPainter.computeDistanceToActualBaseline(baseLine);
|
||||
|
||||
canvas.drawLine(
|
||||
Offset(0, distanceToBaseline),
|
||||
Offset(textPainter.width, distanceToBaseline),
|
||||
paint..color = Colors.blue..strokeWidth = 5,
|
||||
);
|
||||
|
||||
// draw the text
|
||||
final offset = Offset(0, 0);
|
||||
textPainter.paint(canvas, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
```
|
||||
|
||||
如下图所示,蓝色的线就是 baseLine,从效果可以直观看到不同 baseLine 下对齐的位置应该在哪里。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image16)
|
||||
|
||||
但是事实上 baseLine 的作用并不会直接影响 `TextStyle` 中文本的对齐方式,Flutter 中默认显示的文本只会通过 `TextBaseline.alphabetic` 对齐的,如下图所示官方人员也对这个问题有过描述 [#47512](https://github.com/flutter/flutter/issues/47512#issuecomment-568007371)。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image17)
|
||||
|
||||
> 这也是为什么要用 `CustomPaint` 展示的原因,因为用默认 `Text` 展示不出来。
|
||||
|
||||
举个典型的例子,如下代码所示,虽然在 `Row` 和 `Text` 上都是用了 `ideographic` ,但是其实并没有达到我们想要的效果。
|
||||
|
||||
```
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Container(
|
||||
color: Colors.lime,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
'我是中文',
|
||||
style: TextStyle(
|
||||
fontSize: 55,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text('123y56',
|
||||
style: TextStyle(
|
||||
fontSize: 55,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
)),
|
||||
])),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 关键就算 `Row` 设置了 `center` ,这段文本看起来还是不是特别“对齐”。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image18)
|
||||
|
||||
|
||||
|
||||
自从,关于 Flutter 中的字体相关的“冷”知识介绍完了,不知道你“无用”的知识有没有增多呢?
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Cool/image19)
|
|
@ -1,112 +0,0 @@
|
|||
我们都知道在 Flutter 中可以通过 `fontFamily` 来引入第三方字体,例如**通常会将 svg 图标转换为 `iconfont.ttf` 来实现矢量图标的入**,而一般情况下我们是不会设置 `fontFamily` 来使用第三方字体, 那默认情况下 Flutter 使用的是什么字体呢?
|
||||
|
||||
|
||||
会出现这个疑问,是因为有一天设计给我发了下面那张图,问我 *“为什么应用在苹果平台上的英文使用的是 `PingFang SC` 字体而不是 `.SF UI Display` ”* ? 正如下图所示,它们的 *G* 字母在显示效果上会有所差异,比如 平方的 *G* 有明显的转折线。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Other/image1)
|
||||
|
||||
|
||||
**这时候我不禁产生的好奇,在 Flutter 中引擎默认究竟是如何选择字体?**
|
||||
|
||||
通过官方解释,在
|
||||
`typography.dart` 源码中可以看到,
|
||||
|
||||
- Flutter 默认在 Android 上使用的是 `Roboto` 字体;
|
||||
- 在 iOS 上使用的是 `.SF UI Display` 或者 `.SF UI Text` 字体。
|
||||
|
||||
> The default font on Android is Roboto and on iOS it is .SF UI Display or .SF UI Text (SF meaning San Francisco). If you want to use a different font, then you will need to add it to your app.
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Other/image2)
|
||||
|
||||
那理论上在 iOS 使用的就是 `.SF UI Display` 字体才对,因为如下源码所示,在 `Typography` 中当 `platform` 是 `iOS` 时,使用的就是 `Cupertino` 相关的 `TextTheme`,而 `Typography` 中的 `white` 和 `black` 属性最终会应用到 `ThemeData` 的 `defaultTextTheme`、 `defaultPrimaryTextTheme` 和 `defaultAccentTextTheme` 中,所以应该是使用 `.SF` 相关字体才会,为什么会显示的是 `PingFang SC` 的效果?
|
||||
|
||||
```
|
||||
factory Typography({
|
||||
TargetPlatform platform = TargetPlatform.android,
|
||||
TextTheme black,
|
||||
TextTheme white,
|
||||
TextTheme englishLike,
|
||||
TextTheme dense,
|
||||
TextTheme tall,
|
||||
}) {
|
||||
assert(platform != null || (black != null && white != null));
|
||||
switch (platform) {
|
||||
case TargetPlatform.iOS:
|
||||
black ??= blackCupertino;
|
||||
white ??= whiteCupertino;
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
black ??= blackMountainView;
|
||||
white ??= whiteMountainView;
|
||||
}
|
||||
englishLike ??= englishLike2014;
|
||||
dense ??= dense2014;
|
||||
tall ??= tall2014;
|
||||
return Typography._(black, white, englishLike, dense, tall);
|
||||
}
|
||||
```
|
||||
|
||||
为了搞清不同系统上字体的区别,在查阅了资料后可知:
|
||||
|
||||
- 默认在 iOS 上:
|
||||
|
||||
- 中文字体:`PingFang SC`
|
||||
|
||||
- 英文字体:`.SF UI Text` 、`.SF UI Display`
|
||||
|
||||
- 默认在 Android 上:
|
||||
|
||||
- 中文字体:`Source Han Sans` / `Noto`
|
||||
|
||||
- 英文字体:`Roboto`
|
||||
|
||||
也就是就 iOS 上除了 `.SF` 相关的字体外,还有 `PingFang` 字体的存在,这时候我突然想起在之前的 [《Flutter完整开发实战详解(十七、 实用技巧与填坑二)》](https://juejin.im/post/5d6cb579f265da03da24aeb9#heading-10) 中,因为国际化多语言在 `.SF` 会出现显示异常,所以使用了 `fontFamilyFallback` 强行指定了 `PingFang SC` 。
|
||||
|
||||
|
||||
```
|
||||
getCopyTextStyle(TextStyle textStyle) {
|
||||
return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Other/image3)
|
||||
|
||||
终于破案了,因为当 `fontFamily` 没有设置时,就会使用 `fontFamilyFallback` 中的第一个值将作为首选字体,而在 `fontFamilyFallback` 中是顺序匹配的,当`fontFamily` 和 `fontFamilyFallback` 两者都不提供,则将使用默认平台字体。
|
||||
|
||||
|
||||
而在 1.12.13 版本下测试发现 `.SF` 导致的问题已经修复了,所以只需要将 `fontFamilyFallback` 相关的代码去除即可。
|
||||
|
||||
**那在 iOS 上使用 `.SF` 字体有什么好处?** 按照网络上的说法是:
|
||||
|
||||
> `SF Text` 的字距及字母的半封闭空间,比如 `"a"!` 上半部分会更大,因其可读性更好,适用于更小的字体; `SF Display` 则适用于偏大的字体。具体分水岭就是 `20pt` , 即字体小于 `20pt` 时用 `Text` ,大于等于 `20pt` 时用 `Display` 。
|
||||
>
|
||||
> 更棒的是由于 `SF` 属于动态字体,`Text` 和 `Display` 两种字体族是系统动态匹配的,也就是说你不用费心去自己手动调节,系统自动根据字体的大小匹配这两种显示模式。
|
||||
|
||||
|
||||
那能不能在 Android 上也使用`.SF` 字体呢?按照官方的说法:
|
||||
|
||||
- 在使用 Material package 时,在 Android 上使用的是 ·Roboto font· ,而 iOS 使用的是 `San Francisco font(SF)` ;
|
||||
- 在使用 Cupertino package 时,默认主题始终使用 `San Francisco font(SF)` ;
|
||||
|
||||
**但是因为 `San Francisco font license` 限制了该字体只能在 iOS、macOS 或 tvOS 上运行使用,所以如果使用了 Cupertino 主题的话,在 Android 上运行时使用 fallback font。**
|
||||
|
||||
|
||||
所以你觉得能不能在 Android 上使用?
|
||||
|
||||
|
||||
最后再补充下,在官方的 [architecture](https://github.com/flutter/flutter/wiki/The-Engine-architecture) 中有提到,在 Flutter 中的文本呈现逻辑是有分层的,其中:
|
||||
|
||||
- 衍生自 Minikin 的 libtxt 库用于字体选择,分隔行等;
|
||||
- HartBuzz 用于字形选择和成型;
|
||||
- Skia作为 渲染 / GPU后端;
|
||||
- 在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 来渲染字体 。
|
||||
|
||||
|
||||
> 那读完本篇,你奇奇怪怪的知识点有没有增加?
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200619_Flutter-Font-Other/image4)
|
|
@ -1,168 +0,0 @@
|
|||
在以前的 [《Flutter 上默认的文本和字体知识点》](https://juejin.cn/post/6844904164082843655) 和 [《带你深入理解 Flutter 中的字体“冷”知识》](https://juejin.cn/post/6844904174023344136) 中,已经介绍了很多 Flutter 上关于字体有趣的知识点,而本篇讲继续介绍 Flutter 上关于 `Text` 的一个属性:`FontFeature` , **事实上相较于 Flutter ,本篇内容可能和前端或者设计关系更密切**。
|
||||
|
||||
> **相信本篇绝对是你能看到关于 Flutter `FontFeature` 相关的少数资料之一。**
|
||||
|
||||
|
||||
什么是 `FontFeature`? **简单来说就是影响字体形状的一个属性** ,在前端的对应领域里应该是 `font-feature-settings`,它有别于 `FontFamily` ,是用于指定字体内字的形状的一个参数。
|
||||
|
||||
> 如下图所示是 `frac` 分数和 `tnum` 表格数字的对比渲染效果,这种效果可以在不增加字体库时实现特殊的渲染,另外 `Feature` 也有特征的意思,所以也可以理解为字体特征。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image1)
|
||||
|
||||
我们知道 Flutter 默认在 Android 上使用的是 `Roboto` 字体,而在 iOS 上使用的是 `SF` 字体,但是其实 `Roboto` 字体也是分很多类型的,比如你去查阅手机的 `system/fonts` 目录,就会发现很多带有 `Roboto` 字样的字体库存在。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image2)
|
||||
|
||||
|
||||
所以 `Roboto` 之类的字体库是一个很大的字体集,不同的 `font-weight` 其实对应着不同的 `ttf` ,例如默认情况下的 **`Roboto` 是不支持 `font-weight` 为 600 的配置**:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image3)
|
||||
|
||||
所以如下图所示,如果我们设置了 `w400` - `w700` 的 `weight` ,可以很明显看到中间的 500 和 600 其实是一样的粗细,所以在**设置 `weight` 或者设计 UI 时,就需要考虑不同平台上的 `weight` 是否支持想要的效果**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image4)
|
||||
|
||||
|
||||
回归到 `FontFeature` 上,那 `Roboto` 自己默认支持多少种 features 呢? 答案是 26 种,它们的编码如下所示,运行后效果也如下图所示,从日常使用上看,这 26 种 Feature 基本满足开发的大部分需求。
|
||||
|
||||
> "c2sc"、 "ccmp"、 "dlig"、 "dnom"、 "frac"、 "liga"、 "lnum"、 "locl"、 "numr"、 "onum"、 "pnum"、 "salt"、 "smcp"、 "ss01"、 "ss02"、 "ss03"、 "ss04"、 "ss05"、 "ss06"、 "ss07"、 "tnum"、 "unic"、 "cpsp"、 "kern"、 "mark"、 "mkmk"
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image5)
|
||||
|
||||
而 iOS 上的 `SF pro` 默认支持 39 种 Features , 它们的编码如下所示,运行后效果也如下图所示,可以看到 `SF pro` 支持的 Features 更多。
|
||||
|
||||
> "c2sc"、 "calt"、 "case"、 "ccmp"、 "cv01"、 "cv02"、 "cv03"、 "cv04"、 "cv05"、 "cv06"、 "cv07"、 "cv08"、 "cv09"、 "cv10"、 "dnom"、 "frac"、 "liga"、 "locl"、 "numr"、 "pnum"、 "smcp"、 "ss01"、 "ss02"、 "ss03"、 "ss05"、 "ss06"、 "ss07"、 "ss08"、 "ss09"、 "ss12"、 "ss13"、 "ss14"、 "ss15"、 "ss16"、 "ss17"、 "subs"、 "sups"、 "tnum"、 "kern"
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image6)
|
||||
|
||||
所以可以看到,并不是所有字体支持的 Features 都是一样的,比如 iOS 上支持 `sups` 上标显示和 `subs` 下标显示,但是 Android 上的 Roboto 并不支持,甚至很多第三方字体其实并不支持 Features 。
|
||||
|
||||
> 同样在 Web 上也存在各种限制,比如 `swsh`(花体)默认下基本不支持浏览器,`fwid` 、 `nlck` 不支持 Safari 浏览器等。
|
||||
|
||||
有趣的是,在 Flutter Web 有一个渲染文本时会变模糊的问题[#58159](https://github.com/flutter/flutter/issues/58159),这个问题目前官方还没有修复,但是你可以通过给 `Text` 设置任意 `FontFeatures` 来解决这个问题。
|
||||
|
||||
> **因为出现模糊的情况一般都是因为使用了 `canvas` 标签绘制文本,而如果 `Text` 控件具有 `fontFeatures` 时,就会被设置为 `<p>` + `<span>` 进行渲染,从而避免问题**。
|
||||
|
||||
最后,如果对 FontFeature 还感兴趣的朋友,可以通过一下资料深入了解,如果你还有什么关于字体上的问题,欢迎留言讨论。
|
||||
|
||||
|
||||
- 如果你想了解更多的 features 类型,可以通过 https://en.wikipedia.org/wiki/List_of_typographic_features 了解更多;
|
||||
|
||||
- 如果你对自己的使用的字体支持什么 features 感兴趣,可以通过 https://wakamaifondue.com 了解更多;
|
||||
|
||||
## 补充内容
|
||||
|
||||
**基于网友的问题再补充一下拓展知识,毕竟这方面内容也不多**。
|
||||
|
||||
事实上在 dart 里就可以看到对应 `FontWeight` 约定俗称用的是字体集里的什么字体:
|
||||
|
||||
|
||||
| 名称 | 值 |
|
||||
| -------------------- | ---------- |
|
||||
| Thin | w100 |
|
||||
| Extra | w200 |
|
||||
| Light | w300 |
|
||||
| Normal/regular/plain | w400(默认) |
|
||||
| Medium | w500 |
|
||||
| Semi-bold | w600 |
|
||||
| Bold | w700 |
|
||||
| Extra-bold- | w800 |
|
||||
| Black | 900 |
|
||||
|
||||
|
||||
所以如果对于默认字体有疑问,可以在你的手机字体找找是否有对应的字体,**比如虽然我们说 roboto 没有 600 ,但是如果是 roboto mono 字体集是有 600 的 fontweight**,甚至还有 600 斜体: https://fonts.google.com/specimen/Roboto+Mono 。
|
||||
|
||||
|
||||
这里可以用 Android Studio 的 `Device File Explorer` 查看`/system/etc/fonts.xml` 下当前手机的字体编码情况,右键该文件 `save as` 到电脑上,下图是华为上的 `fonts.xml` 截图:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image7)
|
||||
|
||||
你也可以通过如下原生代码,获取到对应现在 Android 系统支持的字体 `Typeface` ,但是这个 `Typeface` 并不是真正的字体名,还是要对应在 `fonts.xml` 下查看。
|
||||
|
||||
```java
|
||||
protected Map<String, Typeface> getSSystemFontMap() {
|
||||
Map<String, Typeface> sSystemFontMap = null;
|
||||
try {
|
||||
//Typeface typeface = Typeface.class.newInstance();
|
||||
Typeface typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL);
|
||||
Field f = Typeface.class.getDeclaredField("sSystemFontMap");
|
||||
f.setAccessible(true);
|
||||
sSystemFontMap = (Map<String, Typeface>) f.get(typeface);
|
||||
for (Map.Entry<String, Typeface> entry : sSystemFontMap.entrySet()) {
|
||||
Log.e("FontMap", entry.getKey() + " ---> " + entry.getValue() + "\n");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return sSystemFontMap;
|
||||
}
|
||||
|
||||
private static List<String> getKeyWithValue(Map map, Typeface value) {
|
||||
Set set = map.entrySet();
|
||||
List<String> arr = new ArrayList<>();
|
||||
for (Object obj : set) {
|
||||
Map.Entry entry = (Map.Entry) obj;
|
||||
if (entry.getValue().equals(value)) {
|
||||
String str = (String) entry.getKey();
|
||||
arr.add(str);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
```
|
||||
|
||||
例如前面我们说过 Roboto 没有 `w600` , 但是通过输出比对,华为上有 `source-sans-pro` 是支持 `w600` :
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image8)
|
||||
|
||||
|
||||
另外注意这是 Flutter 而不是原生,具体实现调用是在 Engine 的 *paragraph_skia.cc* 和 *paragraph_builder_skia.cc* 下对应的 `setFontFamilies` 相关逻辑,当然默认字体库指定在 `typography.dart` 下就看到,例如 `'Roboto'` 、 `'.SF UI Display'` 、`'.SF UI Text'` 、`'.AppleSystemUIFont'` 、 `'Segoe UI'` :
|
||||
|
||||
| 名称 | 值 |
|
||||
| ----------------------- | --------------------------- |
|
||||
| Android,Fuchsia,Linux | Roboto |
|
||||
| iOS | .SF UI Display,.SF UI Text |
|
||||
| MacOS | .AppleSystemUIFont |
|
||||
| Windows | Segoe UI |
|
||||
|
||||
> 例如:**.SF Text 适用于更小的字体;.SF Display 则适用于偏大的字体,我记得分水岭好像是 20pt 左右,不过 SF(San Francisco) 属于动态字体,系统会动态匹配**。
|
||||
|
||||
另外如果你在 Mac 的 Web 上使用 Flutter Web,可以看到指定的是 `.AppleSystemUIFont` ,而对于 `.AppleSystemUIFont` 它其实不算是一种字体,而是苹果上字体的一种集合别称:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image9)
|
||||
|
||||
还有,如果你去看 Flutter 默认自带的 `cupertino/context_menu_action.dart` ,就可以看到一个有趣的情况:
|
||||
|
||||
> **为了强调和 iOS 上的样式尽量一直,当开发者配置 `isDefaultAction == true` 时,会强行指定 `'.SF UI Text'` 并指定为 `FontWeight.w600`**。
|
||||
|
||||
|
||||
**当然,前面我们说了那么多,主要是针对英文的情况下,而在中文下还是有差异的**,之前的文章也介绍过:
|
||||
|
||||
- 默认在 iOS 上:
|
||||
|
||||
- 中文字体:`PingFang SC`
|
||||
- 英文字体:`.SF UI Text` 、`.SF UI Display`
|
||||
|
||||
- 默认在 Android 上:
|
||||
|
||||
- 中文字体:`Source Han Sans` / `Noto`
|
||||
- 英文字体:`Roboto`
|
||||
|
||||
例如,在苹果上的简体中文其实会是 `PingFang SC` 字体,对应还有`PingFang TC` 和 `PingFang HK` 的繁体集,而关于这个问题在 Flutter 上之前还出现过比较有意思的 bug :
|
||||
> 用户在输入拼音时,iOS 会在中文拼音之间添加额外的 `unicode \u2006` 字符,比如输入 `"nihao"` ,iOS 系统会在 skia 中添加文字 `“ni\u2006hao ”`,从而导致字体无效的情况。
|
||||
|
||||
当然后续的 [#16709](https://github.com/flutter/engine/pull/16709/files) 修复了这个问题 ,而在以前的文章我也讲过,当时我遇到了 **“Flutter 在 iOS 系统上,系统语言是韩文时,在和中文一起出现会导致字体显示异常" 的问题** :
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image10)
|
||||
|
||||
解决方法也很简单,就是给 `fontFamilyFallback` 配置上 `["PingFang SC" , "Heiti SC"]` 就可以了,这是因为韩文在苹果手机上使用的应该是 `Apple SD Gothic Neo` 这样的超集字体库,【广】这个字符在这个字体集上是不存在的,所以就变成了中文的【广】;
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-FontFeature/image11)
|
||||
|
||||
**所以可以看到,字体相关是一个平时很少会深入接触的东西,但是一旦涉及多语言和绘制,就很容易碰到问题的领域**。
|
279
Flutter-GB.md
279
Flutter-GB.md
|
@ -1,279 +0,0 @@
|
|||
# 完整解析使用 Github Action 构建和发布 Flutter 应用
|
||||
|
||||
Github Actions 是 Github 提供的免费自动化构建实现,特别适用于持续集成和持续交付的场景,它具备自动化完成许多不同任务的能力,例如构建、测试和部署等等。
|
||||
|
||||
## 一、简单介绍
|
||||
|
||||
用户只需要在自己 Github 的开源项目下创建 `.github/workflows` 脚本就可以完成接入,另外针对 Github Actions 官方还提供了 [marketplace](https://github.com/marketplace/actions) 用于开发者提交或者引用别人写好的 aciton ,**所以很多时候开发者在使用 Github Actions 时,其实会变成了在 [marketplace](https://github.com/marketplace/actions) 里挑选和组合 action 的场景。当然,这样各有利弊,后面我们会讲到** 。
|
||||
|
||||
![image-20220330110809824](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image1)
|
||||
|
||||
要在 Github 存储库中使用 Github Actions,首先需要创建目录`.github/workflows/`,然后在 `workflows` 文件夹里创建不同的 `.yml` 文件用于响应或者执行不同的事件,比如 ` git push` 、`pull request ` 等,例如:
|
||||
|
||||
```yaml
|
||||
name: GitHub Actions Demo
|
||||
on: [push]
|
||||
jobs:
|
||||
Explore-GitHub-Actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
|
||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
|
||||
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ github.workspace }}
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
```
|
||||
|
||||
上面是 [Github doc](https://docs.github.com/en/actions/quickstart) 里关于 Action 的一个基本的工作流 yml 文件,具体参数含义 :
|
||||
|
||||
- **name**:这表示该工作流文件的名称,将在 Github 的 actions 选项卡作为名称显示 ;
|
||||
- **on**: 这将触发该工作流的事件名称,它可以包含事件列表,例如这里监听的事 `push`;
|
||||
- **jobs**: 每个工作流会包含一个或多个 jobs ,在这里只有一个,主要是用于表示不同工作任务;
|
||||
- **Explore-GitHub-Actions** :这是工作 ID,你也可以根据自己的需要命名,会在 action 的执行过程中显示;
|
||||
- **runs-o**: jobs 需要运行在虚拟机上,在这里中使用了 `ubuntu-latest`,当然你也可以使用`windows-latest ` 或者 `macos-latest`;
|
||||
- **steps**:每个 jobs 可以将需要执行的内容划分为不同步骤;
|
||||
- **run**: 用于提供执行命令,例如这里使用了`echo` 打印日志;
|
||||
- **name** :steps 里的 name 是可选项,主要是在日志中用来做标记的;
|
||||
- **uses** : 使用一些官方或者第三方的 actions 来执行,例如这里使用官方的 `actions/checkout@v2`,它会check-out 我们的 repo ,之后工作流可以直接访问 repo 里的文件;
|
||||
|
||||
在 GitHub 仓库添加完对应的 `.github/workflows/ci.yml` 文件之后,以后每次 `push` 都可以触发 action 的自动执行,以此来完成可持续的自动集成和构建能力。
|
||||
|
||||
![image-20220330112846187](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image2)
|
||||
|
||||
|
||||
|
||||
## 二、构建 Flutter 和发布到 Github Release
|
||||
|
||||
简单介绍完 Github Action ,接着我们介绍如何利用 Github Action 构建 Flutter 和发布 apk 到 Github Release,如下代码所示是 [gsy_github_app_flutter](https://github.com/CarGuo/gsy_github_app_flutter) 项目里使用到的 github action 脚本:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 11
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '2.8.1'
|
||||
- uses: finnp/create-file-action@master
|
||||
env:
|
||||
FILE_NAME: lib/common/config/ignoreConfig.dart
|
||||
FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}
|
||||
- run: flutter pub get
|
||||
- run: flutter build apk --release --target-platform=android-arm64 --no-shrink
|
||||
|
||||
apk:
|
||||
name: Generate APK
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 8
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '2.5.3'
|
||||
- uses: finnp/create-file-action@master
|
||||
env:
|
||||
FILE_NAME: lib/common/config/ignoreConfig.dart
|
||||
FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}
|
||||
- run: flutter pub get
|
||||
- run: flutter build apk --release --target-platform=android-arm64 --no-shrink
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: apk
|
||||
path: build/app/outputs/apk/release/app-release.apk
|
||||
release:
|
||||
name: Release APK
|
||||
needs: apk
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download APK from build
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: apk
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
- name: Upload Release APK
|
||||
id: upload_release_asset
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./app-release.apk
|
||||
asset_name: app-release.apk
|
||||
asset_content_type: application/zip
|
||||
```
|
||||
|
||||
根据上述脚本,首先可以看到:
|
||||
|
||||
- 在 `push` 事件里我们指定了只监听 master 分支和 tags 相关的提交;
|
||||
|
||||
- 然后在 `pull_request` 事件里忽略了关于 .md、 .text 和图片相关的内容,也就是这部分内容提交不触发 action ,具体可以看你自己的需求;
|
||||
|
||||
- 接着进入到 jobs 里,首先不管是 `push` 还是 `pull_request` 都会执行到 `Build` 事件,运行在 `ubuntu-latest` 虚拟机上,之后利用 `actions/checkout@v2` checkout 代码;
|
||||
|
||||
- 接着使用 `actions/setup-java@v2` 配置 java 环境,这里使用的是 `Zulu OpenJDK` 版本 11 ,下面表格是 setup-java 支持的可选 java 类型;
|
||||
|
||||
| Keyword | Distribution | Official site | License |
|
||||
| -------------------------- | -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| `temurin` | Eclipse Temurin | [Link](https://adoptium.net/) | [Link](https://adoptium.net/about.html) |
|
||||
| `zulu` | Zulu OpenJDK | [Link](https://www.azul.com/downloads/zulu-community/?package=jdk) | [Link](https://www.azul.com/products/zulu-and-zulu-enterprise/zulu-terms-of-use/) |
|
||||
| `adopt` or `adopt-hotspot` | Adopt OpenJDK Hotspot | [Link](https://adoptopenjdk.net/) | [Link](https://adoptopenjdk.net/about.html) |
|
||||
| `adopt-openj9` | Adopt OpenJDK OpenJ9 | [Link](https://adoptopenjdk.net/) | [Link](https://adoptopenjdk.net/about.html) |
|
||||
| `liberica` | Liberica JDK | [Link](https://bell-sw.com/) | [Link](https://bell-sw.com/liberica_eula/) |
|
||||
| `microsoft` | Microsoft Build of OpenJDK | [Link](https://www.microsoft.com/openjdk) | [Link](https://docs.microsoft.com/java/openjdk/faq) |
|
||||
|
||||
- 接着就是使用第三方的 `subosito/flutter-action@v1` 配置 flutter 环境,直接通过 `flutter-version: '2.8.1'` 指定了 Flutter 版本;
|
||||
|
||||
- 接着是使用第三方的 ` finnp/create-file-action@master` 创建文件,因为 [gsy_github_app_flutter](https://github.com/CarGuo/gsy_github_app_flutter) 项目有一个配置文件是需要用户根据自己的 ID 和 SECRET 手动创建,所以这里通过 create-file-action 创建文件并输入内容;
|
||||
|
||||
- 在上述输入内容部分,有一个 `secrets.xxx` 的参数,因为构建时需要将自己的一些密钥信息配置到 action 里,所以如下图所示,可以在 `Settings` 的 `Secrets` 里添加对应的内容,就可以在 action 里通过 `secrets.xxx` 读取;
|
||||
|
||||
![image-20220330114509039](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image3)
|
||||
|
||||
- 接着配置好环境之后,就可以执行 `flutter pub get` 和 ` flutter build apk` 执行构建;
|
||||
|
||||
完成 Build 任务的逻辑介绍之后,可以看到在 Build 任务下面还有一个 apk 任务,该任务基本和 Build 任务一直,不同之处在于:
|
||||
|
||||
- 多了一个 `if: startsWith(github.ref, 'refs/tags/')` ,也就是存在 tag 的时候才会触发该任务执行;
|
||||
- 多了一个 `actions/upload-artifact@v2` 用于将构建出来的 `build/app/outputs/apk/release/app-release.apk`上传,并等到 release 任务内使用;
|
||||
|
||||
完成 apk 任务之后,会进入到 release 任务,该任务同样通过 if 指定了只在 tag 提交时运行:
|
||||
|
||||
- 任务首先会通过 `actions/download-artifact@v2` 下载刚刚上传的 apk;
|
||||
- 然后就通过 `actions/create-release@v1.1.4` 创建一个 release 版本,这里使用的 `secrets.GITHUB_TOKEN ` 是官方内置的 secrets ,我们直接使用就可以了;
|
||||
- 最后通过 `actions/upload-release-asset@v1.0.1` 将 apk 上传到刚刚创建的 release 版本里,自此就完成了 action 的发布流程;
|
||||
|
||||
**可以看到整个过程其实都是在组合不同的 action ,可以很灵活方便地配置构建逻辑**,例如如果你的项目是单纯的 android sdk 项目,那同样可以通过如下脚本进行发布管理:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '.idea/**'
|
||||
- '.gitattributes'
|
||||
- '.github/**.json'
|
||||
- '.gitignore'
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- '**/*.txt'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- 'LICENSE'
|
||||
- 'NOTICE'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.idea/**'
|
||||
- '.gitattributes'
|
||||
- '.github/**.json'
|
||||
- '.gitignore'
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- '**/*.txt'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- 'LICENSE'
|
||||
- 'NOTICE'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to MavenLocal
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: publishToMavenLocal
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: app:assembleDebug
|
||||
```
|
||||
|
||||
当然,如果你需要打包的是 iOS ,那么你就需要使用 `macos-latest` 的环境,另外还需要配置相关的开发者证书,这个过程可能会比较难受,相关可以参考 [《Flutter 搭建 iOS 命令行服务打包发布全保姆式流程》](https://juejin.cn/post/6953144821611495431) 。
|
||||
|
||||
|
||||
|
||||
## 三、隐私安全问题
|
||||
|
||||
|
||||
|
||||
最后,**关于 Github Actions 之前存在过出现泄露敏感数据的问题,比如 Github 的 Token 等** ,举个例子,如上面的脚本,它在执行任务时都会需要秘钥 ,如果你使用的第三方 action 在执行过程中获取了你的密钥并干了一些“非法” 的事情,就可能出现异常泄漏问题。
|
||||
|
||||
![image-20220330132722744](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image4)
|
||||
|
||||
**所以一般情况下建议大家都要去看下非官方的脚本实现里是否安全**,但是由于 tag 和 branch 是可以修改,所以建议不要@分支或tag,而是应该 checkout 对应的提交哈希,这样有利于你审查使用时的脚本是否安全。
|
||||
|
||||
另外,例如还有人提到可以通过 pull_request 来恶意攻击获取对应隐私:
|
||||
|
||||
* 1、fork 一个正在使用 GitHub Actions 的公开代码库;
|
||||
|
||||
* 2、创建一个基于该项目的 pull 请求;
|
||||
|
||||
* 3、使用 pull_request_target 事件创建一个恶意 Actions 工作流,然后单独向该 fork 库 commit;
|
||||
|
||||
* 4、将第二步基分支的 pull 请求更新为第三步的 commit 哈希;
|
||||
|
||||
之后恶意 Actions 工作流就会运行,并从目标 repos 里获取到执行过程的敏感数据,此时攻击者将拥有对目标存储库的写访问权限,除此之外他们还可以通过 GitHub 访问与仓库之成的任何服务。
|
||||
|
||||
**所以虽然 GitHub Action 很便捷,但是如果出于商业考虑的话,还需要谨慎抉择安全问题**。
|
364
Flutter-GLSL.md
364
Flutter-GLSL.md
|
@ -1,364 +0,0 @@
|
|||
# Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画
|
||||
|
||||
今天聊一个比较有意思的 Flutter 动画实现,如果需要实现一个如下图的 3D 折叠动画效果,你会选择通过什么方式?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image1.gif)
|
||||
|
||||
相信可能很多人第一想法就是:**在 Dart 里通过矩阵变换配合 Canvas 实现**。
|
||||
|
||||
因为这个效果其实也算「常见」,在目前的小说阅读器场景里,类似的翻页效果基本都是通过这个思路完成,而这个思路以前我也「折腾」过不少,比如 [《炫酷的 3D 卡片和帅气的 360° 展示效果》](https://juejin.cn/post/7124064789763981326) 和 [用纯代码实现立体 Dash 和 3D 掘金 Logo](https://juejin.cn/post/7129239231473385503) ,就是在 Dart 里利用矩阵变换实现的视觉 3D 效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image2.gif)
|
||||
|
||||
但是今天通过一个叫 [riveo_page_curl](https://github.com/Rahiche/riveo_page_curl) 的项目,提供了不一样的实现方式,**那就是通过自定义 Fragment Shaders 实现动画** ,使用自定义 shaders 可以直接使用 GLSL 语言来进行编程,最终达到通过 GPU 渲染出更丰富图形效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image3.gif)
|
||||
|
||||
解释这个项目之前,我们先聊聊 Fragment Shader ,**Flutter 在 3.7 开始提供 Fragment Shader API** ,顾名思义,它是一个作用于片段的着色器,也就是通过 Fragment Shader API ,开发者可以直接介入到 Flutter 渲染管道的渲染流程中。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image4.png)
|
||||
|
||||
**那么直接使用 Fragment Shader 而不使用 Dart 矩阵变换的好处是什么**?简单来说就是可以减少 CPU 的耗时,直接通过图形语言(GLSL)直接给 GPU 发送指令,从性能上无疑可以得到提升,并且实现会更简洁。
|
||||
|
||||
> 不过加载着色器这个行为的开销可能会比较大,所以必须在运行时将它编译为适当的特定于平台的着色器。
|
||||
|
||||
当然,在 Flutter 里使用 Fragment Shader 也是有条件限制的,例如一般都需要引入 `#include <flutter/runtime_effect.glsl>` 这个头文件,因为在编写着色器代码时,我们都需要知道当前片段的局部坐标的值,而 `flutter/runtime_effect.glsl` 里就提供了 `FlutterFragCoord().xy;` 来支持访问局部坐标,而这并不是标准 GLSL 的 API 。
|
||||
|
||||
另外, Fragment Shader 只支持 `.frag` 格式文件, 不支持顶点着色文件 `.vert` ,同时还有以下限制:
|
||||
|
||||
- 不支持 UBO 和 SSBO
|
||||
- sampler2D 是唯一受支持的 sampler 类型
|
||||
- texture 仅支持( sampler 和 uv)的两个参数版本
|
||||
- 不能声明额外的可变输入
|
||||
- 不支持无符号整数和布尔值
|
||||
|
||||
所以如果需要搬运一些已有的 GLSL 效果,例如 [shadertoy](https://www.shadertoy.com/) 上的代码时,那么一些必要的「代码改造」还是逃不掉的,例如下方代码是一段渐变动画的着色器:
|
||||
|
||||
```glsl
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
|
||||
float strength = 0.4;
|
||||
float t = iTime/3.0;
|
||||
|
||||
vec3 col = vec3(0);
|
||||
vec2 fC = fragCoord;
|
||||
|
||||
for(int i = -1; i <= 1; i++) {
|
||||
for(int j = -1; j <= 1; j++) {
|
||||
|
||||
fC = fragCoord+vec2(i,j)/3.0;
|
||||
vec2 pos = fC/iResolution.xy;
|
||||
pos.y /= iResolution.x/iResolution.y;
|
||||
pos = 4.0*(vec2(0.5) - pos);
|
||||
for(float k = 1.0; k < 7.0; k+=1.0){
|
||||
pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;
|
||||
pos.y += strength * cos(2.0*t+k*1.5 * pos.x);
|
||||
}
|
||||
col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));
|
||||
}
|
||||
}
|
||||
col /= 9.0;
|
||||
col = pow(col, vec3(0.4545));
|
||||
fragColor = vec4(col,1.0);
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image5.gif)
|
||||
|
||||
而在 Flutter 里,就需要转化为如下代码所示:
|
||||
|
||||
- 首先就是必不可少的 `flutter/runtime_effect.glsl`
|
||||
- 其次定义 `main() ` 函数
|
||||
- 然后我们需要将 `mainImage` 里定义的 `out vec4 fragColor;` 移到全局声明
|
||||
- 因为在 GLSL 里 iResolution 用于表示画布像素高宽,iTime 是程序运行的时间,而这里通过 `uniform` 定义 `resolution` 和 `iTime` 直接用于接受 Dart 端的输入,其余逻辑不变
|
||||
- 对应 `fragCoord` 可以在 Flutter 里通过 `FlutterFragCoord ` 获取坐标
|
||||
|
||||
```glsl
|
||||
#version 460 core
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform float iTime;
|
||||
|
||||
void main(){
|
||||
float strength = 0.25;
|
||||
float t = iTime/8.0;
|
||||
vec3 col = vec3(0);
|
||||
vec2 pos = FlutterFragCoord().xy/resolution.xy;
|
||||
pos = 4.0*(vec2(0.5) - pos);
|
||||
for(float k = 1.0; k < 7.0; k+=1.0){
|
||||
pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;
|
||||
pos.y += strength * cos(2.0*t+k*1.5 * pos.x);
|
||||
}
|
||||
col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));
|
||||
col = pow(col, vec3(0.4545));
|
||||
fragColor = vec4(col,1.0);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 第一行 `#version 460 core` 指定所用的 OpenGL 语言版本。
|
||||
|
||||
可以看到转换一段 GLSL 代码并不特别麻烦,主要是坐标和输入参数的变化,而通过这些已有的片段着色器,却可以给我们提供极其丰富的渲染效果,如下代码所示:
|
||||
|
||||
- 在 `pubspec.yaml` 里引入上面的 shaders 代码
|
||||
|
||||
- 通过 `ShaderBuilder` 加载对应 `'shaders/warp.frag'` 文件,获得 `FragmentShader`
|
||||
- 利用 `FragmentShader` 的 `setFloat` 传递数据
|
||||
- 通过 `Paint()..shader ` 添加着色器进行绘制,就可以完成渲染
|
||||
|
||||
```dart
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/warp.frag
|
||||
|
||||
·············
|
||||
|
||||
late Ticker _ticker;
|
||||
|
||||
Duration _elapsed = Duration.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker = createTicker((elapsed) {
|
||||
setState(() {
|
||||
_elapsed = elapsed;
|
||||
});
|
||||
});
|
||||
_ticker.start();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ShaderBuilder(
|
||||
assetKey: 'shaders/warp.frag',
|
||||
(BuildContext context, FragmentShader shader, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Warp')
|
||||
),
|
||||
body: CustomPaint(
|
||||
size: MediaQuery.of(context).size,
|
||||
painter: ShaderCustomPainter(shader, _elapsed)
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
class ShaderCustomPainter extends CustomPainter {
|
||||
final FragmentShader shader;
|
||||
final Duration currentTime;
|
||||
|
||||
ShaderCustomPainter(this.shader, this.currentTime);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
shader.setFloat(0, size.width);
|
||||
shader.setFloat(1, size.height);
|
||||
shader.setFloat(2, currentTime.inMilliseconds.toDouble() / 1000.0);
|
||||
final Paint paint = Paint()..shader = shader;
|
||||
canvas.drawRect(Offset.zero & size, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
```
|
||||
|
||||
这里唯一需要解释的就是 `shader.setFloat` 流程,因为它其实是通过索引来对应到我们在 `.frag` 文件里的变量,简单来说:
|
||||
|
||||
> 这里我们在 GLSL 里定义了 `uniform vec2 resolution;` 和 `uniform float iTime;` ,那么 vec2 resolution 就占据了索引 0 和 1 ,float iTime 就占据了索引 2 。
|
||||
|
||||
大概理解就是,vec2 就是两个 float 类型的值保存在了一起的意思,所以先声明的 vec2 resolution 就占据了 索引 0 和 1 ,举个例子,如下图所示,此时的 vec2 和 vec3 分了就占据了 0-4 的索引。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image6.png)
|
||||
|
||||
而通过 `uniform ` 在 GLSL 着色器中定义值,然后在 Dart 中就可以通过 `setFloat` 的索引来传递对应数据过去,从而形成了数据交互的完整闭环。
|
||||
|
||||
> 这里的渐变动画在 Flutter 的完整代码可以参考 Github https://github.com/tbuczkowski/flutter_shaders 里的 [warp.frag](https://github.com/tbuczkowski/flutter_shaders/blob/master/shaders/warp.frag) ,
|
||||
|
||||
同时针对前面整个渐变动画,作者在仓库内还提供了对应纯 Dart 代码实现一样效果的对比,通过数据可以看到,利用着色器的实现在性能上得到了巨大的提升。
|
||||
|
||||
![image-20231031175152699](http://img.cdn.guoshuyu.cn/20231031_GLSL/image7.png)
|
||||
|
||||
那么回过头来, [riveo_page_curl](https://github.com/Rahiche/riveo_page_curl) 的项目里的折叠着色器如下所示,除了一堆不懂的矩阵变化,如 `scale` 缩放、`translate` 平移和 `project` 投影转换之外,就是各种看不明白的三角函数计算,简单的核心就是在矩阵变化时计算弯曲部分的弧度,以及增加阴影投影来提高视觉效果。
|
||||
|
||||
```glsl
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform float pointer;
|
||||
uniform float origin;
|
||||
uniform vec4 container;
|
||||
uniform float cornerRadius;
|
||||
uniform sampler2D image;
|
||||
|
||||
const float r = 150.0;
|
||||
const float scaleFactor = 0.2;
|
||||
|
||||
#define PI 3.14159265359
|
||||
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
mat3 translate(vec2 p) {
|
||||
return mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, p.x, p.y, 1.0);
|
||||
}
|
||||
|
||||
mat3 scale(vec2 s, vec2 p) {
|
||||
return translate(p) * mat3(s.x, 0.0, 0.0, 0.0, s.y, 0.0, 0.0, 0.0, 1.0) * translate(-p);
|
||||
}
|
||||
|
||||
vec2 project(vec2 p, mat3 m) {
|
||||
return (inverse(m) * vec3(p, 1.0)).xy;
|
||||
}
|
||||
|
||||
struct Paint {
|
||||
vec4 color;
|
||||
bool stroke;
|
||||
float strokeWidth;
|
||||
int blendMode;
|
||||
};
|
||||
|
||||
struct Context {
|
||||
vec4 color;
|
||||
vec2 p;
|
||||
vec2 resolution;
|
||||
};
|
||||
|
||||
|
||||
bool inRect(vec2 p, vec4 rct) {
|
||||
bool inRct = p.x > rct.x && p.x < rct.z && p.y > rct.y && p.y < rct.w;
|
||||
if (!inRct) {
|
||||
return false;
|
||||
}
|
||||
// Top left corner
|
||||
if (p.x < rct.x + cornerRadius && p.y < rct.y + cornerRadius) {
|
||||
return length(p - vec2(rct.x + cornerRadius, rct.y + cornerRadius)) < cornerRadius;
|
||||
}
|
||||
// Top right corner
|
||||
if (p.x > rct.z - cornerRadius && p.y < rct.y + cornerRadius) {
|
||||
return length(p - vec2(rct.z - cornerRadius, rct.y + cornerRadius)) < cornerRadius;
|
||||
}
|
||||
// Bottom left corner
|
||||
if (p.x < rct.x + cornerRadius && p.y > rct.w - cornerRadius) {
|
||||
return length(p - vec2(rct.x + cornerRadius, rct.w - cornerRadius)) < cornerRadius;
|
||||
}
|
||||
// Bottom right corner
|
||||
if (p.x > rct.z - cornerRadius && p.y > rct.w - cornerRadius) {
|
||||
return length(p - vec2(rct.z - cornerRadius, rct.w - cornerRadius)) < cornerRadius;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec2 xy = FlutterFragCoord().xy;
|
||||
vec2 center = resolution * 0.5;
|
||||
float dx = origin - pointer;
|
||||
float x = container.z - dx;
|
||||
float d = xy.x - x;
|
||||
|
||||
if (d > r) {
|
||||
fragColor = TRANSPARENT;
|
||||
if (inRect(xy, container)) {
|
||||
fragColor.a = mix(0.5, 0.0, (d-r)/r);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
if (d > 0.0) {
|
||||
float theta = asin(d / r);
|
||||
float d1 = theta * r;
|
||||
float d2 = (3.14159265 - theta) * r;
|
||||
|
||||
vec2 s = vec2(1.0 + (1.0 - sin(3.14159265/2.0 + theta)) * 0.1);
|
||||
mat3 transform = scale(s, center);
|
||||
vec2 uv = project(xy, transform);
|
||||
vec2 p1 = vec2(x + d1, uv.y);
|
||||
|
||||
s = vec2(1.1 + sin(3.14159265/2.0 + theta) * 0.1);
|
||||
transform = scale(s, center);
|
||||
uv = project(xy, transform);
|
||||
vec2 p2 = vec2(x + d2, uv.y);
|
||||
|
||||
if (inRect(p2, container)) {
|
||||
fragColor = texture(image, p2 / resolution);
|
||||
} else if (inRect(p1, container)) {
|
||||
fragColor = texture(image, p1 / resolution);
|
||||
fragColor.rgb *= pow(clamp((r - d) / r, 0.0, 1.0), 0.2);
|
||||
} else if (inRect(xy, container)) {
|
||||
fragColor = vec4(0.0, 0.0, 0.0, 0.5);
|
||||
}
|
||||
}
|
||||
else {
|
||||
vec2 s = vec2(1.2);
|
||||
mat3 transform = scale(s, center);
|
||||
vec2 uv = project(xy, transform);
|
||||
|
||||
vec2 p = vec2(x + abs(d) + 3.14159265 * r, uv.y);
|
||||
if (inRect(p, container)) {
|
||||
fragColor = texture(image, p / resolution);
|
||||
} else {
|
||||
fragColor = texture(image, xy / resolution);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231031_GLSL/image8.png)
|
||||
|
||||
其实我知道大家并不关心它的实现逻辑,更多是如何使用,这里有个关键信息就是 `uniform sampler2D image` ,通过引入 `sampler2D` ,我们就可以在 Dart 通过 `setImageSampler(0, image); ` 将 `ui.Image` 传递到 GLSL 里,这样就可以对 Flutter 控件实现上述的折叠动画逻辑。
|
||||
|
||||
对应在 Dart 层,就是除了 `ShaderBuilder` 之外,还可以通过 [flutter_shaders](https://pub.dev/packages/flutter_shaders) 的 `AnimatedSampler` 来实现更简洁的 `shader` 、`image` 和 `canvas` 的配合,其中 `AnimatedSampler` 的最大作用,就是将整个 child 通过 `PictureRecorder` 进行截图,转化成 `ui.Image` 传递给 GLSL,完成 UI 传递交互效果。
|
||||
|
||||
```dart
|
||||
Widget _buildAnimatedCard(BuildContext context, Widget? child) {
|
||||
return ShaderBuilder(
|
||||
(context, shader, _) {
|
||||
return AnimatedSampler(
|
||||
(image, size, canvas) {
|
||||
_configureShader(shader, size, image);
|
||||
_drawShaderRect(shader, size, canvas);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: cornerRadius),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
},
|
||||
assetKey: 'shaders/page_curl.frag',
|
||||
);
|
||||
|
||||
void _configureShader(FragmentShader shader, Size size, ui.Image image) {
|
||||
shader
|
||||
..setFloat(0, size.width) // resolution
|
||||
..setFloat(1, size.height) // resolution
|
||||
..setFloat(2, _animationController.value) // pointer
|
||||
..setFloat(3, 0) // origin
|
||||
..setFloat(4, 0) // inner container
|
||||
..setFloat(5, 0) // inner container
|
||||
..setFloat(6, size.width) // inner container
|
||||
..setFloat(7, size.height) // inner container
|
||||
..setFloat(8, cornerRadius) // cornerRadius
|
||||
..setImageSampler(0, image); // image
|
||||
}
|
||||
|
||||
void _drawShaderRect(FragmentShader shader, Size size, Canvas canvas) {
|
||||
canvas.drawRect(
|
||||
Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height / 2),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
),
|
||||
Paint()..shader = shader,
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 完整项目可见:https://github.com/Rahiche/riveo_page_curl
|
||||
|
||||
所以可以看到,**相比起在 Dart 层实现这样的 3D 翻页折叠,利用 `FragmentShader` 实现的代码会更简洁,并且性能体验上会更优于纯 Dart 实现**,最重要的是,类似 [ShaderToy](https://www.shadertoy.com/) 里的一些着色器代码,通过简单的移植适配,就可以在直接被运用到 Flutter 里,这对于 Flutter 在游戏场景的实现来无疑说非常友好。
|
||||
|
||||
最后,Flutter 3.10 之后, Flutter Web 同样支持了 fragment shaders,所以着色器在 Flutter 的实现目前已经相对成熟,那么如果是之前的我通过 Flutter 实现的《[霓虹灯文本的「故障」效果的实现](https://juejin.cn/post/7214858677173289017?searchId=202310311754299E224DB054AADBBC6AE2)》的逻辑转换成 fragment shaders 来完成,是不是性能和代码简洁程度也会更高?
|
449
Flutter-GPT.md
449
Flutter-GPT.md
|
@ -1,449 +0,0 @@
|
|||
# 面向 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<YellowStar> with TickerProviderStateMixin {
|
||||
bool _isExploded = false;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
setState(() {
|
||||
_isExploded = true;
|
||||
_animationController.forward(from: 0.0);
|
||||
});
|
||||
}
|
||||
|
||||
void _resetStar() {
|
||||
setState(() {
|
||||
_isExploded = false;
|
||||
_animationController.reset();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _handleTapDown,
|
||||
onTap: _resetStar,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: _isExploded
|
||||
? BorderRadius.circular(50)
|
||||
: BorderRadius.zero,
|
||||
),
|
||||
child: _isExploded
|
||||
? _buildParticles()
|
||||
: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: CustomPaint(
|
||||
painter: YellowStarPainter(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParticles() {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Stack(
|
||||
children: List.generate(
|
||||
50,
|
||||
(index) {
|
||||
double radius = _animation.value * 50;
|
||||
double angle = 2 * pi * index / 50;
|
||||
double x = 50 + cos(angle) * radius;
|
||||
double y = 50 + sin(angle) * radius;
|
||||
return Positioned(
|
||||
left: x,
|
||||
top: y,
|
||||
child: Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.yellow,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class YellowStarPainter extends CustomPainter {
|
||||
final double starSizeRatio = 0.4;
|
||||
final double centerOffsetRatio = 0.2;
|
||||
final double rotationOffset = -pi / 2;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
double starSize = min(size.width, size.height) * starSizeRatio;
|
||||
double centerX = size.width / 2;
|
||||
double centerY = size.height / 2;
|
||||
double centerOffset = starSize * centerOffsetRatio;
|
||||
|
||||
Path path = Path();
|
||||
Paint paint = Paint()
|
||||
..color = Colors.yellow
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double radians = 2 * pi / 5 * i + rotationOffset;
|
||||
double x = centerX + cos(radians) * starSize / 2;
|
||||
double y = centerY + sin(radians) * starSize / 2;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
radians += 2 * pi / 10;
|
||||
x = centerX + cos(radians) * centerOffset;
|
||||
y = centerY + sin(radians) * centerOffset;
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
```
|
||||
|
||||
经过一轮「鏖战」如下图所示,面对同一个问题,ChatGPT 总能找到不同的实现方式来回答你,只是这些实现里有的无法运行的,有的是运行没有效果的,有的甚至是给你输出了一个 `//TODO 爆炸动画` ,总之就是让你「血压飙升」。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image29.png)
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image30.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image31.png)
|
||||
|
||||
当然,「胜利终将属于人类」,如下图所示,在经历了无数次「血压飙升」的场景之后,最终我还是得到了我想要的:点击 ⭐️,然后出现了💥的动画效果,尽管还是不完美,但是基本满足了我后续开发的需求。
|
||||
|
||||
> **这个代码完完全全由 ChatGPT 输出,我一句没改**,完整代码贴在了后面。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image32.gif)
|
||||
|
||||
|
||||
|
||||
```dart
|
||||
|
||||
class YellowStarPage extends StatefulWidget {
|
||||
@override
|
||||
_YellowStarPageState createState() => _YellowStarPageState();
|
||||
}
|
||||
|
||||
class _YellowStarPageState extends State<YellowStarPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
bool _isExploded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleStarTap() {
|
||||
if (!_isExploded) {
|
||||
_isExploded = true;
|
||||
_animationController.forward(from: 0);
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
setState(() {
|
||||
_isExploded = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Yellow Star')),
|
||||
body: Center(
|
||||
child: GestureDetector(
|
||||
onTap: _handleStarTap,
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: YellowStarPainter(_animationController.value,
|
||||
isExploded: _isExploded),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YellowStarPainter extends CustomPainter {
|
||||
final double starSizeRatio = 0.4;
|
||||
final double centerOffsetRatio = 0.2;
|
||||
final double rotationOffset = -pi / 2;
|
||||
|
||||
final double animationValue;
|
||||
final bool isExploded;
|
||||
|
||||
YellowStarPainter(this.animationValue, {this.isExploded = false});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
double starSize = min(size.width, size.height) * starSizeRatio;
|
||||
double centerX = size.width / 2;
|
||||
double centerY = size.height / 2;
|
||||
double centerOffset = starSize * centerOffsetRatio;
|
||||
|
||||
Path path = Path();
|
||||
Paint paint = Paint()
|
||||
..color = Colors.yellow
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
if (isExploded) {
|
||||
double particleSize = starSize / 30;
|
||||
paint.strokeWidth = 1;
|
||||
paint.style = PaintingStyle.fill;
|
||||
paint.color = Colors.yellow;
|
||||
Random random = Random();
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
double dx = random.nextDouble() * starSize - starSize / 2;
|
||||
double dy = random.nextDouble() * starSize - starSize / 2;
|
||||
double x = centerX + dx * (1 + animationValue);
|
||||
double y = centerY + dy * (1 + animationValue);
|
||||
|
||||
canvas.drawCircle(Offset(x, y), particleSize, paint);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double radians = 2 * pi / 5 * i + rotationOffset;
|
||||
double x = centerX + cos(radians) * starSize / 2;
|
||||
double y = centerY + sin(radians) * starSize / 2;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
radians += 2 * pi / 10;
|
||||
x = centerX + cos(radians) * centerOffset;
|
||||
y = centerY + sin(radians) * centerOffset;
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
最后,给大家欣赏一下我让 ChatGPT 画一只米老鼠的「心路历程」,**很明显这一次「人类一败涂地」**,从目前的支持上看,让 ChatGPT 输出复杂图像内容并不理想,因为它不的笔画「不会拐弯」。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image33.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image34.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image35.png) | ![](http://img.cdn.guoshuyu.cn/20230315_GPT/image36.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> 真的是又爱又恨。
|
||||
|
||||
# 最后
|
||||
|
||||
经过上面的一系列「折腾」,**可以看到 ChatGPT 并没有我们想象中智能,如果面向 GPT 去开发,甚至可能并不靠谱,因为它并不对单一问题给出固定答案,甚至很多内容都是临场瞎编的**,这也是因为大语言模型本身如何保证「正确」是一个复杂的问题,但是 ChatGPT 的魅力也来自于此:
|
||||
|
||||
> **它并不是完全基于语料来的统计来给答案**。
|
||||
|
||||
当然这也和 ChatGPT 本身的属性有关系, ChatGPT 目前的火爆有很大一部分属于「意外」,目前看它不是一个被精心产品化后的 2C 产品,反而 [ChatPDF](https://www.chatpdf.com/) 和 [BiBiGPT](https://b.jimmylv.cn/video/BV1uM411P7oA?spm_id_from=333.1007.tianma.2-1-4.click) 这种场景化的包装落地会是它未来的方向之一。
|
||||
|
||||
而现在 OpenAI 发布了多模态预训练大模型 [CPT-4](https://mp.weixin.qq.com/s/kA7FBZsT6SIvwIkRwFS-xw) ,**GPT-4 按照官方的说法是又得到了飞跃式提升:强大的识图能力;文字输入限制提升至 2.5 万字;回答准确性显著提高;能够生成歌词、创意文本,实现风格变化等等**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230315_GPT/image37.gif)
|
||||
|
||||
|
||||
|
||||
所以我很期待 ChatGPT 可以用 Flutter 帮我画出一只米老鼠, 尽管 ChatGPT 现在可能会让你因为得到 `1+1=3` 这样的答案而「发疯”」,**但是 AI 的魅力在于,它终有一天能得到准确的结果** 。
|
118
Flutter-Group.md
118
Flutter-Group.md
|
@ -1,118 +0,0 @@
|
|||
## 多余的前言
|
||||
|
||||
Flutter 2.0 发布时,其中最受大家关注之一的内容就是 `Add-to-App` 相关的更新,因为除了**热更新**之外,Flutter 最受大家诟病的就是**混合开发体验**不好。
|
||||
|
||||
> 为什么不好呢?因为 **Flutter 的控件渲染直接脱离了原生平台,也就是无论页面堆栈和渲染树都独立于平台运行**,这固然给 Flutter 带来了较好的跨平台体验,但是也造成了在和原生平台混合时存在高成本的问题。
|
||||
|
||||
|
||||
且不说在已有的原生项目中集成 Flutter ,就是现阶段在 Flutter 中集成原生控件的 [PlatformView 和 Hybrid Composition](https://juejin.cn/post/6858473695939084295) 体验也是有待提升,当然“有支持”和“能用”就已经是很不错的进展。
|
||||
|
||||
|
||||
**所以 Flutter 2.0 在千呼万唤中发布了 `FlutterEngineGroup` 用于支持官方的 `Add Flutter to existing app` 方案。**
|
||||
|
||||
在此方案出现之前,类似的第三方支持有 `flutter_boost` 、 `mix_stack` 、 `flutter_thrio` 等等 ,它们是否好用这里不讨论,但是这些方案都要面对的问题是:
|
||||
|
||||
> 非官方的支持必然存在每个版本需要适配的问题,而按照 Flutter 目前的 `issue closed` 和 `pr merge` 的速度,很可能每个季度的版本都存在较大的变动,**所以如果开发者不维护或者维护不及时,那么侵入性极强的这类框架很容易就成为项目的瓶颈**。
|
||||
|
||||
而官方提供的 `FlutterEngineGroup` 方案有没有缺陷?肯定有的,它目前看起来更像是被催生出来的状态,各方面的问题还是有的,比如某些地方还存在不能 `destroy` 的问题。 (当然这个问题以及在 `master` 分支 merge 了)
|
||||
|
||||
![image.png](http://img.cdn.guoshuyu.cn/20210429_Flutter-Group/image1)
|
||||
|
||||
但是官方提供的方案,就意味着这个设计得到了 Flutter 官方的保证,**在未来的版本中会有兼容的优势**。
|
||||
|
||||
**`FlutterEngineGroup` 方案使用了多 Engine 混合模式,官方宣称除了一个 Engine 对象之外,后续每个 Engine 对象在 Android 和 iOS 上仅占用 180kB** 。
|
||||
|
||||
> 以前的方案每多一个Engine ,可能就会多出 19MB Android 和 13MB iOS 的占用。
|
||||
|
||||
从 Flutter 官方提供的例子上看,`FlutterEngineGroup` 的 API 十分简单,**多个 Engine 实例的内部都是独立维护自己的内部导航堆栈**,所以可以做到每个 Engine 对应一个独立的模块。
|
||||
|
||||
|
||||
|
||||
所以使用 `FlutterEngineGroup` 之后,`FlutterEngine` 都将由 `FlutterEngineGroup` 去生成,生成的 `FlutterEngine` 可以独立应用于 `FlutterActivity`/`FlutterViewController`,甚至是 `FlutterFragment` :
|
||||
|
||||
|
||||
> 所以就像例子上所示,你可以在一个 `Activity` 上显示两个独立的 FlutterView 。
|
||||
|
||||
|
||||
这其实得益于通过 `FlutterEngineGroup` 生成的 `FlutterEngine` 可以**共享 GPU 上下文, font metrics 和 isolate group snapshot** ,从而实现了更快的初始速度和更低的内存占用。
|
||||
|
||||
> **下图是使用官方实例打开16个页面之后的内存使用情况,并且每个页面成功返回且没有出现黑屏。**
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-Group/image2)
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-Group/image3)
|
||||
|
||||
## 简单的使用介绍
|
||||
|
||||
使用 `FlutterEngineGroup` 首先需要创建一个 `FlutterEngineGroup` 单例对象,之后每当需要创建 Engine 时,就通过它的 `createAndRunEngine(activity, dartEntrypoint)` 来创建对应的 `FlutterEngine` 。
|
||||
|
||||
|
||||
```kotlin
|
||||
val app = activity.applicationContext as App
|
||||
// This has to be lazy to avoid creation before the FlutterEngineGroup.
|
||||
val dartEntrypoint =
|
||||
DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(), entrypoint
|
||||
)
|
||||
engine = app.engines.createAndRunEngine(activity, dartEntrypoint)
|
||||
this.delegate = delegate
|
||||
channel = MethodChannel(engine.dartExecutor.binaryMessenger, "multiple-flutters")
|
||||
```
|
||||
|
||||
以官方 Demo 的这段代码为例子:
|
||||
|
||||
1、首先通过 `findAppBundlePath` 和 `entrypoint` 创建出 `DartEntrypoint` 对象,这里的 **`findAppBundlePath` 主要就是默认的 `flutter_assets` 目录**;而 **`entrypoint` 其实就是 dart 代码里启动方法的名称**;也就是绑定了在 dart 中 `runApp` 的方法。
|
||||
|
||||
```dart
|
||||
|
||||
///kotlin
|
||||
app.engines.createAndRunEngine(pathToBundle, "topMain")
|
||||
|
||||
|
||||
///dart
|
||||
@pragma('vm:entry-point')
|
||||
void topMain() => runApp(MyApp());
|
||||
```
|
||||
|
||||
2、通过上面创建的 `dartEntrypoint` 和 `context` ,使用 `FlutterEngineGroup` 就可以创建出对应的 `FlutterEngine` ,其实在内部就是通过`FlutterJNI.nativeSpawn` 和原有的引擎交互,得到新的 Long 地址 id。
|
||||
|
||||
> 在 C++ 层类似于原有的 `RunBundleAndSnapshotFromLibrary` 方法,但是它不能更改包路径或者 asset ,所以只能加载同一份 AOT 文件,这里得到的指针地址就是一个新的 `AndroidShellHolder` 。
|
||||
|
||||
|
||||
3、最后利用生成的 `FlutterEngine` 的 `binaryMessenger` 来得到一个 `MethodChannel` 用于原生和 dart 之间的通信。
|
||||
|
||||
|
||||
通过上述流程得到的 Engine ,自然就可以直接用于渲染运行新的 Flutter UI,比如直接继承 `FlutterActivity` ,然后 override `provideFlutterEngine` 方法返回得到的 Engine 。
|
||||
|
||||
|
||||
```kotlin
|
||||
|
||||
class SingleFlutterActivity : FlutterActivity()
|
||||
|
||||
·······
|
||||
|
||||
override fun provideFlutterEngine(context: Context): FlutterEngine? {
|
||||
return engine
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
是不是很简单?这么简单的接入后:
|
||||
|
||||
- 在 dart 层面可以通过 `MethodChannel` 打开原始页面;
|
||||
- 在原生层可以通过新建 `FlutterEngine` 打开新的 Flutter 页面;
|
||||
- 甚至你还可以在原生层打开一个 `FlutterView` 的 Dialog;
|
||||
|
||||
|
||||
|
||||
当然,到这里你可能已经注意到了,因为每个 Flutter 页面都是一个独立的 Engine ,由于 dart isolate 的设计理念,**每个独立 Engine 的 Flutter 页面内存是无法共享的**。
|
||||
|
||||
也就是说,当你需要共享数据时,只能在原生层持有数据,然后注入或者传递到每个 Flutter 页面中,就像官方所说的,**每个 Flutter 页面更像是一个独立 Flutter 模块**。
|
||||
|
||||
> 当然这也造成了一些不必要的麻烦,比如:**同一张图片,在原生层、不同 Flutter Engine 会出现多次加载的问题**,这种问题可能就需要你针对 Flutter 的图片加载使用外界纹理,来实现在原生层统一的内存管理等。
|
||||
|
||||
|
||||
另外目前我发现问题还有: [Android 11 上的 ARM TBI 问题](https://github.com/flutter/flutter/issues/78389) ,不过通过这次尝试,相信 `FlutterEngineGroup` 的进展将会越来越明朗,更早的被应用到生产环境中。
|
||||
|
156
Flutter-HF.md
156
Flutter-HF.md
|
@ -1,156 +0,0 @@
|
|||
# Harmony 开始支持 Flutter ,聊聊 Harmony 和 Flutter 之间的因果
|
||||
|
||||
相信大家都已经听说过,明年的 [Harmony Next 版本将正式剥离 AOSP 支持](https://juejin.cn/post/7264237761158643773) ,基于这个话题我已经做过一期问题汇总 ,当时在[现有 App 如何兼容 Harmony Next ](https://juejin.cn/post/7266703104112607284#heading-5)问题上提到过:
|
||||
|
||||
> 华为内部也主导适配目前的主流跨平台方案,主动提供反向适配支持,估计后面就会有类似 Flutter for harmony 的社区支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image1.png)
|
||||
|
||||
没想到 HDC 大会才刚过去一个多月,就有网友提醒,针对 OpenHarmony 的 Flutter 版本已经开源:https://gitee.com/openharmony-sig/flutter_flutter ,这既让人惊喜又是「情理之中」,**因为在众多框架里,Harmony 和 Flutter 之间的联系可以说是最密不可分**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image2.png)
|
||||
|
||||
# 关系
|
||||
|
||||
**为什么说 Harmony 和 Flutter 之间的联系很密切?因为不管是 ArkUI 还是 ArkUI-X ,它们的底层支持里都或多或少存在 Flutter 的身影**。
|
||||
|
||||
例如 ArkUI 的 framework [arkui_ace_engine ](https://gitee.com/openharmony/arkui_ace_engine),里面就可以看到很多熟悉的 Flutter 代码,**不过这里面有点特殊在于,这些代码都是用 C++ 实现的**,例如下图中的 `Stack` 的控件。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image3.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image4.png)
|
||||
|
||||
另外,除了 ArkUI 华为还开源了 [ArkUI-X](https://gitee.com/arkui-x) ,**ArkUI-X 扩展了 ArkUI 框架让其支持跨平台开发,而这部分跨平台的底层逻辑,同样来自 Flutter 和 Skia 的支持**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image5.png)
|
||||
|
||||
与 Flutter 不同的是,OpenHarmony 上层开发用的是 ArkTS 和 ArkUI,调用走的是 NAPI(Native API),NAPI 是一套基于 Node.js 规范开发的原生模块扩展开发框架。
|
||||
|
||||
NAPI 可以实现 JS 与 C/C++ 代码之间相互访问,也就是 ArkTS 可以直接和 C/C++ 无缝调用,类似 dart ffi 效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image6.png)
|
||||
|
||||
举个例子,例如通过 ArkUI-X 里的 `getDefaultDisplaySync` 获取设备屏幕信息, 对于 Android 系统而言, ets 下的代码通过 napi 会调用到 C++ 层的 DisplayInfo 对象,从而再通过 jni 调用 java 下的 DisplayInfo 对象。
|
||||
|
||||
```ts
|
||||
var dsp = display.getDefaultDisplaySync();
|
||||
```
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230918_HF/image7.png) | ![](http://img.cdn.guoshuyu.cn/20230918_HF/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230918_HF/image9.png) | ![](http://img.cdn.guoshuyu.cn/20230918_HF/image10.png) | ![image-20230918165856863](http://img.cdn.guoshuyu.cn/20230918_HF/image11.png) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
|
||||
其实这一点对于 Flutter 来说很重要,因为对于 Flutter 兼容 Harmony OS 的支持上, napi 是必不可少的一部分。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image12.png)
|
||||
|
||||
因为在 Flutter 里,Dart 除了可以直接和 C/C++ 调用之外,还支持和 Objective-C/Swift 与 Java/Kotlin 直接调用而不需要通过 Channel 。
|
||||
|
||||
- 其中 [Objective-C / Swift interop](https://dart.dev/guides/libraries/objective-c-interop) 是通过 package:ffigen :
|
||||
|
||||
```yaml
|
||||
ffigen:
|
||||
name: AVFAudio
|
||||
description: Bindings for AVFAudio.
|
||||
language: objc
|
||||
output: 'avf_audio_bindings.dart'
|
||||
headers:
|
||||
entry-points:
|
||||
- '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h'
|
||||
```
|
||||
|
||||
- [Java / kotlin](https://dart.dev/guides/libraries/java-interop) 是通过 jnigen 支持调用,不过目前还属于 **experimental** 的状态:
|
||||
|
||||
```yaml
|
||||
output:
|
||||
c:
|
||||
library_name: example
|
||||
path: src/example/
|
||||
dart:
|
||||
path: lib/example.dart
|
||||
structure: single_file
|
||||
|
||||
source_path:
|
||||
- 'java/'
|
||||
classes:
|
||||
- 'dev.dart.Example'
|
||||
```
|
||||
|
||||
所以,**后续在 Harmony OS 上,就会有多一个类似 napi gen 支持的需要**。
|
||||
|
||||
# 兼容
|
||||
|
||||
本次开源支持 OpenHarmony 的 flutter 社区版本来自 [openharmony-sig]( https://gitee.com/openharmony-sig/) ,该组织主要用于孵化 OpenHarmony 相关开源生态项目。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image13.png)
|
||||
|
||||
> 另外,在 openharmony 组织下 [sig_crossplatformui](https://gitee.com/openharmony/community/blob/master/sig/sig_crossplatformui/sig_crossplatformui_cn.md) 也有 Taro 主导的一些跨平台支持计划。
|
||||
|
||||
OpenHarmony 的 flutter (简称 OP Flutter )版本目前所用的分支应该是 3.7 版本,因为是刚开源,目前 flutter tools 指令仅支持 linux 下使用 ,但是相信后续跟上节奏应该不成问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image14.png)
|
||||
|
||||
> **以下分析基于 2023-09-18 的部分内容,后续肯定会有新的变化,这里主要提供一些思路和方向**。
|
||||
|
||||
SIG 社区适配的主要有 [OP flutter](https://gitee.com/openharmony-sig/flutter_flutter) 和 [OP flutter engine](https://gitee.com/openharmony-sig/flutter_engine) 两个项目,根据目前的提交,OP flutter 目前主要是添加了 flutter tools 对于构建 hap 的支持,例如:
|
||||
|
||||
- 添加环境检测
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image15.png)
|
||||
|
||||
|
||||
|
||||
- 实现 tools 下的自定义的 `build_hap.dart` ,还有识别鸿蒙设备的支持等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image16.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image17.png)
|
||||
|
||||
- 提供 create 时对应的 ets 模版
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image18.png)
|
||||
|
||||
而关于运行支持,主要是通过 OP flutter engine 来实现,主要代码新增在对应的 `ohos/` 目录下:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image19.png)
|
||||
|
||||
从 OP flutter engine 变更的内容上看,主要是从原有 `shell/platform/android` 下的代码拷贝一份进行调整,例如 GL Context 代码部分目前几乎太大区别。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image20.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image21.png)
|
||||
|
||||
另外,**大家比较关心的应该就是 Impeller 在 OP 上是否支持,目前看来 OP Flutter Engine 里对于 Impeller 有一定预设,但是并没有启用**,因为 Flutter 官方目前对于 Android 上的 Impeller 也没有正式发布,所以这个目前看来也不需要着急。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image22.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image23.png)
|
||||
|
||||
关于字体部分, **目前看来 OP 上 Flutter 默认会使用 `sans-serif`** ,这个应该是和 鸿蒙上的 [**HarmonyOS Sans**](https://developer.harmonyos.com/cn/docs/design/des-guides/font-0000001157868583) 保持一致。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image24.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image25.png)
|
||||
|
||||
**关于刷新率部分,目前暂时可以看到是默认写死了 60hz** ,后续应该可以通过 napi 等支持获取实际刷新率,支持动态刷新率这个大家不用担心。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image26.png)
|
||||
|
||||
另外,因为版本问题,目前 OP Flutter Engine 里还保留了 partial repaint 操作,但是其实 Flutter 官方已经在 Android 上 Disable 了 partial repaint ,因为 Android 上的部分重绘存在太多问题,所以该功能被直接屏蔽。
|
||||
|
||||
**Flutter 官方之后打算与 Vulkan Impeller 单独适配后再重新开放 partial repaint,这对 OP Flutter Engine 来说也许也是一个历史包袱,猜测 OP Flutter 后续会跟随 Impeller 同步**。
|
||||
|
||||
当然,因为不同平台,所以 OP Flutter Engine 也有自己需要单独实现的逻辑,例如数据的类型转化处理,在 Android 上对应的是 [shell/platform/android/platform_view_android_jni_impl.cc](https://github.com/flutter/engine/blob/1603fa1bb41271366dceedaaf0663715576e18f2/fml/platform/android/jni_util.cc) , 而在 OP 上对应的就是 [shell/platform/ohos/napi/platform_view_ohos_napi.cpp ](https://gitee.com/openharmony-sig/flutter_engine/blob/e5ff895ce905afd8f9f85f105f41da6c6d9ef8a5/fml/platform/ohos/napi_util.cc) :
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image27.png)
|
||||
|
||||
最后,**Flutter 适配不只是 embedding 和 tools 的适配,还有新的 channel 和 plugin 的支持**,目前看来 SIG 也在致力与这点,一些常用或者知名的 plugin 社区都会逐步增加支持,这看起来是一个苦力活,但是对于 Harmony 脱离 AOSP 构建自己的生态来说,无疑会是历史性的一步。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230918_HF/image28.png)
|
||||
|
||||
# 最后
|
||||
|
||||
通过本篇,相信你应该能简单理解到 Flutter 和 Harmony 之间的「因果关系」,**对于 Flutter 开发来说,Harmony Next 会是一个相对较好的新平台**。
|
||||
|
||||
当然,**这不代表这你可以不学 ArkTS 和 ArkUI** ,因为不管是打包构建或者 napi 都离不开 Harmony 平台本身的支持,而且在于这样一个「百废待兴」的社区环境下,完全靠社区支撑明显不现实,关键时刻还得是「自己动手」才能「丰衣足食」。
|
||||
|
||||
![](https://img.cdn.guoshuyu.cn/WechatIMG1818.jpg)
|
153
Flutter-HV.md
153
Flutter-HV.md
|
@ -1,153 +0,0 @@
|
|||
对于使用过 Flutter 的开发来说,应该对在 Flutter 混合开发中,通过 `PlatformView` 接入原生控件的方式并不陌生,而如果你是从 Flutter 1.20 之前就开始使用 Flutter ,那么应该对于 Android 上 `PlatformView` 的各种体验问题有过深刻的体会,比如:[ `WebView` 里弹出键盘的问题](https://juejin.cn/post/6858473695939084295)。
|
||||
|
||||
> ⚠️注意:文末有惊喜
|
||||
|
||||
## 从一个问题开始
|
||||
|
||||
恰巧最近一位朋友在 Flutter 2.10.1 上使用 `webview_flutter` 和 `flutter_pdfview` 测试时出现了如下的问题:
|
||||
|
||||
```
|
||||
attachToContext: GLConsumer is already attached to a context at
|
||||
android.graphics.SurfaceTexture.attachToGLContext(SurfaceTexture.java:289)
|
||||
```
|
||||
|
||||
**所以借着这个问题来给大家科普下 Flutter 里 `PlatformView` 实现的变迁和未来调整**,首先这个问题的起因是因为:
|
||||
|
||||
> virtual displayes 和 hybrid composition 两种 `PlatformView `实现混合使用。
|
||||
|
||||
因为从 Flutter 2.10 开始,官方的 Plugin 如 `webview_flutter` 默都是使用 *hybrid composition* 的实现,而第三方的 `flutter_pdfview` 目前还是使用以前的 *virtual display* ,这就出现了两种 `PlatformView` 实现同时出现的情况。
|
||||
|
||||
当然,官方在 2.10.2 版本的 [#31390 ](https://github.com/flutter/engine/pull/31390) 上修复了这个问题, 问题的原因在于:**当 rasterizer 任务运行不同的线程时,`GrContext ` 会被重新创建,从而导致 `texture` 变成没有初始化的状态,进而重复调用 `attachToGLContext` 导致崩溃**。
|
||||
|
||||
> 所以后续官方修复这个问题,就是在 `attachToGLContext` 之前,如果 `texture` 已经 attach 过,就先调用 `detachFromGLContext` 进行释放,从而避免了初始化 context 的问题。
|
||||
|
||||
但是从问题上看,其实这个问题并不是 2.10 才会出现,而是只要在 `SurfaceTextureWrapper` 这个对象存在时 ,混合使用 *virtual displayes* 和 *hybrid composition* 就能引发这个 bug 。
|
||||
|
||||
> `SurfaceTextureWrapper` 是官方用于处理同步的问题,因为当 `SurfaceTexture` 被释放时,由于 `SurfaceTexture.release` 是在 platform 线程被调用,而 `attachToGLContext ` 是在 raster 线程被调用,不同线程调用时可能导致:**当 `attachToGLContext ` 被调用时 texture 已经被释放了,所以需要 `SurfaceTextureWrapper` 用于实现 Java 里同步锁的效果**。
|
||||
|
||||
所以如果在低版本不想升级,那么可以选择所有 Plugin 都使用 *virtual display* 模式或者 *hybrid composition* 模式,比如 `webview_flutter` 就提供了 `WebView.platform` 用于用户自由选择 `PlatformView` 的渲染模式。
|
||||
|
||||
**当然一般情况下我是更建议大家目前都使用 *hybrid composition* 模式,虽然两种模式都有潜在问题,但是相比起来目前 *virtual display* 带来的性能和键盘问题会让人更难以接受**。
|
||||
|
||||
|
||||
|
||||
## 区别和演进
|
||||
|
||||
其实在之前的 [《 Hybrid Composition 深度解析》](https://juejin.cn/post/6858473695939084295) 里就介绍过它们实现的区别,这里再结合上面的问题,从不一样的角度介绍下它们的实现差异和变迁。
|
||||
|
||||
### VirtualDisplay
|
||||
|
||||
一般 dart 代码里直接使用 `AndroidView ` 的我们就可以简单认为是使用 *virtual display* ,比如 [flutter_pdfview 1.2.2 版本 ](https://pub.flutter-io.cn/packages/flutter_pdfview) , 这种实现方式是 **通过将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 实现中 ,然后在 `VirtualDisplay` 对应的内存里,绘制的画面就可以通过其 `Surface` 获取得到**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image1)
|
||||
|
||||
> `VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,一般在副屏显示或者录屏场景下会用到。`VirtualDisplay` 会将虚拟显示区域的内容渲染在一个 `Surface`上。
|
||||
|
||||
如上图所示,**简单来说就是原生控件的内容被绘制到内存里,然后 Flutter Engine 通过相对应的 `textureId` 就可以获取到控件的渲染数据并显示出来**。
|
||||
|
||||
关于 *virtual display* 实现,如果你需要对应路径去调试问题,可以参看如下流程:
|
||||
|
||||
![image-20220305161230961](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image2)
|
||||
|
||||
### HybridComposition
|
||||
|
||||
使用 *hybrid composition* 相对会比直接使用 `AndroidView` 在代码上更复杂一点, 需要使用到 [PlatformViewLink](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fwidgets%2FPlatformViewLink-class.html)、 [AndroidViewSurface](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fwidgets%2FAndroidViewSurface-class.html) 和 [PlatformViewsService](https://link.juejin.cn/?target=https%3A%2F%2Fapi.flutter.dev%2Fflutter%2Fservices%2FPlatformViewsService-class.html) 这三个对象,首先我们要创建一个 dart 控件:
|
||||
|
||||
- 通过 `PlatformViewLink` 的 `viewType` 注册了一个和原生层对应的注册名称,这和之前的 `PlatformView` 注册一样;
|
||||
- 然后在 `surfaceFactory` 返回一个 `AndroidViewSurface` 用于处理绘制和接收触摸事件;
|
||||
- 最后在 `onCreatePlatformView` 方法使用 `PlatformViewsService` 初始化 `AndroidViewSurface` 和初始化所需要的参数,同时通过 Engine 去触发原生层的显示。
|
||||
|
||||
```dart
|
||||
Widget build(BuildContext context) {
|
||||
// This is used in the platform side to register the view.
|
||||
final String viewType = 'hybrid-view-type';
|
||||
// Pass parameters to the platform side.
|
||||
final Map<String, dynamic> creationParams = <String, dynamic>{};
|
||||
|
||||
return PlatformViewLink(
|
||||
viewType: viewType,
|
||||
surfaceFactory:
|
||||
(BuildContext context, PlatformViewController controller) {
|
||||
return AndroidViewSurface(
|
||||
controller: controller,
|
||||
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
|
||||
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
||||
);
|
||||
},
|
||||
onCreatePlatformView: (PlatformViewCreationParams params) {
|
||||
return PlatformViewsService.initSurfaceAndroidView(
|
||||
id: params.id,
|
||||
viewType: viewType,
|
||||
layoutDirection: TextDirection.ltr,
|
||||
creationParams: creationParams,
|
||||
creationParamsCodec: StandardMessageCodec(),
|
||||
)
|
||||
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
|
||||
..create();
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
如果通过上面的问题来做个直观的对比,就会是如下图所示的变化:
|
||||
|
||||
![image-20220305160606360](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image3)
|
||||
|
||||
使用 *hybrid composition* 之后, **`PlatformView` 是通过 `FlutterMutatorView` 把原生控件 `addView` 到 `FlutterView` 上,然后再通过 `FlutterImageView` 的能力去实现图层的混合**,简单解释就是:
|
||||
|
||||
> Flutter 只直接通过原生的 `addView` 方法将 `PlatformView` 添加到 `FlutterView` ,这就不需要什么 `surface ` 渲染再去获取的开销,而当你还需要再 `PlatformView` 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 `FlutterImageView` 来承载这个 Widget 。
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image4)
|
||||
|
||||
|
||||
|
||||
举个例子,如下图所示,其中:
|
||||
|
||||
- 两个灰色的 Re 是原生的 `TextView`;
|
||||
|
||||
- 蓝色、黄色、红色的是 Flutter 的 `Text` ;
|
||||
|
||||
![img](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image5)
|
||||
|
||||
从渲染结果上可以看到:
|
||||
|
||||
- 灰色的原生 `TextView` 通过 `PlatformView` 直接就通过原生的 `addView` 方法添加到 `FlutterView` 上;
|
||||
- 而红色的 Flutter 的 `Text` 控件因为和 `PlatformView `没交集,所以还是 Flutter 原本的渲染逻辑;
|
||||
- 黄色和蓝色的 Flutter 控件,因为和 `PlatformView` 有交集,所以通过新的 `FlutterImageView` 做承载渲染。
|
||||
|
||||
使用 *hybrid composition* 后,在 Engine 去 `SubmitFrame` 时,会通过 `current_frame_view_count` 去对每个 view 画面进行规划处理,然后会通过判定区域内是否需要 `CreateSurfaceIfNeeded` 函数,最终触发原生的 `createOverlaySurface` 方法去创建 `FlutterImageView`。
|
||||
|
||||
```c++
|
||||
for (const SkRect& overlay_rect : overlay_layers.at(view_id)) {
|
||||
std::unique_ptr<SurfaceFrame> frame =
|
||||
CreateSurfaceIfNeeded(context, //
|
||||
view_id, //
|
||||
pictures.at(view_id), //
|
||||
overlay_rect //
|
||||
);
|
||||
if (should_submit_current_frame) {
|
||||
frame->Submit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果有需要调试 *hybrid composition* 相关功能的,可以参考如下路径, 和 *virtual display* 不同之处就是在 `create` 之后的路径产生了变化 , 更多详细演示可见:https://juejin.cn/post/6858473695939084295#heading-2
|
||||
|
||||
![image-20220305165318255](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image6)
|
||||
|
||||
![image-20220305141848256](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image7)
|
||||
|
||||
### 结论
|
||||
|
||||
所以可以看到,***hybrid composition* 保留了更多的原生控件效果,也节省了渲染成本** ,当然目前 `PlatformView` 还有一个比较尖锐的问题,例如 [#95343](https://github.com/flutter/flutter/issues/95343) 的闪动问题,这个问题看来在未来会通过更改渲染方式和纹理优化来解决。
|
||||
|
||||
是的,还是因为性能等问题,所以**新的 `PlatforView` 实现来又要来了,从上面提到的 [#31198](https://github.com/flutter/engine/pull/31198) 已经合并可以猜测,下一个稳定版本中,现在的 *virtual displayes* 实现将不复存在,进而替代的是通过新的 `TextureLayer` 实现,未来不排除 *hybrid composition* 也会被取消,不知道大家此刻心情如何?**
|
||||
|
||||
![image-20220305170157117](http://img.cdn.guoshuyu.cn/20220328_Flutter-HV/image8)
|
||||
|
||||
简单说就是:
|
||||
|
||||
- 新的 `PlatformViewWrapper` 会替换掉原本 *virtual display* 里 `SurfaceTextureWrapper` 相关的逻辑,通过对输入的 `Surface` 进行 `lockHardwareCanvas` 获取到 `Canvas` ,再通过 `super.draw(surfaceCanvas);` 进行绘制;
|
||||
- 关于 *hybrid composition* 目前看起里仅是更换了称谓,只要核心逻辑没有大变动;
|
||||
|
||||
而如果未来 `PlatformViewWrapper` 的实现效果良好 ,可以猜测 *hybrid composition* 模式也会进而退出历史舞台,所以唯有感慨, Flutter 的技术演进速度真的好快。
|
132
Flutter-IOW.md
132
Flutter-IOW.md
|
@ -1,132 +0,0 @@
|
|||
# Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC
|
||||
|
||||
随着 [Flutter 3.10 发布](https://juejin.cn/post/7231565908631633979),Flutter Web 也引来了它最具有「里程碑」意义的更新,**这里的「里程碑」不是说这次 Flutter Web 有多么重大的更新,而是 Flutter 官方对于 Web 终于有了明确的定位和方向**。
|
||||
|
||||
# 提升
|
||||
|
||||
首先我们简单聊提升,这不是本篇的重点,只是顺带。
|
||||
|
||||
本次提升主要在于两个大点:**Element 嵌入支持和 fragment shaders 支持 **。
|
||||
|
||||
首先是 Element 嵌入,**Flutter 3.10 开始,现在可以将 Flutter Web嵌入到网页的任何 HTML 元素中,并带有 `flutter.js` 引擎和 `hostElement` 初始化参数**。
|
||||
|
||||
简单来说就是不需要 `iframe` 了,如下代码所示,只需要通过 `initializeEngine ` 的 `hostElement` 参数就可以指定嵌入的元素,**灵活度支持得到了提高**。
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<!-- ... -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Ensure your flutter target is present on the page... -->
|
||||
<div id="flutter_host">Loading...</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", function (ev) {
|
||||
_flutter.loader.loadEntrypoint({
|
||||
onEntrypointLoaded: async function(engineInitializer) {
|
||||
let appRunner = await engineInitializer.initializeEngine({
|
||||
// Pass a reference to "div#flutter_host" into the Flutter engine.
|
||||
hostElement: document.querySelector("#flutter_host")
|
||||
});
|
||||
await appRunner.runApp();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> PS :如果你的项目是在 Flutter 2.10 或更早版本中创建的,要先从目录中删除 `/web` 文件 ,然后通过 `flutter create . --platforms=web` 重新创建模版。
|
||||
|
||||
fragment shaders 部分一般情况下大家可能并不会用到,shaders 就是以 `.frag` 扩展名出现的 GLSL 文件,在 Flutter 里是在 `pubspec.yaml` 文件下的 `shaders` 中声明,现在它支持 Web 了:
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
shaders:
|
||||
- shaders/myshader.frag
|
||||
```
|
||||
|
||||
> 一般运行时会把 frag 文件加载到 `FragmentProgram ` 对象中,通过 program 可以获取到对应的 `shader `,然后通过 `Paint.shader` 进行使用绘制, 当然 Flutter 里 shaders 文件是存在限制的,比如不支持 UBO 和 SSBO 等。
|
||||
|
||||
**当然,这里不是讲解 shaders ,而是宣告一下,Flutter Web 支持 shaders 了**。
|
||||
|
||||
# 未来
|
||||
|
||||
**其实未来才是本篇的重点**,我们知道 Flutter 在 Web 领域的支持上一直在「妥协」,Flutter Web 在整个 Flutter 体系下一直处于比较特殊的位置,因为它一直存在两种渲染方式:[html 和 canvaskit](https://juejin.cn/post/7095294020900880420)。
|
||||
|
||||
简单说 html 就是转化为 [JS + Html Element](https://juejin.cn/post/7095294020900880420) 渲染,而 canvaskit 是采用 [Skia + WebAssembly](https://skia.org/docs/user/modules/canvaskit/) 的方式,**而 html 的模式让 Web 在 Flutter 中显得「格格不入」,路径依赖和维护成本也一直是 Flutter Web 的头痛问题**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image1.png)
|
||||
|
||||
面对这个困境,官方在年初的 [Flutter Forword](https://juejin.cn/post/7192646390948823098) 大会上提出重新规划 Flutter Web 的未来,而随着 Flutter 3.10 的发布,官方终于对于 Web 的未来有了明确的定位:
|
||||
|
||||
> **“Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架。”**
|
||||
|
||||
Flutter 团队表示,**Flutter Web 的定位不是设计为通用 Web 的框架**,类似的 Web 框架现在有很多,比如 Angular 和 React 等在这个领域表现就很出色,而 Flutter 应该是围绕 CanvasKit 和 [WebAssembly](https://webassembly.org/) 等新技术进行架构设计的框架。
|
||||
|
||||
所以 Flutter Web 未来的路线更多会是 CanvasKit ,也就是 WebAssembly + Skia ,同时在这个领域 Dart 也在持续深耕:**从 Dart 3 开始,对于 Web 的支持将逐步演进为 WebAssembly 的 Dart native 的定位**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image2.png)
|
||||
|
||||
什么是 WebAssembly 的 dart native ?一直以来 Flutter 对于 WebAssembly 的支持都是:使用 Wasm 来处理CanvasKit 的 runtime,而 Dart 代码会被编译为 JS,而这对于 Dart 团队来时,其实是一个「妥协」的过渡期。
|
||||
|
||||
而随着官方与 WebAssembly 生态系统中的多个团队的深入合作,**Dart 已经开始支持直接编译为原生的 wasm 代码,一个叫 [WasmGC]((https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md)) 的垃圾收集实现被引入标准**,该扩展实现目前在基于 Chromium 的浏览器和 Firefox 浏览器中在趋向稳定。
|
||||
|
||||
> 目前在基准测试中,执行速度提高了 3 倍
|
||||
|
||||
要将 Dart 和 Flutter 编译成 Wasm,你需要一个支持 [WasmGC ](https://github.com/WebAssembly/gc/tree/main/proposals/gc) 的浏览器,目前 [Chromium V8](https://chromestatus.com/feature/6062715726462976) 和 Firefox 团队的浏览器都在进行支持,比如 Chromium 下:
|
||||
|
||||
> 通过结构和数组类型为 WebAssembly 增加了对高级语言的有效支持,以 Wasm 为 target 的语言编译器能够与主机 VM 中的垃圾收集器集成。在 Chrome 中启用该功能意味着启用类型化函数引用,它会将函数引用存储在上述结构和数组中。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image3.png)
|
||||
|
||||
现在在 Flutter master 分支下就可以提前尝试 wasm 的支持,运行 `flutter build web --help` 如果出现下图所示场, 说明支持 wasm 编译。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image4.png)
|
||||
|
||||
之后执行 `flutter build web --wasm` 就可以编译一个带有 native dart wasm 的 web 包,命令执行后,会将产物输出到 `build/web_wasm` 目录下。
|
||||
|
||||
之后你可以使用 pub 上的 [`dhttpd`](https://pub.dev/packages/dhttpd) 包在 `build/web_wasm`目录下执行本地服务,然后在浏览器预览效果。
|
||||
|
||||
```
|
||||
> cd build/web_wasm
|
||||
> dhttpd
|
||||
Server started on port 8080
|
||||
```
|
||||
|
||||
目前需要版本 112 或更高版本的 Chromium 才能支持,同时需要启动对应的 Chrome 标识位:
|
||||
|
||||
- `enable-experimental-webassembly-stack-switching`
|
||||
- `enable-webassembly-garbage-collection`
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image5.png)
|
||||
|
||||
当然,目前阶段还存在一些限制,例如:
|
||||
|
||||
> Dart Wasm 编译器利用了 [ JavaScript-Promise Integration (JSPI) ](https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md)特性,Firefox 不支持 JSPI 提议,所以一旦 Dart 从 JSPI 迁移出来,Firefox 应启用适当的标志位才能运行。
|
||||
|
||||
另外还需要 JS-interop 支持,因为为了支持 Wasm,Dart 改变了它针对浏览器和 JavaScript 的 API 支持方式, 这种转变是为了防止把 `dart:html ` 或 `package:js` 编译为 Wasm 的 Dart 代码,大多数特定于平台的包如 url_launcher 会使用这些库。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230512_IOW/image6.png)
|
||||
|
||||
最后,**目前 DevTools 还不支持 `flutter run` 去运行和调试 Wasm**。
|
||||
|
||||
# 最后
|
||||
|
||||
很高兴能看到 Flutter 团队最终去定了 Web 的未来路线,这让 Web 的未来更加明朗,当然,正如前面所说的,**Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架**。
|
||||
|
||||
**所以 Flutter Web不是为了设计为通用 Web 的框架去 Angular 和 React 等竞争,它是让你在使用 Flutter 的时候,可以将能力很好地释放到 Web 领域**,而 CanvasKit 带来的一致性更符合 Flutter Web 的定位,当然,解决加载时长问题会是任重道远的需求。
|
||||
|
||||
**最后不得不提 WebGPU**, WebGPU 作为新一代的 WebGL,可以提供在浏览器绘制 3D 的全新实现,它属于 GPU硬件(显卡)向 Web(浏览器)开放的低级 API,包括图形和计算两方面相关接口。
|
||||
|
||||
WebGPU 来自 W3C 制定的标准,与 WebGL 不同,WebGPU 不是基于 OpenGL ,它是一个新的全新标准,发起者是苹果,目前由 W3C GPU 与来自苹果、Mozilla、微软和谷歌一起制定开发,不同于 WebGL (OpenGL ES Web 版本),WebGPU 是基于 Vulkan、Metal 和 Direct3D 12 等,能提供更好的性能和多线程支持。
|
||||
|
||||
> WebGPU 已经被正式集成到 Chrome 113 中,首个版本可在会支持 Vulkan 的 ChromeOS 设备、 Direct3D 12 的 Windows 设备和 macOS 的 Chrome 113 浏览器,除此之外 Linux、Android 也将在 2023 年内开始陆续发布,同步目前也初步登陆了 Firefox 和 Safari 。
|
||||
|
||||
提及 WebGPU 的原因在于:**WebGPU + WebAssembly 是否在未来可以让 Web 也支持 Impeller 的可能?**。
|
||||
|
||||
> 详细可见:https://cohost.org/mcc/post/1406157-i-want-to-talk-about-webgpu 和 https://www.infoq.cn/article/qwawharqawdragtcoxqv
|
|
@ -1,126 +0,0 @@
|
|||
相信 Flutter 的开发者应该遇到过,对于大量数据的列表进行图片加载时,在 iOS 上很容易出现 `OOM `的问题,这是因为 Flutter 特殊的图片加载流程造成。
|
||||
|
||||
> 在 Android 上 Flutter `Image` 主要占用的内存不是 `JVM` 的内存,而是 `Graphics` 相关的内存,这样的内存调用可以最大程度利用 Native 内存。
|
||||
|
||||
## 一、默认流程
|
||||
|
||||
Flutter 默认在进行图片加载时,会先通过对应的 `ImageProvider` 去加载图片数据,然后通过 `PaintingBinding` 对数据进行编码,之后返回包含编码后图片数据和信息的 `ImageInfo` 去实现绘制。
|
||||
|
||||
> 详细图片加载流程可见:[《十、 深入图片加载流程)》](https://mp.weixin.qq.com/s/0sEBzLxXrYSswKolxxJePA)
|
||||
|
||||
|
||||
本身这个逻辑并没有什么问题,**问题就在于 Flutter 中对于图片在内存中的 Cache 对象是一个 `ImageStream` 对象**。
|
||||
|
||||
Flutter 中 `ImageCache` 缓存的是一个异步对象,缓存异步加载对象的一个问题是:**在图片加载解码完成之前,你无法知道到底将要消耗多少内存,并且大量的图片加载,会导致的解码任务需要产生大量的IO**。
|
||||
|
||||
所以一开始最粗暴的情况是:通过 `PaintingBinding.instance` 去设置 `maximumSize` 和 `maximumSizeBytes`,但是这种简单粗爆的处理方法并不能解决长列表图片加载的溢出问题,因为在长列表中,快速滑动的情况下可能会在一瞬间“并发”出大量图片加载需求。
|
||||
|
||||
|
||||
所以在 1.17 版本上,官方针对这种情况提供了场景化的处理方式: `ScrollAwareImageProvider`。
|
||||
|
||||
|
||||
## 二、ScrollAwareImageProvider
|
||||
|
||||
1.17 中可以看到,在 `Image` 控件中原本 `_resolveImage` 方法所使用的 `imageProvider` 被 `ScrollAwareImageProvider` 所代理,并且多了一个叫 `DisposableBuildContext<State<Image>>` 的 context 参数。那 `ScrollAwareImageProvider` 的作用是什么呢?
|
||||
|
||||
```dart
|
||||
void _resolveImage() {
|
||||
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
|
||||
context: _scrollAwareContext,
|
||||
imageProvider: widget.image,
|
||||
);
|
||||
final ImageStream newStream =
|
||||
provider.resolve(createLocalImageConfiguration(
|
||||
context,
|
||||
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
|
||||
));
|
||||
assert(newStream != null);
|
||||
_updateSourceStream(newStream);
|
||||
}
|
||||
```
|
||||
|
||||
其实 `ScrollAwareImageProvider` 对象最主要的使用就是在 `resolveStreamForKey` 方法中,通过 `Scrollable.recommendDeferredLoadingForContext` 方法去判断当前是不是需要推迟当前帧画面的加载,换言之就是:**是否处于快速滑动的过程**。
|
||||
|
||||
那 `Scrollable.recommendDeferredLoadingForContext` 作为一个 `static` 方法,如何判断当前是不是处于列表的快速滑动呢?
|
||||
|
||||
这就需要通过当前 `context` 的 `getElementForInheritedWidgetOfExactType` 方法去获取 `Scrollable` 内的 `_ScrollableScope` 。
|
||||
|
||||
> `_ScrollableScope` 是 `Scrollable` 内的一个 `InheritedWidget` ,而 Flutter 中的可滑动视图内必然会有 `Scrollable` ,所以只要 `Image` 是在列表内,就可以通过 ` context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()` 去获取到 `_ScrollableScope` 。
|
||||
|
||||
|
||||
获取到 `_ScrollableScope` 就可以获取到它内部的 `ScrollPosition` , 进而它的 `ScrollPhysics` 对应的 `recommendDeferredLoading` 方法,判断列表是否处于快速滑动状态。所以判断是否快速滑动的逻辑其实是在 `ScrollPhysics`。
|
||||
|
||||
```dart
|
||||
|
||||
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
|
||||
assert(velocity != null);
|
||||
assert(metrics != null);
|
||||
assert(context != null);
|
||||
if (parent == null) {
|
||||
final double maxPhysicalPixels = WidgetsBinding.instance.window.physicalSize.longestSide;
|
||||
return velocity.abs() > maxPhysicalPixels;
|
||||
}
|
||||
return parent.recommendDeferredLoading(velocity, metrics, context);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 关于 `ScrollPhysics` 的解释可以看 [《十八、 神奇的ScrollPhysics与Simulation》](https://mp.weixin.qq.com/s/Q1uwIb87gKB3gC9ZxSmYng)
|
||||
|
||||
|
||||
然后回到 `resolveStreamForKey` 方法,可以看到当 `Scrollable.recommendDeferredLoadingForContext` 返回 `true` 时就等待,等待就是会通过 ` SchedulerBinding` 在下一帧绘制时再次调用 `resolveStreamForKey`, 递归再走一遍 `resolveStreamForKey` 的逻辑,如果判断此时不再是快速滑动,就走正常的图片加载逻辑。
|
||||
|
||||
|
||||
```dart
|
||||
@override
|
||||
void resolveStreamForKey(
|
||||
ImageConfiguration configuration,
|
||||
ImageStream stream,
|
||||
T key,
|
||||
ImageErrorListener handleError,
|
||||
) {
|
||||
if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
|
||||
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
return;
|
||||
}
|
||||
if (context.context == null) {
|
||||
return;
|
||||
}
|
||||
if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
|
||||
SchedulerBinding.instance.scheduleFrameCallback((_) {
|
||||
scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
|
||||
});
|
||||
return;
|
||||
}
|
||||
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
如上代码所示,可以看到在 `ScrollAwareImageProvider` 的 `resolveStreamForKey` 方法中,当 `stream.completer != null` 且存在缓存时,直接就去加载原本已有的流程,如果快速滑动过程中图片还没加载的,就先不加载。
|
||||
|
||||
|
||||
> Flutter 中为了防止 `context` 在图片异步加载流程中持有导致内存泄漏,又针对 `Image` 封装了一个 `DisposableBuildContext` 。
|
||||
>
|
||||
> `DisposableBuildContext` 是通过持有 `State` 来持有 `context` 的,并且在 `dispose` 时将 `_state = null` 设置为 `null` 来清除对 `State` 的持有。所以可以看到 上述代码中,`context.context == null` 时直接就 `return` 了。
|
||||
|
||||
|
||||
另外前面介绍的 `resolveStreamForKey` 也是新增加的方法,在原本的 `ImageProvider` 进行图片加载时,会通过 `ImageStream resolve` 方法去得到并返回一个 `ImageStream`。
|
||||
|
||||
而 `resolveStreamForKey` 将原本 `imageCache` 和 `ImageStreamCompleter` 的流程抽象出来,并且在 `ScrollAwareImageProvider` 中重写了 `resolveStreamForKey` 方法的执行逻辑,这样快速滑动时,图片的下载和解码可以被中断,从而减少了不必要的内存占用。
|
||||
|
||||
虽然这种方法不能100%解决图片加载时 OOM 的问题,但是很大程度优化了列表中的图片内存占用,官方提供的数据上看理论上可以在原本基础上节省出 70% 的内存。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20200805_Flutter-Image+1_17/image1)
|
||||
|
||||
> 相关推荐:[Merged Defer image decoding when scrolling fast #49389](https://github.com/flutter/flutter/pull/49389/files#diff-5de603a8009bb4e46fc0553915af4277R7)
|
||||
|
||||
|
||||
|
||||
## 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
|
@ -1,62 +0,0 @@
|
|||
# 2024 Impeller:快速了解 Flutter 的渲染引擎的优势
|
||||
|
||||
> 参考原文 :https://tomicriedel.medium.com/understanding-impeller-a-deep-dive-into-flutters-rendering-engine-ba96db0c9614
|
||||
|
||||
最近,在 [Flutter 2024 路线规划](https://juejin.cn/post/7335067315452428297)里明确提出了,**今年 Flutter Team 将计划删除 iOS 上的 Skia 的支持,从而完成 iOS 到 Impeller 的完全迁移**,Android 上的 Impeller 今年预计将完成 Vulkan 和 OpenGL 支持,目前[ Flutter 发布的 3.19 ](https://juejin.cn/post/7334503381200781363)上 Android 就完成了 OpenGL 的预览支持。
|
||||
|
||||
> 所以现在我们有必要了解下 Impeller 是什么,它和 Skia 的区别在哪里。
|
||||
|
||||
Impeller 作为 Flutter 新一代的渲染引擎,**它的核心就是负责绘制应用的界面,包括布局计算、纹理映射和动画处理等等**,它会将代码转换为像素、颜色、形状,所以 Impeller 是会直接影响到应用的性能和渲染效果,这也是很多早期 Flutter 开发者从 Skia 升级到 Impeller 经常遇到的痛点,例如:
|
||||
|
||||
- 字体加载异常,字形和排版与之前不对,如 [#142974 ](https://github.com/flutter/flutter/issues/142974)、[#140475](https://github.com/flutter/flutter/issues/140475) 、[#138670 ](https://github.com/flutter/flutter/issues/138670)、[#138386](https://github.com/flutter/flutter/issues/138386)
|
||||
- 线条渲染或裁剪不一致,如 [#141563 ](https://github.com/flutter/flutter/issues/141563)、 [#137956](https://github.com/flutter/flutter/issues/137956)
|
||||
- 某些纹理合成闪烁/变形,如 [#143719](https://github.com/flutter/flutter/issues/143719) 、[#142753](https://github.com/flutter/flutter/issues/142753) 、[#142549](https://github.com/flutter/flutter/issues/142549) 、[#141850](https://github.com/flutter/flutter/issues/141850)
|
||||
- ····
|
||||
|
||||
可以看到,Impeller 在替换 Skia 这条路上有许多需要处理的 bug ,甚至很多问题在 Skia 上修复过了,在 Impeller 上还要重新修复,那为什么 Flutter 团队还要将 Skia 切换到 Impeller 呢?是 Skia 不够优秀吗?
|
||||
|
||||
首先 Skia 肯定是一个优秀的通用 2D 图形库,例如 Google Chrome 、Android、Firefox 等设备都是用了 Skia ,但是也因为它的「通用性」,所以它不属于 Flutter 的形状,它无法专门针对 Flutter 的要求去进行优化调整,例如 Skia 附带的许多功能超出了 Flutter 的要求,其中一些可能会导致不必要的开销并导致渲染时间变慢,而目前来看,**Skia 的通用性给 Flutter 带来了性能瓶颈**。
|
||||
|
||||
而 Impeller 是专门为 Flutter 而生,它主要核心就是优化 Flutter 架构的渲染过程,它渲染方法在 Flutter 上可以比 Skia 能更有效地利用 GPU ,**让设备的硬件以更少的工作量来渲染动画和复杂的 UI 元素,从而提高渲染速度**。
|
||||
|
||||
另外 **Impeller 还会采用 tessellation 和着色器编译来分解和提前优化图形渲染**,这样 Impeller 就可以减少设备上的硬件工作负载,从而实现更快的帧速率和更流畅的动画。
|
||||
|
||||
> 着色器可以在 GPU 上运行从之控制图形渲染,与 Skia 不同的是,Flutter 上 Skia 会动态编译着色器,这可能导致渲染延迟,而在 Impeller 会提前编译大部分着色器,这种预编译可以显着降低动画过程中出现卡顿,因为 GPU 不必在渲染帧时暂停来编译着色器。
|
||||
|
||||
**Impeller 还采用了新的分层架构来简化渲染过程**,架构允许 Engine 的每个组件以最大效率执行其特定任务,从而减少将 Flutter Widget 的转换为屏幕上的像素所需的步骤。
|
||||
|
||||
所以,Impeller 的设计采用了分层结构,每一层都建立在下一层的基础上执行专门的功能,这种设计使引擎更加高效,并且更易于维护和更新,因为它分离了不同的关注点。
|
||||
|
||||
首先,**Impeller 架构的顶层是 Aiks**,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image1.png)
|
||||
|
||||
Aiks 的下一层下是 Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,**每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息**。
|
||||
|
||||
每个 Entity 都带有 transformation 矩阵(编码位置、旋转、缩放)等属性,以及保存渲染所需 GPU 指令的content object ,这些内容对象非常灵活,可以管理许多 UI 效果,如纯色、图像、渐变和文本,当时现在 Entities 还不能直接作用于 GPU, 因为 Engine 还需要和 Metal 或者 Vulkan 进行通信。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image2.png)
|
||||
|
||||
所以 HAL(Hardware Abstraction Layer) 出现了,它构成了 Impeller 架构的基础,它为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image3.png)
|
||||
|
||||
大家都知道,**渲染引擎中最耗时的任务就是渲染管道和着色器编译**,渲染管线是 GPU 执行渲染图形的一系列步骤,这些是由 HAL 生成处理,所以在性能上 HAL 也当任和很重要的角色。
|
||||
|
||||
> 对渲染管道感兴趣的也可以简单了解下:https://juejin.cn/post/7282245376424345656
|
||||
|
||||
另外就像前面说的, Impeller 提前预编译大部分着色器,这种策略可以显着减少渲染延迟并消除与动态着色器编译相关的卡顿,而**这个预编译发生在 Flutter 应用的构建过程中**,确保着色器在应用启动后立即可用
|
||||
|
||||
> 并且一般情况下,预编译的着色器会导致应用启动时间变成和 App 大小剧增,但是因为 Impeller 专为 Flutter 而生,所以 Impeller 的着色器预编译可以依赖一组比 Skia 更简单的着色器,从而保持应用的启动时间较短且整体大小不会剧增的效果。
|
||||
|
||||
最后,如果你使用 Flutter 有一些时间,那么你应该知道,抗锯齿(Anti-Aliasing)和裁剪(Clip)是一种比较昂贵的操作,而这些在 Impeller 里也得到了底层优化。
|
||||
|
||||
在 Impeller 里抗锯齿是通过多重采样抗锯齿 (MSAA) 来解决, MSAA 的工作原理是在像素内的不同位置对每个像素进行多次采样,然后对这些样本进行平均以确定最终颜色,最后将对象的边缘与其背景平滑地融合,减少其锯齿状外观。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image4.gif)
|
||||
|
||||
对于裁剪操作,Impeller 利用模板缓冲区 stencil buffer(GPU 的一个组件)来管理剪切过程,当 Impeller 渲染 UI 时,它会先让 GPU 使用模板缓冲区,该缓冲区主要充当过滤器,根据 clipping 蒙版确定应改显示哪些像素,最后通过优化模板缓冲区,Impeller 可确保快速执行剪切操作。
|
||||
|
||||
所以,现在你理解 Impeller 的优势了吗?
|
||||
|
||||
虽然从 Skia 到 Impeller 的切换还有需要细节需要优化,但是 2024 年 Impeller 应该毫不意外会成为 Flutter 在 Android 和 iOS 的默认引擎,而 Skia 也大概率会在 2024 和我们说再见,那么,你准备好了迎接 Impeller 的洗礼了吗?
|
|
@ -1,206 +0,0 @@
|
|||
昨天谷歌为在 `Flutter Interact` 上为我们带来了 `Flutter 1.12` ,这是 1.9.x 的版本在经历 6 次 `hotfix` 之后,才带来的 stable 大版本更新。**该版本解决了 4,571 个报错,合并了 1,905 份 pr,同时本次发布也是 Flutter 一年内的第五个稳定版本。**
|
||||
|
||||
结合本次 `Flutter Interact` ,可以总结出几个关键词是: **`Platform` 、 `DartPad` 、`Spuernova` 、`AdobeXD`、`Hot UI` 和 `Layout Explorer` 。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image1)
|
||||
|
||||
## 一、更多的平台
|
||||
|
||||
本次 `Flutter Interact` 提出了让开发者更聚焦于精美的应用开发,从**以设备为中心转变为以应用为中心的开发理念**,Flutter 将帮助开发者忽略 Android、iOS、Web、PC 等不同平台差异,如下图所示是现场一套代码同时调试 7 台设备的演示。
|
||||
|
||||
本次 Flutter 也开始兑现当初的承诺,目前 **Web 的支持已经发布到 Beta 分支,而 MacOS 的支持已经发布到 Master** 分支。虽然进度不算快,但是作为“白嫖党”表示还是很开心能看到有所推进。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image2)
|
||||
|
||||
使用 `Flutter Web` 和 `Flutter MacOS` 需要通过如下命令行打开配置,并且执行 `flutter create xxxx` 就可以创建带有 Web 和 MacOS 的项目(如果已有项目也可以执行 `flutter create` 补全),并且需要注意**调试 MacOS 平台应用需要本地 Flutter SDK 要处于 `master` 分支,如果仅测试 Web 可以使用 `beta` 分支。**
|
||||
|
||||
```
|
||||
flutter config --enable-macos-desktop
|
||||
flutter config --enable-web
|
||||
|
||||
///其他平台的支持
|
||||
flutter config --enable-linux-desktop
|
||||
flutter config --enable-windows-desktop
|
||||
```
|
||||
|
||||
最后可以通过 `run` 或者 `build` 命令运行和打包程序,同时需要注意这里提到的 `linux` 和 `window` 平台目前还未合并到主项目中,如果想测试可在 [Desktop-shells](https://github.com/flutter/flutter/wiki/Desktop-shells) 查看对应配置项目:[flutter-desktop-embedding](https://github.com/google/flutter-desktop-embedding)。
|
||||
|
||||
```
|
||||
///调试运行
|
||||
flutter run -d chrome
|
||||
flutter run -d macOS
|
||||
|
||||
///打包
|
||||
flutter build web
|
||||
flutter build macos
|
||||
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image3)
|
||||
|
||||
## 二、更多开发工具
|
||||
|
||||
### 1、DartPad
|
||||
|
||||
`DartPad` 是用于在线体验 Dart 功能的平台,而本次更新后 `DartPad` 也支持 Flutter 的在线编写预览,这代表着开发者可以在没有 `idea` 的情况下也能实时测试自己的 Flutter 代码,算是补全了 Flutter 的在线用例测试。
|
||||
|
||||
> DartPad 的官方地址:[dartpad.dev](https://dartpad.dev) 和国内镜像地址 [ dartpad.cn](https://dartpad.cn)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image4)
|
||||
|
||||
### 2、Spuernova
|
||||
|
||||
[`Spuernova`](https://supernova.io) 可以说是本次 `Flutter Interact` 的亮点之一,通过导入设计师的 `Sketch` 文件就可以生成 Flutter 代码,这无疑提升了 Flutter 的生产力和可想象空间,**虽然这种生成代码的方法并不罕见,完整实用程度有待考验,但是这也让开发者可以更聚焦于业务逻辑和操作逻辑。**
|
||||
|
||||
> 放心,这个坑不是谷歌 Flutter 团队开的,它属于另外一家商业公司。
|
||||
|
||||
使用 `Spuernova` 可以从 [https://supernova.io](https://supernova.io) 下载 `Supernova Studio` ,之后需要注册用户信息(可能需要科学S网),最后就可以看到如下图所示的界面。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image5)
|
||||
|
||||
在导入 `Sketch` 文件后可以看到设计师完成的界面效果,同时选中 `"</>"` 按键,可以在右侧看到对应的 Flutter 代码,左侧可以看到对应的层级设计,但是这时候的代码看起来还比较简单和笨重,并且不具备交互能力。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image6)
|
||||
|
||||
如果进一步配置,用户需要在对应的控件上,使用右键的弹出框配置控件的功能,比如 `List`、`Button`、`TextField` 等组件去 Convert 原有的控件,让控件更新具备交互能力,同时还可以为控件配置布局属性和动画效果等。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image7)
|
||||
|
||||
当然, `Spuernova` 并不是什么完全的公益项目,目前只有对于 Flutter 的简单支持上是免费的,其他项目支持还是处于收费状态。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image8)
|
||||
|
||||
另外类似的还有 `AdobeXD`, Adobe 的 Creative Cloud 添加了 Flutter 支持,只需一个插件,用户就可以将 `AdobeXD` 导出到 Flutter,目前处于[注册参加优先体验计划](https://xd.adobelanding.com/xd-to-flutter) 的进度。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image9)
|
||||
|
||||
### 3、Hot UI
|
||||
|
||||
Hot UI 就是大家盼星盼月的预览功能,如下图所示,在 Android Studio 的 Flutter 插件中在开发 widget 开发的过程中,直接在 IDE 的镜像里进行预览并与之进行交互。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image10)
|
||||
|
||||
在官方的 [HotUI-Getting-Started-instructions](https://github.com/flutter/flutter-intellij/wiki/HotUI-Getting-Started-instructions) 中可以看到相关的描述:**This feature is currently experimental. To enable, go to Preferences > Languages & Frameworks > Flutter Then check "Enable Hot UI" under "Experiments".** 目前该功能还处于实验阶段,在 Android Studio 的设置中,如图所示底部勾选启动这个功能。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image11)
|
||||
|
||||
但是如下图所示,开启后会发现和官方宣传的不一样?因为目前预览的 `Screen mirror` 处于 `coming soon` 的状态。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image12)
|
||||
|
||||
现阶段的 Hot UI 如下 GIF 所示,暂时只支持用户动态调试和配置控件的属性等逻辑,让我们期待官方填坑吧。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image13)
|
||||
|
||||
|
||||
### 4、Layout Explorer
|
||||
|
||||
[`Layout Explorer`](https://flutter.dev/docs/development/tools/devtools/inspector#flutter-layout-explorer) 是另外实验性的布局调试模式,`Layout Explorer` 主要是用于帮助开发者更直观地适配屏幕和调试如 `overflowed` 等场景的问题。
|
||||
|
||||
在最新的 `Dart DevTools` 工具添加了一个名为 `Layout Explorer` 的功能,它能够以可视化的方式呈现应用的布局信息,从而让检查器可以更好地发挥功,同时 `Layout Explorer` 不仅能以可视化的方式展现正在运行的应用中的 widget 布局,而且还允许以交互的方式更改布局选项。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image14)
|
||||
|
||||
启动 `Layout Explorer` 同样需要 Flutter SDK 处于 `master` 分支,然后在程序运行之后,点击 `DevTools` 在 chrome 打开,之后点击最右侧的按键进入 Flutter 调试模式。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image15)
|
||||
|
||||
|
||||
如下 GIF 所示,当选中的控件是具备 `Flex` 的支持时,可以看到有 `Layout Explorer` 的面板,在面板中可以动态调整控件的显示逻辑和控件的布局情况。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image16)
|
||||
|
||||
比如当控件出现了 `overflowed` ,我们可以很直观的看到问题的根源并且进行调整。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image17)
|
||||
|
||||
另外可以在 `Layout Explorer` 中动态调整控件的 flex 等相关信息,实时预览修改情况。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image18)
|
||||
|
||||
|
||||
## 三、Flutter SDK 改进
|
||||
|
||||
Flutter SDK 相关的更新本次解决了 4,571 个报错,合并了 1,905 份 pr,同时包含了许多的新功能支持。
|
||||
|
||||
- 首先 Flutter 1.12 建议开发者将 Android 项目迁移到 AndroidX,SDK 的瘦身,增加了 [google_fonts](https://pub.flutter-io.cn/packages/google_fonts) 字体的支持等。
|
||||
|
||||
- Android 插件的改进 [Android plugins APIs](https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration),相比起以前更为简单明了,分割了 `FlutterPlugin` and `MethodCallHandler` ,同时提供 `ActivityAware` 、 `ServiceAware` 作为独立支持。
|
||||
|
||||
- iOS 13 深色模式,支持使用 darkTheme 设置,同时还增加了如 `CupertinoContextMenu`、`CupertinoSlidingSegmentedControl`、`CupertinoAlertDialog`、`CupertinoDatePicker` 等 iOS 风格的控件支持。
|
||||
|
||||
```
|
||||
new MaterialApp(
|
||||
title: '',
|
||||
navigatorKey: navigatorKey,
|
||||
theme: model.themeData,
|
||||
darkTheme: model.darkthemeData,
|
||||
locale: model.locale,
|
||||
```
|
||||
|
||||
- [Add-to-App](https://flutter.dev/docs/development/add-to-app) 混合集成模式的进一步的更新。
|
||||
|
||||
- 新增加了不兼容的 `breaking change`,比如: [PageView 启用 RenderSliverFillViewport](https://github.com/flutter/flutter/pull/37024) 、 [WidgetsBinding 中的 attachRootWidget 被替换为 scheduleAttachRootWidget](https://github.com/flutter/flutter/pull/39079/files) 、[Allow gaps in the initial route](https://github.com/flutter/flutter/pull/39440/files)、[TextField's minimum height from 40 to 48 ](https://github.com/flutter/flutter/pull/42449) 等需要开发者注意重新适配的修改,更多可查阅 [release-notes-1.12.13](https://flutter.dev/docs/development/tools/sdk/release-notes/release-notes-1.12.13)。
|
||||
|
||||
- 增加了 [MediaQuery.systemGestureInsets 支持 Android Q 的手势导航](https://github.com/flutter/flutter/pull/37416);增加了 SliverIgnorePointer 、SliverOpacity、SliverAnimatedList 等控件支持;PageRouteBuilder 支持 fullscreenDialog。
|
||||
|
||||
|
||||
- [Dart 2.7 的发布,支持扩展方法](https://medium.com/dartlang/dart-2-7-a3710ec54e97)。
|
||||
|
||||
```
|
||||
extension ExtendsFun on String {
|
||||
int parseInt() {
|
||||
return int.parse(this);
|
||||
} double parseDouble() {
|
||||
return double.parse(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
main() {
|
||||
int i = '42'.parseInt();
|
||||
print(i);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
更多完整的 release-notes 可见 [release-notes-1.12.13](https://flutter.dev/docs/development/tools/sdk/release-notes/release-notes-1.12.13)
|
||||
|
||||
|
||||
### 四、其他
|
||||
|
||||
本次 `Flutter Interact` 还推荐了 [flutter-d-art](https://github.com/Solido/flutter-d-art) 和
|
||||
[gskinner](https://flutter.gskinner.com) 等精美的开源项目,同时
|
||||
**Flutter 本次也表示了将在未来优化代码的开发模式,而 Flutter 在不断开新坑的同时,也需要面对目前层出的问题。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image19)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image20)
|
||||
|
||||
Flutter 过去的一年无疑是火热的,所以暴露的问题也指数级出现,比如最近开发中就遇到了**在断网时加载图后之后,再打开网络无法继续显示图片的问题。**
|
||||
|
||||
不过既然是开源项目,“白嫖”之余也得多靠自己,上述问题经过查找后,在自定义的 `ImageProvider` 里图片加载失败时,可以通过清除了 `ImageCache` 中的 `PendingImage` 来解决问题,同时因为 `Image` 的封装与 `DecorationImage` 的差异化,还需要对 `Image` 的 `didUpdateWidget` 做二次处理才解决了问题。
|
||||
|
||||
说这个问题其实就是想表达开源的意义,用一个框架不能够只是坐享其成的心态,开源的目的更是交流,不管什么框架都不可能尽善尽美,我们可以用更开放的心态去尝试和“批判”,而我们的岗位不就是解决这些问题的么?
|
||||
|
||||
|
||||
### Flutter 文章汇总地址:
|
||||
|
||||
> [Flutter 完整实战实战系列文章专栏](https://juejin.im/collection/5db25bcff265da06a19a304e)
|
||||
>
|
||||
> [Flutter 番外的世界系列文章专栏](https://juejin.im/collection/5db25d706fb9a069f422c374)
|
||||
|
||||
|
||||
### 资源推荐
|
||||
|
||||
* Github : https://github.com/CarGuo
|
||||
* **开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter**
|
||||
* **开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo**
|
||||
* **开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook**
|
||||
* 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20191224_Flutter-Interact-2019/image21)
|
|
@ -1,194 +0,0 @@
|
|||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image1.png)
|
||||
|
||||
# 掘力计划|Flutter 混合开发的混乱之治【直播回顾】
|
||||
|
||||
Hello,大家好,我是 Flutter GDE 郭树煜,今天的主题是 Flutter 的混合开发,但是其实内容并不会很广,主要分享会集中在 Android 平台的 `PlatformView ` 实现上,其实本次内容之前我已经在掘金发过一篇[简要的文字概括](https://juejin.cn/post/7257119213889454139),今天主要是根据这个内容做一个更详细的技术展开。
|
||||
|
||||
> 之所以会集中在 Android 平台的 `PlatformView ` 实现上去分享,是因为正如标题所示那样,Android 平台的 `PlatformView ` 实现目前呈现的状态:混乱。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image2.png)
|
||||
|
||||
## 混乱之始
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image3.png)
|
||||
|
||||
就像每个混乱都有它的起源,比如艾泽拉斯的混乱之治起源于燃烧军团的入侵,而混合开发在 Flutter 领域之所以混乱,主要源自它本身独特的实现。
|
||||
|
||||
我们常说光明总是伴随着黑暗,Flutter 最大的特点在于:渲染的控件是通过 Skia 直接和 GPU 交互,所以可做到在性能不错的同时,在不同平台得到一致性的渲染效果。
|
||||
|
||||
也就是说 Flutter 控件和平台无关,甚至连 UI 绘制线程都和原生平台 UI 线程是相互独立,这就决定了: **Flutter 在和原生平台做混合开发时会有相对高昂的技术成本**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image4.png)
|
||||
|
||||
> 简单想象下,例如你需要把一个原生的按键渲染到 WebView 里面和前端标签混合到一起,这是不是很不可思议?
|
||||
|
||||
还一个更容易理解的角度,其实从渲染的角度看 Flutter 更像是一个「游戏」引擎,只是他可以用来开发 App ,当然它现在也可以用来开发游戏,近两年谷歌的 I/O 大会都用它做了热场游戏,例如今年就做了一个像图片里的卡牌动作游戏,所以 Flutter 其实更像是游戏引擎的逻辑,所以它独立于平台的特性,既是优势,也带来了劣势:
|
||||
|
||||
> 毕竟把原生控件渲染进一个类似 unity 的引擎进行混合并不容易。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image5.gif)
|
||||
|
||||
那如果只是单纯的技术问题,也只是实现成本较高而已,为什么会说混乱呢?这就需要谈到目前 Android `PlatformView` 的实现。
|
||||
|
||||
不过再谈及 Android `PlatformView ` 实现之前,先简单说说 iOS ,iOS 平台是**通过将 Flutter UI 分为两个透明纹理来完成组合**:
|
||||
|
||||
> 需要在 `PlatformView` 下方呈现的 Flutter UI 可以被绘制到其下方的纹理;而需要在 `PlatformView` 上方呈现的 Flutter UI 可以被绘制到其上方的纹理, 它们只需要在最后组合起来就可以了。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image6.png)
|
||||
|
||||
简单来说,就是通过在 `NativeView` 的不同层级设置不同的透明图层,然后把不同位置的控件渲染到不同图层,最终达到组合起来的效果。
|
||||
|
||||
> 那 Android 是否采用这种实现?答案明显并不是,因为这种实现在 iOS 上框架渲染后系统会有回调通知,例如:*当 iOS 视图向下移动 `2px` 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 `2px`*。
|
||||
|
||||
但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染,所以基于此,在各个版本的更新迭代下, Android 的 `PlatformView ` 实现衍生出多种实现逻辑。
|
||||
|
||||
目前活跃在 Android 平台的 `PlatformView` 支持主要有以下三种:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image7.png)
|
||||
|
||||
可以看到官方都已经为大家定义好了简称 VD、HC、TLHC ,有了简称也方便大家提 issue 时沟通,毕竟每次在讨论时都用全称很费劲:
|
||||
|
||||
> **因为你需要不停指出你用的是什么模式,然后在什么模式下正常or不正常,另外知道这些简称最大的作用就是看 issue 时不迷糊**。
|
||||
|
||||
所以后续我们也会用简称来称呼它们,而之所以会有这么多模式,其实就是因为**没有一种模式可以完全满足和覆盖需求** ,这也导致了明明后来出现的模式是为了替代旧的支持,但是最终形成了共存的情况,从而导致了后续混乱的开始。
|
||||
|
||||
> 这就好比兽族入侵艾泽拉斯,最后的结果却是兽族和人族共存下来,各个模式之间最终既相爱又相杀的一种情况。
|
||||
|
||||
## VD
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image8.png)
|
||||
|
||||
我们先说最早的 VD,VD 简单来说就是使用 VirtualDisplay 渲染原生控件到内存。
|
||||
|
||||
`VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,`VirtualDisplay` 一般在副屏显示或者录屏场景下会用到,而在 Flutter 里 `VirtualDisplay` 会将虚拟显示区域的内容渲染在一个内存 `Surface`上。
|
||||
|
||||
在 Flutter 中需要用到 Android 原生 View 的地方会让你使用一个叫 `AndroidView` 的控件,如图所示,**在 Flutter 中通过将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中 ,然后通过 textureId 在 `VirtualDisplay` 对应的内存中提取绘制的纹理**:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image9.png)
|
||||
|
||||
> 通过在 Dart 层提供一个 `AndroidView` ,从而获取到控件所需的大小,位置等参数,然后通过 `textureId` ,主要是这个 id 提交给 Flutter Engine ,通过 id Flutter 就可以在渲染时将画面从内存里提出出来。
|
||||
|
||||
那么这个实现在满足和最初混合开发接入原生控件的同时,也带来和许多的局限,最常见的就是**触摸事件**和**文字输入**的支持问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image10.png)
|
||||
|
||||
#### 触摸事件
|
||||
|
||||
因为控件是被渲染在内存里,所以虽然**你在 UI 上看到它就在那里,但是事实上它并不在那里**,你点击到的是 Flutter 所在的原生 `FlutterView`,**用户产生的触摸事件是直接发送到 `FlutterView`**。
|
||||
|
||||
> 触摸事件需要在 `FlutterView` 到 Dart ,再从 Dart 转发到原生,然后如果原生不处理又要转发回 Flutter ,中间如果还存在其他派生视图,事件就很容易出现丢失和无法响应。
|
||||
|
||||
而 Android 的 `MotionEvent` 在转化到 Flutter 过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。
|
||||
|
||||
#### 文字输入
|
||||
|
||||
另外关于文字输入 的问题,一般情况下 **`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的内存位置会始终被认为是 `unfocused` 的状态**。
|
||||
|
||||
> 而 `InputConnections` 在 `unfocused` 的 View 中通常是会被丢弃。
|
||||
|
||||
所以 **Flutter 重写了 View 的 `checkInputConnectionProxy` 方法,这样 Android 会认为 `FlutterView` 是作为 `AndroidView` 和输入法编辑器(IME)的代理**,这样 Android 就可以从 `FlutterView` 中获取到 `InputConnections` 然后作用于 `AndroidView` 上面。
|
||||
|
||||
> 在 Android Q 开始又因为非全局的 `InputMethodManager` 需要新的兼容
|
||||
|
||||
所以键盘问题在第一代 VD 上最为突出,因为在不同版本的 Android 上可能会经常非常容易异常,为 `WebView` 作为混合开发里最常用到的插件,键盘是它最精彩会用到的能力之一,这个局限对于 VD 来说非常致命。
|
||||
|
||||
## HC
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image11.png)
|
||||
|
||||
Flutter 是在 1.2 版本开始支持 HC,简单说就是直接把原生控件覆盖在 Flutter 上进行堆叠,它使用了类似 iOS 的实现思路,简单来说就是 `HybridComposition` 模式会直接把原生控件通过 `addView` 添加到 `FlutterView` 上 。
|
||||
|
||||
举一个简单的例子,如图所示,一个原生的 `TextView` 被通过 HC 模式接入到 Flutter 里(`NativeView`),而在 Android 的显示布局边界和 Layout Inspector 上可以清晰看到: **灰色 `TextView` 通过 `FlutterMutatorView` 被添加到 `FlutterView` 上被直接显示出来** 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image12.png)
|
||||
|
||||
**所以在 HC 模式里 `TextView` 是直接在原生代码上被 add 到 `FlutterView` 上,而不是提取纹理**。
|
||||
|
||||
那如果我们看一个复杂一点的案例,如图所示,其中蓝色的文本是原生的 `TextView` ,红色的文本是 Flutter 的 `Text` 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:
|
||||
|
||||
- 两个蓝色的 `TextView` 是被添加在 `FlutterView` 之上,并且把没有背景色的红色 RE 遮挡住了
|
||||
- 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 `TextView` 之上,所以这时候多一个 `FlutterImageView` ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image13.png)
|
||||
|
||||
**可以看到 `Hybrid Composition` 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生角度看它就是原生层面的物理堆叠,需要叠加一个层级就多加一个 `FlutterImageView` ,同一个层级的 Flutter 控件共享一个 `FlutterImageView`** 。
|
||||
|
||||
当然,这里出现的 `FlutterImageView` ,其实还有一个作用,就是**为了解决动画同步和渲染**。
|
||||
|
||||
前面说过,HC 是直接被添加到原生 `FlutterView` 上面,所以走的还是原生的渲染流程和时机,而这时候通过 `FlutterImageView` ,也就是把 Flutter 控件渲染也同步到原生的 `OnDraw` 上,这样对于画面同步会更好。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image14.png)
|
||||
|
||||
当然,这样带来了一个问题,因为此时原生控件是直接渲染,所以需要在原生的平台线程上执行,纯在 Flutter 的 UI 线程就存在线程同步问题,所以在此之前一些场景下会有画面闪烁 bug ,例如:
|
||||
|
||||
- `A page` -> `webview page` -> `B page` , 当 `webview page` 打开 `B page` 时,有时候 `A page` 的 UI 在 `B page ` 突然闪动
|
||||
|
||||
- 当 `B page` 返回 `webview page `, 然后再返回 `A page`, 有时候 `B page` UI 突然闪现在 `A page`
|
||||
|
||||
虽然这个问题最后也通过类似线程同步实现解决,但是也带来一定程度的性能开销,另外在 Android 10 之前还会存在 GPU->CPU->GPU的性能损耗,所以 HC 属于会性能开销较大,又需要原生控件特性的场景。
|
||||
|
||||
## TLHC
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image15.png)
|
||||
|
||||
3.0 版本之后开始支持 TLHC 模式,最初它的目的还是取代上面这两种模式,解决混乱之治,但是奈何它最后和阿尔萨斯一样,成了新一代的巫妖王。
|
||||
|
||||
目前 TLHC 和 VD 还有 HC 一起共存下来,该模式的最大特点是控件虽然在还是布局在该有的位置上,但是其实是通过一个 `FrameLayout` 代理 `onDraw` 然后替换掉 child 原生控件的 `Canvas` 来实现混合绘制。
|
||||
|
||||
TLHC 算是参考了 VD 和 HC 的模式,然后利用平台的特点来完成渲染,所以它带了 HC ,但又并不是 HC,最大的特点就是它不在让控件通过原生线程绘制,所以也就不需要做线程同步。
|
||||
|
||||
而说它参考 VD ,主要是它和 VD 很类似,不同之处在于**原生控件纹理的提取方式上**,如图可以看到 :
|
||||
|
||||
- 从 VD 到 TLHC 里, **Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑**
|
||||
- 以前 Flutter 中将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到;**现在 `AndroidView` 需要的内容,会通过 View 的 `draw` 方法被绘制到 `SurfaceTexture` 里,然后同样通过 `TextureId` 获取绘制在内存的纹理**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image16.png)
|
||||
|
||||
简单说就是不需要绘制到副屏里,现在直接通过 override `View` 的 `onDraw` 方法就可以了,然后因为它是绘制到内存,最终渲染还是在 Flutter 线程完成,所以也就不需要线程同步。
|
||||
|
||||
举个例子,还是之前的代码,如图所示,这时候通过 TLHC 模式运行之后,通过 Layout Inspector 的 3D 图层可以看到,两个原生的 `TextView` 通过 `PlatformViewWrapper` 被添加到 `FlutterView` 上。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image17.png)
|
||||
|
||||
但是不同的是,**在 3D 图层里看不到 `TextView` 的内容,因为绘制 `TextView` 的 Canvas 被替换了**,所以 `TextView` 的内容被绘制到内存的 Surface 上,最终会在渲染时同步 Flutter Engine 里。
|
||||
|
||||
> 不过 **`PlatfromViewWrapper` 拦截了 Event ,但是其实还是通过 Dart 做二次分发响应,从而实现不同的事件响应** ,它和 VD 的不同是, VD 的事件响应都是在 `FlutterView` 上,但是TLHC 模式,是有独立的原生 `PlatfromViewWrapper` 控件来开始,所以区域效果和一致性会更好。
|
||||
|
||||
**那么为什么说 TLHC 模式是巫妖王呢**?
|
||||
|
||||
因为这种实现天然不支持 `SurfaceView` ,因为 `SurfaceView` 是双缓冲机制,所以通过 parent 替换 `Canvas` 的实现并不支持,也就是对于类似地图、视频等插件,如果是 `SurfaceView` ,会出现无法支持的问题。
|
||||
|
||||
那有人说,我用 `TextureView` 不就行了?对不起,目前在 [#103686](https://github.com/flutter/flutter/issues/103686) 下,对于 `TextureView` 有时候也会出现不正常更新的异常情况。
|
||||
|
||||
所以 TLHC 没能带来终结,它反而引入的新的致命缺陷,并且和 VD 还有 HC 融合到了一起。
|
||||
|
||||
## 混乱之治
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image18.png)
|
||||
|
||||
那为什么这三种模式会导致混乱?首先我们简单总结下前面介绍的内容:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image19.png)
|
||||
|
||||
而随着三种模式的存在,在 API 层面,目前出现了兼容式运行的情况,在 API 上,在目前 3.0+ 的 Flutter 上同样对应有三个 API ,但是这三个 API 并不是直接对应上述三种模式:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image20.png)
|
||||
|
||||
看到没有,这里有一个问题就是:**你其实没办法主动控制是 TLHC 还是 VD ,对于 HC 你倒是可以强行指定**。
|
||||
|
||||
另外,不知道你注意到没有,不管是 `initAndroidView` 还是 `initSurfaceAndroidView` ,它们都可能会在升级到新版本时使用 TLHC 模式,**也就是如果你的 Plugin 没有针对性做更新,那么可能会在不知觉的情况下换了模式,从而有可能出现 bug** 。
|
||||
|
||||
> 对于 TLHC 还有一个问题,就是如果你原本没有 SurfaceView ,但是后面添加 SurfaceView ,也会触发异常显示的问题。
|
||||
|
||||
现在你看出 PlatformView 的混乱了吧?从底层实现的不统一,到 API 再不同版本下不同的行为变化,这就是目前 Android 在 PlatformView 支持下的混乱生态,同时如果你对于目前 PlatformView 存在的问题感兴趣,可以查阅以下相关 issue:
|
||||
|
||||
- [#103686](https://github.com/flutter/flutter/issues/103686)
|
||||
- [#109690](https://github.com/flutter/flutter/issues/109690)
|
||||
- [#112712](https://github.com/flutter/flutter/issues/112712)
|
||||
- [#130692](https://github.com/flutter/flutter/issues/130692)
|
||||
|
||||
不过整体来说,官方还是建议大家使用 TLHC 模式,因为它的思路总的来说性能会更好,并且更符合预期,在不出现兼容运行的情况下。
|
||||
|
||||
好了,今天分享的内容就这些,谢谢大家。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230728_JJ/image21.png)
|
227
Flutter-KEY.md
227
Flutter-KEY.md
|
@ -1,227 +0,0 @@
|
|||
|
||||
|
||||
最近刚好有网友咨询一个问题,那就顺便借着这个问题给大家深入介绍下 Flutter 中键盘弹起时,`Scaffold` 的内部发生了什么变化,让大家更好理解 Flutter 中的输入键盘和 `Scaffold` 的关系。
|
||||
|
||||
如下图所示,当时的问题是:*当界面内有 `TextField` 输入框时,点击键盘弹起后,界面内底部的按键和 FloatButton 会被挤到键盘上面,有什么办法可以让底部按键和 FloatButton 不被顶上来吗?*
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image1)
|
||||
|
||||
|
||||
其实解决这个问题很简单,那就是只要**把 `Scaffold` 的 `resizeToAvoidBottomInset` 配置为 `false`** ,结果如下图所示,键盘弹起后底部按键和 FloatButton 不会再被顶上来,问题解决。**那为什么键盘弹起会和 `resizeToAvoidBottomInset` 有关系?**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image2)
|
||||
|
||||
|
||||
|
||||
|
||||
### Scaffold 的 resize
|
||||
|
||||
|
||||
`Scaffold` 是 Flutter 中最常用的页面脚手架,前面知道了通过 `resizeToAvoidBottomInset` ,我们可以配置在键盘弹起时页面的底部按键和 FloatButton 不会再被顶上来,其实这个行为是因为 `Scaffold` 的 `body` 大小被 `resize` 了。
|
||||
|
||||
|
||||
那这个过程是怎么发生的呢?首先如下图所示,我们在 `Scaffold` 的源码里可以看到,当`resizeToAvoidBottomInset` 为 true 时,会使用 `mediaQuery.viewInsets.bottom` 作为 `minInsets` 的参数,也就是可以确定:**键盘弹起时的界面 `resize` 和 `mediaQuery.viewInsets.bottom` 有关系**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image3)
|
||||
|
||||
|
||||
而如下图所示, `Scaffold` 内部的布局主要是靠 `CustomMultiChildLayout` ,`CustomMultiChildLayout` 的布局逻辑主要在 `MultiChildLayoutDelegate` 对象里。
|
||||
|
||||
前面获取到的 `minInsets` 会被用到 `_ScaffoldLayout` 这个 `MultiChildLayoutDelegate` 里面,也就是说 **`Scaffold` 的内部是通过 `CustomMultiChildLayout` 实现的布局,具体实现逻辑在 `_ScaffoldLayout` 这个 `Delegate` 里**。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image4)
|
||||
|
||||
|
||||
> 关于 `CustomMultiChildLayout` 的详细使用介绍在之前的文章 [《详解自定义布局实战》](https://juejin.cn/post/6844903878509461518#heading-10) 里可以找到。
|
||||
|
||||
|
||||
接着看 `_ScaffoldLayout` , 在 `_ScaffoldLayout` 进行布局时,会通过传入的
|
||||
`minInsets` 来决定 `body` 显示的 `contentBottom` , 所以可以看到**事实上传入的 `minInsets` 改变的是 `Scaffold` 布局的 bottom 位置**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image5)
|
||||
|
||||
|
||||
> 上图代码中使用的 `_ScaffoldSlot.body` 这个枚举其实是作为 `LayoutId` 的值,`MultiChildLayoutDelegate` 在布局时可以通过 `LayoutId` 获取到对应 child 进行布局操作,详细可见: [《详解自定义布局实战》](https://juejin.cn/post/6844903878509461518#heading-10)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image6)
|
||||
|
||||
|
||||
那么 `Scaffold` 的 `body` 是什么呢? 如上图代码所示,其实 `Scaffold` 的 `body` 是一个叫 `_BodyBuilder` 的对象,而这个 `_BodyBuilder` 内部其实是一个 `LayoutBuilder`。(注意,在 `widget.appbar` 不为 `null` 时,会 `removeTopPadding`)
|
||||
|
||||
所以如下图代码所示 `body` 在添加时,**它父级的`MediaQueryData` 会被重载,特别是 `removeTopPadding` 会被清空,`viewInsets.bottom` 也是会被重置**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image7)
|
||||
|
||||
|
||||
最后如下代码所示,`_BodyBuilder` 的 `LayoutBuilder` 里会获取到一个 `top` 和 `bottom` 的参数,这两个参数都通过前面在 `_ScaffoldLayout` 布局时传入的 `constraints` 去判断得到,最终 `copyWith` 得到新的 `MediaQuery` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image8)
|
||||
|
||||
|
||||
这里就涉及到一个有意思的点,在 `_BodyBuilder` 里的通过 `copyWith` 得到新的 `MediaQuery` 会影响什么呢?如下代码所示,这里用一个简单的例子来解释下。
|
||||
|
||||
|
||||
```dart
|
||||
class MainWidget extends StatelessWidget {
|
||||
final TextEditingController controller =
|
||||
new TextEditingController(text: "init Text");
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print("Main MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}");
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: new Text("MainWidget"),
|
||||
),
|
||||
extendBody: true,
|
||||
body: Column(
|
||||
children: [
|
||||
new Expanded(child: InkWell(onTap: (){
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
})),
|
||||
///增加 CustomWidget
|
||||
CustomWidget(),
|
||||
new Container(
|
||||
margin: EdgeInsets.all(10),
|
||||
child: new Center(
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
new Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
class CustomWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print("Custom MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}\n \n");
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如上代码所示:
|
||||
|
||||
- 代码中定义了 `MainWidget` 和 `CustomWidget` 两个控件;
|
||||
- `MainWidget` 里使用了 `Scaffold` ,并且 `CustomWidget` 在 `MainWidget` 里被使用;
|
||||
- 分别在这两个 Widget 的`build` 方法里打印出对应的 `MediaQuery.of(context).padding` 和 `MediaQuery.of(context).viewInsets.bottom` 的值;
|
||||
|
||||
如下图所示,在键盘弹起和不弹起时可以看到 `padding` 值是不同的,而 `viewInsets.bottom` 都为 0。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image9)
|
||||
|
||||
为什么 `padding` 值的 `top` 会不一致,**自然是因为 `CustomWidget` 和 `MainWidget`获取到的 `MediaQuery.of(context)` 对象不是同一个数据。**
|
||||
|
||||
|
||||
- `MainWidget` 使用的 `MediaQuery.of(context)` 得到的 `MediaQueryData` 是上级往下传递的,里面**包含了 `top:47` 的状态栏高度和 `bottom:34` 的底部安全区域高度**。
|
||||
|
||||
|
||||
- `CustomWidget` 里面 `MediaQuery.of(context)` 得到的 `MediaQueryData` ,自然就是前面分析过的 `_BodyBuilder` 里的通过 `copyWith` 得到新的 `MediaQuery`,所以 `CustomWidget` 得到的 `MediaQueryData` 其实**在 `Scaffold` 内部已经被重置了,所以它的 `top:0` ,获取不到状态栏高度**。
|
||||
|
||||
|
||||
> 事实上这就是大家为什么有时候 **`MediaQuery.of( context)` 可以获取到状态栏高度,有时候又获取不到的原因**,因为你的 `context` 获取到的是 `Scaffold` 之外的 `MediaQueryData`, 还是 `Scaffold` 内被重载过的 `MediaQueryData`,自然会得到不一样的结果。
|
||||
|
||||
|
||||
如下图所示,键盘弹起因为被 resize 了,所以界面的 `bottom` 安全区域变成了 0 ,而
|
||||
|
||||
- 在 `MainWidget` 中可以获取到 `viewInsets.bottom` 也就是键盘的高度;
|
||||
- 在 `CustomWidget` 获取不到 `viewInsets.bottom` ,因为在 `Scaffold` 内被重载清除了。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image10)
|
||||
|
||||
|
||||
总结一下:**`Scaffold` 的 `resizeToAvoidBottomInset` 会通过 `MediaQueryData` 影响 body 的布局,同时在 `Scaffold` 内 `MediaQuery` 会被重载,所以使用的 `context` 位置不同,获取到的 `MediaQueryData` 也不同,如果需要获取键盘高度和状态栏高度的话,最好使用 `Scaffold` 外的 `context` 。**
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image11)
|
||||
|
||||
|
||||
> 这里讲了 `MediaQuery` 和 `MediaQueryData` 的内容,为什么 `MediaQuery` 通过嵌套就可以重载?为什么通过 `context` 可以往上获取到离 `context` 最近的 `MediaQueryData`?因为 `MediaQuery` 是一个 `InheritedWidget` : [《全面理解State》](https://juejin.cn/post/6844903866706706439#heading-5) 。
|
||||
|
||||
|
||||
|
||||
### 键盘如何影响 Scaffold
|
||||
|
||||
|
||||
前面我们聊了 `Scaffold` 的 `resizeToAvoidBottomInset` 会通过 `MediaQueryData` 影响 body 的布局,那是怎么影响的呢?
|
||||
|
||||
|
||||
事实上这得从 `MaterialApp` 说起,在 `MaterialApp` 内部的深处嵌套着一个叫 `_MediaQueryFromWindow` 的 Widget ,它在内部通过 ` WidgetsBinding.instance.addObserver` 对 App 的各种系统事件做了监听,并且对应都执行了 `setState` 。
|
||||
|
||||
所以如下源码所示,当键盘弹出时, `build` 方法会被执行, 而 `MediaQueryData` 就会通过`MediaQueryData.fromWindow` 获取到新的 `MediaQueryData` 数据。
|
||||
|
||||
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
// ACCESSIBILITY
|
||||
|
||||
@override
|
||||
void didChangeAccessibilityFeatures() {
|
||||
setState(() { });
|
||||
}
|
||||
|
||||
// METRICS
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeTextScaleFactor() {
|
||||
setState(() { });
|
||||
}
|
||||
|
||||
// RENDERING
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
|
||||
if (!kReleaseMode) {
|
||||
data = data.copyWith(platformBrightness: debugBrightnessOverride);
|
||||
}
|
||||
return MediaQuery(
|
||||
data: data,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
举个例子,如下图所示,从 Android 的 Java 层弹出键盘开始,会把改变后的视图信息传递给 C++ 层,最后回调到 Dart 层,从而触发 `MaterialApp` 内的 `didChangeMetrics` 方法执行 ` setState(() {});` ,进而让 `_MediaQueryFromWindow` 内的 `build` 更新了 `MediaQueryData` ,最终改变了 `Scaffod` 的 `body` 大小。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image12)
|
||||
|
||||
那么到这里,你知道如何在 Flutter 里正确地去获取键盘的高度了吧?
|
||||
|
||||
### 最后
|
||||
|
||||
从一个简单的 `resizeToAvoidBottomInset` 去拓展到 `Scaffod` 的内部布局和 `MediaQueryData` 与键盘的关系,其实这也是学习框架过程中很好的知识延伸,通过特定的问题去深入理解框架的实现原理,最后再把知识点和问题关联起来,这样问题在此之后便不再是问题,因为入脑了~
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image13)
|
||||
|
||||
|
272
Flutter-LA.md
272
Flutter-LA.md
|
@ -1,272 +0,0 @@
|
|||
APP 启动页在国内是最常见也是必备的场景,其中启动页在 iOS 上算是强制性的要求,其实配置启动页挺简单,因为在 Flutter 里现在只需要:
|
||||
|
||||
|
||||
- iOS 配置 `LaunchScreen.storyboard`;
|
||||
- Android 配置 `windowBackground`;
|
||||
|
||||
一般只要配置无误并且图片尺寸匹配,基本上就不会有什么问题,**那既然这样,还有什么需要适配的呢?**
|
||||
|
||||
事实上大部分时候 iOS 是不会有什么问题,**因为 `LaunchScreen.storyboard` 的流程本就是 iOS 官方用来做应用启动的过渡;而对于 Andorid 而言,直到 12 之前 `windowBackground` 这种其实只能算“民间”野路子**,所以对于 Andorid 来说,这其中就涉及到一个点:
|
||||
|
||||
> [Flutter's first frame] + [time needed to jump from raster to main thread and get a next Android vsync] = [Android's first frame].
|
||||
|
||||
|
||||
所以下面主要介绍 Flutter 在 Android 上为了这个启动图做了哪些骚操作~
|
||||
|
||||
|
||||
## 一、远古时期
|
||||
|
||||
**在已经忘记版本的“远古时期”**, `FlutterActivity` 还在 `io.flutter.app.FlutterActivity` 路径下的时候,那时启动页的逻辑相对简单,主要是通过 App 的 `AndroidManifest` 文件里是否配置了 `SplashScreenUntilFirstFrame` 来进行判断。
|
||||
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />
|
||||
```
|
||||
|
||||
**在 `FlutterActivity` 内部 `FlutterView` 被创建的时候,会通过读取 `meta-data` 来判断是否需要使用 `createLaunchView` 逻辑**:
|
||||
|
||||
- 1、获取当前主题的 `android.R.attr.windowBackground` 这个 `Drawable` ;
|
||||
- 2、创建一个 `LaunchView` 并加载这个 `Drawable`;
|
||||
- 3、将这个 `LaunchView` 添加到 `Activity` 的 `ContentView`;
|
||||
- 4、在Flutter `onFirstFrame` 时将这个 `LaunchView` 移除;
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-LA/image1)
|
||||
|
||||
|
||||
```java
|
||||
private void addLaunchView() {
|
||||
if (this.launchView != null) {
|
||||
this.activity.addContentView(this.launchView, matchParent);
|
||||
this.flutterView.addFirstFrameListener(new FirstFrameListener() {
|
||||
public void onFirstFrame() {
|
||||
FlutterActivityDelegate.this.launchView.animate().alpha(0.0F).setListener(new AnimatorListenerAdapter() {
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
((ViewGroup)FlutterActivityDelegate.this.launchView.getParent()).removeView(FlutterActivityDelegate.this.launchView);
|
||||
FlutterActivityDelegate.this.launchView = null;
|
||||
}
|
||||
});
|
||||
FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this);
|
||||
}
|
||||
});
|
||||
this.activity.setTheme(16973833);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
是不是很简单,那就会有人疑问为什么要这样做?我直接配置 `Activity` 的 `android:windowBackground` 不就完成了吗?
|
||||
|
||||
这就是上面提到的时间差问题,**因为启动页到 Flutter 渲染完第一帧画面中间,会出现概率出现黑屏的情况,所以才需要这个行为来实现过渡**。
|
||||
|
||||
|
||||
## 2.5 之前
|
||||
|
||||
|
||||
经历了“远古时代”之后,`FlutterActivity` 来到了 `io.flutter.embedding.android.FlutterActivity`, 在到 2.5 版本发布之前,Flutter 又针对这个启动过程做了不少调整和优化,其中主要就是 `SplashScreen`。
|
||||
|
||||
|
||||
自从开始进入`embedding` 阶段后,`FlutterActivity` 主要用于实现了一个叫 `Host` 的 `interface`,其中和我们有关系的就是 `provideSplashScreen`。
|
||||
|
||||
**默认情况下它会从 `AndroidManifest` 文件里是否配置了 `SplashScreenDrawable` 来进行判断**。
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
```
|
||||
|
||||
默认情况下当 AndroidManifest 文件里配置了 `SplashScreenDrawable`,那么这个 Drawable 就会在 `FlutterActivity` 创建 `FlutterView` 时被构建成 `DrawableSplashScreen`。
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-LA/image2)
|
||||
|
||||
|
||||
`DrawableSplashScreen` 其实就是一个实现了 `io.flutter.embedding.android.SplashScreen` 接口的类,它的作用就是:
|
||||
|
||||
> 在 Activity 创建 FlutterView 的时候,将 `AndroidManifest` 里配置的 `SplashScreenDrawable` 加载成 `splashScreenView`(ImageView);,并提供 `transitionToFlutter` 方法用于执行。
|
||||
|
||||
|
||||
之后 `FlutterActivity` 内会创建出 `FlutterSplashView`,它是个 FrameLayout。
|
||||
|
||||
`FlutterSplashView` 将 `FlutterView` 和 `ImageView` 添加到一起, 然后通过 `transitionToFlutter` 的方法来执行动画,最后动画结束时通过 `onTransitionComplete` 移除 `splashScreenView` 。
|
||||
|
||||
|
||||
所以整体逻辑就是:
|
||||
|
||||
- 根据 meta 创建 `DrawableSplashScreen` ;
|
||||
- `FlutterSplashView` 先添加了 `FlutterView` ;
|
||||
- `FlutterSplashView` 先添加了 `splashScreenView` 这个 ImageView;
|
||||
- 最后在 `addOnFirstFrameRenderedListener` 回调里执行 `transitionToFlutter` 去触发 animate ,并且移除 `splashScreenView`。
|
||||
|
||||
|
||||
当然这里也是分状态:
|
||||
|
||||
- 等引擎加载完成之后再执行 `transitionToFlutter`;
|
||||
- 引擎已经加载完成了马上执行 `transitionToFlutter`;
|
||||
- 当前的 `FlutterView` 还没有被添加到引擎,等待添加到引擎之后再 `transitionToFlutter`;
|
||||
|
||||
```java
|
||||
public void displayFlutterViewWithSplash(@NonNull FlutterView flutterView, @Nullable SplashScreen splashScreen) {
|
||||
if (this.flutterView != null) {
|
||||
this.flutterView.removeOnFirstFrameRenderedListener(this.flutterUiDisplayListener);
|
||||
this.removeView(this.flutterView);
|
||||
}
|
||||
|
||||
if (this.splashScreenView != null) {
|
||||
this.removeView(this.splashScreenView);
|
||||
}
|
||||
|
||||
this.flutterView = flutterView;
|
||||
this.addView(flutterView);
|
||||
this.splashScreen = splashScreen;
|
||||
if (splashScreen != null) {
|
||||
if (this.isSplashScreenNeededNow()) {
|
||||
Log.v(TAG, "Showing splash screen UI.");
|
||||
this.splashScreenView = splashScreen.createSplashView(this.getContext(), this.splashScreenState);
|
||||
this.addView(this.splashScreenView);
|
||||
flutterView.addOnFirstFrameRenderedListener(this.flutterUiDisplayListener);
|
||||
} else if (this.isSplashScreenTransitionNeededNow()) {
|
||||
Log.v(TAG, "Showing an immediate splash transition to Flutter due to previously interrupted transition.");
|
||||
this.splashScreenView = splashScreen.createSplashView(this.getContext(), this.splashScreenState);
|
||||
this.addView(this.splashScreenView);
|
||||
this.transitionToFlutter();
|
||||
} else if (!flutterView.isAttachedToFlutterEngine()) {
|
||||
Log.v(TAG, "FlutterView is not yet attached to a FlutterEngine. Showing nothing until a FlutterEngine is attached.");
|
||||
flutterView.addFlutterEngineAttachmentListener(this.flutterEngineAttachmentListener);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isSplashScreenNeededNow() {
|
||||
return this.flutterView != null && this.flutterView.isAttachedToFlutterEngine() && !this.flutterView.hasRenderedFirstFrame() && !this.hasSplashCompleted();
|
||||
}
|
||||
|
||||
private boolean isSplashScreenTransitionNeededNow() {
|
||||
return this.flutterView != null && this.flutterView.isAttachedToFlutterEngine() && this.splashScreen != null && this.splashScreen.doesSplashViewRememberItsTransition() && this.wasPreviousSplashTransitionInterrupted();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**当然这个阶段的 FlutterActivity 也可以通过 `override` `provideSplashScreen` 方法来自定义 SplashScreen**。
|
||||
|
||||
> 注意这里的 SplashScreen 不等于 Android 12 的 SplashScreen。
|
||||
|
||||
看到没有,做了这么多其实也就是为了弥补启动页和 Flutter 渲染之间,**另外还有一个优化,叫 `NormalTheme`**。
|
||||
|
||||
|
||||
> 当我们设置了一个 `Activity` 的 `windowBackground` 之后,其实对性能还是多多少少会有影响,所以官方就增加了一个 `NormalTheme` 的配置,**在启动完成之后将主题设置为开发者自己配置的 `NormalTheme`**。
|
||||
|
||||
通过该配置 `NormalTheme` ,在 `Activity` 启动时,就会首先执行 `switchLaunchThemeForNormalTheme();` 方法将主题从 `LaunchTheme` 切换到 `NormalTheme`。
|
||||
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
|
||||
```
|
||||
|
||||
大概配置完就是如下样子,**前面分析那么多其实就是为了告诉你,如果出现问题了,你可以从哪个地方去找到对应的点**。
|
||||
|
||||
```xml
|
||||
<activity
|
||||
android:name=".MyActivity"
|
||||
android:theme="@style/LaunchTheme"
|
||||
// ...
|
||||
>
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
```
|
||||
|
||||
## 2.5 之后
|
||||
|
||||
|
||||
讲了那么多,**Flutter 2.5 之后 `provideSplashScreen` 和 `io.flutter.embedding.android.SplashScreenDrawable` 就被弃用了,惊不喜惊喜,意不意外,开不开心** ?
|
||||
|
||||
|
||||
> Flutter 官方说: Flutter 现在会自动维持着 Android 启动页面的效显示,直到 Flutter 绘制完第一帧后才消失。
|
||||
|
||||
通过源码你会发现,当你设置了 `splashScreen` 的时候,会看到一个 log 警告:
|
||||
|
||||
|
||||
```java
|
||||
if (splashScreen != null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"A splash screen was provided to Flutter, but this is deprecated. See"
|
||||
+ " flutter.dev/go/android-splash-migration for migration steps.");
|
||||
FlutterSplashView flutterSplashView = new FlutterSplashView(host.getContext());
|
||||
flutterSplashView.setId(ViewUtils.generateViewId(FLUTTER_SPLASH_VIEW_FALLBACK_ID));
|
||||
flutterSplashView.displayFlutterViewWithSplash(flutterView, splashScreen);
|
||||
|
||||
return flutterSplashView;
|
||||
}
|
||||
```
|
||||
|
||||
为什么会弃用?
|
||||
其实这个提议是在 https://github.com/flutter/flutter/issues/85292 这个 issue 上,然后通过 https://github.com/flutter/engine/pull/27645 这个 pr 完成调整。
|
||||
|
||||
大概意思就是:**原本的设计搞复杂了,用 `OnPreDrawListener` 更精准,而且不需要为了后面 Andorid12 的启动支持做其他兼容,只需要给 FlutterActivity 等类增加接口开关即可**。
|
||||
|
||||
也就是2.5之后 Flutter 使用 [ViewTreeObserver.OnPreDrawListener](https://developer.android.com/reference/android/view/ViewTreeObserver.OnPreDrawListener) 来实现延迟直到加载出 Flutter 的第一帧。
|
||||
|
||||
为什么说默认情况?**因为这个行为在 FlutterActivity 里,是在 `getRenderMode() == RenderMode.surface` 才会被调用,而 `RenderMode` 又和 `BackgroundMode` 有关心**。
|
||||
|
||||
> 默认情况下 BackgroundMode 就是 `BackgroundMode.opaque` ,所以就是 `RenderMode.surface`
|
||||
|
||||
所以在 2.5 版本后, FlutterActivity 内部创建完 FlutterView 后就会执行一个 `delayFirstAndroidViewDraw` 的操作。
|
||||
|
||||
```java
|
||||
|
||||
private void delayFirstAndroidViewDraw(final FlutterView flutterView) {
|
||||
if (this.host.getRenderMode() != RenderMode.surface) {
|
||||
throw new IllegalArgumentException("Cannot delay the first Android view draw when the render mode is not set to derMode.surface`.");
|
||||
} else {
|
||||
if (this.activePreDrawListener != null) {
|
||||
flutterView.getViewTreeObserver().removeOnPreDrawListener(this.activePreDrawListener);
|
||||
}
|
||||
|
||||
this.activePreDrawListener = new OnPreDrawListener() {
|
||||
public boolean onPreDraw() {
|
||||
if (FlutterActivityAndFragmentDelegate.this.isFlutterUiDisplayed && terActivityAndFragmentDelegate.this.activePreDrawListener != null) {
|
||||
flutterView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
FlutterActivityAndFragmentDelegate.this.activePreDrawListener = null;
|
||||
}
|
||||
|
||||
return FlutterActivityAndFragmentDelegate.this.isFlutterUiDisplayed;
|
||||
}
|
||||
};
|
||||
flutterView.getViewTreeObserver().addOnPreDrawListener(this.activePreDrawListener);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**这里主要注意一个参数:`isFlutterUiDisplayed`。**
|
||||
|
||||
当 Flutter 被完成展示的时候,`isFlutterUiDisplayed` 就会被设置为 true。
|
||||
|
||||
**所以当 Flutter 没有执行完成之前,`FlutterView` 的 `onPreDraw` 就会一直返回 false**,这也是 Flutter 2.5 开始之后适配启动页的新调整。
|
||||
|
||||
|
||||
|
||||
## 最后
|
||||
|
||||
|
||||
看了这么多,大概可以看到其实开源项目的推进并不是一帆风顺的,没有什么是一开始就是最优解,而是经过多方尝试和交流,才有了现在的版本,事实上开源项目里,类似这样的经历数不胜数:
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-LA/image3)
|
280
Flutter-M3D.md
280
Flutter-M3D.md
|
@ -1,280 +0,0 @@
|
|||
# Flutter 小技巧之 3.16 升级最坑 M3 默认适配技巧
|
||||
|
||||
如果要说 Flutter 3.16 升级里是最坑的是什么?那我肯定要说是 Material 3 default (M3)。
|
||||
|
||||
倒不是说 M3 bug 多,也不是 M3 在 3.16 上使用起来多麻烦,因为**虽然从 3.16 开始,`MaterialApp` 里的 `useMaterial3` 默认会是 true,但是你是可以直接 使用 `useMaterial3: false` 来关闭**。
|
||||
|
||||
那为什么还收坑?因为未来 **Material 2 相关的东西会被弃用并删除**,所以 Material 3 default(M3) 是一个警告,你可以通过 `useMaterial3: false` 来关闭无视,但是这个技术债未来会很坑。
|
||||
|
||||
> 难道你还能一直苟着不更新?
|
||||
|
||||
为什么说它很坑?因为适配它纯纯是一个体力活,而且还是一个细节工作,M3 是一套配色方案,**一套和 M2 「毫不相关」的配色方案**:
|
||||
|
||||
- 配色方案代表着它已经帮你默认确定了什么地方应该用什么颜色
|
||||
- M2 毫不相干,代表着你之前用这 M2 的 Widget 默认的 UI 效果,用了 M3 会完全不一样
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image1.gif)
|
||||
|
||||
如上图所示,看起来好像是就是:
|
||||
|
||||
- `AppBar` 配色发生了变化
|
||||
- `FloatingActionButton` 从圆的变成方的,颜色发生变化
|
||||
- 默认 Button 按照风格发生了变卦
|
||||
- ······
|
||||
|
||||
似乎看起来也没什么,但是你知道有多少地方用了 `FloatingActionButton` ?每个地方的 `AppBar` 难道都要手动去调整?`ElevatedButton` 和 `TextButton` 有没有办法全局配置?本篇就是为了让你少走适配弯路,提供适配思路的角度。
|
||||
|
||||
> 核心还是国内的产品有谁愿意使用 Material Design ? 像这种 M2 到 M3 的变化,对于开发者来说纯粹就是负优化。
|
||||
|
||||
# 开始
|
||||
|
||||
首先,**官方 Material 3 配色首推是使用 `ColorScheme.fromSeed()` 来生成配色**,当然你也可以通过 `ColorScheme.fromImageProvider` 的图片来生成配色,不过一般人应该不会这么干,另外还有 `ColorScheme.fromSwatch` ,不过这个的灵活适配程度不如 fromSeed,所以使用 fromSeed 是比较好的选择。
|
||||
|
||||
> 因为 M3 默认从蓝色系列变成紫色系统,所以如果你用的是默认色系,那就更需要配置来恢复,本篇的目的就是,**让 App 在 M3 下恢复到 M2 的 UI 效果,因为它真的不是仅仅一个颜色变化而已。**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image2.png)
|
||||
|
||||
|
||||
|
||||
如果你以前的 `ThemeData` 是如下所示代码,那么运行之后你会看到,原本应该是 M2 效果的正常列表,现在变成了 M3 那种「无法言喻」的效果,可以看到此时 M3 下 `primarySwatch` 其实并没有起到作用。
|
||||
|
||||
```dart
|
||||
ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
////
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
| M2 | M3 |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231123_M3/image3.png) | ![](http://img.cdn.guoshuyu.cn/20231123_M3/image4.png) |
|
||||
|
||||
那么首先我们要做的就是增加 `colorScheme` ,但是你在加完会发现并没有什么变化,这是因为此时控件还是处于 M3 的色系下,所以接下来我们要首先全局恢复 `Appbar`。
|
||||
|
||||
```dart
|
||||
ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
),
|
||||
```
|
||||
|
||||
> Do it。
|
||||
|
||||
# AppBar
|
||||
|
||||
如下代码所示,我们先添加 `AppBarTheme` ,可以看到 AppBar 的背景这样就变回了蓝色,但是这时候 Appbar 的文本和图标还是黑色。
|
||||
|
||||
```dart
|
||||
ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231123_M3/image5.png) | ![](http://img.cdn.guoshuyu.cn/20231123_M3/image6.png) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------ |
|
||||
|
||||
为了让图标和文本恢复到 M2 的白色,我们可以在 `AppBarTheme` 里配置 `iconTheme` 和 `titleTextStyle` ,可以看到配置后如下图所示,UI 上 `AppBar` 已经恢复到 M2 的效果,那么此时你可以会疑惑,为什么修改的配置是 `size: 24.0` 和 `Typography.dense2021.titleLarge` ?
|
||||
|
||||
```dart
|
||||
AppBarTheme(
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.white,
|
||||
size: 24.0,
|
||||
),
|
||||
backgroundColor: Colors.blue,
|
||||
titleTextStyle: Typography.dense2014.titleLarge,
|
||||
)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image7.png)
|
||||
|
||||
其实这就是本篇的核心:**在 M2 控件还没被剔除的时候,通过参考源码将 M3 UI 恢复到 M2** 。
|
||||
|
||||
例如在 3.16 的源码里,`theme.useMaterial3 ?` 这样的代码目前随处可见,而此时 `AppBar` 里:
|
||||
|
||||
- `_AppBarDefaultsM3` 下 icon 的颜色是通过 `onSurface` 字段,大小是 24
|
||||
- `_AppBarDefaultsM2` 下 icon 是直接使用 theme 下默认的样式,也就是 size 24, 颜色白色。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image8.png)
|
||||
|
||||
| M2 | M3 |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231123_M3/image9.png)![](http://img.cdn.guoshuyu.cn/20231123_M3/image10.png) | ![](http://img.cdn.guoshuyu.cn/20231123_M3/image11.png) |
|
||||
|
||||
所以我们可以在上面的 `IconThemeData` 里可以直接配置 `color: Colors.white, size: 24.0,` 来恢复到 M2 的效果。
|
||||
|
||||
> 当然你也可以配置 `ColorScheme` 的 `onSurface` 来改变颜色,但是这个影响返回太大,还是推荐配置 `AppBarTheme` 的 `IconThemeData` 。
|
||||
|
||||
另外可以看到,此时还有一个 `Typography.dense2014.titleLarge` ,这又是哪里来的?还是回到`_AppBarDefaultsM3` 里,在 M3 下, AppBar 使用的是 `ThemeData `下的 `textTheme.titleLarge` ,而默认字体样式配置,基本来自 `Typography` 对象。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image12.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image13.png)
|
||||
|
||||
`Typography` 里默认配置了大量字体配置,例如 `Typography.dense2014` 对应就是如下所示配置,从上面代码可以看到**默认情况下 M2 用的是 `Typography.material2014 `,对应就是 `Typography.dense2014`**,也就是在 AppBar 上 `Typography.dense2014.titleLarge` 就可以让 M3 的 AppBar 文本恢复到 M2 的样式。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image14.png)
|
||||
|
||||
看到这里你是否已经学会了大概的思路?
|
||||
|
||||
**通过 `theme.useMaterial3 ` 去检索控件,然后在源码里找到 M2 的实现,然后将其修改到全局的主题设置里**,比如 AppBar 的就通过 `AppBarTheme` 配置,如果是 M2 的实现又引用了某些默认配置,就去检索这些默认配置的起源,所以说 M3 这个坑是一个体力活。
|
||||
|
||||
当然,这个思路下,有一些控件适配起来还是会有坑,因为它的变化确实有点大,例如 Card 控件。
|
||||
|
||||
# Card
|
||||
|
||||
如图所示,这是 `Card` 控件在 M2 和 M3 下的变化,除了默认弧度之后,最主要就是颜色发生了改变,从默认白色变成了带着浅蓝色的效果,但是这里有个坑,就是,**此时就算你给 Card 设置 `color: Colors.white,` ,它也依旧会带着这个浅蓝色的效果**。
|
||||
|
||||
| M2 | M3 |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231123_M3/image15.png) | ![](http://img.cdn.guoshuyu.cn/20231123_M3/image16.png) |
|
||||
|
||||
那么这个颜色如何去除?其实只要 `ColorScheme` 下设置 `surfaceTint` 为透明色就可以了,如下图所示,因为 `Card` 的效果是通过封装 `Material` 控件实现,而 `Material` 在 M3 下会通过 `elevation` 和 `surfaceTint` 去合成一个覆盖色。
|
||||
|
||||
```dart
|
||||
ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
|
||||
///影响 card 的表色,因为 M3 下是 applySurfaceTint ,在 Material 里
|
||||
surfaceTint: Colors.transparent,
|
||||
),
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image17.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image18.png)
|
||||
|
||||
所以根据判断,**将 `surfaceTint` 设置成透明就可以去除 `Card `这个覆盖色,这个逻辑在 `BottomAppBar` 里同样存在**,所以如果你需要把它们都恢复都 M2 效果,那么就只需要把 `surfaceTint` 设置成透明色即可。
|
||||
|
||||
![image-20231123172627998](http://img.cdn.guoshuyu.cn/20231123_M3/image19.png)
|
||||
|
||||
所以类似的变动才是 M3 里最坑的点,如果你不了解他们的底层实现,那么在升级之后,发现明明代码给了白色,为什么它还是会有浅蓝色效果?这对于开发者来就是一个找🐛的天坑,所以在这里也用 `Card` 提供一个解决问题的典型思路。
|
||||
|
||||
另外还有一个典型的控件,那就是 `FloatingActionButton`(FAB) 。
|
||||
|
||||
# FloatingActionButton
|
||||
|
||||
从 M2 到 M3, `FloatingActionButton`(FAB) 控件最大的变化就是变成了方形,其次颜色也不跟随之前和主题蓝色,我们不说 M3 这个「优化」如何,就说如何恢复到 M2 的效果。
|
||||
|
||||
| M2 | M3 |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20231123_M3/image20.png) | ![](http://img.cdn.guoshuyu.cn/20231123_M3/image21.png) |
|
||||
|
||||
首先按照惯例,肯定有一个叫 `floatingActionButtonTheme` 的参数,可以用于配置 `FloatingActionButtonThemeData` ,所以这里我们首先添加上配置,然后通过 `shape` 先变回原形,并且修改 `backgroundColor` 变成蓝色。
|
||||
|
||||
```dart
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.blue,
|
||||
shape: CircleBorder()
|
||||
),
|
||||
```
|
||||
|
||||
那么此时剩下的就是 `Icon` 的颜色,我们当然可以在用到 `Icon` 的地方手动修改为白色,但是我们希望的是全局配置默认恢复到 M2 时代,所以我们就要去找 FAB 下 `Icon` 是如何获取到颜色的。
|
||||
|
||||
> 而寻找这个颜色的实现,居然就让我开启了一段漫长的旅程·····
|
||||
|
||||
首先 `Icon` 肯定是通过` IconThemeData` 去获取默认颜色,因为 FAB 的主题下没有 `iconTheme` 可以配置,那么首先就想到配置一个全局的 `iconTheme: IconThemeData` ,但是神奇的问题来了,配置之后居然无效。
|
||||
|
||||
那么就开始往上查找,然后依次返现, FAB 内部是通过 `RawMaterialButton` 实现的点击,而 `RawMaterialButton` 内部就有一个 `IconTheme.merge` 的实现,**那么 FAB 里的 `Icon` 默认应该是使用了 `effectiveTextColor` 这个颜色**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image22.png)
|
||||
|
||||
之后开始经历一番漫长检索关联,最终可以看到:
|
||||
|
||||
- 这个 `effectiveTextColor` 来自从 FAB 传入的 TextSytle 的 color
|
||||
- 而 `textSytle` 来自 `extendedTextStyle`
|
||||
- 而 `extendedTextStyle` 来自 `foregroundColor`
|
||||
- `foregroundColor ` 默认来自 `floatingActionButtonTheme` 的 `foregroundColor`
|
||||
|
||||
![image-20231123174431747](http://img.cdn.guoshuyu.cn/20231123_M3/image23.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image24.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image25.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image26.png)
|
||||
|
||||
所以破案了,**需要全局设置 FAB 下 ` Icon` 的颜色,是要配置 `FloatingActionButtonThemeData` 的 `foregroundColor`** ,这个设定和名称正常情况下谁能想得到呢?
|
||||
|
||||
而且这个传递嵌套如此“隐晦”,只能说, FAB 是 Flutter 样式跟踪里很典型的一个代表:**传递深,theme 引用复杂,类似 `merge`/`copy` 的局部实现太过隐蔽**。
|
||||
|
||||
```dart
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.blue,
|
||||
shape: CircleBorder()),
|
||||
```
|
||||
|
||||
另外关于 **`IconThemeData` 还有一个冷知识,参数不全的情况下,也就是不满足 `isConcrete` 的情况下,其他的参数在 `of(context) `的时候是会被 `fallback` 覆盖**,这个对于 M3 - M2 的降级适配里也是一个关键信息。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image27.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image28.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image29.png)
|
||||
|
||||
# primarySwatch
|
||||
|
||||
最后在聊一个 `ThemeData` 的 `primarySwatch`,为什么聊它,因为如果你的代码里用了 `primaryColorDark` 和 `primaryColorLight` 作为配置,那么使用 ` ColorScheme.fromSeed` 之后,它们会发生一些「奇妙的变化」,所以为了它们可以恢复到 M2 模式,那么设置 `primarySwatch` 可以将它们恢复到原有的效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image30.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image31.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20231123_M3/image32.png)
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
如下所示是本次升级适配里的示例代码总和,其实 M3 模式下「降级」到 M2 UI 效果真的是一个体力活,类似上面三个典型的例子,都可以看出来跟踪默认 UI 的实现并不轻松,虽然对于 Flutter 团队来说,升级到 M3 可能是一次正向优化,但是对于不喜欢 Material Design 的国区而言,M3 只能是一个负优化,不知道大家同意不?
|
||||
|
||||
```dart
|
||||
return ThemeData(
|
||||
///用来适配 Theme.of(context).primaryColorLight 和 primaryColorDark 的颜色变化,不设置可能会是默认蓝色
|
||||
primarySwatch: color as MaterialColor,
|
||||
|
||||
/// Card 在 M3 下,会有 apply Overlay
|
||||
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: color,
|
||||
primary: color,
|
||||
|
||||
brightness: Brightness.light,
|
||||
|
||||
///影响 card 的表色,因为 M3 下是 applySurfaceTint ,在 Material 里
|
||||
surfaceTint: Colors.transparent,
|
||||
),
|
||||
|
||||
/// 受到 iconThemeData.isConcrete 的印象,需要全参数才不会进入 fallback
|
||||
iconTheme: IconThemeData(
|
||||
size: 24.0,
|
||||
fill: 0.0,
|
||||
weight: 400.0,
|
||||
grade: 0.0,
|
||||
opticalSize: 48.0,
|
||||
color: Colors.white,
|
||||
opacity: 0.8,
|
||||
),
|
||||
|
||||
///修改 FloatingActionButton的默认主题行为
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: color,
|
||||
shape: CircleBorder()),
|
||||
appBarTheme: AppBarTheme(
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.white,
|
||||
size: 24.0,
|
||||
),
|
||||
backgroundColor: color,
|
||||
titleTextStyle: Typography.dense2014.titleLarge,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
```
|
|
@ -1,196 +0,0 @@
|
|||
|
||||
本篇主要帮助剖析理解 Flutter 里的列表和滑动的组成,用比较通俗易懂的方式,从常见的 `ListView` 到 `NestedScrollView` 的内部实现,帮助你更好理解和运用 Flutter 里的滑动列表。
|
||||
|
||||
> **本篇不是教你如何使用 API ,而是一些日常开发中不常接触,但是很重要的内容**。
|
||||
|
||||
|
||||
|
||||
## Flutter 滑动列表
|
||||
|
||||
|
||||
在 Flutter 里我们常见的滑动列表场景,简单地说其实是由三部分组成:
|
||||
|
||||
- *`Viewport`* : 它是一个 *MultiChildRenderObjectWidget* 的控件 ,**它提供的是一个“视窗”的作用,也就是列表所在的可视区域大小;**
|
||||
- *`Scrollable`* :**它主要通过对手势的处理来实现滑动效果** ,比如*VerticalDragGestureRecognizer* 和 *HorizontalDragGestureRecognizer;*
|
||||
- *`Sliver`* : 准确来说应该是 *RenderSliver*, **它主要是用于在 Viewport 里面布局和渲染内容;**
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image1)
|
||||
|
||||
以 `ListView` 为例,如上图所示是 `ListView` 滑动过程的变化,其中:
|
||||
|
||||
- 绿色的 `Viewport` 就是我们看到的列表窗口大小;
|
||||
- 紫色部分就是处理手势的 `Scrollable`,让黄色部分 `SliverList` 在 `Viewport` 里产生滑动;
|
||||
- 黄色的部分就是 `SliverList` , 当我们滑动时其实就是它在 `Viewport` 里的位置发生了变化;
|
||||
|
||||
|
||||
了解完这个基础理念后,就可以知道一般情况下 `Viewport` 和 `Scrollable` 的实现都是很通用的,所以一般在 **Flutter 里要实现不同的滑动列表,就是通过自定义和组合不同的 `Sliver` 来完成布局**。
|
||||
|
||||
> **准确说是完成 `RenderSliver` 的 `performLayout` 过程,通过 `SliverConstraints` 来得到对应的 `SliverGeometry`**。
|
||||
|
||||
|
||||
所以在 Flutter 里:
|
||||
|
||||
- `ListView` 使用的是 `SliverFixedExtentList` 或者 `SliverList`;
|
||||
- `GridView` 使用的是 `SliverGrid`;
|
||||
- `PageView` 使用的是 `SliverFillViewport`;
|
||||
|
||||
> 当然这里有一个特殊的是 `SingleChildScrollView` , 因为它是单个 `child` 的可滑动控件,它并没有使用 `RenderSliver`,而是直接自定义了一个 `RenderObject`(RenderBox) ,并且**在 `performLayout` 时直接调整 `child` 的 `offset` 来达到滑动效果**。
|
||||
|
||||
|
||||
|
||||
## RenderSliver
|
||||
|
||||
我们都知道 Flutter 中的整体渲染流程是 *Widget* -> *Element* -> *RenderObejct* -> *Layer* 这样的过程,而 **Flutter 里的布局和绘制逻辑都在 `RenderObejct`**。
|
||||
|
||||
而事实上 `RenderObejct` 也可以分为两大基础子类:
|
||||
|
||||
- `RenderBox` : 我们**常用的布局控件都是基于 RenderBox** 来实现布局;
|
||||
- `RenderSliver` :**主要用在 Viewport 里实现布局**, *Viewport* 里的直属 *children* 也需要是 *RenderSliver*;
|
||||
|
||||
|
||||
那到这里你可能会有一个疑问:既然前面 `SingleChildScrollView` 里没有使用 `RenderSliver` ,直接使用 `RenderBox` 也可以实现滑动,**为什么还要用 Viewport + RenderSliver 的方式来实现列表滑动?**
|
||||
|
||||
### RenderBox
|
||||
|
||||
在 `SingleChildScrollView` 内部使用的是 `RenderBox` ,那么在布局过程中自然而然会把整个 `child` 都进行布局和计算,绘制时主要也是通过 `offset` 和 `clip` 等来完成移动效果,这样的实现当 **`child` 比较复杂或者过长时,性能就会变差**。
|
||||
|
||||
### RenderSliver
|
||||
|
||||
`RenderSliver` 的实现相对 `RenderBox` 就复杂更多,前面介绍过 **`RenderSliver` 就是通过 `SliverConstraints` 来得到一个 `SliverGeometry`**,其中:
|
||||
|
||||
- `SliverConstraints` 中有 *remainingPaintExtent* 可以用来表示剩余的可绘制具体的大小;
|
||||
|
||||
- `SliverGeometry` 里也有 `scrollExtent` (可滑动的距离)、`paintExtent`(可绘制大小)、`layoutExtent` (布局大小范围)、`visible`(是否需要绘制)等参数;
|
||||
|
||||
|
||||
所以通过这部分参数,**在 `Viewport` 里可以实现动态管理,节省资源,根据 `SliverGeometry` 判断需要绘制多大区域的内容,还剩多少内容可以绘制,需要加载的布局是哪些等等。**
|
||||
|
||||
**简单地说就是可以实现“懒加载”,按需绘制,从而得到更流畅的滑动体验。**
|
||||
|
||||
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image2)
|
||||
|
||||
|
||||
以 `ListView` 为例,如上图所示是一个高为 701 的 `ListView` ,实际布局渲染之后,对于 `SliverList` 输出的 `SliverGeometry` 而言:
|
||||
|
||||
- 设定里每个 item 的高度为 114;
|
||||
- `scrollExtent` 是 2353,也就是整体可滑动距离等于 2353;
|
||||
- `paintExtent` 是 701 , 因为 `ListView` 的 `Viewport` 是 701 ,所以从 `SliverConstraints` 得到的 `remainingPaintExtent` 是 701,**所以默认只需要绘制和布局高度为 701 的部分;** (因为默认 paintExtent = layoutExtent )
|
||||
- 对 item 多出的蓝色 8-9 部分,这是因为在 `SliverConstraints` 内会有一个叫 `remainingCacheExtent` 的参数,它表示了需要提前缓存的布局区域, 也就是“预布局”的区域,这个区域默认大小是 **defaultCacheExtent= 250.0;**
|
||||
|
||||
> `ListView` 高度为 701,`defaultCacheExtent` 为默认的 250,也就是得到**第一次需要布局到底部的距离其实为 951**,按照每个 item 高度是 114 ,那么其实是有 8.3 个 item 高度,取整数也就是 9 个 item ,最终得到整体需要处理的区域大小为 114 * 9 = 1026 ,在 **`SliverList` 内部就是 `endScrollOffset` 参数**。
|
||||
|
||||
所以根据以上情况,**`ListView` 会输出一个 `paintExtent` 为 701 ,`cacheExtent` 为 1026 的 `SliverGeometry`**。
|
||||
|
||||
|
||||
从这个例子可以看出,**`RenderSliver` 在实现可滑动列表的开销和逻辑上,会比直接使用 `RenderBox` 好和灵活很多**,同时也是为什么 `Viewport` 里需要使用 `RenderSliver` 而不是 `RenderBox` 的原因。
|
||||
|
||||
|
||||
> ⚠️注意,这里比较容易有一个误区,那就是 `ListView` 是由 `Viewport` + `Scrollable` 和一个`RenderSliver` 组成,所以在 **`ListView` 里只会有一个 `RenderSliver` 而不是多个**,想使用多个 `RenderSliver` 需要使用 `CustomScrollView` 。
|
||||
|
||||
|
||||
最后顺便聊下 `CustomScrollView` ,事实上就是一个**开放了可自定义配置 `RenderSliver` 数组的滑动控件**,例如:
|
||||
|
||||
- 通过利用 `SliverList` + `SliverGrid` 就可以搭配出多样化的滑动列表;
|
||||
- 通过 `CupertinoSliverRefreshControl` + `SliverList` 实现类似 iOS 原生的下拉刷新列表;
|
||||
|
||||
其他可用的内置 `Sliver` 还有:`SliverPadding` 、`SliverFillRemaining` 、`SliverFillViewport` 、`SliverPersistentHeader` 、`SliverAppbar` 等等。
|
||||
|
||||
|
||||
## NestedScrollView
|
||||
|
||||
|
||||
为什么会把 `NestedScrollView` 单独拿出来说呢?这是因为 `NestedScrollView` 和前面介绍的滑动列表实现不大一样。
|
||||
|
||||
### 内部组成
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image3)
|
||||
|
||||
|
||||
如上图所示,`NestedScrollView` 内部主要是通过继承 `CustomScrollView` ,然后自定义一个 `NestedScrollViewViewport` 来实现联动的效果。
|
||||
|
||||
那这有什么特别的呢?如下代码所示,这是使用 `NestedScrollView` 常用的模式,那有看出什么特别的地方了吗?
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image4)
|
||||
|
||||
代码里 `NestedScrollView` 的 `body` 嵌套的是 `ListView` , 前面我们介绍了 `ListView` 本身就是 `Viewport` + `Scrollable` + `SliverList` 组合,而 `NestedScrollView` 本身也有 `NestedScrollViewViewport`。
|
||||
|
||||
**所以 `NestedScrollView` 的实现本质上其实就是 `Viewport` 嵌套 `Viewport`,会有两个 `Scrollable` 的存在** ,并且嵌套的 `ListView` 是被放在了 `NestedScrollView` 的 `Sliver` 里面,大致如下图所示。
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image5)
|
||||
|
||||
|
||||
|
||||
这里面有几个关键的对象,其中:
|
||||
|
||||
- `SliverFillRemaining` :用于充满 `Viewport` 的剩余空间,在 `NestedScrollView` 里面就是充满 `header` 之外的剩余空间;
|
||||
|
||||
- `NestedScrollViewViewport` : 在原 `Viewport` 的基础上增加了一个 `SliverOverlapAbsorberHandle` 参数,`SliverOverlapAbsorberHandle` 本身是一个 `ChangeNotifier` , 主要是用来当 `markNeedsLayout` 时对外发出通知,比如对 header 部分;
|
||||
|
||||
所以 `NestedScrollView` 本质上两个 `Viewport` 之间的嵌套,那他们之间是滑动关系是如何处理的?**这就要说到 `NestedScrollView` 里的 `_NestedScrollCoordinator` 对象。**
|
||||
|
||||
### _NestedScrollCoordinator
|
||||
|
||||
|
||||
`_NestedScrollCoordinator` 的实现比较复杂,简单地说 `_NestedScrollCoordinator` 内部创建了两个 `_NestedScrollController`:
|
||||
|
||||
- `_outerController` :属于 `_NestedScrollViewCustomScrollView` 的 *controller* ,也就是它自己 *controller*;
|
||||
- `_innerController` :属于 `body` 的 *controller*;
|
||||
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image6)
|
||||
|
||||
> 在 `ListView` 的父类 `ScrollView` 内部,默认情况下使用的就是 `PrimaryScrollController.of(context)` 这个 *controller* ,因为 `PrimaryScrollController` 是一个 `InheritedWidget` 。
|
||||
|
||||
而整个联动滑动的流程,主要就是 `_NestedScrollCoordinator` 里和它创建的两个 `_NestedScrollController` 有关系:
|
||||
|
||||
- `_NestedScrollController` 的主要作用就是使用 `_NestedScrollPosition` 来替换 `ScrollPosition` ;
|
||||
|
||||
- `_NestedScrollCoordinator` 将 _outer 和 _inner 两个 `_NestedScrollController` 组合起来(_outer 和 _inner 分别被应用到 `NestedScrollView` 和 `body`);
|
||||
|
||||
- `_NestedScrollPosition` 内部将 `Drag` 等手势操作传递回 `_NestedScrollCoordinator` 里。
|
||||
|
||||
- 最后在 `_NestedScrollCoordinator` 的 `drag` 和 `applyUserOffset` 等方法里进行内外滚动的分配;
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image7)
|
||||
|
||||
|
||||
### SliverPersistentHeader
|
||||
|
||||
了解完 `NestedScrollView` 的布局和联动实现之外,最后简单介绍一下 `SliverPersistentHeader` , 因为经常在 `NestedScrollView` 里使用的 `SliverAppBar`,本质上 **`SliverAppBar` 的实现靠的就是 `SliverPersistentHeader`**。
|
||||
|
||||
|
||||
`SliverPersistentHeader` 主要是具备 `floating` 和 `pinned` 两个属性,它们的区别主要在于使用了不同的 `RenderSliver` 实现,而**最终不同的地方其实就是输出 `SliverGeometry` 的不同**。
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image8)
|
||||
|
||||
以第一个 `_SliverFloatingPinnedPersistentHeader` 和最后一个 `_SliverScrollingPersistentHeader` 之间的对比为例子,如下代码所示,在需要 `floating` 和 `pinned` 的 `Sliver` 上,可以看到 `paintExtent` 和 `layoutExtent` 都有一个最小值。
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image9)
|
||||
|
||||
**所以 `Sliver` 被固定住的原理,其实就是 `Viewport` 得到了它的 `paintExtent` 和 `layoutExtent` 并不为 0,所以会继续为这个 `Sliver` 绘制对应区域的内容。**
|
||||
|
||||
|
||||
最后需要注意的是,**当你使用 `SliverPersistentHeader` 去固定住头部的时候,作为 `body` 的列表是不知道顶部有个固定区域。** 所以如果这时候不额外做一些处理,那么对于 `body` 而言,它的 `paintOrigin` 还是从最顶部开始而不是固定区域的下方。
|
||||
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image10)
|
||||
|
||||
> 如上动图所示,可以看到 item0 并没有在橙色区域停止滑动,而是继续往上滑动,这就是因为作为 `body` 的列表不知道顶部有固定区域。
|
||||
|
||||
这时候就可以通过使用 `SliverOverlapAbsorber` + `SliverOverlapInjector` 的组合来解决这个问题:
|
||||
|
||||
- 在 `SliverPersistentHeader` 的外层嵌套一个 `SliverOverlapAbsorber` 用于吸收 `SliverPersistentHeader` 的高度;
|
||||
|
||||
- 使用 `SliverOverlapInjector` 将这个高度配置到 `body` 列表中,让列表知道顶部存在一个固定高度的区域;
|
||||
|
||||
|
||||
![image](http://img.cdn.guoshuyu.cn/20210429_Flutter-N-Scroll/image11)
|
||||
|
||||
|
||||
这部分例子可见:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart
|
||||
|
||||
|
||||
好了,本篇关于 Flutter 滑动列表的实现原理就介绍完了,如果你还有什么想说的,欢迎留言讨论。
|
||||
|
179
Flutter-N1.md
179
Flutter-N1.md
|
@ -1,179 +0,0 @@
|
|||
# Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty
|
||||
|
||||
**今天分享一个简单轻松的内容: `ButtonStyle` 和 `MaterialStateProperty`** 。
|
||||
|
||||
大家是否还记得去年 Flutter 2.0 发布的时候,除了空安全之外 ,还有更新一系列关于控件的 breaking change,其中就有 `FlatButton` 被标志为弃用,需要替换成 `TextButton` 的情况。
|
||||
|
||||
如今已经 Flutter 3.0 ,不大知道大家对 `TextButton` 是否已经足够了解,或者说对 `MaterialStateProperty` 是否已经足够了解?
|
||||
|
||||
为什么 `TextButton` 会和 `MaterialStateProperty` 扯到一起?
|
||||
|
||||
首先,说到 `MaterialStateProperty ` 就不得不提 Material Design ,**`MaterialStateProperty` 的设计理念,就是基于 Material Design 去针对全平台的交互进行兼容**。
|
||||
|
||||
![image-20220530103804444](http://img.cdn.guoshuyu.cn/20220531_N/image1.png)
|
||||
|
||||
相信大家当初在从 Flutter 1 切换到 Flutter 2 的时候,应该都有过这样一个疑问:
|
||||
|
||||
> **为什么 `FlatButton` 和 `RaisedButton` 会被弃用替换成 `TextButton ` 和 `RaisedButton`** ?
|
||||
|
||||
![image-20220530104346216](http://img.cdn.guoshuyu.cn/20220531_N/image2.png)
|
||||
|
||||
因为以前只需要使用 `textColor` 、`backgroundColor` 等参数就可以快速设置颜色,但是现在使用 `ButtonStyle` ,从代码量上看相对会麻烦不少。
|
||||
|
||||
当然,**在后续里官方也提供了类似 `styleFrom` 等静态方法来简化代码,但是本质上切换到 `ButtonStyle` 的意义是什么 ?`MaterialStateProperty` 又是什么**?
|
||||
|
||||
![image-20220530104739603](http://img.cdn.guoshuyu.cn/20220531_N/image3.png)
|
||||
|
||||
首先我们看看 `MaterialStateProperty` ,在 `MaterialStateProperty` 体系里有一个 `MaterialState` 枚举,它主要包含了:
|
||||
|
||||
- disabled:当控件或元素不能交互性时
|
||||
- hovered:鼠标交互悬停时
|
||||
- focused: 在键盘交互中突出显示
|
||||
- selected:例如 check box 的选定状态
|
||||
- pressed:通过鼠标、键盘或者触摸等方法发起的轻击或点击
|
||||
- dragged:用户长按并移动控件时
|
||||
- error:错误状态下,比如 `TextField` 的 Error
|
||||
|
||||
![image-20220530114532550](http://img.cdn.guoshuyu.cn/20220531_N/image4.png)
|
||||
|
||||
所以现在理解了吧? 随着 Web 和 Desktop 平台的发布,原本的 `FlatButton` 无法很好满足新的 UI 交互需要,例如键鼠交互下的 hovered ,**所以 `TextButton ` 开始使用 `MaterialStateProperty` 来组成 `ButtonStyle` 支持不同平台下 UI 的状态展示**。
|
||||
|
||||
在此之前,如果需要多平台适配你可能会这么写,你需要处理很多不同的状态条件,从而产生无数` if` 或者 `case` :
|
||||
|
||||
```dart
|
||||
getStateColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
///在 hovered 时还 focused 了
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return Colors.red;
|
||||
} else {
|
||||
return Colors.blue;
|
||||
}
|
||||
} else if (states.contains(MaterialState.focused)) {
|
||||
return Colors.yellow;
|
||||
}
|
||||
return Colors.green;
|
||||
}
|
||||
```
|
||||
|
||||
但是现在, 你只需要继承 `MaterialStateProperty` 然后 @override `resolve` 方法就可以了,例如 `TextButton ` 里的 hovered 效果,在 `TextButton ` 内默认就是通过 `_TextButtonDefaultOverlay` 实现,对 `primary.withOpacity` 来实现 hovered 效果。
|
||||
|
||||
```dart
|
||||
@immutable
|
||||
class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> {
|
||||
_TextButtonDefaultOverlay(this.primary);
|
||||
|
||||
final Color primary;
|
||||
|
||||
@override
|
||||
Color? resolve(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.hovered))
|
||||
return primary.withOpacity(0.04);
|
||||
if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed))
|
||||
return primary.withOpacity(0.12);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '{hovered: ${primary.withOpacity(0.04)}, focused,pressed: ${primary.withOpacity(0.12)}, otherwise: null}';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220531_N/image5.gif)
|
||||
|
||||
|
||||
|
||||
其实在 `TextButton ` 的内部,默认同样是通过 `styleFrom` 来配置所需的 `MaterialState` 效果,其中有:
|
||||
|
||||
- `_TextButtonDefaultForeground` : 用于处理 disabled ,通过 `onSurface?.withOpacity(0.38)` 变化颜色;
|
||||
- `_TextButtonDefaultOverlay`: 用于处理 hovered 、 focused 和 pressed ,通过 `primary.withOpacity` 变化颜色;
|
||||
- `_TextButtonDefaultMouseCursor` : 用于处理鼠标 MouseCursor 的 disabled;
|
||||
|
||||
剩下的参则是通过我们熟悉的 ` ButtonStyleButton.allOrNull` 进行添加,也就是不需要特殊处理的参数。
|
||||
|
||||
那 ` ButtonStyleButton.allOrNull` 的作用是什么?
|
||||
|
||||
其实 ` ButtonStyleButton.allOrNull` 就是 `MaterialStateProperty.all` 方法的可 null 版本,对应内部实现最终还是实现了 `resolve` 接口的 `MaterialStateProperty` ,所以如果需要支持 null,你也可以做直接使用 `MaterialStateProperty.all` 。
|
||||
|
||||
```dart
|
||||
static MaterialStateProperty<T>? allOrNull<T>(T? value) => value == null ? null : MaterialStateProperty.all<T>(value);
|
||||
```
|
||||
|
||||
![image-20220530142530429](http://img.cdn.guoshuyu.cn/20220531_N/image6.png)
|
||||
|
||||
当然,如果不想创建新的 class 但是又想定制逻辑,如下代码所示,那你也可以使用 `resolveWith` 静态方法:
|
||||
|
||||
````dart
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return Colors.green;
|
||||
}
|
||||
return Colors.transparent;
|
||||
})),
|
||||
onPressed: () {},
|
||||
child: new Text(
|
||||
"TEST",
|
||||
style: TextStyle(fontSize: 100),
|
||||
),
|
||||
),
|
||||
````
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220531_N/image7.gif)
|
||||
|
||||
|
||||
|
||||
当然,谷歌在对 Flutter 控件进行 `MaterialState` 的 UI 响应时,也是遵循了 Material Design 的设计规范,比如 Hover 时 `primary.withOpacity(0.04);` ,所以不管在 `TextButton` 还是 `RaisedButton` 内部都遵循类似的规范。
|
||||
|
||||
|
||||
|
||||
![image-20220530113735250](http://img.cdn.guoshuyu.cn/20220531_N/image8.png)
|
||||
|
||||
|
||||
|
||||
另外,有时候你肯定不希望每个地方单独去配置 Style ,那这时候你就需要配合 Theme 来实现。
|
||||
|
||||
事实上 `TextButton` 、 `ElevatedButton ` 和 `OutlinedButton` 都是 `ButtonStyleButton` 的子类,他们都会遵循以下的原则:
|
||||
|
||||
```dart
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
assert(defaultStyle != null);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
```
|
||||
|
||||
也就是 ` return widgetValue ?? themeValue ?? defaultValue;` ,其中:
|
||||
|
||||
- widgetValue 就是控件单独配置的样式
|
||||
- themeValue 就是 Theme 里配置的全局样式
|
||||
- defaultValue 就是默认内置的样式,也即是 `styleFrom` 静态方法,当然 `styleFrom` 里也会用一些 `ThemeData` 的对象,例如 `colorScheme.primary` 、 `textTheme.button` 、`theme.shadowColor` 等
|
||||
|
||||
所以,例如当你需要全局去除按键的水波纹时,如下代码所示,你可以修改 `ThemeData` 的 `TextButtonTheme` 来实现,因为 `TextButton` 内的 `themeStyleOf` 使用的就是 `TextButtonTheme` 。
|
||||
|
||||
```dart
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
// 去掉 TextButton 的水波纹效果
|
||||
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
![image-20220530151634041](http://img.cdn.guoshuyu.cn/20220531_N/image9.png)
|
||||
|
||||
最后做个总结:
|
||||
|
||||
- 如果只是简单配置背景颜色,可以直接用 `styleFrom`
|
||||
- 如果单独配置,可以使用 ` ButtonStyleButton.allOrNull`
|
||||
- 如果需要灵活处理,可以使用 ` ButtonStyleButton.resolveWith` 或者实现 `MaterialStateProperty` 的 `resolve` 接口
|
165
Flutter-N10.md
165
Flutter-N10.md
|
@ -1,165 +0,0 @@
|
|||
# 给掘金 Logo 快速添加动画效果,并支持全平台开发框架
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image1.gif)
|
||||
|
||||
我正在参加「创意开发 投稿大赛」详情请看:[掘金创意开发大赛来了!](https://juejin.cn/post/7120441631530549284)
|
||||
|
||||
**如果需要在 Android、 iOS、Web、Desktop 等平台快速实现如上图所示的动画效果,你第一考虑会怎么做**?
|
||||
|
||||
也许你会说使用 Flutter ?不不不,如果还需要兼容多技术栈呢?例如支持 Flutter 、React、Vue、C++ 等不同语言和技术平台呢?
|
||||
|
||||
这时候也许你会想到 [Lottie](https://lottiefiles.com/) ,诚然 Lottie 的动画效果确实十分优秀,也支持 Android、 iOS、React Native、Web、 Windows 等平台,但是它的输入来源于 After Effects 动画特效,并且依赖于 `Bodymovin` 插件,这对于个人开发或者 UI 设计师来说,从 0 开始学习的门槛还是不低的。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image2.gif)
|
||||
|
||||
|
||||
|
||||
**而本篇将给你推荐另外一个更简单又强大的动画开发平台: [rive](https://rive.app)** ,对于 rive 可能大家会感觉比较陌生,做过 Flutter 开发的可能对 rive 会有所耳闻,因为 rive 在此之前叫 flare ,是 2dimensions 公司的开源动画产品,在发布之初由于和 Flutter 团队有深入合作,所以在初期一直是 Flutter 官方推荐的动画框架。
|
||||
|
||||
后来由于 flare 项目被合并所以升级为 rive ,**升级后的 rive 开始把动画效果拓展到全平台,这个全平台不只是物理设备的全平台,还包括了跨语言和框架的全平台,不过可惜和第一代 flare 存在断档不兼容**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image3.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image4.png) |
|
||||
| ------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
本篇之所以推荐 rive 来实现多端动画,主要有以下几个原因:
|
||||
|
||||
- 支持手机端、Web 端 和PC 端等平台支持
|
||||
- 支持 React 、Flutter、Unity 等多种框架,Vue 和 Angular 也有社区支持
|
||||
- 支持 JS、Dart、C++ 等多种语言
|
||||
- **不用安装工具,直接 Web [Editor](https://editor.rive.app/) 就可以进行可视化开发,并附带工程管理**
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image5.png) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image6.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
**无需安装,打开即用,多平台多语言支持就是本次推荐 rive 的主要原因**,那么回到主体,接下来我们将通过 rive 来实现一个掘金动画 logo。
|
||||
|
||||
首先打开 rive 的 Web [Editor](https://editor.rive.app/) ,这里需要你有账户登陆,注册登陆是免费的,在登陆之后我们就可以进入到 rive 的动画编辑界面。
|
||||
|
||||
因为我们是要基于掘金的 logo 实现一个动画,所以开始之前可以先拿到掘金 logo 的 svg ,这里**只需要直接从文件夹把 svg 文件拖拽到 Web Editor 里就可以**,它会自己自动上传,上传成功之后就可以看到下图的界面效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image7.png)
|
||||
|
||||
如果你说我没有 svg 文件怎么办?不用担心, rive 提供丰富又简单的绘制工具,如下图 1 所示,通过 Pen Tool 你就可以快速绘制出一些简单的图形,复杂的路径也可以如图 3 一样描绘出来。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image8.png) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image9.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image10.gif) |
|
||||
| ------------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
|
||||
回到上传完 svg 的界面,这时候主要看 3 部分:
|
||||
|
||||
- 红框 1 里的是 Artboards 画板(`brand-with-text.svg`) 和画板内的各种 Shape 图形
|
||||
- 选中 Shape 图形,可以看到红框 2 里对应的图形进入可操作的状态
|
||||
- 红框 3 是用于切换设计和动画界面,在设计(Design)界面下是调整 UI ,在动画(Animate)界面是调整动画效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image11.png)
|
||||
|
||||
如下图所示,当我们选中一个 Shape 的时候,你就可以对图形进行移动、旋转、缩放等操作,从而来调整 UI 的变化,达到我们需要的动画效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image12.gif)
|
||||
|
||||
接下来我们点击切换到 Animate 下,可以看到此时地步多了一个时间轴,**这个时间轴就是我们控制整个动画过程的关键**,这里为了实现前面的动画效果,首先需要把整个掘金 logo 挪动到了画布的外面,为后面的掉落动画做准备。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image13.png)
|
||||
|
||||
接下开始我们的动画,开始之前我们随意调整 svg 里图形的位置或者角度 ,比如:
|
||||
|
||||
- 这里对【稀】字进行了55° 的旋转
|
||||
- 对【掘】字进行了 -180° 的旋转
|
||||
- 对 【金】字行了50° 的旋转
|
||||
- 对 logo 上的小方块位置调整移动
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image14.png)
|
||||
|
||||
做完上面的操作之后,**可以看到时间轴上多了一排点,这些点就是当前动画 Shape 在这个时间戳上的状态** ,如果你觉得用鼠标控制不够精确,你也可以在右边的窗口上对参数进行精确调整。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image15.png) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image16.png) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image17.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
另外如上图 2 所示,在时间轴上可以通过调整 Duration 来设定动画的总时长,还可以调整动画循环播放等等。
|
||||
|
||||
接下来就是体力活了, 比如我们需要掘金 logo 从顶部掉下来,那么我们可以在时间轴上拖动蓝色的进度到合适位置,然后挪动图形,然后就可以看到时间戳上多了新的状态点,接着点击播放就可以看到动画效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image18.gif)
|
||||
|
||||
如果你需要两个 Shape 之间掉落存在时间差,那么如下图所示,你可以直接调整时间轴上对应的点位,就可以轻松实现动画里 Shape 的移动时间差。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image19.gif)
|
||||
|
||||
|
||||
|
||||
这里有一个需要注意的是,当你选中时间戳上的某个节点时,在右侧是可以调整动画的插值状态的,默认情况下是线性 Linear ,但是我们可以根据需要设置想要的 Cubic 计算方式。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image20.png)
|
||||
|
||||
不同 Interpolation 效果如下图所示,其中 Cubic 状态下是可以自定义调整动画的插值计算方式,所以一般情况下都会选择 Cubic 来调整动画的插值计算。
|
||||
|
||||
| Linear | Cubic | Hold |
|
||||
| ------------------------------------------------ | ----------------------------------------------- | ---------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image21.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image22.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image23.gif) |
|
||||
|
||||
通过调整动画的差值效果,就可以让生硬的动画过度变得更加自然,例如下图就通过调整 Cubic Points 之后,可以实现快进慢出的效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image24.gif)
|
||||
|
||||
而在经历一系列【体力劳动】之后,你就可以看到类似下图的效果,通过对各种 Shape 进行移动,旋转,缩放,然后通过 Cubic Points 调整动画的丝滑程度,最后排布好时间戳,就可以完成最初的动画效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image25.gif)
|
||||
|
||||
> 这不比你用代码和意念写来的香?
|
||||
|
||||
当然,这里还有一个需要注意的是,**如果你存在多个画板和动画,那么画板名字和动画名字的命名就很重要**,因为如可能会需要在代码里需要用它来指定动画效果,当然,**这也代表了你可以在一个 rive 文件你设置多组画板和多组动画效果**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image26.png)
|
||||
|
||||
然后你就可以导出 rive 文件到工程里去使用,同时 rive 文件是支持本地加载和远程加载的,官方贴心地提供了分享链接,你可以把动画通过 Embed link 或者 iframe 添加到 Web 里,甚至还贴心地提供了 React 代码复制。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image27.png) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image28.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
例如在 Flutter 里,你可以通过 `RiveAnimation.network` 或者 `RiveAnimation.asset` 来加载动画文件,当然你也可以自定义 `RiveAnimationController` 来做一些自定义控制,比如通过 `animationName` 来指定对应的动画效果。
|
||||
|
||||
```js
|
||||
class SimpleAnimation extends StatelessWidget {
|
||||
const SimpleAnimation({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: RiveAnimation.network(
|
||||
'https://cdn.rive.app/animations/vehicles.riv',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当然,前面介绍的只是简单的动画效果,rive 其实可以实现很强的各种动画交互,比如:
|
||||
|
||||
- 通过 Bone 来设置骨骼交互
|
||||
- 通过 Draw Order 动态设置层级交替
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image29.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image30.gif) |
|
||||
| -------------------------------------------------- | -------------------------------------------- |
|
||||
|
||||
甚至在 rive 里还有 **State Machine 模式,从而支持根据不同条件和逻辑状态触发不同的动画效果,节省可开发者需要在代码里进行逻辑判断的部分,并且这部分逻辑是可以跨平台跨语言支持**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220731_N10/image31.gif) | ![](http://img.cdn.guoshuyu.cn/20220731_N10/image32.gif) |
|
||||
| -------------------------------------------- | ---------------------------------------------------- |
|
||||
|
||||
> 更多 rive 的丰富功能可查阅 https://help.rive.app
|
||||
|
||||
那么到这里,相信大家最关心的问题就是:**rive 能不白嫖 ?答案是可以的!** rive 默认对于 free 用户来说支持 3 个文件免费,这对于个人而言其实够用,因为前面说的,rive 支持一个文件下创建多个画板和多个动画,所以正常情况下个人使用 3 个免费的限制其实问题不大。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image33.png)
|
||||
|
||||
同时 rive 社区也有很多免费开放的动画资源,对于懒癌患者来说也是不错的选择之一,**当然你也可以把文章转发给设计师,安利他们使用 rive,将开发成本“嫁接”给他们,你只负责岁月静安地用几行代码完成动画接入就可以了**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220731_N10/image34.gif)
|
||||
|
||||
|
||||
|
||||
**rive 就是这么一个将 “开发不行” 变成 “设计不行” 的工具,相比较 AE ,它不需要安装工具,而且操作更加简单支持,如果没有设计师也可以自己上手,这也是我最近喜欢上 rive 的原因**。
|
||||
|
||||
如果你对 rive 还有什么想法或者疑问,欢迎留言交流~
|
||||
|
197
Flutter-N11.md
197
Flutter-N11.md
|
@ -1,197 +0,0 @@
|
|||
# Flutter 实现 “真” 3D 动画效果,用纯代码实现立体 Dash 和 3D 掘金 Logo
|
||||
|
||||
|
||||
|
||||
我正在参加「创意开发 投稿大赛」详情请看:[掘金创意开发大赛来了!](https://juejin.cn/post/7120441631530549284)
|
||||
|
||||
**本篇将给你带来更加炫酷动画效果,最后教你如何通过纯代码实现一只立体的 Flutter 的吉祥物 Dash 和 3D 的掘金 logo 动画**。
|
||||
|
||||
> ❤️ **本文正在参加征文投稿活动,还请看官们走过路过来个点赞一键三连,感激不尽~**
|
||||
|
||||
在之前的 [《炫酷的 3D 卡片和帅气的 360° 展示效果》](https://juejin.cn/post/7124064789763981326) 里,我们使用手势代码和角度切换,在 2D 画板里实现了“伪” 3D 的视觉效果,就在我觉得效果还不错时, 有一位掘友提出了一个关键性的问题:**卡片缺少厚度,也就是没有 3D 的质感** 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image1.gif) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image2.png) |
|
||||
| -------------------------------------------- | ------------------------------------------------------------ |
|
||||
|
||||
确实,如下图所示,在之前的实现里,随着卡片角度的倾斜,有两个问题特别明显:
|
||||
|
||||
- 当卡片旋转到侧边时,卡片的缺少“厚度”的质感,**甚至出现了消失的情况**
|
||||
- 卡片上的文字虽然做了类似凹凸的视觉效果,但是从侧面看时也是缺少立体质感
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image3.gif)
|
||||
|
||||
而为了在 2D 平面实现三唯的质感,在查阅相关资料时我发现了前端的 [Zdog](https://zzz.dog) 框架,**Zdog 是一个使用 `Canvas` 实现的伪 3D 引擎, 它支持通过 2D 的 `Canvas` API渲染出类似 3D 的效果**。
|
||||
|
||||
> [Zdog](https://zzz.dog) 作为一个 js 框架,它大概只有 2800 多行代码,并且其最小体积为 28KB ,可以说十分轻量级。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image4.gif)
|
||||
|
||||
|
||||
|
||||
虽然 Zdog 是一个纯 js 框架, 但既然它是通过 `Canvas` 实现的逻辑,那就完全可以 “轻松” 迁移到 Flutter ,毕竟 Flutter 本身就是一个重度依赖于 `Canvas` 的框架,而恰巧在 Flutter 社区就有针对 Zdog 的移植版本: [zflutter](https://pub.flutter-io.cn/packages/zflutter) 。
|
||||
|
||||
> 虽然这个 package 作者已经两年不维护,也没有发布 null-safety 的 pub 支持,但是既然是开源项目,自己动手风衣足食,在经过一番“简单”的迁移适配之后, [zflutter 再次在 Flutter 3.0 下“焕发新春”](https://github.com/carguo/zflutter) 。
|
||||
|
||||
我们先看效果,在结合 zflutter 的实现之后,可以看到卡片的立体效果得到了全面的提升:
|
||||
|
||||
- **首先卡片有了厚度的质感,旋转到侧边也不会“消失”**
|
||||
- **卡片上的字体在倾斜时也有了立体的效果**
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image5.gif)
|
||||
|
||||
那在讲解实现之前,我们要解决一个疑惑: **zflutter 究竟是如何在 2D 画板上实现 3D 的质感** ?而其实这个问题的关键就在于:**通过手势产生的矩阵变换是作用于画板还是作用于路径** 。
|
||||
|
||||
我们首先看一个例子,如下代码所示,我们创建了一个 `CustomPaint` ,然后在代码里绘制了 4 条相同红色直线,接着对其中 3 条直线的 `Canvas` 进行不同程度的矩阵旋转,如下图 2 可以看到有两条红线消失不见了:
|
||||
|
||||
- 当红线绕 Y 轴旋转 `pi / 2`(90°)时,因为此时画板恰好和我们呈垂直状态,所以会出现看不到的情况
|
||||
- 当红线绕 XY 轴旋转 `pi / 4` 时,可以看到画板此时和我们视觉成 45° 的情况
|
||||
- 当红线绕 XY 轴旋转 `pi / 2`(90°) 时,因为此时画板还是和我们呈垂直状态,所以出现看不到的情况
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image6.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image7.png) |
|
||||
| :----------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
|
||||
如果觉得上面的描述太抽象,那么结合下面动图,可以看到当红线在围绕 XY 轴做旋转时,如果画布(`Canavas`)和我们呈 90° 垂直的时候,此时就会出现消失不见的情况,**因为画布是 2D 的平面,这也是为什么之前实现的卡片没有“厚度”的原因** 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image8.gif) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image6.png) |
|
||||
| --------------------------------------------- | ------------------------------------------------------------ |
|
||||
|
||||
**那如果不对 `Canavs` ,而是对绘制路径 `Path` 进行矩阵变换呢** ?不对画布进行旋转,不就不会出现消失的情况了吗?
|
||||
|
||||
如下代码所示,同样是围绕 XY 轴进行旋转,但是此时是直接对 `Path` 进行 `path.transform` 操作,也就是此时画布`Canvas` 不会出现角度变换,出现变化的是绘制的 `Path` 路径,可以看到:
|
||||
|
||||
- 当红线绕 Y 轴旋转 `pi / 2`(90°)时,此时红线成了红点,因为它此时它是“头正对着我们”
|
||||
- 当红线绕 XY 轴旋转 `pi / 4` 时,可以看到此时红线整体成 45° 的情况对着我们
|
||||
- 当红线绕 XY 轴旋转 `pi / 2`(90°) 时,可以看到此时红线是“垂直正对着我们”
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image9.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image10.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
结合下面的动图,可以看到对 `Path` 进行矩阵变换的旋转之后,整体的立体感就不一样了,**也就是一开始是调整我们和画布之间的角度,但是现在我们是改变了“笔”在画布上的绘制方式来产生的视差,这也是 zflutter 里实现 3D 立体感的关键:对 `Path` 做矩阵运算而不只是对 `Canvas`** 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image11.gif)
|
||||
|
||||
题外话,借着这个机会顺带普及个小知识点:**在前面的代码里可以看到会对矩阵进行 `leftTranslate` 和 `translate` 的操作** ,这是因为我们需要在不同位置绘制多条红线,所以它们的位置并非都在起点,而使用 `leftTranslate` 和 `translate` 来对矩阵进行平移,才能达到每次旋转时都是以红线的“中心”去旋转,举个例子:
|
||||
|
||||
- 如图 1 所示是红线没有绕 Z 轴旋转的情况
|
||||
- 如图 2 所示是红线在绕 Z 轴旋转 `pi / 2` 时没有进行矩阵平移的情况,可以看到此时它们的中心点还在起始位置
|
||||
- 如图 3 所示是红线在绕 Z 轴旋转 `pi / 2` 时,进行了 `leftTranslate` 和 `translate` 操作的情况
|
||||
|
||||
| ![](https://img.cdn.guoshuyu.cn/Simulator%20Screen%20Shot%20-%20iPhone%20SE%20(3rd%20generation)%20-%202022-08-04%20at%2010.18.23.png) | ![](https://img.cdn.guoshuyu.cn/Simulator%20Screen%20Shot%20-%20iPhone%20SE%20(3rd%20generation)%20-%202022-08-04%20at%2010.18.38.png) | ![](https://img.cdn.guoshuyu.cn/Simulator%20Screen%20Shot%20-%20iPhone%20SE%20(3rd%20generation)%20-%202022-08-04%20at%2010.18.12.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/transform_canvas_demo_page.dart
|
||||
>
|
||||
> Web 体验地址,PC 端记得开 Chrome 手机模式:https://guoshuyu.cn/home/web/#%E5%B1%95%E7%A4%BA%20canvas%20transform 。
|
||||
|
||||
那么回到 zflutter 里,**在 zflutter 里就是通过组合各类图形和线条,然后利用对 `Path` 进行矩阵变换,从而实现类似 3D 立体的视觉效果** ,例如下面图 2 的立体正方形,就符合我们对增加厚度的需要。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image12.gif) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image13.gif) |
|
||||
| ----------------------------------------------- | ----------------------------------------- |
|
||||
|
||||
这里先简单介绍下 zflutter 里常用对象的作用:
|
||||
|
||||
- `ZIllustration` 类似于画板的作用,可以配置 `zoom` 属性来调整画板的缩放
|
||||
- `ZPositioned` 用于配置位置和大小信息,例如 `scale` 、`translate` 、 `rotate` 等属性(其实它就是在内部将接收到的矩阵参数配置到 `ParentData` ,然后传递给 child)
|
||||
- `ZDragDetector` 用于处理手势相关信息,主要是配置 `ZPositioned` 的 `rotate` 就可以快速实现上面的 360° 拖拽效果
|
||||
- `ZGroup` 用于组合多个图形的层叠
|
||||
- `ZToBoxAdapter` 用于嵌套普通的 Flutter 控件
|
||||
- `ZRect` 、`ZRoundedRect` 、 `ZCircle` 、`ZEllipse` 、`ZPolygon` 、`ZCone` 、`ZCylinder` 、`ZHemisphere` 等是内置的形状,如下图
|
||||
- `ZShape` 类似于 Canvas ,用于配合 `ZMove` 、`ZLine` 、`ZBezier` 、`ZArc` 等绘制自定义形状
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image14.png)
|
||||
|
||||
所以要实现卡片的 “真” 3D 效果,简单来说我们需要做的是:
|
||||
|
||||
- 添加一个 `ZIllustration` 画布
|
||||
- 添加一个 `ZDragDetector ` 配合 `ZPositioned` 用于处理手势旋转
|
||||
- 添加一个 `ZGroup` ,然后在里面通过 `ZToBoxAdapter` 添加银行卡的前后两张 png 图片
|
||||
- 在两张图片之间添加一个 `ZRoundedRect` 做边框,配置颜色为 ` Color(0x8A000000);` 实现厚度效果
|
||||
- 利用 `ZShape` 绘制数字,这样绘制出现的数字就会有立体的感觉
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image15.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image16.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image17.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
如上图所示,可以看到经过 zflutter 的处理之后,**不只是卡片本身有了“厚度”的质感,在倾斜也可以看到文字立体视觉,现在就算是如图 3 一样旋转到 90° 的情况,依然可以看到卡片和文字之间的层次关系** 。
|
||||
|
||||
> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/card_real_3d_demo_page.dart
|
||||
>
|
||||
> Web 体验地址,PC 端记得开 Chrome 手机模式: https://guoshuyu.cn/home/web/#%E7%A1%AC%E6%A0%B8%203D%20%E5%8D%A1%E7%89%87%E6%97%8B%E8%BD%AC 。
|
||||
|
||||
详细源码可以直接看上方链接,那认识了 zflutter 之后,**我们还能利用 zflutter做什么呢** ?其实在官方的 Demo 里就有一个很有典型的示例,那就是 Flutter 的吉祥物 Dash ,**接下来我们看如何利用 zflutter 开始实现一只立体质感的 Dash** 。
|
||||
|
||||
首先我们利用 `ZCircle` 画一个圆,用于实现 Dash 的身体
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image18.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image19.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
然后我们通过 3 个不同位置和角度的 ` ZEllipse` 椭圆来组成 Dash 的头发,事实上 zflutter 里很多效果就是通过类似这样的图形组合来实现的。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image20.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image21.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
接着我们在 `ZShape` 里利用 `ZArc` 实现不同角度的弧形组合实现尾巴,这里的关键是 z 轴上需要有部分落差,如下图展示是尾巴在 3 个不同角度的可视效果。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image22.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image23.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
再通过调整两个 ` ZEllipse` 椭圆的角度来实现 Dash 的手部效果,在这一点上 zflutter 确实很考验开发者对于图形在平面上的空间感。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image24.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image25.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
接着通过 `ZCone` 就可以快速实现 Dash 的嘴巴。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image26.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image27.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
然后这部分相信不用代码大家也知道,就是通过组合多个 `ZEllipse` 和 `ZCircle` 堆叠来实现 Dash 的眼睛。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image28.png)
|
||||
|
||||
最后,把上面的零部件组合到一起,在配置上循环的动画参数,当当当~一只生动立体的 Dash 就完成了。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image29.gif) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image30.gif) |
|
||||
| ----------------------------------------- | ----------------------------------------- |
|
||||
|
||||
> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/dash_3d_demo_page.dart
|
||||
>
|
||||
> Web 体验地址,PC 端记得开 Chrome 手机模式: https://guoshuyu.cn/home/web/#3D%20Dash 。
|
||||
|
||||
对比实物 Dash ,可以看到利用 zflutter 实现的 Dash ,乍看之下形似度还是蛮高的,同时 zflutter 本身也只有 82k 左右的大小,作为一个超轻量级的伪 3D 动画框架,它在接入成本很低的情况下,尽可能做到了我们对 3D 空间所需的视觉效果,这里面的关键还是在于:**矩阵变换是作用于画板还是作用于路径** 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220806_N11/image31.png)
|
||||
|
||||
那在知道原理之后,**我们接下来就可以通过三个简单的 `ZShape` 组合,利用 `ZMove` 和 `ZLine` 就能组合出具有 3D 质感的掘金 Logo ,里面的参数直接从 SVG 的 path 映射过来就可以了** 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image32.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image33.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image34.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image35.gif) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------- |
|
||||
|
||||
因为我们的矩阵旋转改变的是 Path 而不是 Canvas ,所以 Logo 的立体效果可以通过 `skroke` 的粗细配合画布 `zoom` 放大来体现。
|
||||
|
||||
> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/juejin_3d_logo_demo_page.dart
|
||||
>
|
||||
> Web 体验地址,PC 端记得开 Chrome 手机模式: https://guoshuyu.cn/home/web/#%E6%8E%98%E9%87%91%203d%20logo 。
|
||||
|
||||
**那可能就有人要说了,这个 logo 立体感还是不够强,因为它还是太扁平了** ~ 确实,受制于 `stroke` 参数的影响,在侧面的立体感上确实有所缺失,而为了提升立体感,我们可以通过 zflutter 里的 `ZBoxToBoxAdapter` 来实现。
|
||||
|
||||
在 zflutter 里, `ZBoxToBoxAdapter` 可以通过配置 `front` 、`rear` 、`left` 、`right` 、`top` 、`bottom` 等参数来配置长方体每个面的 UI,并且它本身就会根据 `width` 、`height` 、`depth` 参数生成一个立体长方形,如下图 1所示。
|
||||
|
||||
| ![](https://img.cdn.guoshuyu.cn/Simulator%20Screen%20Shot%20-%20iPhone%20SE%20(3rd%20generation)%20-%202022-08-05%20at%2016.34.37.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image36.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image37.gif) |
|
||||
| ------------------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
接着我们简单通过图 2 的量角器确定掘金 logo 的角度,然后如下代码所示,利用不同位置和角度,通过 `ZBoxToBoxAdapter` 组合堆叠不同的长方体,从而形成如上图 3 所示的立体掘金 logo,**当然,这个组合过程很明显是体力活**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220806_N11/image38.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image39.png) | ![](http://img.cdn.guoshuyu.cn/20220806_N11/image40.png) |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
|
||||
|
||||
|
||||
> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/juejin_3d_box_logo_demo_page.dart
|
||||
>
|
||||
> Web 体验地址,PC 端记得开 Chrome 手机模式: https://guoshuyu.cn/home/web/#%E6%8E%98%E9%87%91%E6%9B%B4%203d%20logo 。
|
||||
|
||||
|
||||
|
||||
可以看到 zflutter 虽然没有之前 [用 rive 给掘金 Logo 快速添加动画效果 ](https://juejin.cn/post/7126661045564735519)来的强大和方便,**但是好在它体积够小,不需要加载任何资源,纯代码就可以实现各种立体的 3D 动画效果** ,这对于程序员来说更加可控,至少它不需要依赖于任何第三方设计工具,就是开发速度上确实不如 rive 来的高效,**需要一定的空间想象力** 。
|
||||
|
||||
好了,本篇动画特效就到此为止,**如果你有什么想法,欢迎留言评论,感谢大家耐心看完,也还请看官们走过路过的来个点赞一键三连,感激不尽** ~
|
189
Flutter-N12.md
189
Flutter-N12.md
|
@ -1,189 +0,0 @@
|
|||
# Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它
|
||||
|
||||
随着 Flutter 3.3 正式版发布,Global Selection 终于有了官方的正式支持,**该功能补全了 Flutter 长时间存在 Selection 异常等问题,特别是在 Flutter Web 下经常会有选择文本时与预期的行为不匹配的情况**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image1.gif)
|
||||
|
||||
## 使用
|
||||
|
||||
**使用 `SelectionArea` 也十分简单,如下代码所示,只需要在你想要支持的地方添加 `SelectionArea` 即可**,甚至可以在每个路由下的 `Scaffold` 添加 `SelectionArea` 来全面启用支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image2.png)
|
||||
|
||||
默认情况下 `SelectionArea` 已经实现了所有常见的功能,并且 Flutter 针对不同平台进行了差异化实现,如下图所示 Android 和 iOS 会有不同的样式效果。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image3.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image4.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image5.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
**当然,也许这时候你会发现在 iOS 上的 Toolbar 居然没有全选**,其实这是因为 iOS 使用了 `TextSelectionControls` 默认的 `canSelectAll` 判断,这个判断里有一个条件就是需要 selection 的 `start == end` 才符合条件。
|
||||
|
||||
![image-20220906113547331](http://img.cdn.guoshuyu.cn/20220906_N12/image6.png)
|
||||
|
||||
所以如果你觉得这个判断有问题,完全可以自己 `override` 一个自定义的 `TextSelectionControls` ,比如在 `canSelectAll` 直接 `return true` 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image7.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image8.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
是的,**对于 `SelectionArea` 我们可以通过继承 `TextSelectionControls` 来自定义**:
|
||||
|
||||
- 通过 `buildToolbar` 自定义弹出的 Toolbar 样式和逻辑,甚至你可以添加一些额外的标签能力,比如 “插入图片”
|
||||
- 通过 `buildHandle` 自定义 Selection Handle 可拖动部分的样式
|
||||
|
||||
而在 `SelectionArea` 里,不管是 Handle 还是 Toolbar ,都是通过新增 `Overlay` 来实现样式,这部分的逻辑主要在 `SelectionOverlay` 对象:
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image9.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image10.png) |
|
||||
| ------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> 如果你还不了解 `Overlay` ,可以简单理解为:**默认情况下所有的路由页面都在一个 `Overlay` 下,打开一个 Route 就是添加一个 `OverlayEntry` 到 `Overlay` 里**。
|
||||
|
||||
所以 Handle 和 Toolbar 都是通过 `OverlayEntry` 打开的特殊“路由”控件,拥有新的层级,例如下方右图就是 Toolbar 所在的 `OverlayEntry` 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image11.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image12.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
**另外,对于 Handle 的颜色定义,默认情况下主要来自 `TextSelectionTheme` 和 `Theme`** 。
|
||||
|
||||
例如 `MaterialTextSelectionControls` 里,start 和 end 两个 Handle 的颜色,默认是通过 `TextSelectionTheme` 的 `selectionHandleColor` 或者 `Theme` 的 `primary` 来设置。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image13.png)
|
||||
|
||||
那文字的选中区域的颜色是怎么来的?难道也是 `OverlayEntry` 吗?
|
||||
|
||||
答案是否定的,这部分颜色主要是来自于文本绘制时 Canvas 的渲染。
|
||||
|
||||
如下代码所示,**当文本被绘制时,会判断当前是否有被选中的片段,如果存在选中的片段,会调用绘制对应的选中图层**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image14.png)
|
||||
|
||||
而对于文字的选中区块的颜色,默认是通过 `DefaultSelectionStyle` 的 `selectionColor` 来显示,当然,如下右图所示,在 `MaterialApp` 里它依然和 `TextSelectionTheme` 的 `selectionColor` 或者 `Theme` 的 `primary` 有关系。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image15.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image16.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
**那如果你还想要在 `SelectionArea` 下的某些内容不允许被选中呢**?
|
||||
|
||||
这里 Flutter 提供了 `SelectionContainer.disabled` 实现,只要在对应内容嵌套 `SelectionContainer.disabled` ,那么这部分内容下的文本就无法被选中。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image17.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image18.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
为什么嵌套 `SelectionContainer.disabled` 就可以禁用文本选中的能力?这其实和 `SelectionArea` 的实现有关系:
|
||||
|
||||
> `SelectionContainer` 内部实现了一个 `InheritedWidget` ,它会往下共享一个 `SelectionRegistrar ` ,而默认情况下 `SelectionArea` 内部使用了 `SelectionContainer` 并且往下共享了对应的 Registrar 实现。
|
||||
|
||||
- `SelectionArea` 内部的 `SelectionContainer` 是有对应的 `registrar` 实现往下共享
|
||||
- `SelectionContainer.disabled` 内部的 `registrar` 是 `null`
|
||||
|
||||
**所以根本区别就在于 `SelectionContainer.disabled` 里没有 `registrar`** ,如下左图所示,加了 disabled 后获取到的 `registrar` 是 null ,那么如下右侧代码所示,在后续可选中区域的更新逻辑中就会直接 return 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image19.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image20.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
到这里你应该大致理解了如何使用和自定义一些 `SelectionArea` 的能力,那么接下来介绍两个 “Bug” ,通过这两个 “Bug” 我们深入理解 `SelectionArea` 内部的实现情况 。
|
||||
|
||||
## 问题1
|
||||
|
||||
如下代码所示,**当使用了 `WidgetSpan` 之后,默认情况下,用户在开始位置拖拽 Handle 进行选择时会无法选中 `WidgetSpan` 里的文本**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image21.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image22.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> PS:其实拖动可以选中,只是这里暂时以不能选中的情况下作为切入点。
|
||||
|
||||
为什么会这样?首先要知道,上面代码在使用了 `WidgetSpan` 包裹 `Hello World` 之后,其实是存在两个` Text` ,也就是上述的 UI 是由两个 `RenderParagraph` 绘制完成。
|
||||
|
||||
那么对于最外层的 `Text` ,其实它的文本内容是 `“Flutter is the best!”`,注意这段文本,其实文本里此时是多了两个空格。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image23.png)
|
||||
|
||||
之所以会有这两个空格,其实是因为 `WidgetSpan` 使用了 `0xFFFC` 的占位符,这段占位符在渲染时,就会被替换为 `WidgetSpan` 对应的 `Hello World` 和猫头图片。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image24.png)
|
||||
|
||||
**那么这时候如果我们选择复制,复制出来的内容会是 `Flutter isthe best! `** ,中间的两个占位符是不会复制出来,因为在获取可选择片段时,会把对应的 `placeholderCodeUnit` 剔除。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image25.png)
|
||||
|
||||
另外,当我们点击复制的时候, `WidgetSpan` 所在的 `Hello World` 并没有被选中,所以此时调用 `getSelectedContent` 就会得到 null ,也就是没有内容。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image26.png)
|
||||
|
||||
所以可以看到:此时在手动拖拽选择时,` WidgetSpan` 里的文本是不会被选中,因为它处于不同的 `Text` ,对于外层 `Text` 而言它只是个占位符。
|
||||
|
||||
> 当然,**其实在拖动 Handle 还是可以选中 ` WidgetSpan` 里的文本,比如你从 `Hello World` 开始拖动,这里拖动选中不了的原因后面会解释**。
|
||||
|
||||
## 问题 2
|
||||
|
||||
如果当我们点击了全选会怎么样?如下图所示,在我们点击全选之后,可以看到两个“奇怪”的问题:
|
||||
|
||||
- ` WidgetSpan` 里的 `Hello World` 可以被选中了
|
||||
- 左侧的 Start Handle 位置不是在文本开头,而是在 ` WidgetSpan` 开始
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image27.png)
|
||||
|
||||
我们首先看第一点,**为什么点击全选时,`WidgetSpan` 里的 `Hello World` 可以被选中**?
|
||||
|
||||
其实全选操作和拖拽 Handle 最大的不同就是:它是往下直接发出全选事件 `SelectAllSelectionEvent` ,而该事件会触发所有 child 响应事件,自然也就包括了 `WidgetSpan` 里的 `Hello World` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image28.png)
|
||||
|
||||
最后负责响应 SelectAll 事件的对象是 `_SelectableFragment` ,这里主要有两个关键逻辑:
|
||||
|
||||
- `_handleSelectAll` 获取得到 `_textSelectionStart` 和 `_textSelectionEnd ` ,表明此时控件已经被选中
|
||||
- `didChangeSelection` 里通过 `paragraph.markNeedsPaint()` 触发重绘,然后增加选中时的覆盖颜色
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image29.png)
|
||||
|
||||
可以看到,由于此时 `WidgetSpan` 里的 `Hello World` 也直接响应了全选事件,所以它会处于选中状态,这样之后在 `getSelectedContent` 调用里也可以获取到内容,也就是能够 `Hello World` 能被复制出来。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image30.png)
|
||||
|
||||
**但是此时复制出来的内容会是 `Hello World!Flutter isthe best!` ** ,是不是感觉还不对?这就是我们要说的第二个问题,左侧的 Start Handle 位置不是在文本开头。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image27.png)
|
||||
|
||||
首先我们看,为什么复制出来之后的内容会是 `Hello World!Flutter isthe best!` ?
|
||||
|
||||
正如前面说到的,复制调用的是 `getSelectedContent` 方法,如下代码所示,**可以看到在 `selectables` 这个 `List` 的第一位就是 `Hello World` ,所以最终拼接出来的文本会是 `Hello World!Flutter isthe best!`** 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image31.png)
|
||||
|
||||
那为什么 `Hello World` 会排在 `selectables` 的第一位? 这就需要讲到 Flutter 里对 Selectable 的一个排序逻辑。
|
||||
|
||||
我们知道 `Text` 内部是通过 `RenderParagraph` 实现文本绘制,而 `RenderParagraph` 在初始化的时候,**如果存在 `_registrar` ,也就是存在 `SelectionArea` 的时候,就会通过 `add` 把支持选中的片段添加 `SelectionArea` 内部的 `_additions`** 里。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image32.png)
|
||||
|
||||
之后 `SelectionArea` 内部会对可选中的内容进行排序,如下代码所示,在` sort` 之前,此时的 `Hello World` 在 `_additions ` 列表的最末端,因为它处于 `WidgetSpan` 的 child 里,所以是最晚被加入到 `_additions ` 的。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image33.png)
|
||||
|
||||
而在执行完 `sort `之后 ,可以看到此时 `Hello World` 跑到了列表的最前面,**这也是为什么复制出来的内容顺序是 `Hello World` 开头,然后 Start Handle 会显示在 `Hello World` 的原因**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image34.png)
|
||||
|
||||
`sort ` 的逻辑主要是通过 `compareOrder` 实现,简单分析 `compareOrder` 的排序实现,可以看到其中有一个 `_compareVertically` 的逻辑,通过调试对比,**可以看到此时因为 `Hello World` 所处的 `Rect`(top)比其他文本高,所以它被认为是更高优先级的位置,类似于被误认为是上一行的情况**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image35.png)
|
||||
|
||||
知道了问题那就很好处理了,**如下代码所示,如果此时调整一下 `WidgetSpan` 的高度,可以看到全选逻辑下 Start Handle 正常了,但是.... End Handle 位置又不对了**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image36.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image37.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
此时复制出来的内容会是 `Flutter isthe best!Hello World!` ,**因为这个时候会有一个很“微妙”的偏差值,导致 `Hello World` 排序时被排列到最后面**,从而导致 End Handle 不是预期的位置。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220906_N12/image38.png)
|
||||
|
||||
另外,这时候你会发现,如下左侧动图所示,**此时拖动 Handle 是可以选中 `WidgetSpan` 里的 `Hello World`** ,其实之前的情况下也可以,不过需要如右侧动图所示,需要从 `Hello World` 开始拖动,**因为最开始的情况下 `selectables` 里 `Hello World` 的排序层级更高,所以如果想要拖动选中,也需要从它开始**。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image39.gif) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image40.gif) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> 目前这个问题在 master 和 stable 分支均可以复现,对应 issue 我也提交在 [#111021 ](https://github.com/flutter/flutter/issues/111021) 。
|
||||
|
||||
## 最后
|
||||
|
||||
虽然 `SelectionArea` 的出现补全了 Flutter 的长久以来的短板之一,不过基于 `SelectionArea` 实现的复杂程度,目前 `SelectionArea` 还有不少的细节需要优化,但是万事开头难,本次 3.3 `SelectionArea` 的落地也算是一个不错的开始。
|
||||
|
||||
最后,相信通过本文大家应该对 `SelectionArea` 的使用和实现都有了一定的了解,如果你还有什么问题,欢迎留言评论交流~
|
141
Flutter-N13.md
141
Flutter-N13.md
|
@ -1,141 +0,0 @@
|
|||
# Flutter 小技巧之优化你的代码性能
|
||||
|
||||
又到了小技巧系列更新时间,今天我们分享一个比较轻松的内容:**Flutter 里的代码优化,优化的目的主要是为了提高性能和可维护性**,放心,本篇我们不讲深入的源码分析,就是分享最最最基础的布局代码优化。
|
||||
|
||||
我们先从一个简单的例子开始,相信大家对于 Flutter 的 UI 构建不会陌生,那么如下代码所示,日常开发过程中 `A` 和 `B` 这两种代码组织方式,你更常用的是哪一种?
|
||||
|
||||
| A (函数方式) | B (Component Class 方式) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image1.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image2.png) |
|
||||
|
||||
如果是从代码运行之后的 UI 效果来看,这两个方式运行之后的布局效果并不会有什么差异,而**通常因为可以写更少代码和参数调用更方便等原因**,我们可能在编写页面的内部控件时,会更经常使用 `A (函数方式)` 这种写法,也有称之为 Helper Method 的叫法。
|
||||
|
||||
**那使用函数方式构建 UI 有没有问题?答案肯定是没问题,但是某些场景下,对比使用 `B (Component Class 方式)` ,可能性能表现上相对没那么优秀**。
|
||||
|
||||
举个例子,如下代码所示,在 `renderA` 函数里我们通过点击按键修改 `count`,在修改之后触发 UI 渲染时就需要用到 `setState` ,也就是我们每点一下,当前整个页面就是触发一次 rebuild ,但是我们只是想要改变当前 `renderA` 里的 `count` 文本而已。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image3.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image4.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
**这就是使用函数构建内部控件最常见的问题之一**,因为子控件更新时是通过父容器的 `setState` ,所以每次子控件比如 `renderA` 发生变化时,就会触发整个 Widget 都出现 rebuild ,这其实并不是特别符合我们的预期。
|
||||
|
||||
> 科普一个众所周知的知识点, **`setState` 其实就是调用 `StatefulWidget` 对应的 `StatefulElement` 里的 `markNeedsBuild` 方法,也就是对 `Element` (`BuildContext`) 里的 `_dirty` 标识为设置为 `true` ,仅此而已, 然后等待下次渲染更新**。
|
||||
|
||||
当然,你说像 `renderA` 这种写法会引起很严重的性能问题吗?事实上并不会,**因为众所周知 Flutter 里的 UI 构建是通过多个不同的树来完成的,而 Widget 并不是真实的控件**,所以一般情况下 `renderA` 这种写法导致的 rebuild 是不会产生严重的性能缺陷。
|
||||
|
||||
但是,如果同级下你的 `renderB` 是如下所示这样的情况呢?虽然这段代码毫无意义,但是我们在 `renderA` 点击改变 `count` 的时候,其实并没有改变 `renderB` 的用到的 `status` 参数,但是因为 `renderA` 里调用了 `setState` ,导致 `renderB` 每次都会进行重复进行浮点计算。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221021_N13/image5.png)
|
||||
|
||||
当然你可以说我写个变量进行缓存提前判断也可以解决,但这并不是这个例子的关键,那如果把上面这个例子变成 Component Class 的方式会有什么好处:
|
||||
|
||||
- A 在点击更新 `count` 时不会影响其他控件
|
||||
- B 控件通过 `didUpdateWidget` 可以用更优雅的方式决定更新条件
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image6.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image7.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
> 这样看起来是不是更合理一些?另外 Component Class 的实现方式,也能在一定层度解决代码层级嵌套的问题,有时候实现一些 Component Class 的模版也可以成为 Flutter 里提高效率的工具,这个后面我们会聊到。
|
||||
|
||||
**当然使用 Component Class 在无形之中会需要你写更多的代码,同时控件之间的状态联动成本也会有所提高**,例如你需要在 B 控件关联 A 的 `count` 变化去改变高度,这时候可能就需要加入 `InheritedWidget` 或者 `ValueNotifier` 等方式来实现。
|
||||
|
||||
例如 Flutter 里 `DefaultTabController` 配合 `TabBar` 和 `TabBarView` 的实现就是一个很好的参考。
|
||||
|
||||
```dart
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: myTabs.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
bottom: TabBar(
|
||||
tabs: myTabs,
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: myTabs.map((Tab tab) {
|
||||
final String label = tab.text.toLowerCase();
|
||||
return Center(
|
||||
child: Text(
|
||||
'This is the $label tab',
|
||||
style: const TextStyle(fontSize: 36),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 所以到这里我们理解一个小技巧:**在不偷懒的情况下,使用 Component Class 的方式实现子控件会比使用函数方式可能得到更好的性能和代码结构**。
|
||||
|
||||
当然,**使用 Component Class 实现的方式,在调试时也会比函数方式更方便**,如下图所示,当使用函数方式布局时,你在 Flutter Inspector 里看到的 Widget Tree 和 Details Tree 是完全铺平的情况,也没办法定制调试参数。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221021_N13/image8.png)
|
||||
|
||||
**但是当你 Component Class 组织布局的时候,你就可以通过 `override debugFillProperties` 方法来可视化一些参数状态**,例如 `ItemA` 里可以把 count 添加到 `debugFillProperties` 里,这样在 Details Tree 里也可以直观看到目前的 `count` 状态信息。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image9.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image10.png) |
|
||||
| ------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> 所以这里又有一个小技巧:**通过 `override debugFillProperties` ,可以定制一些 Debug 时的可视化参数来帮助我们更好调试布局**。
|
||||
|
||||
既然讲到利用 Component Class 组织布局,那就不得不聊一个典型的控件:`AnimatedBuilder` 。
|
||||
|
||||
`AnimatedBuilder` 可以是最常说到的一个性能优化的例子, 一般情况下在页面的子控件里使用动画,特别是循环动画的话,我们都会建议使用前面介绍的 Component Class 方式,不然动画导致当前页面不停 rebuild 肯定会导致性能影响。
|
||||
|
||||
但是有时候我就不想用 Component Class 该怎么办?我就是想写在当前 Page 里,那就可以使用 `AnimatedBuilder` ,你只要把需要执行动画的部分放到 `builder` 方法里就好了。
|
||||
|
||||
**因为 `AnimatedBuilder` 的内部会有一个 `_AnimatedState` 用于独立触发 `setState`,从而执行外部 builder 方法执行动画效果**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221021_N13/image11.png)
|
||||
|
||||
类似 `AnimatedBuilder` 的模版实现,可以在一定程度上解决使用 Component Class 的痛点,当然,在使用 `AnimatedBuilder` 还是有一些需要注意, **比如 child 如果不需要跟随动画进行其他变化,一般是要放到 `AnimatedBuilder` 的 `child` 配置里**,因为如果直接放在 `builder` 方法里,那就会出现 child 也跟随动画重新 rebuild 的情况,但是如果是放到 `child` 配置项里,那就是调用了 `child` 的对象缓存。
|
||||
|
||||
| 不正确使用 | 正确使用 |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image12.png) | ![image-20221020175113790](http://img.cdn.guoshuyu.cn/20221021_N13/image13.png) |
|
||||
|
||||
> 如果对于这个缓存概念不理解,可以参考 [《MediaQuery 和 build 优化你不知道的秘密》](https://juejin.cn/post/7114098725600903175) 里的“**缓存区域不随帧变化,以便得到最小化的构建**”。
|
||||
|
||||
**当然类似 `AnimatedBuilder` 的构建方式还要注意 `context` 问题,不要拿错 `context`** ,这也是很多时候会犯的潜在错误,特别是在调用 `of(context)` 的时候。
|
||||
|
||||
*那有的人可能到这里会觉得,那你之前一直说 Widget 很轻,Widget 不是真正的控件,那 rebuild 多几次有什么问题*?
|
||||
|
||||
一般情况下确实不会有太大问题,但是当你的控件有 ` Opacity ` 、`ColorFilter` 、 `ShaderMash` 或者 `ClipRect`(`Clip.antiAliasWithSaveLayer`)时,就可能会有较大的性能影响,因为他们都是可能会触发 `saveLayer` 的操作。
|
||||
|
||||
> 为什么 `saveLayer` 对性能影响很大?因为需要在 GPU 绘制是需要增加额外的缓冲区域,粗俗点说就是需要做图层的保存和合成,这就会对 GPU 渲染时产生较大影响的耗时。
|
||||
|
||||
而这里面最常遇到的应该就是 ` Opacity ` 带来的性能问题,因为它看起来是那么的轻便,但是从官方的介绍里,除非真的有必要,不然可以使用效果类似的实现去做场景替代,例如:
|
||||
|
||||
**你需要对图片做透明度相关的动画是,那么使用 `AnimatedOpacity` 或 `FadeInImage` 代替 ` Opacity ` 会对性能更有帮助**。
|
||||
|
||||
> `AnimatedOpacity` 和 ` Opacity ` 不一样吗?某种程度上还真不大一样, ` Opacity ` 的内部是 `pushOpacity ` 的操作,而 `AnimatedOpacity` 里虽然有 `OpacityLayer` ,但是变动时是 `updateCompositedLayer` ;而 `FadeInImage` 会使用 GPU 的 fragment shader 去处理透明度的问题,所以性能也会更好一些。
|
||||
|
||||
或者在类似有颜色透明度的场景时,可以通过 `Color.fromRGBO` 来替代 `Opacity` ,**除非你需要将不透明度应用到一大组较为复杂的 child 里,你才会需要使用 `Opacity`** 。
|
||||
|
||||
```dart
|
||||
/// no
|
||||
Opacity(opacity: 0.5, child: Container(color: Colors.red))
|
||||
|
||||
/// yes
|
||||
Container(color: Color.fromRGBO(255, 0, 0, 0.5))
|
||||
```
|
||||
|
||||
另外还有 `IntrinsicHeight` / `IntrinsicWidth` 的场景,**因为它们是可以通过 child 的内部宽高来调整 child 的大小**,但是这个推算布局的过程会比较费时,可能会到 O(N²),虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。
|
||||
|
||||
这么说可能有点抽象,举一个官方介绍过的例子,如下代码所示,当你在 `ListView` 里对 `Row` 的 `children` 进行 `Align` 排列时,你可能会发现它没有效果,因为此时通过 `Border` 可以看到,绿色和蓝色方框的父容器大小一致。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image14.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image15.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
但是在加上 `IntrinsicHeight` 之后, 因为通过 `IntrinsicHeight` 的测算之后再返回 size,`Row` 里的三个 Item 现在高度一致,,这时候 `Align` 就可以生效了,但是正如前面所说,这个操作性对性能来说相对昂贵,虽然系统有缓存参数,但是如果出现动画 rebuild ,也会对性能造成影响。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image16.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image17.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
> 对这部分感兴趣的可以看 : [《带你了解不一样的 Flutter》](https://juejin.cn/post/7053777774707736613#heading-2)
|
||||
|
||||
**到这里我们就理解了 (函数方式) 和 (Component Class 方式)组织布局的不同之处,同时也知道了 Component Class 方式可以帮助我们更好地调试布局代码,也举例了一些 UI 布局里常见的耗时场景**。
|
||||
|
||||
那本篇的小技巧到这里就结束了,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~
|
192
Flutter-N16.md
192
Flutter-N16.md
|
@ -1,192 +0,0 @@
|
|||
# Flutter 小技巧之 3.7 性能优化background isolate
|
||||
|
||||
Flutter 3.7 的 background isolate 绝对是一大惊喜,尽管它在 [release note](https://juejin.cn/post/7192468840016511034#heading-10) 里被一笔带过 ,但是某种程度上它可以说是 3.7 里最实用的存在:**因为使用简单,提升又直观**。
|
||||
|
||||
> Background isolate YYDS
|
||||
|
||||
## 前言
|
||||
|
||||
我们知道 Dart 里可以通过新建 isolate 来执行”真“异步任务,而本身我们的 Dart 代码也是运行在一个独立的 isolate 里(简称 root isolate),而 isolate 之间不共享内存,只能通过消息传递在 isolates 之间交换状态。
|
||||
|
||||
> 所以 Dart 里不像 Java 一样需要线程锁。
|
||||
|
||||
而在 Dart 2.15 里新增了 isolate groups 的概念,**isolate groups 中的 isolate 共享程序里的各种内部数据结构**,也就是虽然 isolate groups 还是不允许 isolate 之间共享可变对象,但 groups 可以通过共享堆来实现结构共享,例如:
|
||||
|
||||
> Dart 2.15 后可以将对象直接从一个 isolate 传递到另一 isolate,而在此之前只支持基础数据类型。
|
||||
|
||||
那么如果使用场景来到 Flutter Plugin ,**在 Flutter 3.7 之前,我们只能从 root isolate 去调用 Platform Channels** ,如果你尝试从其他 isolate 去调用 Platform Channels ,就会收获这样的错误警告:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image1.png)
|
||||
|
||||
> 例如,在 Flutter 3.7 之前,Platform Channels 是和 `_DefaultBinaryMessenger ` 这个全局对象进行通信,但是一但切换了 isolate ,它就会变为 null ,因为 isolate 之间不共享内存。
|
||||
|
||||
而从 Flutter 3.7 开始,简单地说,Flutter 会通过新增的 BinaryMessenger 来实现非 root isolate 也可以和 Platform Channels 直接通信,例如:
|
||||
|
||||
> 我们可以在全新的 isolate 里,通过 Platform Channels 获取到平台上的原始图片后,在这个独立的 isolate 进行一些数据处理,然后再把数据返回给 root isolate ,这样数据处理逻辑既可以实现跨平台通用,又不会卡顿 root isolate 的运行。
|
||||
|
||||
|
||||
|
||||
# Background isolate
|
||||
|
||||
现在 Flutter 在 Flutter 3.7 里引入了 `RootIsolateToken` 和 `BackgroundIsolateBinaryMessenger` 两个对象,当 background isolate 调用 Platform Channels 时, background isolate 需要和 root isolate 建立关联,所以在 API 使用上大概会是如下代码所示:
|
||||
|
||||
```dart
|
||||
RootIsolateToken rootIsolateToken =
|
||||
RootIsolateToken.instance!;
|
||||
|
||||
Isolate.spawn((rootIsolateToken) {
|
||||
doFind2(rootIsolateToken);
|
||||
}, rootIsolateToken);
|
||||
|
||||
doFind2(RootIsolateToken rootIsolateToken) {
|
||||
// Register the background isolate with the root isolate.
|
||||
BackgroundIsolateBinaryMessenger
|
||||
.ensureInitialized(rootIsolateToken);
|
||||
//......
|
||||
}
|
||||
```
|
||||
|
||||
通过 `RootIsolateToken` 的单例,我们可以获取到当前 root isolate 的 Token ,然后在调用 Platform Channels 之前通过 `ensureInitialized` 将 background isolate 需要和 root isolate 建立关联。
|
||||
|
||||
> 大概就是 token 会被注册到 `DartPluginRegistrant` 里,然后 `BinaryMessenger` 在 `_findBinaryMessenger` 时会通过 `BackgroundIsolateBinaryMessenger.instance` 发送到对应的 `listener`。
|
||||
|
||||
完整代码如下所示,逻辑也很简单,就是在 root isolate 里获取 `RootIsolateToken` ,然后在调用 Platform Channels 之前 `ensureInitialized` 关联 Token 。
|
||||
|
||||
```dart
|
||||
InkWell(
|
||||
onTap: () {
|
||||
///获取 Token
|
||||
RootIsolateToken rootIsolateToken =
|
||||
RootIsolateToken.instance!;
|
||||
Isolate.spawn(doFind, rootIsolateToken);
|
||||
},
|
||||
|
||||
////////////////
|
||||
|
||||
doFind(rootIsolateToken) async {
|
||||
/// 注册 root isolaote
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
|
||||
|
||||
///获取 sharedPreferencesSet 的 isDebug 标识位
|
||||
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
|
||||
.then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
|
||||
/// 获取本地目录
|
||||
final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
|
||||
|
||||
/// 合并执行
|
||||
var values = await Future.wait([sharedPreferencesSet, tempDirFuture]);
|
||||
|
||||
final Directory? tempDir = values[1] as Directory?;
|
||||
final String dbPath = path.join(tempDir!.path, 'database.db');
|
||||
File file = File(dbPath);
|
||||
if (file.existsSync()) {
|
||||
///读取文件
|
||||
RandomAccessFile reader = file.openSync();
|
||||
List<int> buffer = List.filled(256, 0);
|
||||
while (reader.readIntoSync(buffer) == 256) {
|
||||
List<int> foo = buffer.takeWhile((value) => value != 0).toList();
|
||||
///读取结果
|
||||
String string = utf8.decode(foo);
|
||||
print("######### $string");
|
||||
}
|
||||
reader.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 这里之所以可以在 isolate 里直接传递 `RootIsolateToken` ,就是得益于前面所说的 Dart 2.15 的 isolate groups
|
||||
|
||||
其实入下代码所示,上面的实现换成 `compute` 也可以正常执行,当然,**如果是 `compute` 的话,有一些比较特殊情况需要注意**。
|
||||
|
||||
```dart
|
||||
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
|
||||
compute(doFind, rootIsolateToken);
|
||||
```
|
||||
|
||||
如下代码所示, `doFind2` 方法在 `doFind` 的基础上,将 `Future.wait` 的 `await` 修改为 `.then` 去执行,如果这时候你再调用 `spawn` 和 `compute` ,你就会发现 **`spawn` 下代码依然可以正常执行,但是 `compute` 却不再正常执行**。
|
||||
|
||||
```dart
|
||||
onTap: () {
|
||||
RootIsolateToken rootIsolateToken =
|
||||
RootIsolateToken.instance!;
|
||||
compute(doFind2, rootIsolateToken);
|
||||
},
|
||||
|
||||
onTap: () {
|
||||
RootIsolateToken rootIsolateToken =
|
||||
RootIsolateToken.instance!;
|
||||
Isolate.spawn(doFind2, rootIsolateToken);
|
||||
},
|
||||
|
||||
|
||||
doFind2(rootIsolateToken) async {
|
||||
/// 注册 root isolaote
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
|
||||
|
||||
///获取 sharedPreferencesSet 的 isDebug 标识位
|
||||
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
|
||||
.then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
|
||||
|
||||
/// 获取本地目录
|
||||
final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
|
||||
|
||||
|
||||
///////////////////// Change Here //////////////////
|
||||
/// 合并执行
|
||||
Future.wait([sharedPreferencesSet, tempDirFuture]).then((values) {
|
||||
final Directory? tempDir = values[1] as Directory?;
|
||||
final String dbPath = path.join(tempDir!.path, 'database.db');
|
||||
///读取文件
|
||||
File file = File(dbPath);
|
||||
if (file.existsSync()) {
|
||||
RandomAccessFile reader = file.openSync();
|
||||
List<int> buffer = List.filled(256, 0);
|
||||
while (reader.readIntoSync(buffer) == 256) {
|
||||
List<int> foo = buffer.takeWhile((value) => value != 0).toList();
|
||||
String string = utf8.decode(foo);
|
||||
print("######### $string");
|
||||
}
|
||||
reader.closeSync();
|
||||
}
|
||||
}).catchError((e) {
|
||||
print(e);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
为什么会这样?`compute` 不就是 Flutter 针对 `Isolate.spawn` 的简易封装吗?
|
||||
|
||||
> 其实原因就在这个封装上,**`compute` 现在不是直接执行 `Isolate.spawn` 代码,而是执行 `Isolate.run`** ,而 `Isolate.run` 针对 `Isolate.spawn` 做了一些特殊封装。
|
||||
|
||||
`compute` 内部会将执行对象封装成 `_RemoteRunner` 再交给 `Isolate.spawn` 执行,而 `_RemoteRunner` 在执行时,会在最后强制调用 `Isolate.exit` ,这就会导致前面的 `Future.wait` 还没执行,而 `Isolate` 就退出了,从而导致代码无效的原因。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image2.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image3.png)
|
||||
|
||||
另外在 Flutter 3.7 上 ,如果 background isolate 调用 Platform Channels 没有关联 root isolate,也能看到错误提示你初始化关联,所以这也是为什么我说它使用起来很简单的原因。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image4.png)
|
||||
|
||||
除此之外,最近刚好遇到有“机智”的小伙伴说 background isolate 无法正常调用,看了下代码是把 `RootIsolateToken.instance!;` 写到了 background isolate 执行的方法里。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230203_isolate/image5.png)
|
||||
|
||||
> 你猜如果这样有效,为什么官方不直接把这个获取写死在 framewok?
|
||||
|
||||
其实这也是 isolates 经常引起歧义的原因,isolates 是隔离,内存不共享数据,所以 root isolate 里的 `RootIsolateToken` 在 background isolate 里直接获肯定是 null ,所以这也是 isolate 使用时需要格外注意的一些小细节。
|
||||
|
||||
> 另外还有如 [#36983](https://github.com/dart-lang/sdk/issues/36983 ) 等问题,也推动了前面所说的 `compute` 相关的更改。
|
||||
|
||||
最后,如果需要一个完整 Demo 的话,可以参考官方的 [background_isolate_channels](https://github.com/flutter/samples/tree/294ea4ff8fce588f332e82e3ee97fa3d3429c9a4/background_isolate_channels) ,项目里主要通过 `SimpleDatabase` 和 `_SimpleDatabaseServer` 的交互,来模拟展示 root isolate 和 background isolate 的调用实现。
|
||||
|
||||
# 最后
|
||||
|
||||
总的来说 background isolate 并不难理解,自从 2018 年在 [issue #13937](https://github.com/flutter/flutter/issues/13937) 被提出之后就饱受关注,甚至官方还建议过大家通过 ffi 另辟蹊径去实现,当时的 issue 也被搭上了` P5` 的 Tag。
|
||||
|
||||
> 相信大家都知道 P5 意味着什么。
|
||||
|
||||
所以 background isolate 能在 Flutter 3.7 看到是相当难得的,当然这也离不开 Dart 的日益成熟的支持,同时 background isolate 也给我们带来了更多的可能性,其中最直观就是性能优化上多了新的可能,代码写起来也变得更顺畅。
|
||||
|
||||
期待 Flutter 和 Dart 在后续的版本中还能给我们带来更多的惊喜。
|
|
@ -1,96 +0,0 @@
|
|||
# Flutter 之快速理解混合开发里的手势事件传递
|
||||
|
||||
本篇我们聊聊 `PlatformView` 里的手势事件传递,为什么会有这么一篇?其实在此之前已经写过很多 Flutter 里关于混合开发里 `PlatformView` 的内容,而随着 Flutter 版本的迭代, `PlatformView` 的实现也出现了一定的历史包袱问题,恰好最近和[大佬](https://juejin.cn/user/4309694831660711)讨论了混合使用 `VirtualDisplay` 和 `HybirdComposition` 时手势事件有什么区别,就顺便把讨论结果梳理出来。
|
||||
|
||||
> 对历史包袱问题感兴趣的可以看 [《混合开发的摸爬滚打》](https://juejin.cn/post/7153184663077388295),之前写过 `PlatformView` 的文章最早的已经两年多前,关于事件处理经历过太多版本,如今可能会产生了一些误解或者错误引导,就在本篇一次性解释。
|
||||
|
||||
首先在当前 3.3 的版本里,Flutter `PlatformView` 主要有 `VirtualDisplay` 、 `HybirdComposition` 和 `TextureLayer` 三种实现,而这三种实现在手势事件传递实现有差异,但是流程一致,所以本篇的目的就是快速梳理它们的异同。
|
||||
|
||||
**如果从当前的实现逻辑上总结,他们在流程上基本是一致的,事件都是从原生 -> Dart -> 原生这样的一个响应处理过程** ,也就是如下图所示,由原生的 `onTouchEvent` 产生手势事件,然后经过 dart 的统一的[事件竞技场](https://juejin.cn/post/6844903841742192648)处理后,最后回到原生层再去触发原生控件响应事件。
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image1.png)
|
||||
|
||||
也就是在当前的设计里,**无论是哪种 `PlatformView` 的实现,原生控件都不会马上响应触摸事件,而是统一发送到 dart 进行处理,之后再返回触发 Native 控件进行响应**,这样处理的好坏在于:
|
||||
|
||||
- 好处是处理逻辑能在 dart 里统一,并且针对原生控件的事件处理也可以在 dart 层进行拦截处理
|
||||
- 坏处是原生 Event 经历了多次转换,中间可能出现精度丢失和响应速度的问题,特别是在需要大量拖拽的场景
|
||||
|
||||
> 所以在 `PlatformView` 的 dart 实现里会有 `gestureRecognizers` 参数用于开发者处理自定义事件响应的支持,例如配置 `EagerGestureRecognizer` 可以用于获得所有手势,解决手势冲突问题。
|
||||
|
||||
那么它们在实现上有什么差异?其实这些差异不会直接影响你的使用,如果不感兴趣可以不关心,但是对于理解整个手势事件传递来说又是必不可缺。
|
||||
|
||||
# VirtualDisplay
|
||||
|
||||
`VirtualDisplay `可以说是老骥伏枥了,兜兜转转最后在 3.3 版本系还继续服役,我们都知道 `VirtualDisplay ` 的实现是采用 Android 上副屏的渲染逻辑,然后把控件渲染到内存,通过纹理 id 提取合成画面,也就是:
|
||||
|
||||
> **虽然你看到控件在那里,但是其实它并不是真的在那里,你看的是只是合成之后的纹理,所以 `VirtualDisplay ` 上原生端接受到的触摸事件,其实是来自于 `FluterView`** 。
|
||||
|
||||
在 `VirtualDisplay ` 里触摸事件的发起和普通 Flutter 控件一样,都是从 `FlutterView` 的 `onTouchEvent` 开始,经过统一的[事件竞技场](https://juejin.cn/post/6844903841742192648)处理后,最终回到 java 层去触发 NativeView 响应手势信息。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image2.png)
|
||||
|
||||
所以在 `VirtualDisplay ` 里所有的 Event 都是直接来自 `FlutterView` ,走的是 `AndroidTouchProcessor` 进行发送。
|
||||
|
||||
# `HybirdComposition`
|
||||
|
||||
对于 `HybirdComposition` 来说这个实现又不大一样,因为 `HybirdComposition` 是直接把原生 View 通过 `addView` 添加到 `FlutterView` 上面,中间通过 `FlutterMutatorView` 作为容器,大概效果如下图所示。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image3.png)
|
||||
|
||||
**那是不是 `HybirdComposition` 上用户的触摸点击事件是直接由原生控件进行响应呢?答案是否定的**。
|
||||
|
||||
其实[一开始](https://github.com/flutter/engine/commit/1832613e0961902d9d368b3b4b6541b858050eb4#diff-efdceec13b333498e1451586d96adc90030b07bc1b7818cc4dbb16b85f1aba32) `HybirdComposition` 的设定确实是这样,但是后来为了统一和方便处理, `FlutterMutatorView` 上添加了 `onInterceptTouchEvent` 进行了拦截,所以事件都无法传递到它的子控件上,而是在 `FlutterMutatorView` 通过 `AndroidTouchProcessor` 发送到 Dart 层。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image4.png)
|
||||
|
||||
当然,**事实上在坐标处理上也有差异**,因为这里的 `onTouchEvent` 是 `FlutterMutatorView` 上的触摸事件坐标,而为了能够匹配到 dart 里的坐标进行响应,还需要通过矩阵转化为屏幕坐标,而这部分换算在 `VirtualDisplay `里是不需要的。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image5.png)
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image6.png)
|
||||
|
||||
|
||||
|
||||
事实上 `HybirdComposition` 的实现在触摸事件响应上比较有迷惑性,特别是某些场景下会很有趣,例如在下面这个场景上:
|
||||
|
||||
> 红色的是 Flutter 控件,蓝色是 Native 控件,它们恰好有一部分重叠在一起。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20221026_N17/image7.png) | ![](http://img.cdn.guoshuyu.cn/20221026_N17/image8.png) |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
|
||||
我们知道在 `HybirdComposition` 里,如果 Flutter 控件需要覆盖在 Native 控件之上是,就会需要一个 `FlutterImageView` 来做新的图层承载,但是 `FlutterImageView` 本身并没有做触摸事件处理,所以如果这时候点击红色 RE ,就会有两种情况:
|
||||
|
||||
- 点击的是和蓝色 Native 控件相交的区域,因为事件穿透的影响,此时会是通过 `FlutterMutatorView` 触发事件发送到 Dart
|
||||
- 点击的是没有相交的区域时,因为事件穿透的影响,此时会是通过 `FlutterView` 触发事件发送到 Dart
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image9.png)
|
||||
|
||||
|
||||
|
||||
> 虽然这个过程其实很诡异,但是实际上并不会影响最终结果,详细感兴趣可以看 [《Flutter 3.0下的混合开发演进》](https://juejin.cn/post/7113655154347343909)
|
||||
|
||||
# TextureLayer
|
||||
|
||||
其实 `TextureLayer` 的事件实现和 `HybirdComposition` 类似,不同之处在于它是通过 `PlatformViewWrapper` 做父容器来拦截事件。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image10.png)
|
||||
|
||||
`PlatformViewWrapper` 同样通过 `onInterceptTouchEvent` 进行了事件拦截,所以事件都无法传递到它的子控件上,而是通过 `AndroidTouchProcessor` 发送到 Dart 层,同时在对应的 `onTouchEvent` 上需要做事件转化。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image11.png)
|
||||
|
||||
> PS ,这里看到 ` TextView` 是空白的原因就是 `PlatformViewWrapper` 通过 Hook 了 Canvas 从而提取 Child 纹理的过程,详细感兴趣可见:[《Flutter 3.0下的混合开发演进》](https://juejin.cn/post/7113655154347343909)。
|
||||
|
||||
所以本质上 `TextureLayer` 和 `HybirdComposition` 在事件消费处理上类似,只是不会有像 `HybirdComposition` 一样会有 `FlutterImageView` 那样诡异的传递方式而已。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20221026_N17/image12.png)
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
好了,本篇的内容其实并不复杂,**主要是帮助你理清 `PlatformView` 里手势事件传递和处理的相关逻辑**,理清这部分逻辑,在你使用 add-to-app 时针对一些手势冲突会更有帮助,如果还有什么想说的,欢迎留言讨论~
|
264
Flutter-N18.md
264
Flutter-N18.md
|
@ -1,264 +0,0 @@
|
|||
# Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?
|
||||
|
||||
|
||||
|
||||
随着 Flutter 3.7 的更新, `dart:ui` 下多了 `Picture.toImageSync` 和 `Scene.toImageSync` 这两个方法,和`Picture.toImage` 以及 `Scene.toImage` 不同的是 ,`toImageSync` 是一个同步执行方法,所以它不需要 `await` 等待,而调用 `toImageSync` 会直接返回一个 Image 的句柄,并在 Engine 后台会异步对这个 Image 进行光栅化处理。
|
||||
|
||||
# 前言
|
||||
|
||||
那 `toImageSync ` 有什么用?不是有个 `toImage` 方法了,为什么要多一个 Sync 这样的同步方法?
|
||||
|
||||
- **目前 `toImageSync ` 最大的特点就是图像会在 GPU 中常驻** ,所以对比 `toImage` 生成的图像,它的绘制速度会更快,并且可以重复利用,提高效率。
|
||||
|
||||
> `toImage` 生成的图像也可以实现 GPU 常驻,但目前没有未实现而已。
|
||||
|
||||
- `toImageSync ` 是一个同步方法,在某些场景上弥补了 `toImage` 必须是异步的不足。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image1.png)
|
||||
|
||||
而 `toImageSync ` 的使用场景上,官方也列举了一些用途,例如:
|
||||
|
||||
- 快速捕捉一张昂贵的栅格化图片,用户支持跨多帧重复使用
|
||||
- 应用在图片的多路过滤器上
|
||||
- 应用在自定义着色器上
|
||||
|
||||
具体在 Flutter Framework 里,目前 `toImageSync ` 最直观的实现,就是被使用在 Android 默认的页面切换动画 `ZoomPageTransitionsBuilder ` 上,得意于 `toImageSync ` 的特性,Android 上的页面切换动画的性能,**几乎减少了帧光栅化一半的时间**,从而减少了掉帧和提高了刷新率。
|
||||
|
||||
> 当然,这是通过牺牲了一些其他特性来实现,后面我们会讲到。
|
||||
|
||||
# SnapshotWidget
|
||||
|
||||
前面说了 `toImageSync ` 让 Android 的默认页面切换动画性能得到了大幅提升,那究竟是如何实现的呢?这就要聊到 Flutter 3.7 里新增加的 `SnapshotWidget` 。
|
||||
|
||||
其实一开始 `SnapshotWidget` 是被定义为 `RasterWidget` ,从初始定义上看它的 Target 更大,但是最终在落地的时候,被简化处理为了 `SnapshotWidget` ,而从使用上看确实 Snapshot 更符合它的设定。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image2.png)
|
||||
|
||||
|
||||
|
||||
## 概念
|
||||
|
||||
**`SnapshotWidget` 的作用是可以将 Child 变成的快照(`ui.Image`)从而替换它们进行显示,简而言之就是把子控件都变成一个快照图片**,而 `SnapshotWidget` 得到快照的办法就是 `Scene.toImageSync` 。
|
||||
|
||||
> 那么到这里,你应该知道为什么 `toImageSync ` 可以提高 Android 上的页面切换动画的性能了吧?因为 `SnapshotWidget` 会在页面跳转时把 Child 变成的快照,而 `toImageSync ` 栅格化的图片还可以跨多帧重复使用。
|
||||
|
||||
那么问题来了,`SnapshotWidget` 既然是通过 `toImageSync ` 将 Child 变成的快照(`ui.Image`)来提高性能,那么带来的副作用是什么?
|
||||
|
||||
答案是动画效果,**因为子控件都变成了快照,所以如果 Child 控件带有动画效果,会呈现“冻结”状态**,更形象的对比如下图所示:
|
||||
|
||||
| FadeUpwardsPageTransitionsBuilder | ZoomPageTransitionsBuilder |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image3.gif) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image4.gif) |
|
||||
|
||||
默认情况下 Flutter 在 Android 上的页面切换效果使用的是 `ZoomPageTransitionsBuilder` ,而 `ZoomPageTransitionsBuilder` 里在页面切换时会开启 `SnapshotWidget` 的截图能力,所以可以看到,它在页面跳转时,对比 `FadeUpwardsPageTransitionsBuilder` 动图, `ZoomPageTransitionsBuilder` 的红色方块和掘金动画会停止。
|
||||
|
||||
> 因为动画很短,所以可以在代码里设置 **` timeDilation = 40.0;`** 和 `SchedulerBinding.resetEpoch` 来全局减慢动画执行的速度,另外可以配置 `MaterialApp ` 的 `ThemeData` 下对应的 `pageTransitionsTheme` 来切换页面跳转效果。
|
||||
|
||||
所以在官方的定义中,**`SnapshotWidget` 是用来协助执行一些简短的动画效果**,比如一些 scale 、 skew 或者 blurs 动画在一些复杂的 child 构建上开销会很大,而使用 `toImageSync ` 实现的 `SnapshotWidget` 可以依赖光栅缓存:
|
||||
|
||||
> 对于一些简短的动画,例如 `ZoomPageTransitionsBuilder` 的页面跳转, `SnapshotWidget` 会将页面内的 children 都转化为快照(`ui.Image`),尽管页面切换时会导致 child 动画“冻结”,但是实际页面切换时长很短,所以看不出什么异常,**而带来的切换动画流畅度是清晰可见的**。
|
||||
|
||||
再举个更直观的例子,如下代码所示,运行后我们可以看到一个旋转的 logo 在屏幕上随机滚动,这里分别使用了 `AnimatedSlide` 和 `AnimatedRotation` 执行移动和旋转动画。
|
||||
|
||||
```dart
|
||||
Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||
final random = Random();
|
||||
x = random.nextInt(6) - 3;
|
||||
y = random.nextInt(6) - 3;
|
||||
r = random.nextDouble() * 2 * pi;
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
AnimatedSlide(
|
||||
offset: Offset(x.floorToDouble(), y.floorToDouble()),
|
||||
duration: Duration(milliseconds: 1500),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedRotation(
|
||||
turns: r,
|
||||
duration: Duration(milliseconds: 1500),
|
||||
child: Image.asset(
|
||||
'static/test_logo.png',
|
||||
width: 100,
|
||||
height: 100,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image5.gif)
|
||||
|
||||
|
||||
|
||||
如果这时候在 `AnimatedRotation` 上层加多一个 `SnapshotWidget` ,并且打开 `allowSnapshotting` ,可以看到此时 logo 不再转动,因为整个 child 已经被转化为快照(`ui.Image`)。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image6.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image7.gif) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
>所以 `SnapshotWidget` 不适用于子控件还需要继续动画或有交互响应的地方,例如轮播图。
|
||||
|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
如之前的代码所示,使用 `SnapshotWidget` 也相对简单,你只需要配置 `SnapshotController` ,然后通过 `allowSnapshotting `控制子控件是否渲染为快照即可。
|
||||
|
||||
```dart
|
||||
controller.allowSnapshotting = true;
|
||||
```
|
||||
|
||||
`SnapshotWidget` 在捕获快照时,会生成一个全新的 `OffsetLayer` 和 `PaintingContext`,然后通过 `super.paint` 完成内容捕获(这也是为什么不支持 PlatformView 的原因之一),之后通过 `toImageSync` 得到完整的快照(`ui.Image`)数据,并交给 `SnapshotPainter` 进行绘制。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image9.png) |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
|
||||
所以 `SnapshotWidget` 完成图片绘制会需要一个 `SnapshotPainter` ,默认它是通过内置的 `_DefaultSnapshotPainter` 实现,当然我们也可以自定义实现 `SnapshotPainter` 来完成自定义逻辑。
|
||||
|
||||
> 从实现上看,`SnapshotPainter` 用来绘制子控件快照的接口,正如上面代码所示,会根据 child 是否支持捕获(`_childRaster == null`),从而选择调用 `paint` 或 `paintSnapshot` 来实现绘制。
|
||||
|
||||
另外,目前受制于 `toImageSync ` 的底层实现, `SnapshotWidget` 无法捕获 PlatformView 子控件,如果遇到 PlatformView,`SnapshotWidget` 会根据 `SnapshotMode` 来决定它的行为:
|
||||
|
||||
| normal | 默认行为,如果遇到无法捕获快照的子控件,直接 thrown |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| permissive | 宽松行为,遇到无法捕获快照的子控件,使用未快照的子对象渲染 |
|
||||
| forced | 强制行为,遇到无法捕获快照的子控件直接忽略 |
|
||||
|
||||
另外 `SnapshotPainter` 可以通过调用 `notifyListeners` 触发 `SnapshotWidget` 使用相同的光栅进行重绘,简单来说就是:
|
||||
|
||||
> **你可以在不需要重新生成新快照的情况下,对当然快照进行一些缩放、模糊、旋转等效果,这对性能会有很大提升**。
|
||||
|
||||
所以在 `SnapshotPainter` 里主要需要实现的是 `paint` 和 `paintSnapshot` 两个方法:
|
||||
|
||||
- paintSnapshot 是绘制 child 快照时会被调用
|
||||
|
||||
- paint 方法里主要是通过 `painter` (对应 `super.paint`)这个 Callback 绘制 child ,当快照被禁用或者 `permissive` 模式下遭遇 PlatformView 时会调用此方法
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image10.png)
|
||||
|
||||
举个例子,如下代码所示,在 `paintSnapshot` 方法里,通过调整 `Paint ..color` ,可以在前面的小 Logo 快照上添加透明度效果:
|
||||
|
||||
```dart
|
||||
class TestPainter extends SnapshotPainter {
|
||||
final Animation<double> animation;
|
||||
|
||||
TestPainter({
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, ui.Offset offset, Size size,
|
||||
PaintingContextCallback painter) {}
|
||||
|
||||
@override
|
||||
void paintSnapshot(PaintingContext context, Offset offset, Size size,
|
||||
ui.Image image, Size sourceSize, double pixelRatio) {
|
||||
final Rect src = Rect.fromLTWH(0, 0, sourceSize.width, sourceSize.height);
|
||||
final Rect dst =
|
||||
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
||||
final Paint paint = Paint()
|
||||
..color = Color.fromRGBO(0, 0, 0, animation.value)
|
||||
..filterQuality = FilterQuality.low;
|
||||
context.canvas.drawImageRect(image, src, dst, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant TestPainter oldDelegate) {
|
||||
return oldDelegate.animation.value != animation.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image11.gif)
|
||||
|
||||
其实还可以把移动的动画部分挪到 `paintSnapshot` 里,然后通过对 animation 的状态进行管理,然后通过 `notifyListeners` 直接更新快照绘制,这样在性能上会更有优势,Android 上的 `ZoomPageTransitionsBuilder` 就是类似实现。
|
||||
|
||||
```dart
|
||||
animation.addListener(notifyListeners);
|
||||
animation.addStatusListener(_onStatusChange);
|
||||
|
||||
void _onStatusChange(_) {
|
||||
notifyListeners();
|
||||
}
|
||||
@override
|
||||
void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
|
||||
_drawMove(context, offset, size);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
|
||||
switch (animation.status) {
|
||||
case AnimationStatus.completed:
|
||||
case AnimationStatus.dismissed:
|
||||
return painter(context, offset);
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
}
|
||||
....
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
> 更多详细可以参考系统 `ZoomPageTransitionsBuilder` 里的代码实现。
|
||||
|
||||
|
||||
|
||||
# 拓展探索
|
||||
|
||||
其实除了 `SnapshotWidget` 之外,`RepaintBoundary` 也支持了 `toImageSync ` , 因为 `toImageSync ` 获取到的是 GPU 中的常驻数据,所以在**实现类似控件截图和高亮指引等场景绘制上**,理论上应该可以得到更好的性能预期。
|
||||
|
||||
```dart
|
||||
final RenderRepaintBoundary boundary =
|
||||
globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final ui.Image image = boundary.toImageSync();
|
||||
```
|
||||
|
||||
除此之外,`dart:ui `里的 `Scene` 和 `_Image` 对象其实都是 `NativeFieldWrapperClass1` ,以前我们解释过:**`NativeFieldWrapperClass1` 就是它的逻辑是由不同平台的 Engine 区分实现** 。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image12.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image13.png) |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------- |
|
||||
|
||||
> 所以如果你直接在 `flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart `下去断点 `toImageSync` 是无法成功执行到断点位置的,因为它的真实实现在对应平台的 Engine 实现。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image14.png)
|
||||
|
||||
另外,前面我们一直说 `toImageSync` 对比 `toImage` 是 GPU 常驻,那它们的区别在哪里?从上图我们就可以看出:
|
||||
|
||||
- `toImageSync` 执行了 `Scene:RasterizeToImage` 并返回 `Dart_Null` 句柄
|
||||
- `toImage` 执行了 `Picture:RasterizeLayerTreeToImage` 并直接返回
|
||||
|
||||
简单展开来说,就是:
|
||||
|
||||
- `toImageSync` 最终是通过 `SkImage::MakeFromTexture` 通过纹理得到一个 GPU `SkImage` 图片
|
||||
- `toImage` 是通过 `makeImageSnapshot` 和 `makeRasterImage` 生成 `SkImage` , `makeRasterImage` 是一个复制图像到 CPU 内存的操作。
|
||||
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image15.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image16.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image17.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image18.png) |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
|
||||
|
||||
其实一开始 `toImageSync` 是被命令为 `toGpuImage` ,但是为了更形象通用,最后才修改为 `toImageSync` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230207_sync/image19.png)
|
||||
|
||||
而 `toImageSync` 等相关功能的落地可以说同样历经了漫长的讨论,关于是否提供这样一个 API 到最终落地,其执行难度丝毫不比 [background isolate ](https://juejin.cn/post/7195825738472620087) 简单,比如:是否定义异常场景,遇到错误是否需要在Framwork 层消化,是否真的需要这样的接口来提高性能等等。
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image20.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image21.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image22.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image23.png) |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
|
||||
|
||||
而 `toImageSync` 等相关功能最终能落地,其中最重要的一点我认为是:
|
||||
|
||||
> `toGoulmage` gives the framework the ability to take performance into their own hands, which is important given that our priorities don't always line up.
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
`toImageSync` 只是一个简单的 API ,但是它的背后经历了很多故事,同时 `toImageSync` 和它对应的封装 `SnapshotWidget` ,最终的目的就是提高 Flutter 运行的性能。
|
||||
|
||||
也许目前对于你来说 `toImageSync` 并不是必须的,甚至 `SnapshotWidget` 看起来也很鸡肋,但是一旦你需要处理复杂的绘制场景时, `toImageSync` 就是你必不可少的菜刀。
|
118
Flutter-N19.md
118
Flutter-N19.md
|
@ -1,118 +0,0 @@
|
|||
# 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 能给我们带来更好的体验。
|
213
Flutter-N2.md
213
Flutter-N2.md
|
@ -1,213 +0,0 @@
|
|||
# Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3
|
||||
|
||||
**本篇分享一个简单轻松的内容: `ThemeExtensions ` 和 `Material3`** ,它们都是 Flutter 3.0 中的重要组成部分,相信后面的小知识你可能还没了解过~。
|
||||
|
||||
# ThemeExtensions
|
||||
|
||||
相信大家都用过 Flutter 里的 ` Theme` ,在 Flutter 里可以通过修改全局的 ` ThemeData` 就来实现一些样式上的调整,比如 :全局去除 `InkWell` 和 `TextButton` 的点击效果。
|
||||
|
||||
```dart
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
// 去掉 InkWell 的点击水波纹效果
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
// 去除 InkWell 点击的 highlight
|
||||
highlightColor: Colors.transparent,
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
// 去掉 TextButton 的水波纹效果
|
||||
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
当然,开发者也可以通过 `Theme.of(context)` 去读取 `ThemeData` 的一些全局样式,从而让自己的控件配置更加灵活,**但是如果 `ThemeData` 里没有符合你需求的参数,或者你希望这个参数只被特定控件是用,那该怎么办** ?
|
||||
|
||||
Flutter 3 给我们提供了一个解决方案: `ThemeExtensions ` 。
|
||||
|
||||
开发者可以通过继承 `ThemeExtension` 并 override 对应的 `copyWith` 和 `lerp` 方法来自定义需要拓展的 `ThemeData` 参数,比如这样:
|
||||
|
||||
```dart
|
||||
@immutable
|
||||
class StatusColors extends ThemeExtension<StatusColors> {
|
||||
static const light = StatusColors(open: Colors.green, closed: Colors.red);
|
||||
static const dark = StatusColors(open: Colors.white, closed: Colors.brown);
|
||||
|
||||
const StatusColors({required this.open, required this.closed});
|
||||
|
||||
final Color? open;
|
||||
final Color? closed;
|
||||
|
||||
@override
|
||||
StatusColors copyWith({
|
||||
Color? success,
|
||||
Color? info,
|
||||
}) {
|
||||
return StatusColors(
|
||||
open: success ?? this.open,
|
||||
closed: info ?? this.closed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
StatusColors lerp(ThemeExtension<StatusColors>? other, double t) {
|
||||
if (other is! StatusColors) {
|
||||
return this;
|
||||
}
|
||||
return StatusColors(
|
||||
open: Color.lerp(open, other.open, t),
|
||||
closed: Color.lerp(closed, other.closed, t),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'StatusColors('
|
||||
'open: $open, closed: $closed'
|
||||
')';
|
||||
}
|
||||
```
|
||||
|
||||
之后就可以将上面的 `StatusColors` 配置到 `Theme` 的 `extensions` 上,然后通过 ` Theme.of(context).extension<StatusColors>()` 读取配置的参数。
|
||||
|
||||
```dart
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
StatusColors.light,
|
||||
],
|
||||
),
|
||||
|
||||
·····
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
/// get status color from ThemeExtensions
|
||||
final statusColors = Theme.of(context).extension<StatusColors>();
|
||||
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: Container(
|
||||
alignment: Alignment.center,
|
||||
child: new ElevatedButton(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: statusColors?.open,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: new Text("Button")),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
是不是很简单?**通过 `ThemeExtensions ` ,第三方 package 在编写控件时,也可以提供对应的 `ThemeExtensions` 对象,实现更灵活的样式配置支持**。
|
||||
|
||||
# Material3
|
||||
|
||||
Material3 又叫 MaterialYou , 是谷歌在 Android 12 时提出的全新 UI 设计规范,现在 Flutter 3.0 里你可以通过 `useMaterial3: true` 打开配置支持。
|
||||
|
||||
```dart
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
///打开 useMaterial3 样式
|
||||
useMaterial3: true,
|
||||
),
|
||||
```
|
||||
|
||||
当然,**在你开启 Material3 之前,你需要对它有一定了解,因为它对 UI 风格的影响还是很大的,知己知彼才能不被背后捅刀**。
|
||||
|
||||
如下图所示,是在 `primarySwatch: Colors.blue` 的情况下,`AppBar` 、`Card`、`TextButton`、 `ElevatedButton` 的样式区别:
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image1.png)
|
||||
|
||||
可以看到圆角和默认的颜色都发生了变化,并且除了 UI 更加圆润之外,交互效果也发生了一些改变,比如:
|
||||
|
||||
- 点击效果和 `Dialog` 的默认样式都发生了变化;
|
||||
- Android 上列表滚动的默认 `OverscrollIndicator` 效果也发生了改变;
|
||||
|
||||
| 交互 | 列表 |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| ![](http://img.cdn.guoshuyu.cn/20220605_N2/image2.gif) | ![333333](http://img.cdn.guoshuyu.cn/20220605_N2/image3.gif) |
|
||||
|
||||
目前在 Flutter 3 中受到 `useMaterial3` 影响的主要有以下这些 Widget ,可以看到主要影响的还是具有交互效果的 Widget 居多:
|
||||
|
||||
* [AlertDialog]
|
||||
|
||||
* [AppBar]
|
||||
* [Card]
|
||||
* [Dialog]
|
||||
* [ElevatedButton]
|
||||
* [FloatingActionButton]
|
||||
* [Material]
|
||||
* [NavigationBar]
|
||||
* [NavigationRail]
|
||||
* [OutlinedButton]
|
||||
* [StretchingOverscrollIndicator]
|
||||
* [GlowingOverscrollIndicator]
|
||||
* [TextButton]
|
||||
|
||||
**那 Material3 和之前的 Material2 有什么区别呢**?
|
||||
|
||||
以 `AppBar` 举例,可以看到在 M2 和 M3 中背景颜色的获取方式就有所不同,在 M3 下没有了 `Brightness.dark` 的判断,那是说明 M3 不支持暗黑模式吗?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image4.png)
|
||||
|
||||
回答这个问题之前,我们先看 `_TokeDefaultsM3` 有什么特别之处,从源码注释里可以看到 `_TokeDefaultsM3` 是通过脚本自动生成,并且目前版本号是 `v0_92` ,**所以 M3 和 M2 最大的不同之一就是它的样式代码现在是自动生成**。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image5.png)
|
||||
|
||||
在 Flutter 的 [gen_defaults](https://github.com/flutter/flutter/tree/ca2d60e8e2344d8c0ed938869f7c974cb745e841/dev/tools/gen_defaults/lib) 下就可以看到,基本上涉及 M3 的默认样式,都是通过 `data` 下的数据利用模版自动生成,比如 `Appbar` 的 `backgroundColor` 指向的就是 `surface` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image6.png)
|
||||
|
||||
**而之所以 M3 的默认样式不再需要 `Brightness.dark` 的判断,是因为在 M3 使用的 `ColorScheme` 里已经做了判断**。
|
||||
|
||||
![image-20220602214139954](http://img.cdn.guoshuyu.cn/20220605_N2/image7.png)
|
||||
|
||||
**事实上现在 Flutter 3.0 里 `colorScheme` 才是主题颜色的核心,而 `primaryColorBrightness` 和 `primarySwatch` 等参数在未来将会被弃用**,所以如果目前你还在使用 `primarySwatch` ,在 `ThemeData` 内部会通过 `ColorScheme.fromSwatch` 方法转换为 `ColorScheme` 。
|
||||
|
||||
```dart
|
||||
ColorScheme.fromSwatch(
|
||||
primarySwatch: primarySwatch,
|
||||
primaryColorDark: primaryColorDark,
|
||||
accentColor: accentColor,
|
||||
cardColor: cardColor,
|
||||
backgroundColor: backgroundColor,
|
||||
errorColor: errorColor,
|
||||
brightness: effectiveBrightness,
|
||||
);
|
||||
```
|
||||
|
||||
另外你也可以通过 `ColorScheme.fromSeed` 或者 `colorSchemeSeed ` 来直接配置 `ThemeData` 里的 `ColorScheme` ,**那 `ColorScheme` 又是什么** ?
|
||||
|
||||
```dart
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF4285F4)),
|
||||
///打开 useMaterial3 样式
|
||||
useMaterial3: true,
|
||||
),
|
||||
```
|
||||
|
||||
**这里其实就涉及到一个很有趣的知识点:Material3 下的 HCT 颜色包: [material-color-utilities](https://github.com/material-foundation/material-color-utilities)** 。
|
||||
|
||||
在 Material3 下颜色其实不是完全按照 RGB 去计算,而是会经过 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 的转化,通过内部的 `CorePalette` 对象,RGB 会转化为 HCT 相关的值去计算显示。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image8.png)
|
||||
|
||||
对于 HCT 其实是 Hue、Chroma、Tone 三个单词的缩写,可以解释为色相、色度和色调,通过谷歌开源的 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 插件就可以方便实现 HCT 颜色空间的接入,目前该 repo 已支持 Dart、Java 和 Typecript 等语言,另外 C/C++ 和 Object-C 也在即将支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image9.png)
|
||||
|
||||
得益于 HCT ,例如我们前面的 `ColorScheme.fromSeed(seedColor: Color(0xFF4285F4))`,就可以通过一个 seedColor 直接生成一系列主题颜色,这就是 Material3 里可以拥有更丰富的主题色彩的原因。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20220605_N2/image10.png)
|
||||
|
||||
> 更多可见 [《HCT 的色彩原理》](https://material.io/blog/science-of-color-design)
|
||||
|
||||
# 最后
|
||||
|
||||
最后我们回顾一下,今天的小技巧有:
|
||||
|
||||
- 通过 `ThemeExtensions` 拓展想要的自定义 `ThemeData`
|
||||
- 通过 `useMaterial3` 启用 Material3 ,并通过 `ColorScheme` 配置更丰富的 HCT 颜色
|
||||
|
||||
好了,现在你可以去问你的设计师:你知道什么是 HCT 么?
|
823
Flutter-N20.md
823
Flutter-N20.md
|
@ -1,823 +0,0 @@
|
|||
# Flutter 小技巧之实现一个精美的动画相册效果
|
||||
|
||||
今天的小技巧主要是「抄袭」一个充满设计感的相册控件,如下图所示是 [gskinner](https://gskinner.com/) 开源应用 [wonderous](https://github.com/gskinnerTeam/flutter-wonderous-app) 里一个相片集的实现效果,可以看到**相册支持上下左右滑动,并带有高亮展示的动画效果,而且相册整体布局可以超出屏幕滚动**,因为是开源的 App, 我们只需要「照搬」就可以实现一摸一样的效果,那么如果要实现这样的效果,你第一反应是用什么基础控件?
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230317_W/image1.gif)
|
||||
|
||||
因为需要支持上下左右自由滑动,可能大家第一反应会是 `Table` ,还是嵌套两个 `ListView` ?但是从上面的效果体验上看,控件滑动的过程并不是一个正常 Scroll 控件的线性效果,因为它并不是「跟随手指滑动」的状态。
|
||||
|
||||
既然是开源代码,我们通过源码可以发现它是用了 `GridView` 来实现,这也是这个效果里最有趣的点,一个 `GridView` 如何变成一个带有动画的 Photo Gallery 。
|
||||
|
||||
> **所以本篇的核心是分析 wonderous 里的 Photo Gallery 是如何实现的,并剥离出简单代码**。
|
||||
|
||||
# Photo Gallery
|
||||
|
||||
要实现上述的 Photo Gallery 效果,主要需要解决三个方面核心的要点:
|
||||
|
||||
- 1、`GridView` 所在区域的上下左右要超出屏幕
|
||||
- 2、`GridView` 如何实现上下左右自由切换
|
||||
- 3、高亮展示选中 Item 的动画效果
|
||||
|
||||
首先是第一点的方案肯定是 `OverflowBox` ,因它支持解放 Child 的布局约束,允许 Child 溢出父布局,因为前面的 Photo Gallery 在水平方向设定是 5 个 Item,而 `GridView` 是默认是上下滑动,所以可以简单的设定一个 `maxWidth` 和 `maxHeight` 来作为 Child 超出屏幕后大小。
|
||||
|
||||
```dart
|
||||
OverflowBox(
|
||||
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
|
||||
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
|
||||
alignment: Alignment.center,
|
||||
child:
|
||||
```
|
||||
|
||||
可以看到「超出屏幕」这个需求还是比较简单,接下里就是 「`GridView` 如何实现上下左右自由切换」这个问题。
|
||||
|
||||
> **小技巧 1 :在合适场合使用 OverflowBox 可以溢出屏幕**
|
||||
|
||||
默认情况下 `GridView` 肯定只支持一个方向滑动,所以干脆我们禁止 `GridView` 的滑动逻辑,让 `GridView` 只管布局,后面滑动逻辑通过自定义的 `GestureDetector` 来实现。
|
||||
|
||||
```dart
|
||||
GridView.count(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
```
|
||||
|
||||
如下代码所示,我们通过封装 `GestureDetector` 来实现手势识别,这里核心的要点就是 `_maybeTriggerSwipe` 的实现,它的作用就是得到手势滑动的方向结果,对于滑动具体大于 `threshold` 的参数,通过「采样」将数据变成 -1、 0 、 1 这样的结果来代表方向:
|
||||
|
||||
- Offset(1.0, 0.0) 是手指右滑
|
||||
- Offset(-1.0, 0.0) 是手指左滑
|
||||
- Offset(0.0, 1.0) 是手指下滑
|
||||
- Offset(0.0, -1.0) 是手指上滑
|
||||
|
||||
```dart
|
||||
class _EightWaySwipeDetectorState extends State<EightWaySwipeDetector> {
|
||||
Offset _startPos = Offset.zero;
|
||||
Offset _endPos = Offset.zero;
|
||||
bool _isSwiping = false;
|
||||
|
||||
void _resetSwipe() {
|
||||
_startPos = _endPos = Offset.zero;
|
||||
_isSwiping = false;
|
||||
}
|
||||
|
||||
///这里主要是返回一个 -1 ~ 1 之间的数值,具体用于判断方向
|
||||
/// Offset(1.0, 0.0) 是手指右滑
|
||||
/// Offset(-1.0, 0.0) 是手指左滑
|
||||
/// Offset(0.0, 1.0) 是手指下滑
|
||||
/// Offset(0.0, -1.0) 是手指上滑
|
||||
void _maybeTriggerSwipe() {
|
||||
// Exit early if we're not currently swiping
|
||||
if (_isSwiping == false) return;
|
||||
|
||||
/// 开始和结束位置计算出移动距离
|
||||
// Get the distance of the swipe
|
||||
Offset moveDelta = _endPos - _startPos;
|
||||
final distance = moveDelta.distance;
|
||||
|
||||
/// 对比偏移量大小是否超过了 threshold ,不能小于 1
|
||||
// Trigger swipe if threshold has been exceeded, if threshold is < 1, use 1 as a minimum value.
|
||||
if (distance >= max(widget.threshold, 1)) {
|
||||
// Normalize the dx/dy values between -1 and 1
|
||||
moveDelta /= distance;
|
||||
// Round the dx/dy values to snap them to -1, 0 or 1, creating an 8-way directional vector.
|
||||
Offset dir = Offset(
|
||||
moveDelta.dx.roundToDouble(),
|
||||
moveDelta.dy.roundToDouble(),
|
||||
);
|
||||
widget.onSwipe?.call(dir);
|
||||
_resetSwipe();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipeStart(d) {
|
||||
_isSwiping = true;
|
||||
_startPos = _endPos = d.localPosition;
|
||||
}
|
||||
|
||||
void _handleSwipeUpdate(d) {
|
||||
_endPos = d.localPosition;
|
||||
_maybeTriggerSwipe();
|
||||
}
|
||||
|
||||
void _handleSwipeEnd(d) {
|
||||
_maybeTriggerSwipe();
|
||||
_resetSwipe();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPanStart: _handleSwipeStart,
|
||||
onPanUpdate: _handleSwipeUpdate,
|
||||
onPanCancel: _resetSwipe,
|
||||
onPanEnd: _handleSwipeEnd,
|
||||
child: widget.child);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **小技巧 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<Offset>(
|
||||
tween: Tween(begin: gridOffset, end: gridOffset),
|
||||
duration: offsetTweenDuration,
|
||||
curve: Curves.easeOut,
|
||||
builder: (_, value, child) =>
|
||||
Transform.translate(offset: value, child: child),
|
||||
child: GridView.count(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
```
|
||||
|
||||
解决完移动,最后就是实现蒙层和高亮动画效果,这个的核心主要是通过 `flutter_animate` 包和 `ClipPath` 实现,如下代码所示:
|
||||
|
||||
- 使用 `Animate` 并在上面添加一个具有透明度的黑色 `Container`
|
||||
- 利用 `CustomEffect` 添加自定义动画
|
||||
- 在动画里利用 `ClipPath` ,并通过自定义 `CustomClipper` 结合动画 value 实现 `PathOperation.difference` 的「挖空」效果
|
||||
|
||||
> 动画效果就是根据 `Animate` 的 value 得到的 `cutoutSize` ,默认是从 `1 - 0.25 * x` 开始,这里的 x 是滑动方向,最终表现就是从 0.75 到 1 的过程,所以动画会根据方向有一个从 0.75 到 1 的展开效果。
|
||||
|
||||
```dart
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
// 用 ClipPath 做一个动画抠图
|
||||
Animate(
|
||||
effects: [
|
||||
CustomEffect(
|
||||
builder: _buildAnimatedCutout,
|
||||
curve: Curves.easeOut,
|
||||
duration: duration)
|
||||
],
|
||||
key: animationKey,
|
||||
onComplete: (c) => c.reverse(),
|
||||
// 用一个黑色的蒙层,这里的 child 会变成 effects 里 builder 里的 child
|
||||
// 也就是黑色 Container 会在 _buildAnimatedCutout 作为 ClipPath 的 child
|
||||
child: IgnorePointer(
|
||||
child: Container(color: Colors.black.withOpacity(opacity))),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Scales from 1 --> (1 - scaleAmt) --> 1
|
||||
Widget _buildAnimatedCutout(BuildContext context, double anim, Widget child) {
|
||||
// controls how much the center cutout will shrink when changing images
|
||||
const scaleAmt = .25;
|
||||
final size = Size(
|
||||
cutoutSize.width * (1 - scaleAmt * anim * swipeDir.dx.abs()),
|
||||
cutoutSize.height * (1 - scaleAmt * anim * swipeDir.dy.abs()),
|
||||
);
|
||||
return ClipPath(clipper: _CutoutClipper(size), child: child);
|
||||
}
|
||||
|
||||
class _CutoutClipper extends CustomClipper<Path> {
|
||||
_CutoutClipper(this.cutoutSize);
|
||||
|
||||
final Size cutoutSize;
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
double padX = (size.width - cutoutSize.width) / 2;
|
||||
double padY = (size.height - cutoutSize.height) / 2;
|
||||
|
||||
return Path.combine(
|
||||
PathOperation.difference,
|
||||
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
|
||||
Path()
|
||||
..addRRect(
|
||||
RRect.fromLTRBR(
|
||||
padX,
|
||||
padY,
|
||||
size.width - padX,
|
||||
size.height - padY,
|
||||
Radius.circular(6),
|
||||
),
|
||||
)
|
||||
..close(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(_CutoutClipper oldClipper) =>
|
||||
oldClipper.cutoutSize != cutoutSize;
|
||||
}
|
||||
```
|
||||
|
||||
从这里可以看到,其实高亮的效果就是在黑色的蒙层上,利用 ` PathOperation.difference` 「挖」出来一个空白的 Path 。
|
||||
|
||||
> **小技巧 3 : ` PathOperation.difference` 可以用在需要「镂空」 的场景上**。
|
||||
|
||||
更直观的可以参考一下例子,就是对两个路径进行 difference 操作,,利用 Rect2 把 Rect1 中间给消除掉,得到一个中间 「镂空」的绘制 Path。
|
||||
|
||||
```dart
|
||||
class ShowPathDifference extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('ShowPathDifference'),
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage("static/gsy_cat.png"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: CustomPaint(
|
||||
painter: ShowPathDifferencePainter(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowPathDifferencePainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
paint.color = Colors.blue.withAlpha(160);
|
||||
canvas.drawPath(
|
||||
Path.combine(
|
||||
PathOperation.difference,
|
||||
Path()
|
||||
..addRRect(
|
||||
RRect.fromLTRBR(-150, -150, 150, 150, Radius.circular(10))),
|
||||
Path()
|
||||
..addOval(Rect.fromCircle(center: Offset(0, 0), radius: 100))
|
||||
..close(),
|
||||
),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230317_W/image4.png)
|
||||
|
||||
最终效果如下图所依,这里是把 wonderous 里关键部分代码剥离出来后的效果,因为 wonderous 并没有把这部分代码封装为 package ,所以我把这部分代码剥离出来放在了后面,感兴趣的可以自己运行试试效果。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20230317_W/image5.gif)
|
||||
|
||||
## 源码
|
||||
|
||||
```dart
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
/// 来自 https://github.com/gskinnerTeam/flutter-wonderous-app 上的一个 UI 效果
|
||||
class PhotoGalleryDemoPage extends StatefulWidget {
|
||||
const PhotoGalleryDemoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PhotoGalleryDemoPage> createState() => _PhotoGalleryDemoPageState();
|
||||
}
|
||||
|
||||
class _PhotoGalleryDemoPageState extends State<PhotoGalleryDemoPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PhotoGallery();
|
||||
}
|
||||
}
|
||||
|
||||
class PhotoGallery extends StatefulWidget {
|
||||
const PhotoGallery({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PhotoGallery> createState() => _PhotoGalleryState();
|
||||
}
|
||||
|
||||
class _PhotoGalleryState extends State<PhotoGallery> {
|
||||
static const int _gridSize = 5;
|
||||
|
||||
late List<Color> colorList;
|
||||
|
||||
// Index starts in the middle of the grid (eg, 25 items, index will start at 13)
|
||||
int _index = ((_gridSize * _gridSize) / 2).round();
|
||||
|
||||
Offset _lastSwipeDir = Offset.zero;
|
||||
|
||||
bool _skipNextOffsetTween = false;
|
||||
|
||||
///根据屏幕尺寸,决定 Padding 的大小,通过 scale 缩放
|
||||
_getPadding(Size size) {
|
||||
double scale = 1;
|
||||
final shortestSide = size.shortestSide;
|
||||
const tabletXl = 1000;
|
||||
const tabletLg = 800;
|
||||
const tabletSm = 600;
|
||||
const phoneLg = 400;
|
||||
if (shortestSide > tabletXl) {
|
||||
scale = 1.25;
|
||||
} else if (shortestSide > tabletLg) {
|
||||
scale = 1.15;
|
||||
} else if (shortestSide > tabletSm) {
|
||||
scale = 1;
|
||||
} else if (shortestSide > phoneLg) {
|
||||
scale = .9; // phone
|
||||
} else {
|
||||
scale = .85; // small phone
|
||||
}
|
||||
return 24 * scale;
|
||||
}
|
||||
|
||||
int get _imgCount => pow(_gridSize, 2).round();
|
||||
|
||||
Widget _buildImage(int index, Size imgSize) {
|
||||
/// Bind to collectibles.statesById because we might need to rebuild if a collectible is found.
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: imgSize.width,
|
||||
height: imgSize.height,
|
||||
color: colorList[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts a swipe direction into a new index
|
||||
void _handleSwipe(Offset dir) {
|
||||
// Calculate new index, y swipes move by an entire row, x swipes move one index at a time
|
||||
int newIndex = _index;
|
||||
|
||||
/// Offset(1.0, 0.0) 是手指右滑
|
||||
/// Offset(-1.0, 0.0) 是手指左滑
|
||||
/// Offset(0.0, 1.0) 是手指下滑
|
||||
/// Offset(0.0, -1.0) 是手指上滑
|
||||
|
||||
/// dy > 0 ,就是手指下滑,也就是页面要往上,那么 index 就需要 -1,反过来就是 + 1
|
||||
if (dir.dy != 0) newIndex += _gridSize * (dir.dy > 0 ? -1 : 1);
|
||||
|
||||
/// dx > 0 ,就是手指右滑,也就是页面要往左,那么 index 就需要 -1,反过来就是 + 1
|
||||
if (dir.dx != 0) newIndex += (dir.dx > 0 ? -1 : 1);
|
||||
|
||||
///这里判断下 index 是不是超出位置
|
||||
// After calculating new index, exit early if we don't like it...
|
||||
if (newIndex < 0 || newIndex > _imgCount - 1)
|
||||
return; // keep the index in range
|
||||
if (dir.dx < 0 && newIndex % _gridSize == 0)
|
||||
return; // prevent right-swipe when at right side
|
||||
if (dir.dx > 0 && newIndex % _gridSize == _gridSize - 1)
|
||||
return; // prevent left-swipe when at left side
|
||||
/// 响应
|
||||
_lastSwipeDir = dir;
|
||||
HapticFeedback.lightImpact();
|
||||
_setIndex(newIndex);
|
||||
}
|
||||
|
||||
void _setIndex(int value, {bool skipAnimation = false}) {
|
||||
print("######## $value");
|
||||
if (value < 0 || value >= _imgCount) return;
|
||||
_skipNextOffsetTween = skipAnimation;
|
||||
setState(() => _index = value);
|
||||
}
|
||||
|
||||
/// Determine the required offset to show the current selected index.
|
||||
/// index=0 is top-left, and the index=max is bottom-right.
|
||||
Offset _calculateCurrentOffset(double padding, Size size) {
|
||||
/// 获取水平方向一半的大小,默认也就是 2.0,因为 floorToDouble
|
||||
double halfCount = (_gridSize / 2).floorToDouble();
|
||||
|
||||
/// Item 大小加上 Padding,也就是每个 Item 的实际大小
|
||||
Size paddedImageSize = Size(size.width + padding, size.height + padding);
|
||||
|
||||
/// 计算出开始位置的 top-left
|
||||
// Get the starting offset that would show the top-left image (index 0)
|
||||
final originOffset = Offset(
|
||||
halfCount * paddedImageSize.width, halfCount * paddedImageSize.height);
|
||||
|
||||
/// 得到要移动的 index 所在的行和列位置
|
||||
// Add the offset for the row/col
|
||||
int col = _index % _gridSize;
|
||||
int row = (_index / _gridSize).floor();
|
||||
|
||||
/// 负数计算出要移动的 index 的 top-left 位置,比如 index 比较小,那么这个 indexedOffset 就比中心点小,相减之后 Offset 就会是正数
|
||||
/// 是不是有点懵逼?为什么正数 translate 会往 index 小的 方向移动??
|
||||
/// 因为你代入的不对,我们 translate 移动的是整个 GridView
|
||||
/// 正数是向左向下移动,自然就把左边或者上面的 Item 显示出来
|
||||
final indexedOffset =
|
||||
Offset(-paddedImageSize.width * col, -paddedImageSize.height * row);
|
||||
|
||||
return originOffset + indexedOffset;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
colorList = List.generate(
|
||||
_imgCount,
|
||||
(index) => Color((Random().nextDouble() * 0xFFFFFF).toInt())
|
||||
.withOpacity(1));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var mq = MediaQuery.of(context);
|
||||
var width = mq.size.width;
|
||||
var height = mq.size.height;
|
||||
bool isLandscape = mq.orientation == Orientation.landscape;
|
||||
|
||||
///根据横竖屏状态决定 Item 大小
|
||||
Size imgSize = isLandscape
|
||||
? Size(width * .5, height * .66)
|
||||
: Size(width * .66, height * .5);
|
||||
|
||||
var padding = _getPadding(mq.size);
|
||||
|
||||
final cutoutTweenDuration =
|
||||
_skipNextOffsetTween ? Duration.zero : Duration(milliseconds: 600) * .5;
|
||||
|
||||
final offsetTweenDuration =
|
||||
_skipNextOffsetTween ? Duration.zero : Duration(milliseconds: 600) * .4;
|
||||
|
||||
var gridOffset = _calculateCurrentOffset(padding, imgSize);
|
||||
gridOffset += Offset(0, -mq.padding.top / 2);
|
||||
|
||||
//动画效果
|
||||
return _AnimatedCutoutOverlay(
|
||||
animationKey: ValueKey(_index),
|
||||
cutoutSize: imgSize,
|
||||
swipeDir: _lastSwipeDir,
|
||||
duration: cutoutTweenDuration,
|
||||
opacity: .7,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
// Place content in overflow box, to allow it to flow outside the parent
|
||||
child: OverflowBox(
|
||||
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
|
||||
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
|
||||
alignment: Alignment.center,
|
||||
// 手势获取方向上下左右
|
||||
child: EightWaySwipeDetector(
|
||||
onSwipe: _handleSwipe,
|
||||
threshold: 30,
|
||||
// A tween animation builder moves from image to image based on current offset
|
||||
child: TweenAnimationBuilder<Offset>(
|
||||
tween: Tween(begin: gridOffset, end: gridOffset),
|
||||
duration: offsetTweenDuration,
|
||||
curve: Curves.easeOut,
|
||||
builder: (_, value, child) =>
|
||||
Transform.translate(offset: value, child: child),
|
||||
child: GridView.count(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: _gridSize,
|
||||
childAspectRatio: imgSize.aspectRatio,
|
||||
mainAxisSpacing: padding,
|
||||
crossAxisSpacing: padding,
|
||||
children:
|
||||
List.generate(_imgCount, (i) => _buildImage(i, imgSize)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EightWaySwipeDetector extends StatefulWidget {
|
||||
const EightWaySwipeDetector(
|
||||
{Key? key,
|
||||
required this.child,
|
||||
this.threshold = 50,
|
||||
required this.onSwipe})
|
||||
: super(key: key);
|
||||
final Widget child;
|
||||
final double threshold;
|
||||
final void Function(Offset dir)? onSwipe;
|
||||
|
||||
@override
|
||||
State<EightWaySwipeDetector> createState() => _EightWaySwipeDetectorState();
|
||||
}
|
||||
|
||||
class _EightWaySwipeDetectorState extends State<EightWaySwipeDetector> {
|
||||
Offset _startPos = Offset.zero;
|
||||
Offset _endPos = Offset.zero;
|
||||
bool _isSwiping = false;
|
||||
|
||||
void _resetSwipe() {
|
||||
_startPos = _endPos = Offset.zero;
|
||||
_isSwiping = false;
|
||||
}
|
||||
|
||||
///这里主要是返回一个 -1 ~ 1 之间的数值,具体用于判断方向
|
||||
/// Offset(1.0, 0.0) 是手指右滑
|
||||
/// Offset(-1.0, 0.0) 是手指左滑
|
||||
/// Offset(0.0, 1.0) 是手指下滑
|
||||
/// Offset(0.0, -1.0) 是手指上滑
|
||||
void _maybeTriggerSwipe() {
|
||||
// Exit early if we're not currently swiping
|
||||
if (_isSwiping == false) return;
|
||||
|
||||
/// 开始和结束位置计算出移动距离
|
||||
// Get the distance of the swipe
|
||||
Offset moveDelta = _endPos - _startPos;
|
||||
final distance = moveDelta.distance;
|
||||
|
||||
/// 对比偏移量大小是否超过了 threshold ,不能小于 1
|
||||
// Trigger swipe if threshold has been exceeded, if threshold is < 1, use 1 as a minimum value.
|
||||
if (distance >= max(widget.threshold, 1)) {
|
||||
// Normalize the dx/dy values between -1 and 1
|
||||
moveDelta /= distance;
|
||||
// Round the dx/dy values to snap them to -1, 0 or 1, creating an 8-way directional vector.
|
||||
Offset dir = Offset(
|
||||
moveDelta.dx.roundToDouble(),
|
||||
moveDelta.dy.roundToDouble(),
|
||||
);
|
||||
widget.onSwipe?.call(dir);
|
||||
_resetSwipe();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipeStart(d) {
|
||||
_isSwiping = true;
|
||||
_startPos = _endPos = d.localPosition;
|
||||
}
|
||||
|
||||
void _handleSwipeUpdate(d) {
|
||||
_endPos = d.localPosition;
|
||||
_maybeTriggerSwipe();
|
||||
}
|
||||
|
||||
void _handleSwipeEnd(d) {
|
||||
_maybeTriggerSwipe();
|
||||
_resetSwipe();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPanStart: _handleSwipeStart,
|
||||
onPanUpdate: _handleSwipeUpdate,
|
||||
onPanCancel: _resetSwipe,
|
||||
onPanEnd: _handleSwipeEnd,
|
||||
child: widget.child);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedCutoutOverlay extends StatelessWidget {
|
||||
const _AnimatedCutoutOverlay(
|
||||
{Key? key,
|
||||
required this.child,
|
||||
required this.cutoutSize,
|
||||
required this.animationKey,
|
||||
this.duration,
|
||||
required this.swipeDir,
|
||||
required this.opacity})
|
||||
: super(key: key);
|
||||
final Widget child;
|
||||
final Size cutoutSize;
|
||||
final Key animationKey;
|
||||
final Offset swipeDir;
|
||||
final Duration? duration;
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
// 用 ClipPath 做一个动画抠图
|
||||
Animate(
|
||||
effects: [
|
||||
CustomEffect(
|
||||
builder: _buildAnimatedCutout,
|
||||
curve: Curves.easeOut,
|
||||
duration: duration)
|
||||
],
|
||||
key: animationKey,
|
||||
onComplete: (c) => c.reverse(),
|
||||
// 用一个黑色的蒙层,这里的 child 会变成 effects 里 builder 里的 child
|
||||
// 也就是黑色 Container 会在 _buildAnimatedCutout 作为 ClipPath 的 child
|
||||
child: IgnorePointer(
|
||||
child: Container(color: Colors.black.withOpacity(opacity))),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Scales from 1 --> (1 - scaleAmt) --> 1
|
||||
Widget _buildAnimatedCutout(BuildContext context, double anim, Widget child) {
|
||||
// controls how much the center cutout will shrink when changing images
|
||||
const scaleAmt = .25;
|
||||
final size = Size(
|
||||
cutoutSize.width * (1 - scaleAmt * anim * swipeDir.dx.abs()),
|
||||
cutoutSize.height * (1 - scaleAmt * anim * swipeDir.dy.abs()),
|
||||
);
|
||||
print("### anim ${anim} ");
|
||||
return ClipPath(clipper: _CutoutClipper(size), child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an overlay with a hole in the middle of a certain size.
|
||||
class _CutoutClipper extends CustomClipper<Path> {
|
||||
_CutoutClipper(this.cutoutSize);
|
||||
|
||||
final Size cutoutSize;
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
double padX = (size.width - cutoutSize.width) / 2;
|
||||
double padY = (size.height - cutoutSize.height) / 2;
|
||||
|
||||
return Path.combine(
|
||||
PathOperation.difference,
|
||||
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
|
||||
Path()
|
||||
..addRRect(
|
||||
RRect.fromLTRBR(
|
||||
padX,
|
||||
padY,
|
||||
size.width - padX,
|
||||
size.height - padY,
|
||||
Radius.circular(6),
|
||||
),
|
||||
)
|
||||
..close(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(_CutoutClipper oldClipper) =>
|
||||
oldClipper.cutoutSize != cutoutSize;
|
||||
}
|
||||
|
||||
class ShowPathDifference extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('ShowPathDifference'),
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage("static/gsy_cat.png"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: CustomPaint(
|
||||
painter: ShowPathDifferencePainter(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowPathDifferencePainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
paint.color = Colors.blue.withAlpha(160);
|
||||
canvas.drawPath(
|
||||
Path.combine(
|
||||
PathOperation.difference,
|
||||
Path()
|
||||
..addRRect(
|
||||
RRect.fromLTRBR(-150, -150, 150, 150, Radius.circular(10))),
|
||||
Path()
|
||||
..addOval(Rect.fromCircle(center: Offset(0, 0), radius: 100))
|
||||
..close(),
|
||||
),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue