This commit is contained in:
guoshuyu 2022-03-28 14:36:58 +08:00
parent e1fc305920
commit 1b11856198
17 changed files with 2776 additions and 6 deletions

64
Dart-216.md Normal file
View File

@ -0,0 +1,64 @@
> 原文链接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

View File

@ -58,4 +58,26 @@
* [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 Web 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md)

74
Flutter-2022-roadmap.md Normal file
View File

@ -0,0 +1,74 @@
最近 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 全平台正式发布更主流。**

242
Flutter-210-FIX.md Normal file
View File

@ -0,0 +1,242 @@
> **相信大家已经都在对 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 Normal file
View File

@ -0,0 +1,179 @@
> 原文链接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 之后删除了已弃用的 APIhttps://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 Clienthttps://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),帮助我们识别这些重大变化。

209
Flutter-Chat2.md Normal file
View File

@ -0,0 +1,209 @@
聊天列表是一个很扣细节的场景,在之前的 [《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

488
Flutter-DevFest2021.md Normal file
View File

@ -0,0 +1,488 @@
> 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)

168
Flutter-FontFeature.md Normal file
View File

@ -0,0 +1,168 @@
在以前的 [《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'`
| 名称 | 值 |
| ----------------------- | --------------------------- |
| AndroidFuchsiaLinux | Roboto |
| iOS | .SF UI Display.SF UI Text |
| MacOS | .AppleSystemUIFont |
| Windows | Segoe UI |
> 例如:**.SF Text 适用于更小的字体;.SF Display 则适用于偏大的字体,我记得分水岭好像是 20pt 左右,不过 SFSan 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)
**所以可以看到,字体相关是一个平时很少会深入接触的东西,但是一旦涉及多语言和绘制,就很容易碰到问题的领域**。

153
Flutter-HV.md Normal file
View File

@ -0,0 +1,153 @@
对于使用过 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 的技术演进速度真的好快。

422
Flutter-Riverpod.md Normal file
View File

@ -0,0 +1,422 @@
随着 Flutter 的发展,这些年 Flutter 上的状态管理框架如“雨后春笋”般层出不穷,而**近一年以来最受官方推荐的状态管理框架无疑就是 `Riverpod`** ,甚至已经超过了 `Provider` ,事实上 `Riverpod` 官方也称自己为 “`Provider`,但与众不同”。
> `Provider` 本身用它自己的话来说是 “`InheritedWidget` 的封装,但更简单且复用能力更强。” ,而 `Riverpod` 就是在 `Provider` 的基础上重构了新的可能。
关于过去一年状态管理框架的对比可以看 [《2021 年的 Flutter 状态管理:如何选择?》](https://juejin.cn/post/7061784793150652452) **本文主要是带你解剖 `RiverPod` 的内部是如何实现,理解它的工作原理,以及如何做到比 `Provider` 更少的模板和不依赖 `BuildContext` 。**
## 前言
如果说 `Riverpod` 最明显的特点是什么,那就是外部不依赖 `BuildContext` (其实就是换了另外一种依赖形态),因为不依赖 `BuildContext` ,所以它可以比较简单做到类似如下的效果:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image1)
也就是 **`Riverpod` 中的 `Provider` 可以随意写成全局,并且不依赖 `BuildContext` 来编写我们需要的业务逻辑**。
> ⚠️ 提前声明下,**这里和后续的 `Provider` ,和第三方库 [*`provider`*](https://pub.flutter-io.cn/packages/provider) 没有关系**。
`Riverpod` 具体内部是怎么实现的呢?接下来让我们开始探索 `Riverpod` 的实现原理。
> `Riverpod` 的实现相对还是比较复杂,所以还耐心往下看,因为本篇是逐步解析,**所以如果看的过程有些迷惑可以先不必在意,通篇看完再回过来翻阅可能就会更加明朗**。
## 从 ProviderScope 开始
在 Flutter 里只要使用了状态管理,就一定避不开 `InheritedWidget` Riverpod 里也一样,**在 Riverpod 都会有一个 `ProviderScope` 一般只需要注册一个顶级的 `ProviderScope`。**
> 如果对于 InheritedWidget 还有疑问,可以看我掘金:[《全面理解State与Provider》](https://juejin.cn/post/6844903866706706439#heading-5)
先从一个例子开始,如下图所示,是官方的一个简单的例子,可以看到这里:
- 嵌套一个顶级 `ProviderScope`
- 创建了一个全局的 `StateProvider`
- 使用 `ConsumerWidget``ref` 对创建的 `counterProvider` 进行 `read` 从而读取 State ,获取到 `int` 值进行增加
- 使用另一个 `Consumer``ref` 对创建的 `counterProvider` 进行 `watch` ,从而读取到每次改变后的 `int` 值;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image2)
很简单的例子,**可以看到没有任何 `of(context)` 而全局的 `counterProvider` 里的数据,就可以通过 `ref` 进行 read/watch并且正确地读取和更新。**
> 那这是怎么实现的呢?`counterProvider` 又是如何被注入到 `ProviderScope` 里面?为什么没有看到 `context` 带着这些疑问我们继续往下探索。
首先我们看 `ProviderScope` ,它是唯一的顶级 `InheritedWidget` ,所以 `counterProvider` 必定是被存放在这里:
> 在 RiverPod 里, **`ProviderScope` 最大的作用就是提供一个 `ProviderContainer`** 。
更具体地说,就是通过内部嵌套的 `UncontrolledProviderScope` 提供,所以到这里我们可以知道:**`ProviderScope` 可以往下提供状态共享,因为它内部有一个 `InheritedWidget` ,而主要往下共享的是 `ProviderContainer` 这个类**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image3)
所以首先可以猜测:**我们定义的各种 Providers 比如上面的 `counterProvider` 都是被存到 `ProviderContainer` 中,然后往下共享。**
> 事实上官方对于 `ProviderContainer` 的定义就是:*用于保存各种 Providers 的 State ,并且支持 override 一些特殊 Providers 的行为*。
## ProviderContainer
这里出现了一个新的类,叫 `ProviderContainer` ,其实一般情况下使用 RiverPod 你都不需要知道它,**因为你不会直接操作和使用它,但是你使用 RiverPod 的每个行为都会涉及到它的实现**,例如
- `ref.read` 会需要它的 `Result read<Result>`
- `ref.watch` 会需要它的 `ProviderSubscription<State> listen<State>`
- `ref.refresh` 会需要它的 `Created refresh<Created>`
就算是各种 `Provider` 的保存和读取基本也和它有关系,所以它作为一个对各种 `Provider` 的内部管理的类,实现了 RiverPod 里很关键的一些逻辑。
## “Provider” 和 “Element”
那前面我们知道 `ProviderScope` 往下共享了 `ProviderContainer` 之后,**`Provider` 又是怎么工作的呢?为什么 `ref.watch`/ `ref.read` 会可以读取到它 `Provider` 里的值?**
继续前面的代码,这里只是定义了 `StateProvider` ,并且使用了 `ref.watch` ,为什么就可以读取到里面的 `state` 值?
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image4)
首先 `StateProvider` 是一个特殊的 `Provider` ,在它的内部还有一个叫 `_NotifierProvider` 的帮它实现了一层转换,**所以我们先用最基础的 `Provider` 类作为分析对象**。
基本是各种类似的 `Provider` 都是 `ProviderBase` 的子类,所以我们先解析 `ProviderBase`
在 RiverPod 内部,**每个 `ProviderBase` 的子类都会有其对应的 `ProviderElementBase` 子类实现** ,例如前面代码使用的 `StateProvider``ProviderBase` 的之类,同样它也有对应的 `StateProviderElement``ProviderElementBase` 的子类;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image5)
**所以 RiverPod 里基本是每一个 “Provider” 都会有一个自己的 “Element” 。**
> ⚠️**这里的 “Element” 不是 Flutter 概念里三棵树的 `Element`,它是 RiverPod 里 `Ref` 对象的子类**。`Ref` 主要提供 RiverPod 内的 “Provider” 之间交互的接口,并且提供一些抽象的生命周期方法,所以它是 RiverPod 里的独有的 “Element” 单位。
那 “Provider” 和 “Element” 的作用是什么?
首先,在上面例子里我们**构建 `StateProvider` 时传入的 `(ref) => 0` ,其实就是 `Create<State, StateProviderRef<State>>`** 函数,我们就从这个 `Create` 函数作为入口来探索。
## Create<T, R extends Ref> = T Function(R ref)
RiverPod 里构建 “Provider” 时都会传入一个 `Create` 函数,而这个函数里一遍我们会写一些需要的业务逻辑,比如 `counterProvider` 里的 `()=> 0` 就是初始化时返回一个 `int` 为 0 的值,**更重要的是决定了 `State` 的类型**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image6)
如果在上面代码的基础上增加了 `<int>` 就更明显,**事实上前面我们一直在说的 `State` 就是一个泛型,而我们定义 “Provider” 就需要定义这个泛型 `State` 的类型,比如这里的 `int`** 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image7)
回归到普通 `Provider` 的调用,**我们传入的 `Create` 函数,其实就是在 `ProviderElementBase` 里被调用执行**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image8)
如上图所示,简单来说当 **`ProviderElementBase` 执行 “setState” 时,就会调用 `Create` 函数,从而执行获取到我们定义的泛型 `State`,得到 `Result` 然后通知并更新 UI**。
> ⚠️ 这里的 “setState” 也不是 Flutter Framework 里的 `setState` ,而是 RiverPod 内自己首先的一个 “setState” 函数,和 Flutter 框架里的 `State` 无关。
所以每个 “Provider” 都会有自己的 “Element” ,而构建 “Provider” 时是传入的 `Create` 函数会在 “Element” 内通过 `setState` 调用执行。
**“Element” 里的 `setState` 主要是通过新的 newState 去得到一个 RiverPod 里的 `Result` 对象,然后通过 `_notifyListeners` 去把得到 `Result` 更新到 `watch` 的地方。**
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image9)
`Result` 的作用主要是通过 `Result.data` 、`Result.error`、 `map``requireState` 等去提供执行结果,一般情况下状态都是通过 `requireState` 获取,具体在 RiverPod 体现为:
> **我们调用 `read()` 时,其实最后都调用到 `element.readSelf();` ,也就是返回 `requireState`** (其实一般也就是我们的泛型 `State`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image10)
是不是有点乱?
简单点理解就是:构建出 “Provider” 之后, “Element” 里会执行`setState(_provider.create(this));` 调用我们传入的 `Create` 函数,并把 “Element” 自己作为 `ref` 传入进入,**所以我们使用的 `ref` 其实就是 `ProviderElementBase`**。
> 所以 RiverPod 里的起名是有原因的,这里的 “Provider” 和 “Element” 的关系就很有 Flutter 里 `Widget``Element` 的即视感。
分步骤来说就是:
- 构建 Provider 时我们传入了一个 `Create` 函数;
- `Create` 函数会被 `ProviderElementBase` 内部的 `setState` 所调用,得到一个 `Reuslt`
- `Reuslt` 内的 `requireState` 就可以让我们在使用 `read()` 的时候,获取到我们定义的 泛型 `State` 的值。
## WidgetRef
前面介绍了那么多,但还是没有说 `StateProvider` 怎么和 `ProviderScope` 关联到一起,也就是 “Provider” 怎么和 `ProviderContainer` 关联到一起,**凭什么 `ref.read` 就可以读到 `State` **
那么前面代码里,我们用到的 `ConsumerWidget``Consumer` 都是同个东西,**而这个 `ref` 就是前面我们一直说的 “Element” ,或者说是 `ProviderElementBase`** 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image11)
在源码里可以看到, `ConsumerWidget` 的逻辑主要在 `ConsumerStatefulElement` 而`ConsumerStatefulElement` 继承了 `StatefulElement `,并实现了 `WidgetRef` 接口。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image12)
如上代码就可以看到前面很多熟悉的身影了: `ProviderScope` 、`ProviderContainer` 、 `WidgetRef`
首先我们看 `ProviderScope.containerOf(this)` ,终于看到我们熟悉的 `BuildContext` 有没有,**这个方法其实就是以前我们常用的 `of(context)` ,但是它被放到了 `ConsumerStatefulElement` 使用,用于获取 `ProviderScope` 往下共享的 `ProviderContainer`**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image13)
所以我们看到了,`ConsumerWidget` 里的 `ConsumerStatefulElement` 获取到了 `ProviderContainer` ,所以 **`ConsumerStatefulElement` 可以调用到 `ProviderContainer` 的 read/watch** 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image14)
然后回过头来看,`ConsumerStatefulElement` 实现了 `WidgetRef` 接口,所以 我们使用的 `WidgetRef` 就是 `ConsumerStatefulElement` 本身
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image15)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image16)
**也就是 `ref.read` 就是执行 `ConsumerStatefulElement``read` 从而执行到 `ProviderContainer``read`。**
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image17)
所以我们可以总结: **`BuildContext``Element` 然后 `Element` 又实现了 `WidgetRef` ,所以此时的 `WidgetRef` 就是 `BuildContext` 的替代**。
> 这里不要把 Flutter 的 `Element` 和 RiverPod 里的 “`ProviderElementBase`” 搞混了。
所以 `WidgetRef` 这个接口成为了 `Element` 的抽象,替代了 `BuildContext` ,所以这就是 Riverpod 的“魔法”之一 。
## read
所以前面我们已经理清了 `ProviderScope``Provider``ProviderElementBase``ProviderContainer``ConsumerWidget`*`ConsumerStatefulElement`*`WidgetRef` 等的关系和功能,那最后我们就可以开始理清楚 `read` 的整个工作链条。
我们理清和知道了 的概念与作用之后,结合 `ref.read` 来做一个流程分析,那整体就是:
- `ConsumerWidget` 会通过内部的 `ConsumerStatefulElement` 获取到顶层 `ProviderScope` 内共享的 `ProviderContainer`
- 当我们通过 `ref` 调用 `read`/`watch` 时,其实就是通过 `ConsumerStatefulElement` 去调用 `ProviderContainer` 内的 `read ` 函数;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image18)
那最后就是 `ProviderContainer` 内的 `read ` 函数如何读取到 `State`
这就要结合前面我们同样介绍过的 `ProviderElementBase` 事实上 `ProviderContainer` 在执行 `read ` 函数时会调用 `readProviderElement`
`readProviderElement` 顾名思义就是通过 `Provider` 去获取到对应的 `Element`,例如
```dart
ref.read(counterProvider),
```
一般情况下 read/watch 简单来说就是从 `ProviderContainer` 里用 `proivder` 做 key 获取得到 `ProviderElementBase` 这个 “Element”**这个过程又有一个新的对象需要简单介绍下,就是:`_StateReader`**。
`readProviderElement` 其中一个关键就是获取 `_StateReader` ,在 `ProviderContainer` 里有一个 `_stateReaders` 的内部变量,它就是用于缓存 `_StateReader` 的 Map 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image19)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image20)
所以在 `ProviderContainer` 内部:
- 1、首先会根据 `read` 时传入的 `provider` 构建得到一个 `_StateReader`
- 2、以 `provider` 为 key `_StateReader` 为 value 存入 `_stateReaders` 这个 Map并返回 `_StateReader`
- 3、通过 `_StateReader``getElement()` 获取或者创建到 `ProviderElementBase`
> 这里的以 `ProviderBase` 为 Key `_StateReader` 为 value 存入 `_stateReaders` **其实就是把 “provider” 存入到了 `ProviderContainer`,也就是和 `ProviderScope` 关联起来,也就是自此 “provider” 和 `ProviderScope` 就绑定到一起**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image21)
没用使用到明面上的 `BuildContext` 和多余的嵌套,就让 `Provider``ProviderScope` 关联起来。
另外这里可以看到,**在 `ref.read` 时,如何通过 `provider` 构建或者获取到 `ProviderElementBase`**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image22)
得到 `ProviderElementBase` 还记得前面我们介绍 “Provider” 和 "Element" 的部分吗?`ProviderElementBase` 会调用 `setState` 来执行我们传入的 `Create` 函数,得到 `Result` 返回 `State`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image23)
可以看到,这里获取的 `ProviderElementBase` 之后 `return element.readSelf()` ,其实就是返回了 `requireState`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image24)
**自从整个 RiverPod 里最简单的 `ref.read` 流程就全线贯通了**
- `ProviderScope` 往下共享 `ProviderContainer`
- `ConsumerWidget` 内部的 `ConsumerStatefulElement` 通过 `BuildContext` 读取到 `ProviderContainer` 并且实现 `WidgetRef` 接口;
- 通过 `WidgetRef` 接口的 `read(provider)` 调用到 `ProviderContainer` 里的 `read`
- `ProviderContainer` 通过 `read` 方法的 `provider` 创建或者获取得到 `ProviderElementBase`
- `ProviderElementBase` 会执行 `provider` 里的 `Create` 函数,来得到 `Result` 返回 `State`
其他的`watch``refresh` 流程大同小异,就是一些具体内部实现逻辑更复杂而已,比如刷新时:
> 通过 `ref.refresh` 方法, 其实触发的就是 `ProviderContainer``refresh` ,然后最终还是会通过 `_buildState` 去触发 ` setState(_provider.create(this))` 的执行。
**而从这个流程分析,也看到了 RiverPod 如何不暴露使用 `BuildContext` 实现全线关联的逻辑**。
## 额外分析
前面基本介绍完整个调用流程,这里在额外介绍一些常见的调用时如何实现,比如在 Riverpod 里面会看到很多 “Element” ,比如 `ProviderElement` 、`StreamProviderElement` 、 `FutureProviderElement` 等这些 `ProviderElementBase` 的子类。
我们结果过它们并不是 Flutter 里的 `Element` ,而是 Riverpod 里的的 State 单位,用于处理 `Provider` 的状态,比如 **`FutureProviderElement` 就是在 `ProviderElementBase` 的基础上提供一个 `AsyncValue<State>`,主要在 `FutureProvider` 里使用**。
## AsyncValue
在 RiverPod 里正常情况下的 create 方法定义是如下所示:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image25)
而在 `FutureProvider` 下是多了一个 `_listenFuture`,这个 Function 执行后的 `value` 就会是 `AsyncValue<State>` 的 State 类型。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image26)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image27)
`_listenFuture` 的执行上看, 内部会对这个 `future()` 执行,会先进入 `AsyncValue<State>.loading()` 之后,根据 `Future` 的结果返回决定返回`AsyncValue<State>.data` 或者 `AsyncValue<State>.error`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image28)
**所以比如在 `read` / `watch` 时,返回的泛型 `requireState` 其实变成了 `AsyncValue<State>`**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image29)
而针对 `AsyncValue` 官方做了一些 `extension` ,在 `AsyncValueX` 上,其中出了获取 `AsyncData` 的`data` \ `asData` 和 T value 之外,最主要提供了起那么所说的不同状态的构建方法,比如 `when` 方法:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image30)
## autoDispose & family
在 Riverpod 里应该还很常见一个叫 `autoDispose``family` 的静态变量,几乎每个 `Provider` 都有,又是用来干什么的呢?
举个例子,前面代码里我们有个 `FutureProvider` 我们用到了里的 `autoDispose`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image31)
**其实 `FutureProvider.autoDispose` 主要就是 `AutoDisposeFutureProvider` ,以此类推基本每个 `Provider` 都有自己的 `autoDispose` 实现,`family` 也是同理**。
如果说正常的 `Provider` 是继承了 `AlwaysAliveProviderBase`,那 `AutoDisposeProvider` 就是继承于 `AutoDisposeProviderBase` :
从名字可以看出来:
- `AlwaysAliveProviderBase` 是一只活跃的;
- `AutoDisposeProviderBase` 自然就是不 `listened` 的时候就销毁;
也就是内部 `_listeners` 、`_subscribers`、`_dependents` 都是空的时候,当然它还有另外一个 `maintainState` 的控制状态,默认它就是 `false` 的时候,就可以执行销毁。
> **简单理解就是用“完即焚烧” 。**
比如前面我们介绍调用 `read` 的时候,都会调用 `mayNeedDispose` 去尝试销毁:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image32)
> 销毁也就是调用 `element.dispose() `和从 `_stateReaders` 这个 `map` 里移除等等。
同样的 `family` 对应是 `ProviderFamily`,它的作用是:**使用额外的参数构建 provider ,也即是增加一个参数**。
例如默认是把
```dart
final tagThemeProvider = Provider<TagTheme>
```
可以变成
```dart
final tagThemeProvider2 = Provider.family<TagTheme, Color>
```
然后你就可以使用额外的参数,在 `read`/`watch` 的时候
```dart
final questionsCountProvider = Provider.autoDispose((ref) {
return ref
.watch(tagThemeProvider2(Colors.red));
});
```
之所以可以实现这个功能,就要看它的实现 `ProviderFamily` ,对比一般 `Provider` 默认的 `create` `ProviderFamily` 的是:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image33)
可以看到 `create` 的是新的一个 `Provider`**也就是 `family` 下其实是 `Provider` 嵌套 `Provider`**。
所以从上面的例子出发,以前我们是通过 `ref.watch(tagThemeProvider); `就可以了,因为我们的 `tagThemeProvider` 的直接就是 `ProviderBase`
但是如果使用 `ref.watch(tagThemeProvider2);` 就会看到错误提示
```dart
The argument type 'ProviderFamily<TagTheme, Color>' can't be assigned to the parameter type 'ProviderListenable<dynamic>'.
```
是的,因为这里是 `Provider` 嵌套 `Provider` ,我们先得到是的 `ProviderFamily<TagTheme, Color>` ,所以我们需要改为 `ref.watch(tagThemeProvider2(Colors.red));`
**通过 `tagThemeProvider2(Colors.red)` 执行一次变为我们需要的 `ProviderBase `**。
`tagThemeProvider2` 这个 `ProviderFamily` 为什么是这样执行? `ProviderFamily` 明明没有这样的构造函数。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image34)
> 这就涉及到 Dart 语言的特性,如果有兴趣可以看 https://juejin.cn/post/6968369768596242469
首先这里拿到的是一个 `ProviderFamily<TagTheme, Color>` ,在 Dart 中所有函数类型都是 `Function` 的子类型,所以函数都固有地具有 `call` 方法。
我们执行 `tagThemeProvider2(Colors.red)` 其实就是执行了 `ProviderFamily``call `方法,从而执行了 `create` 方法,得到 `FamilyProvider<State>` `FamilyProvider` 也就是 `ProviderBase` 的子类 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-Riverpod/image35)
> ⚠️注意这里有点容易看错的地方,**一个是 `ProviderFamily` 一个是 `FamilyProvider`** 我们从 `ProviderFamily` 里面得到了 `FamilyProvider` 作为 `ProviderBase``ref.watch`
## 最后
很久没有写这么长的源码分析了,不知不觉就写到了半夜凌晨,其实相对来说,整个 Riverpod 更加复杂,所以阅读起来也更加麻烦,但是使用起来反而会相对更便捷,特别是**没有了 `BuildContext` 的限制,但是同时也是带来了 `ConsumerWidget` 的依赖,所有利弊只能看你自己的需求,但是整体 Riverpod 肯定是一个优秀的框架,值得一试。**

332
Flutter-SQS.md Normal file
View File

@ -0,0 +1,332 @@
Hello 大家好我是《Flutter 开发实战详解》的作者Github GSY 系列开源项目的负责人郭树煜,目前开源的 [gsy_github_app_flutter](https://github.com/CarGuo/gsy_github_app_flutter) 以 13k+ 的 star 在中文总榜的 dart 排行上暂处第一名。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image1)
> 数据来源: https://github.com/GrowingGit/GitHub-Chinese-Top-Charts/blob/master/content/charts/overall/software/Dart.md
## 开始之前
Flutter 开源至今其实已经将近 7 年的时间,而我是从 2017 年开始接触的 Flutter ,如今在 2022 年看来,**Flutter 已经是不再是以前小众的跨平台框架**。
如图所示,可以看到如今的 Flutter 已经有高达 `135k` 的 star `10k+` Open 和 `50k+` Closed 的 issue 也足以说明 Flutter 社区和用户的活跃度。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image2)
在去年下半旬的数据调查中,**Flutter 也成为了排名第一的被使用和被喜爱的跨平台框架**,这里说这么说并不是说你一定要去学 Flutter ,而是说不管我们喜不喜欢,目前 Flutter 已经证明了它的价值。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image3)
![image.png](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image4)
> 数据来源: https://rvtechnologies.com/10-reasons-why-flutter-is-growing-as-a-cross-platform-framework/
其实在去年和前年,我也做过一些简单的统计:
- 2020 年 `52` 个样本中有 `19` 个 App 里出现了 Flutter
- 2021 年 `46` 个样本中有 `24` 个 App 里出现了 Flutter
这份数据样本比较小,主要是从我个人常用的 App 进行统计,所以不准确也不具备代表性,但是可以一定程度反映了国内现在 Flutter 应用使用的情况。
> 数据来源: https://juejin.cn/post/7012382656578977806
最后 Flutter 在 Web 和 PC 端也有支持,但是我暂时还未投入生产使用,目前可以简单总结就是:
#### Web
- Flutter Web 目前支持 `HtmlCanvas``CanvasKit`(WASM),默认是移动端使用 HTML 而桌面端使用 WASM
- pub.dev 上 `60%` 左右的包是 Web 兼容;
- 体积和 SEO 是使用过程中最需要提前考虑的问题;
> 可参考资料: https://juejin.cn/post/7059619009213726733
#### Desktop
PC 端目前相对更弱势一些,如果是和 `Electron` 比较,可以简单认为, Flutter PC 版可以使用更低的内存占用和更小的体积,甚至更好的 FFI 继承 C 的能力,但是同样的生态目前也更弱,第三方支持相对较少,需要自己独立解决的问题会相对更多。
> Window 可投入生产版本已经正式发布
>
> 可参考资料: https://juejin.cn/post/7018450473292136456
## Flutter 和原生开发的不同
Flutter 作为跨平台的 UI 框架,它主要的特点是做到:**在性能还不错的情况下,框架的 UI 与平台无关**,而从平台的角度上看, Flutter 其实就是一个“单页面”的应用。
### 1、单页面应用
什么是“单页面”应用?
也就是对于原生 Android 和 iOS 而言,**整个跨平台 UI 默认都是运行在一个 `Activity` / `ViewController` 上面**,默认情况下只会有一个 `Activity` / `ViewController` 事实上 Flutter、 ReactNative 、Weex 、Ionic 默认情况下都是如此,**所以一般情况下框架的路由和原生的路由也是没有直接关系**。
举个例子,如下图所示,
- 在当前 Flutter 端路由堆栈里有 `FlutterA``FlutterB` 两个页面 Flutter 页面;
- 这时候打开新的 `Activity` / `ViewController`,启动了**原生页面X**,可以看到**原生页面 X** 作为新的原生页面加入到原生层路由后,把 `FlutterActivity` / `FlutterViewController` 给挡住,也就是把 `FlutterA``FlutterB` 都挡住;
- 这时候在 Flutter 层再打开新的 `FlutterC` 页面可以看到依然会被原生页面X挡住
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image5)
所以通过这部分内容可以看出来,**跨平台应用默认情况下作为单页面应用,他们的路由堆栈是和原生层存在不兼容的隔离**。
> 当然这里面重复用了一个词:**“默认”**,也就是其实可以支持自定义混合堆栈的,比如官方的 `FlutterEngineGroup` ,第三方框架 `flutter_boost``mix_stack` 、`flutter_thrio` 等等都是为了解决混合开发的场景。
### 2、渲染逻辑
介绍完“单页面”部分的不同,接下来讲讲 Flutter 在渲染层面的不同。
在渲染层面 Flutter 和其他跨平台框架存在较大差异,如下图所示是现阶段常见的渲染模式对比:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image6)
- 对于原生 Android 而言,是**原生代码经过 skia 最后到 GPU 完成渲染绘制**Android 原生系统本身自带了 skia
- 对于 Flutter 而言,**Dart 代码里的控件经过 skia 最后到 GPU 完成渲染绘制**,这里在 Andriod 上使用的系统的 skia ,而在 iOS 上使用的是打包到项目里的 skia
- 对于 ReactNative/Weex 等类似的项目,它们是**运行在各自的 JS 引擎里面,最后通过映射为原生的控件,利用原生的渲染能力进行渲染** PS今年官方终于要发布重构的版本了[2022 年 React Native 的全新架构更新](https://juejin.cn/post/7063738658913779743)
- 对于 ionic 等这类 Hybird 的跨平台框架,使用的主要就是 **WebView 的渲染能力**
> skia 在 Android 上根据不同情况就可能会是 `OpenGL` 或者 `Vulkan` ,在 iOS 上如果有支持 `Metal` 也会使用 `Metal` 加速渲染。
通过前面的介绍,可以看出了:
`ReactNative/Weex` 这类跨平台和原生平台存在较大关联:
- 好处就是:如果需要使用原生平台的控件能力,接入成本会比较低;
- 坏处自然就是: 渲染严重依赖平台控件的能力,耦合较多,不同系统之间原生控件的差异,同个系统的不同版本在控件上的属性和效果差异,组合起来在后期开发过程中就是很大的维护成本。
> 例如:*在 iOS 上调试好的样式,在 Android 上出现了异常;在 Android 上生效的样式,在 iOS 上没有支持;在 iOS 平台的控件效果,在 Android 上出现了不一样的展示比如下拉刷新Appbar等* 如果这些问题再加上每个系统版本 Framework 的细微差别,就会变得细思极恐。
>
> 另外再说个例子Android 和 iOS 的阴影效果差异。
`Flutter` 与之不同的地方就是渲染直接利用 skia 和 GPU 交互,在 Android 和 iOS 平台上实现了平台无关的控件,简单说就是 `Flutter` 里的 `Widget` 大部分都是和 Android 和 iOS 没有关系。
**本质上原生平台是提供一个类似 `Surface` 的画板,之后剩下的只需要由 Flutter 来渲染出对应的控件**
> 一般是使用 `FlutterView` 作为渲染承载,它在 Android 上内部使用可以是 `SurfaceView``TextureView` 或者 `FlutterImageView` ;在 iOS 上是 `UIView` 通过 `Layer` 实现的渲染。
**所以 Flutter 的控件在不同平台可以得到一致效果,但是和原生控件进行混合也会有较高的成本和难度**在接入原生控件的能力上Flutter 提供了 `PlatformView` 的机制来实现接入, `PlatformView` 本身的实现会比较容易引发内存和键盘等问题,所以也带来了较高的接入成本。
> 目前最新版本基本强制要求 Hybrid Composition ,所以相对以前的 `PlatformView` 会好一点点,当然可能遇到的问题还是有的。比如密码键盘切换,切换页面时 `PlatformView` 时页面闪动。
### 3、项目结构
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image7)
如上图所示,默认情况下 Flutter 工程结构是这样的:
- `android` 原生的工程目录,可以配置原生的 `appName` `logo` ,启动图, `AndroidManifest` 等等;
- `ios` 工程目录,配置启动图,`logo`,应用名称,`plist` 文件等等;
- `build` 目录,这个目录是编译后出现,一般是 git 的 ignore 目录打包过程和输入结果都在这个目录下Android 原生的打包过程输出也被重定向输出到这里;
- `lib` 目录,用来写 dart 代码的,入口文件一般是 `main.dart`
- `pubspec.yaml` 文件Flutter 工程里最重要的文件之一,不管是静态资源引用(图片,字体)、第三方库依赖还是 Dart 版本声明都写在这里。
如下图是使用是关于 `pubspec.yaml` 文件的结构介绍
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image8)
> 需要注意,当这个文件发生改变时,需要重新执行 `flutter pub get`,并且 `stop` 应用之后重新运行项目,而不是使用 `hotload`
如下所示是 Flutter 的插件工程Flutter 中分为 `Package``Plugin` ,如果是
- `Package` 项目属于 Flutter 包工程,不会包含原生代码;
- `Plugin` 项目属于 Flutter 插件工程,包含了 Android 和 iOS 代码;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image9)
### 4、打包调试
Flutter 运行之前都需要先执行 `flutter pub get` 来先同步下载第三方代码下载的第三方代码一般存在于Mac `/Users/你的用户名/.pub-cache` 目录下 。
下载依赖成功后,可以直接通过 `flutter run` 或者 IDE 工具点击运行来启动 Flutter 项目,这个过程会需要原生工程的一些网络同步工作,比如:
- Android 上的 Gradle 和 aar 依赖包同步;
- iOS 上需要 pod install 同步一些依赖包;
如果需要在项目同步过程中查看进度:
- Android 可以到 `android/` 目录下执行 `./gradlew assembleDebug` 查看同步进度;
- iOS 可以到 `ios/` 目录下执行 `pod install`,查看下载进度;
同步的插件中,如果是 `Plugin` 带有原生平台的代码逻辑,那么可以在项目根目录下看到一个叫做 `.flutter_plugins``.flutter-plugins-dependencies` 的文件,它们是 git ignore 的文件Android 和 iOS 中会根据这个文件对本地路径的插件进行引用,后面 Flutter 运行时会根据这个路径动态添加依赖。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image10)
默认情况下 Flutter 在 **debug 下是 JIT 的运行模式**所以运行效率会比较低,速度相对较慢,但是可以 hotload。
**release 下是 AOT 模式**,运行速度会快很多,同时 Flutter 在**模拟器上一般默认会使用 CPU 运行,在真机上会使用 GPU 运行**,所以性能表现也不同。
> 另外 iOS 14 真机上 debug 运行,断后链接后再次启动是无法运行的。
如果项目存在缓存问题,可以**直接执行 `flutter clean` 来清理缓存**。
**最后说下 Flutter 的为什么不支持热更新?**
前面讲过 ReactNative 和 Weex 是通过将 JS 代码里的控件转化为原生控件进行渲染,所以本质上 JS 代码部分都只是文本而已,利用 `code-push` 推送文本内容本质上并不会违法平台要求。
而 Flutter 打包后的文件是二进制文件,推送二进制文件明显是不符合平台要求的。
> release 打包后的 Android 会生成 `app.so``flutter.so` 两个动态库iOS 会生成 `App.framework``Flutter.framework` 两个文件。
所以 Flutter 的第三方热更新市面上常见的有:`MxFlutter`、`Fair`、`Kraken`、`liteApp`、`NEJFlutter`、`Flap`MTFlutter、`flutter_code_push` (chimera) 等等,而这些框架都不会是直接下发可执行的二进制文件,大致市面上根据 DSL 的不同,动态化方案可以分为两大类:面向前端的和面向终端。
如下图所示,例如 WXG 的 `LiteApp`、腾讯的 `MxFlutter` 和阿里的 `Kraken` (北海) 就是面向前端 ,使用 JS/TS 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image11)
> 参考资料: https://mp.weixin.qq.com/s/OpgqjTIiB6z9YiN1FTDeYQ
如下图所示:例如 `Flap` 、`flutter_code_push` 就是面向终端,主要是对 Dart 的 DSL 或者编码下功夫。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image12)
> 参考资料: https://tech.meituan.com/2020/06/23/meituan-flutter-flap.html
最后,关于 Flutter 热更新动态化的支持,可以参考这个表格:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image13)
> 参考资料Flutter实现动态化更新-技术预研 https://juejin.cn/post/7033708048321347615
### 5、Flutter 简单介绍
这里介绍下 Flutter Dart 部分相关的内容对于原生开发来说Flutter 主要优先了解**响应式和`Widget`** 。
#### 响应式
响应式编程也叫做声明式编程,这是现在前端开发的主流,当然对于客户端开发的一种趋势,比如 `Jetpack Compose` 、`SwiftUI` 。
> Jetpack Compose 和 Flutter 的在某些表层上看真的很相似。
**响应式简单来说其实就是你不需要手动更新界面,只需要把界面通过代码“声明”好,然后把数据和界面的关系接好,数据更新了界面自然就更新了。**
从代码层面看,对于原生开发而言,**没有 `xml` 的布局,没有 `storyboard`**,布局完全由代码完成,所见即所得,同时也**不会需要操作界面“对象”去进行赋值和更新,你所需要做的就是配置数据和界面的关系**。
> 响应式开发比数据绑定或者 MVVM 不同的地方是,它每次都是重新构建和调整整个渲染树,而不是简单的对 UI 进行 `visibility` 操作。
如下图所示,是 Flutter 下针对响应式 UI 的典型第三方示例: `responsive_framework`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image14)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image15)
##### Widget
`Widget` 是 Flutter 里的基础概念,也是我们写代码最直接接触的对象,**Flutter 内一切皆 Widget Widget 是不可变的immutable每个 Widget 状态都代表了一帧。**
所以 `Widget` 作为一个 `immutable` 对象,它不可能是真正工作的 UI 对象,**在 Flutter 里真正的 `View` 级别对象是 `Element``RenderObject` 其中 `Element` 的抽象对象就是我们经常用到的 `BuildContext`**。
举个例子,如下代码所示,其中 `testUseAll` 这个 `Text` 在同一个页面下在三处地方被使用,并且代码可以正常运行渲染,如果是一个真正的 `View` ,是不能在一个页面下这样被多个地方加载使用的。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image16)
所以 Flutter 中 **`Widget` 更多只是配置文件的地位**,用于描述界面的配置代码,具体它们的实现逻辑、关系还有分类,可以看我写的书 **《Flutter开发实战详解》中** 的第三章和第四章部分。
#### 有趣的问题
最后说一个比较有意思的问题,之前有人说 **Flutter 里是传递值还是引用**?这个问题看过网上有不少文章解释得很奇怪,存在一些误导性的解释,其实这个问题很简单:
**Flutter 里一切皆是对象, 就连 `int``double` 、`bool` 也是对象,你觉得对象传递的是什么?**
但是对于对象的操作是有区别的,比如对于 `int``double``class``+` 、`-` 、`*` 、 `\` 等操作,其实是执行了这个 `class``operator` 操作符的操作, 然后返回了一个 `num` 对象。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image17)
而对于这个操作,只需要要去 `dart vm` 看看 `Double` 对象在进行加减乘除时做了什么,如下图所示,看完相信就知道方法里传递 `int` 、`double` 对象后进行操作会是什么样的结果。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image18)
## Flutter 和 Compose
最后聊一聊 Flutter 和 Compose。
其实自从 Jetpack Compose 面世以来,关于 Flutter 与 Compose 之间的选择问题就开始在 Android 开发中出现,就如同之前有 iOSer 纠结在 Flutter 和 SwiftUI 之间选谁一样,**对于 Android 开发来说似乎“更头痛”的是 Flutter 与 Compose “同出一爹”**。
这里我只是提供一些我个人的理解,并不代表官方的观点:
**Flutter 和 Compose 的未来目标会比较一致,但是至少它们出现的初衷是不一样。**
**首先 Compose 是 Jetpack 系列的全新 UI 库**理解下这点Compose 是 Jetpack 系列的成员之一,所以可以被应用到 Android 界面开发中,**所以你也可以选择不用,用不用都能开发 Android 的 UI** 。
然后再说 Compose 出生的目的:就是为了重新定义 Android 上 UI 的编写方式,为了**提高 Android 原生的 UI 开发效率,让 Android 的 UI 开发方式能跟上时代的步伐**。
> 不管你喜不喜欢,声明式的界面开发就是如今的潮流,不管是 React 、SwiftUI 、Flutter 等都在表明这一点。
而对于 **Flutter 而言就是跨平台,因为 Flutter 没有自己的平台** ,有人说 `Fuchsia` 会是 Flutter 的家,但那已经属于后话,毕竟 `Fuchsia` 要先能养活自己。
因为 Flutter 出生就是为了跨平台存在的全新 UI 框架,从底层到上层都是“创新”和“大胆”的设计,就选择 Dart 本身就是一项很“大胆”的决定,甚至在 Web 平台都敢支持选用 `Canvaskit``WASM` 模式。
> 所以 Flutter 的“任性”从一出来就不被看好,当然至今也有不看好它的人,因为它某种程度很“偏激”和不友好。
另外从起源和维护上:
- Flutter 起源是 Chrome 项目组,选用了 Dart ,所以 Flutter 并不是归属于 Android 的项目;
- Compose 起源于 Android 团队,它使用的是 Kotlin
所以他们起源和维护都属于不同 Group ,所以从我们外界看可能会觉得有资源冲突,但是本质上他们是不同的大组在维护的。
好了,扯了那么多,总结下就是:
- **Compose 是 Android UI 的未来,现阶段你可以不会,但是如果未来你会继续在 Android 平台的话,你就必须会。** ,而 Compose 的跨平台支持也在推进,不过不是谷歌维护,而是由 Jetpack 提供的 Compose for Compose Multiplatform 。
- **Flutter 的未来在于多平台,更稳定可靠的多平台 UI 框架。如果你的路线方向不是大前端或者多端开发者,那你可以不会也没关系。**
说带了这些框架主要还是做 UI 的,学哪个看你喜欢哪个就行~当然,可能更重要是看你领导要求你用哪个,而回归到冲突的问题上, **Flutter 和 Compose 冲突吗?**
从立项的意义上看 Flutter 和 Compose 好像是冲突的,但是**从使用者的角度看,它们并不冲突**。
因为对于开发者而言,不管你是先学会 Compose 还是先学会 Flutter对于你掌握另外一项技能都有帮助相当于学会一种就等于学会另一种的 70%
从未来的角度看:
- **如果你是原生开发,还没接触过 Flutter 那先去学 Compose** ,这对你的 Android 生涯更有帮助,然后再学 Flutter 也不难。
- **如果你已经在使用或者学习 Flutter ,那么请继续深造**,不必因为担心 Compose 而停滞不前,当你掌握了 Flutter 后其实离 Compose 也不远了。
> 它们二者的未来都会是多平台,而我认为的冲突主要是在于动手学起来,而不是在二者之间徘徊纠结。
从现实角度出发:目前 Flutter 2.0 下的 Android 和 iOS 已经趋向稳定Web 已经进入 Stable 分支,而 Macos/Linux/Win 也进入了 Beta 阶段,并且可以在 Stable 分支通过 snapshot 预览。**所以从这个阶段考虑,如果你需要跨平台开发,甚至 PC 平台,那么优先考虑 Flutter 吧。**
> 你选择 React Native 也没问题,说起来最近 React Native 的版本号已经到了 0.67 了,还是突破不到 1.0 ····
当然大家可能会关心框架是否有坑的问题,**本质上所有框架都有坑,甚至网络因素都可能会成为你的痛点,问题在于你是否接受这些坑**,平台的背后本身就是“脏活”和“累活”, Flutter 的全平台之路很艰难,能做好 Android 和 iOS 的支持和兼容就很不容易了。
最后还是要例行补充这一点:
> **跨平台之所以是跨平台,首先就是要有对应原生平台的存在,** 很多原生平台的问题都需要回归到平台去解决,那些喜欢吹 xxx 制霸原生要凉的节奏,仅仅是因为“你的焦虑会成为它们的利润”。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-SQS/image19)

100
Flutter-StateM.md Normal file
View File

@ -0,0 +1,100 @@
> 原文链接: https://levelup.gitconnected.com/flutter-state-management-in-2021-when-to-use-what-98722093b8bc
有时候选择比较少也是一件好事,例如在 `React` 中通常只盛行一到两个状态管理解决方案,而`Flutter` 自从 2020 年末开始,每个月似乎都有新的状态管理方案出现,因此这里主要罗列出它们的一些优劣,从而帮助你选择最适合的状态管理方案。
## 基础
一般在无需修改 `pubspec.yaml`文件的情况下,你默认有两种状态管理解决方案可以选择,大多数时候这就足够了。
### setState
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-StateM/image1)
`setState` 仅在本地范围内有效,如果一个 `Widget` 需要改变它自己的状态,那么 `setState` 就是你最好的选择。
例如:修改开关是打开还是关闭,或者存储改变正在输入的文本内容,**这种场景你真的不需要考虑任何其它状态管理包**。
我的经验法则是:**如果只在此 `Widget` 中需要有状态变量,或者在该控件树下恰好只有 1 个上下的 `Widget`,则它属于本地范围,这时候直接使用 `StatefulWidget` 是最合适不过。**
如果你需要将状态在控件树内向下传递树,那么只需将变量放在子 `Widget` 的构造函数中;如果相同的变量需要传递到 2+ `Widget` 的构造函数,那么这时候才需要研究更高范围的状态管理。
### InheretedWidget
前面我们在 `setState` 中讨论的那个开关,如果它是控制应用是处于暗模式还是亮模式,如果在这种情况下,你就需要将状态提升到可以更好地沿控件树传播的某个位置。
而在你使用第三方框架去完成这个需求时,让我们看一下 `InheretedWidget`
`InheretedWidget` 允许它下面的任何 `Widget` 访问它的属性,这意味着可以有一个变量,例如:
```dart
enum Theme {
dark,
light
}
```
`InheretedWidget` 内部,任何与主题有关的 `Widget` 都可以通过 `MyInheretedWidget.of(context).theme` 访问主题,并且该`Widget` 还会在主题更新时自动重建。
直接使用 `InheretedWidget` 不好的地方在于会有很多样板,`Widget` 系统有大量重复的代码。
## 你需要安装的那些
### BLoC (Cubit?)
`Bloc` 可能是 Flutter 中状态管理最古老的解决方案之一(不考虑 `scoped_model` 的话),并且现在看来仍然还不错。
最近 `BLoC` 已将 `Cubit` 添加到组合中,这使得 `BLoC` 或多或少不会显得过气,因为 `Cubit` 降低了所需的样板,这意味着以后迁移更容易,而在我看来 `BLoC` 在这两个不同的领域中表现出色:
#### 1、与团队合作
`BLoC`*不灵活* 方面做得非常好,可能对于很多人来说这是一件坏事:他们希望能更快速地更改他们的应用,而无需编写或更改太多代码。
但是对于团队来说情况并非如此:通过让事情变得不灵活,你可以保证一切都按照最初开发人员的预期工作——例如 `BLoC` 中的状态仅仅有 1、2 和 3 这样的值,你在使用 `BLoC` 更改为这些值时,其他程序员不会意外地将其值移动到 4这就是它不灵活的好处。
#### 1、事件驱动状态
`BLoC` 是基于事件驱动的,你必须定义你的事件,执行 API 调用可能会触发一个事件,该事件会推出一个 `CallingAPIState` 的 state然后当 API 调用完成时,它会推出一个`HaveAPIResultsState`.
**如果你想严格定义你的事件和状态,那么 `BLoC` 很适合你,如果你需要灵活性和开发速度,那么 `BLoC` 可能不是正确的选择。**
## Provider
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-StateM/image2)
出于遗留原因这里将 `Provider` 列入介绍,它很简单,很干净,很棒……但有一些缺陷和改进的余地。
`Provider` 视为 `InheretedWidget` 使用可以减少样板文件,事实上 `Provider` 是建立在 `InheretedWidget` 之上,它只是减少了你需要编写的代码量。
如果你的应用已经在使用 `Provider`,那么你可以继续使用它,这是一个非常好的状态管理包,没有理由需要迁移到另一个解决方案。
但是它还有一些改进的余地,我认为 **`RiverPod``Provider` 有改进余地的地方做得更好**。
## RiverPod
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-StateM/image3)
`RiverPod` 的网站上可以看到,他们称自己为“`Provider`,但与众不同”。
这样的形容很贴切,`Provider` 即使削减了很多的模版,但仍然有一些是可以进一步减少的。此外`Provider` 依赖于 `BuildContext`——我认为在很多情况下这确实很棒(它会迫使你使用 `Widget` 树),但有时就像应用的生命周期一样,在任何的地方获取 `BuildContext` 是不切实际的。
`RiverPod``Provider` 的优点上改进如下:
- **比 `Provider` 更少的样板**`RiverPod` 在减少 `Provider` 模版方面做得很好,允许开发者只注册一个顶级存储而不必单独提供每个提供。(可能有人一想到把所有东西都集中在一个地方而畏缩——别担心,你可以确定你的 pod。
- 不依赖 `BuildContext`:这也是一个很好的选择,原因前面已经提到。有时你只是无法在需要的地方获得 `BuildContext`
- **编译安全**:到目前为止这是状态管理的最佳创新,只要代码能编译就是安全的,我们不再需要知道为什么不能在树中找到我们的 `Provider` 这是一项巨大的创新,可以为你节省很多的时间。
`RiverPod` 只是 `Provider `的不同皮肤——但是它是更光滑、更好的皮肤,如果您正在启动一个新应用并想使用 `Provider` ,我强烈建议你考虑 `RiverPod`
## 其他
以下是在考虑状态管理的时候调研过的方案,但最终没有使用:
- `GetX`:我不是 `GetX` 的粉丝,`GetX` 试图完成很多工作,但这限制了你的灵活性,如果你希望有“完整应用场景” 的第三方包,那么 `GetX` 是你的最佳选择没,我对此尝试过接入,但不喜欢它。
- `get_it``get_it` 不是一种状态管理方案——但大家一直在使用它,如果用作状态管理方案来看,它会非常混乱。
- `redux` / `fish_redux` / `mobx`:这些都来自 `React`,并且具有非常相似的风格——但我认为 `React``Flutter` 是两个看起来相似却不同的框架,如果你习惯了它们,那么你可以使用它们,但在我看来,为 `Flutter` 设计的状态管理框架更为干净。

145
Flutter-W2022.md Normal file
View File

@ -0,0 +1,145 @@
> 原文链接 https://medium.com/iecse-hashtag/flutter-for-web-in-2022-a-deep-dive-96cf1b5695a9 原文的意思是深入探索,但是个人觉得其实是简单探索。
跨平台开发已经成为过去几年的趋势之一,毫无疑问大多数时候开发人员对跨平台社区充满热情,而 Google 凭借着其 UI 框架—— Flutter 进入了这个市场。
但是将跨平台的支持也扩展到 Web 上其实并不容易,而 Flutter 的解决方案就是 *Flutter for Web*
# **简介:是什么和为什么**
*Flutter* 是一个 Google 的一个跨平台 UI 框架,旨在帮助开发人员创建更接近原生、高性能和更有吸引力的移动端应用,然而 **Flutter 的目标是为每个设备窗口创建用户界面,而不仅仅是在移动应用上**
对于 Web 的支持Flutter 提供了与其移动端相同的开发体验,这得益于便捷的 Dart 、Web 平台生态的强大、以及 Flutter 框架的灵活拓展, 现在开发者可以直接创建 iOS 和 Android 之外的 Web 应用。
由于对于 Web 来说,它和 iOS 和 Android 是使用同一个 Flutter 框架, Web 只是开发者项目里可支持的框架之一,所以一般情况下,**你可以将用 Dart 编写的原有 Flutter 项目直接编译成 Web 体验**。
在这里我们将分析 Flutter web与 React、Angular 和 Vue 等 SPA 框架的对比)、桌面(与 Electron 和 Qt 对比)的当前状态,并希望在未来通过一些额外的努力实现兼容嵌入式设备等等。
# **但是..它是如何工作的?**
Flutter (Mobile) 拥有自己的渲染引擎 `Skia`,它为 Flutter SDK 提供了对屏幕上每个像素的完全控制能力,本身具备很高的精度和速度。
而 Flutter 在 Web 上通过构建 HTML 组件并将整个屏幕用作画布,从而实现完全控制每个像素。这里是使用 `HTML/CSS``Javascript` 创建的,它们都是目前主流的 Web 技术。因此 Flutter 在 Web 上可以使用 Flutter 的所有功能,例如动画和路由等,而无需额外编写任何的代码去适配。
目前 Flutter 对 Web 兼容,包括了在传统浏览器 API 之上构建 Flutter 的基础图形层,并将 Dart 编译为 `JavaScript`,而不是像移动应用中使用的 ARM 机器代码,通过结合 `DOM`、`Canvas ` 和 `WebAssembly` Flutter 可以在不同浏览器上提供高质量和高性能的用户体验。
# **重要的细节:优点和缺点**
**好的,那么 Flutter 是另一个试图降低 Web 市场上的 `Reacts``Angulars` 的框架吗 **
嗯,是的,也不是。
让我们看看 Flutter Web 带来了什么,并通过它的缺点去思考这个问题。
**优点:**
- 1. 支持 Flutter for Mobile 基本相同的控件。
- 2. 几乎所有比较知名的库,都支持在移动端和 Web 端上运行。
> 60.04% 的 pub.dev 包是 Web 兼容的。
- 3. 在三个平台开发的时间显着减少。
- 4. *定制:* Flutter 还提供了根据操作系统为 Web 开发定制版本的选项——就像它为 Android 和 iOS 所做的那样。
好的,**这是否意味着 Flutter Web 以及其 优势会成为构建跨平台应用(包括 Web 端)的理想工具**
不完全是Flutter web 也有一些严重的***缺点***
- 1. Flutter Web 的 SEO 能力支持不友好。
- 2. 无法修改生成的 HTML、CSS 和 JavaScript 代码。
> Flutter web 对 SEO 不友好,缺乏 SEO 也是它越来越难以用于大型商业产品的原因之一。
# **性能和渲染**
Flutter 为开发者提供了两个可选的渲染器:
- 1. HTML 渲染
- 2. Canvas Kit
*HTML* 渲染器优化的是下载大小而不是原始性能。
*Canvaskit* 优先考虑性能和像素完美的一致性,但是会影响下载大小,这会使得你的应用在首先运行速度会有点慢,同时 Canvaskit 呈现的总文件大小比原始文件大小增加了 400% 以上,但同时也突飞猛进地提高了性能。
*PS* 默认渲染器是自动模式,它优先考虑移动浏览器的 HTML 和桌面浏览器的 CanvasKit。
如果要单独测试渲染器:
**HTML**
```
flutter run -d chrome — 网页渲染器 html
```
**Canvaskit**
```
flutter run -d chrome — 网页渲染器 canvaskit
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-W2022/image1)
# 初始化和运行 Web 应用程序
**初始化 Web 的步骤:**
在 Flutter 2.0 及更高版本上创建的所有项目都内置了对 Flutter web 的支持,所以可以通过以下方式初始化和运行 Flutter Web 项目
```
flutter create app_name
flutter devices
```
devices 命令至少应该列出:
```
1 connected device:
Chrome (web) • chrome • web-javascript • Google Chrome 88.0.4324.150
```
然后在 chrome 上运行:
```
flutter run -d chrome
```
要为以前版本的 Flutter 创建添加 Web 支持,请从项目目录运行以下命令:
```
flutter create .
```
# **文件夹结构**
运行 *` flutter create . `* 命令会创建一个名为 “web” 的文件夹,并在其中填充在 Web 上运行所需的文件。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-W2022/image2)
> 顺便说一句,你不能编辑 `index.html``javascript` 文件。
# **Demo**
这是一个非常简单的待办事项列表应用,分别是运行之后在手机和 Web 上的比较效果:
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-W2022/image3)
代码https://github.com/geekyprawins/Todo-List-App
# 结论
在当前阶段,使用 Flutter Web 来满足所有的 Web 开发需求是不大现实的,但如果你想使用它构建它 Web 应用,它还是有用且高效的:
- 支持集成渐进式 Web 应用 (PWA)。
- 实现一些内部应用,例如 dashboards。
- 在现有的 Flutter 移动应用代码下生成对应的 Web 版本,你可以使用**现有的逻辑和 UI 元素来更快地输出 Web 应用**,其中一般情况下 Web 版本不需要实现移动应用上的所有功能。
Flutter Web 允许开发者构建高性能和交互式的 Web 应用程序,但是 **它不适用于静态网页**
> Flutter Web**非常适合带有动画和繁重 UI 元素的单页交互应用**,如果您的网站需要 SEO请务必不要使用 Flutter Web至少在当前状态下
对于具有大量密集文本的静态网页,传统的 Web 开发方法支持更快的加载时间和更容易维护。
> 总而言之 Flutter Web 是市场上所有框架的强大竞争对手之一,如上所述它有其优点和缺点,但它还没有完全达到成为标准的水平。

135
Flutter-WP.md Normal file
View File

@ -0,0 +1,135 @@
Flutter Web 作为 Flutter 框架中最特殊的平台,由于 Web 平台的特殊性,它默认就具备了两种不同的渲染引擎:
- html 通过平台的 canvas 和 Element 完成布局绘制;
- canvaskit 通过 Webassembly + Skia 绘制控件;
虽然都知道 canvavskit 更接近 Flutter 的设计理念,**但是由于它构建的 wasm 文件大小和字体加载等问题带来的成本考虑,业界一般会选用更轻量化的 html 引擎**,而今天的问题也是基于 html 引擎来展开。
> **本篇算是目前少有关于 `deferred-components` 和 Flutter Web 构建过程分析的文章**
## 一、deferred-components
我们都知道 Flutter Web 打包构建后的 `main.dart.js` 文件会很大,所以**一般都会采用一些方法来对包大小进行优化,而其中最常用的方式之一就是使用 [deferred-components](https://docs.flutter.dev/perf/deferred-components) **
> 对于 `deferred-components` 官方起初主要是用于支持 Android App Bundle 上的动态发布,而经过适配后这项能力被很好地拓展到了 Web 上,通过 `deferred-components` 可以方便地根据需求来拆分 `main.dart.js` 文件的大小。
当然这里并不是介绍如何使用 `deferred-components` ,而是在使用 `deferred-components` 时,**遇到了一个关于 Flutter Web 在打包构建上的神奇问题**。
首先,代码如下图所示,可以看到,这里主要是**通过 `deferred as` 关键字将一个普通页面变成 `deferred-components` ,然后在路由打开时通过 `libraryFuture` 加载后渲染页面**。
![image-20220325173721875](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image1)
这里省略了无关的 yaml 文件代码,*那么上述简略的代码,大家觉得有没有什么问题*
一开始我也觉得没什么问题, 通过 `flutter run -d chrome --web-renderer html ` 运行到浏览器调试也没问题,页面都可以正常加载打开,**但是当我通过 ` flutter build web --release --web-renderer html` 打包部署到服务器后,打开时却遇到了这个问题**
```
Deferred library scroll_listener_demo_page was not loaded.
main.dart.js:16911 at Object.d (http://localhost:64553/main.dart.js:3532:3)
main.dart.js:16911 at Object.aL (http://localhost:64553/main.dart.js:3690:34)
main.dart.js:16911 at asV.$1 (http://localhost:64553/main.dart.js:54352:3)
main.dart.js:16911 at pB.BE (http://localhost:64553/main.dart.js:36580:23)
main.dart.js:16911 at akx.$1 (http://localhost:64553/main.dart.js:51891:10)
main.dart.js:16911 at eT.t (http://localhost:64553/main.dart.js:47281:22)
main.dart.js:16911 at Cw.bp (http://localhost:64553/main.dart.js:48714:51)
main.dart.js:16911 at Cw.ih (http://localhost:64553/main.dart.js:48691:9)
main.dart.js:16911 at Cw.rz (http://localhost:64553/main.dart.js:48659:6)
main.dart.js:16911 at Cw.zk (http://localhost:64553/main.dart.js:48689:11)
```
这就很奇怪了,**明明 debug 运行时没有问题,为什么 release 发布就会 `not loaded` 了?**
经过简单调试和打印发现,在出错时代码时根本进入不到 `ContainerAsyncRouterPage` 这个容器里,也就是在外部就出现了 `not loaded`异常,但是明明 `widget` 是在 `ContainerAsyncRouterPage` 容器内才调用,为什么会在外部就抛出 `not loaded` 的异常?
通过异常信息比对源码发现,**编译时在对于 `deferred as` 进行处理时,会插入一段 `checkDeferredIsLoaded` 的检查逻辑,所以抛出异常的代码是在编译期时处理 `import * deferred as` 时添加**。
![image-20220325231047005](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image2)
通过查看打包后的文件,可以看到如果在 `checkDeferredIsLoaded` 之前没有完成加载,也就是对应 `importPrefix` 没有被添加到 `set` 里,就会抛出异常。
![image-20220325214838143](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image3)
所以初步推断,问题应该是出现在 debug 和 release 时,对于 `import * deferred as` 的编译处理有不同之处。
## 二、构建区别
通过资料可以发现,**Flutter Web 在不同编译期间会使用 `dartdevc``dart2js` 两个不同的编译器**,而如下图所示,**默认 debug 运行到 chrome 时采用的是 `dartdevc` ,因为 `dartdevc` 支持增量编译**,所以可以很方便用 hot reload 来调试,通过这种方式运行的 Flutter Web 并不会在 build 目录下生成 web 目录,而是会在 build 目录下生成一个临时的 `*.cache.dill.track.dill` 用于加载和更新。
![image-20220325165759471](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image4)
> .dill 属于 Flutter 编译过程的中间文件,该文件一般是二进制的编码,如果想要查看它的内容,可以在完整版 `dart-sdk` 的`/Users/xxxxx/workspace/dart-sdk/pkg/vm/bin` 目录下)执行 `dart dump_kernel.dart xxx.dill output.dill.txt` 查看,注意是完整版 dart-sdk 。
而 Flutter Web 在 release 编译时,如下图所示,**会经过 `flutter_tools``web.dart` 内的对应配置逻辑进行打包,使用的是 `dart2js` 的命令**,打包后会在 build 下生成包含 main.dart.js 等产物的 web目录而打包过程中的产物例如 `app.dill` 则是存在 `.dart_tool/flutter_build/一串特别编码/` 目录下。
![image-20220325164442683](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image5)
> .dart_tool/flutter_build/ 目录下根据编译平台会输出不同的编译过程目录,点开可以看到是带 armeabi-v7a 之类的一般是 Android 、带有 *.framework 的一般是 iOS ,带有 main.dart.js 的一般是 Web 。
而打开 `web.dart` 文件可以看到很多可配置参数,其中关键的比如:
- --no-source-maps 是否需要生成 source-maps
- -O4 :代表着优化等级,默认就是 -O4dart2js 支持 O0-O4其中 0 表示不做任何优化4 表示优化开到最大;
- --no-minify 表示是否混淆压缩 js 代码,默认` build web --profile` 就可以关闭混淆;
![image-20220325180245530](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image6)
所以到这里,我初步怀疑是不是优化等级 -O4 带来的问题但是正常情况下Flutter 打包时的 `flutter_tools` 并不是使用源码路径,而是使用以下两个文件:
> `/Users/xxxx/workspace/flutter/bin/cache/flutter_tools.stamp`
>
> `/Users/xxxx/workspace/flutter/bin/cache/flutter_tools.snapshot`
难道就为了改个参数就去编译整个 engine ?这样肯定是不值得的,所幸的是官方提供了使用源码 `flutter_tools` 编译的方式,同样是在项目目录下,通过一下方式就可以用 `flutter_tools` 源码的形式进行编译:
> dart ~/workspace/flutter/packages/flutter_tools/bin/flutter_tools.dart build web --release --web-renderer html
而在源码里直接将 -O4 调整了 -O0 之后,我发现编译后的 web 居然无法正常运行,但是基于编译后的产物,我可以直接比对它们的差异,如下图所示,左边是 O0右边是O4
![image-20220325163734572](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image7)
![image-20220325164259841](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image8)
> -O0 之后为什么会无法运行有谁知道吗?
首先可以看到, O4 确实做了不少优化从而精简了它们的体积,但是在关键的 `loadDeferredLibrary` 部分基本一样,所以问题并不是出现在这里。
但是到这里可以发现另外一个问题,因为 `loadDeferredLibrary` 方法是异步的,而从编译后的 `js` 代码上看,在执行完 `loadDeferredLibrary` 之后马上就进入到了` checkDeferredIsLoaded` ,这显然存在问题。
那为什么 debug 可以正常执行呢? 通过查看 debug 运行时的 js 代码,我发现同样的执行逻辑,在 `dartdevc` 构建出来后居然完全不一样。
![image-20220325181735145](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image9)
可以看到 ` checkDeferredIsLoaded` 函数和对应的 `Widget` 是被一起放在逗号表达式里,所以从执行时序上会是和 `Widget` 在调用时被一起被执行,也就是在 `loadDeferredLibrary` 之后,所以代码可以正常运行。
通过断点调试也验证了这个时序问题,在 debug 下会先走完 `loadDeferredLibrary` 的全部逻辑,之后再进入 ` checkDeferredIsLoaded`
![image-20220325141938694](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image10)
而在 release 模式下,代码虽然也会先进入 `loadDeferredLibrary` , 但是会在 ` checkDeferredIsLoaded` 执行之后才进入到 `add(0.this.loadId) `,从而导致前面的异常被抛出。
![image-20220325141617745](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image11)
![image-20220325141632451](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image12)
那到这里问题基本就很清楚了,**前面的代码写法在当前2.10.3)的 Flutter Web 上,经过 dart2js 的 release 编译后会出现某些时序不一致的问题**,知道了问题也很好解决,如下代码所示,只需要把原先代码里的 `Widget` 变成 `WidgetBuilder` 就可以了。
![image-20220325194206188](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image13)
我们再去看 release 编译后的 js 文件,可以看到此时的因为多了 `WidgetBuilder` ,传入的内容变成了 `closure69` ,这样就可以保证在调用到 `call` 之后才触发` checkDeferredIsLoaded` 。
![image-20220325182649022](http://img.cdn.guoshuyu.cn/20220328_Flutter-WP/image14)
## 三、最后
虽然这个问题不难解决,但是通过这个问题去了解 dart2js 的编译和构建过程,可以看到很多平时不会接触的内容,不过**现在我还是不是特别确定是我写法有问题,还是有官方的 dart2js 有 bug** 。
另外 -O0 的转化为什么会不能成功运行也没有头绪,如果有小伙伴知道的欢迎评论告知下~ 。

View File

@ -82,7 +82,6 @@
* **[Flutter SDK 更新集锦](UPDATE.md)**
- **Flutter**
- [Flutter Interact 的 Flutter 1.12 大进化和回顾](Flutter-112.md)
- [Flutter 1.17 | 2020年度第一个稳定版本](Flutter-117.md)
- [Announcing Flutter 1.20](Flutter-120.md)
@ -90,10 +89,12 @@
- [Flutter 2.2 全新功能介绍](Flutter-220.md)
- [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md)
- [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md)
- [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
- [Dart 2.15 发布的新特性](Dart-215.md)
- [Dart 2.16 发布的新特性](Dart-216.md)
* [番外](FWREADME.md)
@ -126,7 +127,18 @@
* [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 Web 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md)

View File

@ -49,7 +49,6 @@
* [Flutter SDK 更新集锦](UPDATE.md)
- **Flutter**
- [Flutter Interact 的 Flutter 1.12 大进化和回顾](Flutter-112.md)
- [Flutter 1.17 | 2020年度第一个稳定版本](Flutter-117.md)
- [Announcing Flutter 1.20](Flutter-120.md)
@ -57,10 +56,12 @@
- [Flutter 2.2 全新功能介绍](Flutter-220.md)
- [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md)
- [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md)
- [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md)
- **Dart**
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
- [Dart 2.15 发布的新特性](Dart-215.md)
- [Dart 2.16 发布的新特性](Dart-216.md)
* [番外](FWREADME.md)
@ -120,9 +121,31 @@
* [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 Web 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md)

View File

@ -9,6 +9,7 @@
- [Flutter 2.2 全新功能介绍](Flutter-220.md)
- [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md)
- [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md)
- [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md)
@ -19,4 +20,5 @@
- [Dart 2.12 发布稳定空安全声明和FFI版本Dart 未来的计划](Dart-212.md)
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
- [Dart 2.15 发布的新特性](Dart-215.md)
- [Dart 2.16 发布的新特性](Dart-216.md)