This commit is contained in:
guoshuyu 2022-11-03 10:02:48 +08:00
parent ec13ab98e8
commit 164cb988ad
7 changed files with 577 additions and 0 deletions

View File

@ -63,4 +63,8 @@
* [如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果](Flutter-N9.md)
* [给掘金 Logo 快速添加动画效果,并支持全平台开发框架](Flutter-N10.md)
* [Flutter 实现 “真” 3D 动画效果,用纯代码实现立体 Dash 和 3D 掘金 Logo](Flutter-N11.md)
* [Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它](Flutter-N12.md)
* [Flutter 小技巧之优化你的代码性能](Fluttter-N13.md)
* [Flutter 之快速理解混合开发里的手势事件传递](Flutter-N17.md)
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)

139
Flutter-CCK.md Normal file
View File

@ -0,0 +1,139 @@
# 一文快速带你了解 KMM 、 Compose 和 Flutter 的现状
又到了喜闻乐见的环节,**本篇主要是科普 KMM 、 Compose 和 Flutter 的最新现状**,对于 Compose 和 Flutter 大家可能并不陌生,但是对于 KMM 也许会存在疑惑KMM 全称 Kotlin Multiplatform Mobile ,故名思义它是用 Kotlin 实现的跨平台框架,那为什么今天突然会聊到它?
起因如下图所示,今天突然有群友提及了 KMM ,并且用了“变天”的词汇,顿时就勾起了我的兴起,因为 KMM 这些年来一直“不温不火”,可以说很多使用 Kotlin 开发的 “Androider” 对它都很陌生,难道最近它又有了什么突破性的进展?
| ![](http://img.cdn.guoshuyu.cn/20221027_M1/image1.png) | ![](http://img.cdn.guoshuyu.cn/20221027_M1/image2.png) |
| ------------------------------------------------------ | ------------------------------------------------------ |
而在求证一番之后,原来起因来自 10 月初 **Android 官方宣布 [ Jetpack 开始要支持 KMM](https://android-developers.googleblog.com/2022/10/announcing-experimental-preview-of-jetpack-multiplatform-libraries.html)** 了,目前 [Collections](https://developer.android.com/jetpack/androidx/releases/collection) 和 [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) 已经可以通过依赖 `-dev01` 版本在多平台上使用,同时 **KMM 进入 Beta 版本阶段**
**所以目前 KMM 变不了天,至少它还处于 Beta 阶段,但是 Jetpack 开始支持 KMM 是个很好的消息,这意味着 KMM 的社区支持有了官方保证**。
> 好了,介绍完起因,接下来开始进入今天的主题,什么是 KMM 、 Compose 和 Flutter。
# KMM
Kotlin Multiplatform Mobile KMM 是基于 Kotlin 并应用在 iOS 和 Android 的一种跨平台技术,它的特点是结合了跨平台和原生开发协同开发的模式,如下图所示,简单的理解就是:**从纯原生开发变成了 KMM + 原生 UI 开发**。
![](http://img.cdn.guoshuyu.cn/20221027_M1/image3.png)
**使用 KMM 可以把你的业务逻辑和基建部分的能力跨平台化**,例如网络请求、数据存储,状态上报等模块通过 KMM 实现 Android 和 iOS 通用,例如前面介绍的 DataStore 就可以在 iOS 上支持使用。
> 在官方的介绍里 KMM 的早期使用者有百度、Netflix、VMWare、Philips 等,目前收到的反馈都挺不错,而 Beta 版本也意味着现在 KMM 已经具备了使用的基础。
*那你可能会好奇KMM 支持 Web 吗*
聊到这个话题就很有趣,从我的角度上看,我会说 Kotlin Mutiplatform 支持,但是 KMM 不支持。
如果你安装过 KMM 插件和创建过 KMM 项目,你会看到 KMM 不管是从 logo 还是项目创建都只有 Android 和 iOS 但是Kotlin Mutiplatform 是支持 Web 的,通过 Kotlin JS 。
| ![](http://img.cdn.guoshuyu.cn/20221027_M1/image4.png) | ![image-20221027153602062](http://img.cdn.guoshuyu.cn/20221027_M1/image5.png) |
| ------------------------------------------------------ | ------------------------------------------------------------ |
如果接触 Kotlin Mutiplatform 比较早,那你那么可能还听说过 KMP KN 之类的缩写,那它们和 KMM 又是什么关系?简单来说:
- KMP 一般指的就是 Kotlin Mutiplatform ,我依稀记得 KMP 这个概念是在 Kotlin 1.2 的时候被提出可以将Kotlin 运行到特定平台的 JVM 和 JS 代码上
- KN 一般指的是 Kotlin Native KN 属于是将 Kotlin 编译为 Native 二进制文件的技术,甚至可以在没有虚拟机的情况下运行,例如 KMM 上的 iOS 就是使用了 KN 的能力,
- KMM 是利用了 JVM 和 KN 能力实现的针对 Android 和 iOS 平台的 Kotlin 框架AndroidKotlin/JVM和 iOS Kotlin/Naitve
![](http://img.cdn.guoshuyu.cn/20221027_M1/image6.png)
另外还有 Kotlin JS 用于 Web 平台,**所以 KMP 可以看作是大集合,而 KMM 是其中针对 Android 和 iOS 的支持,另外通过 Kotlin Native 和 Kotlin JS 也可以支持拓展到 PC 端和 Web 端**。
那么到这里你应该理解:**KMM 主要是用来写跨平台逻辑,涉及到 UI 部分你还是需要通过原生实现**,如果你从另外一个角度看,用 KMM 对于 Android 开发来说几乎等于白送的能力,因为它只需要 Kotlin。
> 至少 Compose 你还需要适应下响应式开发模式。
*那或者有人就问:那 KMM 这也的意义何在*
事实上还真有,**KMM 在 App 的基建上会很实用,比如做数据上报,崩溃统计,数据分析等等**,纯逻辑的跨平台不影响 UI 部分,目前也是在这些场景上 KMM 应用较多。
> 另外还有人问我KMM 可以用 Java 开发吗? 嗯,这是个好问题,下次不要再问了。
当然KMM 也存在一些局限,比如使用 ViewModel 和协程如何在 iOS 上运行的问题,不过社区针对这部分也有一些第三方支持,所以对于 KMM 的未来还是值得期待。
# Compose
Compose 相信大家不会陌生,**其实 Compose 也可以分两部分看待, Jetpack Compose 和 Compose Multiplatform**
- 由 Android 官方维护的 Jetpack Compose
- 由 JetBrains 维护的 [compose-jb ](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FJetBrains%2Fcompose-jb)实现的 Compose Multiplatform
**如果说 KMM 时用于实现跨平台的业务逻辑,那么 Compose Multiplatform 就是专注于跨平台 UI 上的支持**,那 KMM 和 Compose Multiplatform 是什么关系呢?
从项目角度看, compose-jb 和 KMM 其实没有关系,因为 KMM 还在 beta ,但是 Compose Multiplatform 正式已经发布接近一年的时间。
> 但是你要说完全没关系显然是不可能,毕竟 Kotlin Native 和 Kotlin JS 的能力其实在 Compose Multiplatform 里很重要。
当然如下图所示Compose Multiplatform 在跨平台开发体验上还是有所区别,**Compose 目前是通过多个模块不同实现来支持多平台,所以目前 Jetpack Compose 和 Compose Multiplatform 有一些“割裂”**,特别是在 Web 端,想要达到 Flutter 一样共享代码的比例还需要继续努力。
![](http://img.cdn.guoshuyu.cn/20221027_M1/image7.png)
> PS 图比较老iOS 其实目前已经进入[实验阶段](https://github.com/JetBrains/compose-jb/issues/2397#issuecomment-1277536570) [` androidx.compose.ui.main.defaultUIKitMain` ](https://github.com/JetBrains/compose-jb/blob/master/experimental/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt) 相关的支持距离正式发布可以期待。
另外 Compose Multiplatform 还有的问题就是缺少插件社区,这其实是跨平台领域必不可少的配置:**前端有 npm 、Flutter 有 pub你可以通过它们的中央官网搜索你想要的库查看它们的热度版本兼容和使用量等等信息设置官方认证和安全保障但是 Maven 时代在这方面一直很弱**。
另一方面 Compose 的优势也很明显:
- Kotlin 生态
- Android 开发友好
- 打包体积增长不大,代码压缩比例高
- 性能不错compose-android 和 compose-desktop 都使用 Skia
**而随着 Jetpack 开始支持 KMM ,那么 Compose Multiplatform 的社区支持力度将得到进一步提升,因为变相 Compose Multiplatform 也可以支持 Jetpack** 。
至于前面所说的“割裂”问题,目前可以看到官方也在有序推进,其中就有 desktop 的部分代码已经挪到了androidx 上,从这里看或者统一的 Compose lib 并不遥远。
![](http://img.cdn.guoshuyu.cn/20221027_M1/image8.png)
> PS JetBrains 目前就已经将 Toolbox 应用通过 Compose Multiplatform 实现并且发布使用。
# Flutter
常看我文章的应该对 Flutter 更不陌生,现在 Flutter 已经是 3.3 的版本Flutter 的特点就是跨平台,因为它并没有自己的平台,同时它也是 single codebase 的跨平台实现。
![](http://img.cdn.guoshuyu.cn/20221027_M1/image9.png)
关于 Flutter 和其他框架的对比或者使用数据这里就不多赘述,因为这方便之前我已经分享过很多,感兴趣的可以参考下方链接,这里介绍一些其他比较有意思的话题。
> - [Flutter VS Other 量化对比](https://juejin.cn/post/7084533408986054669)
>
> - [国内大厂应用在移动端 Flutter 框架使用分析](https://juejin.cn/post/7012382656578977806)
>
> - [国内大厂在移动端跨平台的框架接入分析](https://juejin.cn/post/6844904177949212680)
**在 Jetbrains 的开源项目里有一个叫 [skiko ](https://github.com/JetBrains/skiko) 的项目**SkikoKotlin 的 Skia 的缩写)是一个图形库,它支持 Kotlin/JVM 、Kotlin/JS 、Kotlin/Native 等相关实现,目前支持有:
- Kotlin/JVM - Linux、Windows、macOS、Android
- Kotlin/JS - web
- Kotlin/Native - iOS 、macOS
如果从这个角度看 Compose Multiplatform 未来的方向会和 Flutter 很像,甚至因为 Flutter 走过更多的坑,所以 Compose Multiplatform 在对接 Skia 上可以有更多的参考。
> 其实未来 Linux、Windows 等平台也完全可以脱离 JVM 通过 Kotlin/Native + Skiko 实现支持,只是维护成本会变高。
**Flutter 在自建渲染引擎上其实已经越来越激进,因为直接使用 Skia 已经无法满足日益增长的 Bug 和性能极限,所以官方开始了自研[渲染引擎Impeller](https://mp.weixin.qq.com/s/GptJbPXPediNRc4KvZzr6g)**
因为 Flutter 团队现在出现问题每次都要和 Skia 团队沟通,然后等跟进,这样的节奏太慢了,从官方的更新日志上就可以看出目前 Flutter 的迭代速度依然很夸张。
![](http://img.cdn.guoshuyu.cn/20221027_M1/image10.png)
所以**这次自研的 Impeller 本质上是为了解决 Skia 需要运行时遇到的问题, Impeller 可以直接在编译器就完成 GLSL 和 MSL ,不需要 SKSL 从而提高了性能和运行时的稳定性** ,目前优先在 iOS 平台上开始支持 ,配合 Metal 做优化,后续如果没问题也会同步支持 Android 和 Vulkan 。
> 从这个角度猜测Flutter 在 Skia 遇到的问题 Compose Multiplatform 也很可能会遇上,而如果后续 Impeller 项目进展顺利,那它或者并不会局限在 Flutter ,也许也可以拓展支持到 Compose Multiplatform上。
其实自研发引擎并不奇怪,随着项目的发展和深入,很多底层问题没办法快速推进就会反推自研,例如 [Hermes 在 RN 0.7 成为默认 Engine](https://juejin.cn/post/7140474062211383333) 也是类似问题的体现,**自研底层属于是一个负责任的开源团队的必经之路**。
# 最后
今天这篇文章的内容更多的科普性质而非技术行,主要是针对目前 KMM 、Compose 和 Flutter 的现状做一个陈述,其实很多时候它们之间并不冲突,但是作为开发者很经常就像开头一样,用“对立”的角度来看 A 火了 B 就要挂,这种心态大可不必。
另外,**我更喜欢“百花齐放”的氛围,当然你也可以万花丛中只取一朵,所以不必过于焦虑,需要什么就用什么就可以**,技术服务于业务,就像我接触到的很多开发,他们需要使用什么技术并不是自己能决定的。
> 就比如前面那位问我 “KMM 上可以用 Java” 的那位兄弟,他是因为公司 leader 觉得 Kotlin 不成熟而不给用在 Android 上,嗯,他的 Leader 是一位后端开发。

189
Flutter-N12.md Normal file
View File

@ -0,0 +1,189 @@
# Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它
随着 Flutter 3.3 正式版发布Global Selection 终于有了官方的正式支持,**该功能补全了 Flutter 长时间存在 Selection 异常等问题,特别是在 Flutter Web 下经常会有选择文本时与预期的行为不匹配的情况**。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image1.gif)
## 使用
**使用 `SelectionArea` 也十分简单,如下代码所示,只需要在你想要支持的地方添加 `SelectionArea` 即可**,甚至可以在每个路由下的 `Scaffold` 添加 `SelectionArea` 来全面启用支持。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image2.png)
默认情况下 `SelectionArea` 已经实现了所有常见的功能,并且 Flutter 针对不同平台进行了差异化实现,如下图所示 Android 和 iOS 会有不同的样式效果。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image3.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image4.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image5.png) |
| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
**当然,也许这时候你会发现在 iOS 上的 Toolbar 居然没有全选**,其实这是因为 iOS 使用了 `TextSelectionControls` 默认的 `canSelectAll` 判断,这个判断里有一个条件就是需要 selection 的 `start == end` 才符合条件。
![image-20220906113547331](http://img.cdn.guoshuyu.cn/20220906_N12/image6.png)
所以如果你觉得这个判断有问题,完全可以自己 `override` 一个自定义的 `TextSelectionControls` ,比如在 `canSelectAll` 直接 `return true`
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image7.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image8.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
是的,**对于 `SelectionArea` 我们可以通过继承 `TextSelectionControls` 来自定义**
- 通过 `buildToolbar` 自定义弹出的 Toolbar 样式和逻辑,甚至你可以添加一些额外的标签能力,比如 “插入图片”
- 通过 `buildHandle` 自定义 Selection Handle 可拖动部分的样式
而在 `SelectionArea` 里,不管是 Handle 还是 Toolbar ,都是通过新增 `Overlay` 来实现样式,这部分的逻辑主要在 `SelectionOverlay` 对象:
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image9.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image10.png) |
| ------------------------------------------------------- | -------------------------------------------------------- |
> 如果你还不了解 `Overlay` ,可以简单理解为:**默认情况下所有的路由页面都在一个 `Overlay` 下,打开一个 Route 就是添加一个 `OverlayEntry``Overlay` 里**。
所以 Handle 和 Toolbar 都是通过 `OverlayEntry` 打开的特殊“路由”控件,拥有新的层级,例如下方右图就是 Toolbar 所在的 `OverlayEntry`
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image11.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image12.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
**另外,对于 Handle 的颜色定义,默认情况下主要来自 `TextSelectionTheme``Theme`** 。
例如 `MaterialTextSelectionControls`start 和 end 两个 Handle 的颜色,默认是通过 `TextSelectionTheme``selectionHandleColor` 或者 `Theme``primary` 来设置。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image13.png)
那文字的选中区域的颜色是怎么来的?难道也是 `OverlayEntry` 吗?
答案是否定的,这部分颜色主要是来自于文本绘制时 Canvas 的渲染。
如下代码所示,**当文本被绘制时,会判断当前是否有被选中的片段,如果存在选中的片段,会调用绘制对应的选中图层**。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image14.png)
而对于文字的选中区块的颜色,默认是通过 `DefaultSelectionStyle``selectionColor` 来显示,当然,如下右图所示,在 `MaterialApp` 里它依然和 `TextSelectionTheme``selectionColor` 或者 `Theme``primary` 有关系。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image15.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image16.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
**那如果你还想要在 `SelectionArea` 下的某些内容不允许被选中呢**
这里 Flutter 提供了 `SelectionContainer.disabled` 实现,只要在对应内容嵌套 `SelectionContainer.disabled` ,那么这部分内容下的文本就无法被选中。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image17.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image18.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
为什么嵌套 `SelectionContainer.disabled` 就可以禁用文本选中的能力?这其实和 `SelectionArea` 的实现有关系:
> `SelectionContainer` 内部实现了一个 `InheritedWidget` ,它会往下共享一个 `SelectionRegistrar ` ,而默认情况下 `SelectionArea` 内部使用了 `SelectionContainer` 并且往下共享了对应的 Registrar 实现。
- `SelectionArea` 内部的 `SelectionContainer` 是有对应的 `registrar` 实现往下共享
- `SelectionContainer.disabled` 内部的 `registrar``null`
**所以根本区别就在于 `SelectionContainer.disabled` 里没有 `registrar`** ,如下左图所示,加了 disabled 后获取到的 `registrar` 是 null ,那么如下右侧代码所示,在后续可选中区域的更新逻辑中就会直接 return 。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image19.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image20.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
到这里你应该大致理解了如何使用和自定义一些 `SelectionArea` 的能力,那么接下来介绍两个 “Bug” ,通过这两个 “Bug” 我们深入理解 `SelectionArea` 内部的实现情况 。
## 问题1
如下代码所示,**当使用了 `WidgetSpan` 之后,默认情况下,用户在开始位置拖拽 Handle 进行选择时会无法选中 `WidgetSpan` 里的文本**。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image21.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image22.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
> PS其实拖动可以选中只是这里暂时以不能选中的情况下作为切入点。
为什么会这样?首先要知道,上面代码在使用了 `WidgetSpan` 包裹 `Hello World` 之后,其实是存在两个` Text` ,也就是上述的 UI 是由两个 `RenderParagraph` 绘制完成。
那么对于最外层的 `Text` ,其实它的文本内容是 `“Flutter is the best!”`,注意这段文本,其实文本里此时是多了两个空格。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image23.png)
之所以会有这两个空格,其实是因为 `WidgetSpan` 使用了 `0xFFFC` 的占位符,这段占位符在渲染时,就会被替换为 `WidgetSpan` 对应的 `Hello World` 和猫头图片。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image24.png)
**那么这时候如果我们选择复制,复制出来的内容会是 `Flutter isthe best! `** ,中间的两个占位符是不会复制出来,因为在获取可选择片段时,会把对应的 `placeholderCodeUnit` 剔除。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image25.png)
另外,当我们点击复制的时候, `WidgetSpan` 所在的 `Hello World` 并没有被选中,所以此时调用 `getSelectedContent` 就会得到 null ,也就是没有内容。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image26.png)
所以可以看到:此时在手动拖拽选择时,` WidgetSpan` 里的文本是不会被选中,因为它处于不同的 `Text` ,对于外层 `Text` 而言它只是个占位符。
> 当然,**其实在拖动 Handle 还是可以选中 ` WidgetSpan` 里的文本,比如你从 `Hello World` 开始拖动,这里拖动选中不了的原因后面会解释**。
## 问题 2
如果当我们点击了全选会怎么样?如下图所示,在我们点击全选之后,可以看到两个“奇怪”的问题:
- ` WidgetSpan` 里的 `Hello World` 可以被选中了
- 左侧的 Start Handle 位置不是在文本开头,而是在 ` WidgetSpan` 开始
![](http://img.cdn.guoshuyu.cn/20220906_N12/image27.png)
我们首先看第一点,**为什么点击全选时,`WidgetSpan` 里的 `Hello World` 可以被选中**
其实全选操作和拖拽 Handle 最大的不同就是:它是往下直接发出全选事件 `SelectAllSelectionEvent` ,而该事件会触发所有 child 响应事件,自然也就包括了 `WidgetSpan` 里的 `Hello World`
![](http://img.cdn.guoshuyu.cn/20220906_N12/image28.png)
最后负责响应 SelectAll 事件的对象是 `_SelectableFragment` ,这里主要有两个关键逻辑:
- `_handleSelectAll` 获取得到 `_textSelectionStart``_textSelectionEnd ` ,表明此时控件已经被选中
- `didChangeSelection` 里通过 `paragraph.markNeedsPaint()` 触发重绘,然后增加选中时的覆盖颜色
![](http://img.cdn.guoshuyu.cn/20220906_N12/image29.png)
可以看到,由于此时 `WidgetSpan` 里的 `Hello World` 也直接响应了全选事件,所以它会处于选中状态,这样之后在 `getSelectedContent` 调用里也可以获取到内容,也就是能够 `Hello World` 能被复制出来。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image30.png)
**但是此时复制出来的内容会是 `Hello World!Flutter isthe best!` ** ,是不是感觉还不对?这就是我们要说的第二个问题,左侧的 Start Handle 位置不是在文本开头。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image27.png)
首先我们看,为什么复制出来之后的内容会是 `Hello World!Flutter isthe best!`
正如前面说到的,复制调用的是 `getSelectedContent` 方法,如下代码所示,**可以看到在 `selectables` 这个 `List` 的第一位就是 `Hello World` ,所以最终拼接出来的文本会是 `Hello World!Flutter isthe best!`** 。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image31.png)
那为什么 `Hello World` 会排在 `selectables` 的第一位? 这就需要讲到 Flutter 里对 Selectable 的一个排序逻辑。
我们知道 `Text` 内部是通过 `RenderParagraph` 实现文本绘制,而 `RenderParagraph` 在初始化的时候,**如果存在 `_registrar` ,也就是存在 `SelectionArea` 的时候,就会通过 `add` 把支持选中的片段添加 `SelectionArea` 内部的 `_additions`** 里。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image32.png)
之后 `SelectionArea` 内部会对可选中的内容进行排序,如下代码所示,在` sort` 之前,此时的 `Hello World``_additions ` 列表的最末端,因为它处于 `WidgetSpan` 的 child 里,所以是最晚被加入到 `_additions ` 的。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image33.png)
而在执行完 `sort `之后 ,可以看到此时 `Hello World` 跑到了列表的最前面,**这也是为什么复制出来的内容顺序是 `Hello World` 开头,然后 Start Handle 会显示在 `Hello World` 的原因**。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image34.png)
`sort ` 的逻辑主要是通过 `compareOrder` 实现,简单分析 `compareOrder` 的排序实现,可以看到其中有一个 `_compareVertically` 的逻辑,通过调试对比,**可以看到此时因为 `Hello World` 所处的 `Rect`top比其他文本高所以它被认为是更高优先级的位置类似于被误认为是上一行的情况**。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image35.png)
知道了问题那就很好处理了,**如下代码所示,如果此时调整一下 `WidgetSpan` 的高度,可以看到全选逻辑下 Start Handle 正常了,但是.... End Handle 位置又不对了**。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image36.png) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image37.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
此时复制出来的内容会是 `Flutter isthe best!Hello World!` **因为这个时候会有一个很“微妙”的偏差值,导致 `Hello World` 排序时被排列到最后面**,从而导致 End Handle 不是预期的位置。
![](http://img.cdn.guoshuyu.cn/20220906_N12/image38.png)
另外,这时候你会发现,如下左侧动图所示,**此时拖动 Handle 是可以选中 `WidgetSpan` 里的 `Hello World`** ,其实之前的情况下也可以,不过需要如右侧动图所示,需要从 `Hello World` 开始拖动,**因为最开始的情况下 `selectables``Hello World` 的排序层级更高,所以如果想要拖动选中,也需要从它开始**。
| ![](http://img.cdn.guoshuyu.cn/20220906_N12/image39.gif) | ![](http://img.cdn.guoshuyu.cn/20220906_N12/image40.gif) |
| -------------------------------------------------------- | -------------------------------------------------------- |
> 目前这个问题在 master 和 stable 分支均可以复现,对应 issue 我也提交在 [#111021 ](https://github.com/flutter/flutter/issues/111021) 。
## 最后
虽然 `SelectionArea` 的出现补全了 Flutter 的长久以来的短板之一,不过基于 `SelectionArea` 实现的复杂程度,目前 `SelectionArea` 还有不少的细节需要优化,但是万事开头难,本次 3.3 `SelectionArea` 的落地也算是一个不错的开始。
最后,相信通过本文大家应该对 `SelectionArea` 的使用和实现都有了一定的了解,如果你还有什么问题,欢迎留言评论交流~

141
Flutter-N13.md Normal file
View File

@ -0,0 +1,141 @@
# Flutter 小技巧之优化你的代码性能
又到了小技巧系列更新时间,今天我们分享一个比较轻松的内容:**Flutter 里的代码优化,优化的目的主要是为了提高性能和可维护性**,放心,本篇我们不讲深入的源码分析,就是分享最最最基础的布局代码优化。
我们先从一个简单的例子开始,相信大家对于 Flutter 的 UI 构建不会陌生,那么如下代码所示,日常开发过程中 `A``B` 这两种代码组织方式,你更常用的是哪一种?
| A (函数方式) | B Component Class 方式) |
| ------------------------------------------------------- | ------------------------------------------------------- |
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image1.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image2.png) |
如果是从代码运行之后的 UI 效果来看,这两个方式运行之后的布局效果并不会有什么差异,而**通常因为可以写更少代码和参数调用更方便等原因**,我们可能在编写页面的内部控件时,会更经常使用 `A (函数方式)` 这种写法,也有称之为 Helper Method 的叫法。
**那使用函数方式构建 UI 有没有问题?答案肯定是没问题,但是某些场景下,对比使用 `B Component Class 方式)` ,可能性能表现上相对没那么优秀**。
举个例子,如下代码所示,在 `renderA` 函数里我们通过点击按键修改 `count`,在修改之后触发 UI 渲染时就需要用到 `setState` ,也就是我们每点一下,当前整个页面就是触发一次 rebuild ,但是我们只是想要改变当前 `renderA` 里的 `count` 文本而已。
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image3.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image4.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
**这就是使用函数构建内部控件最常见的问题之一**,因为子控件更新时是通过父容器的 `setState` ,所以每次子控件比如 `renderA` 发生变化时,就会触发整个 Widget 都出现 rebuild ,这其实并不是特别符合我们的预期。
> 科普一个众所周知的知识点, **`setState` 其实就是调用 `StatefulWidget` 对应的 `StatefulElement` 里的 `markNeedsBuild` 方法,也就是对 `Element` (`BuildContext`) 里的 `_dirty` 标识为设置为 `true` ,仅此而已, 然后等待下次渲染更新**。
当然,你说像 `renderA` 这种写法会引起很严重的性能问题吗?事实上并不会,**因为众所周知 Flutter 里的 UI 构建是通过多个不同的树来完成的,而 Widget 并不是真实的控件**,所以一般情况下 `renderA` 这种写法导致的 rebuild 是不会产生严重的性能缺陷。
但是,如果同级下你的 `renderB` 是如下所示这样的情况呢?虽然这段代码毫无意义,但是我们在 `renderA` 点击改变 `count` 的时候,其实并没有改变 `renderB` 的用到的 `status` 参数,但是因为 `renderA` 里调用了 `setState` ,导致 `renderB` 每次都会进行重复进行浮点计算。
![](http://img.cdn.guoshuyu.cn/20221021_N13/image5.png)
当然你可以说我写个变量进行缓存提前判断也可以解决,但这并不是这个例子的关键,那如果把上面这个例子变成 Component Class 的方式会有什么好处:
- A 在点击更新 `count` 时不会影响其他控件
- B 控件通过 `didUpdateWidget` 可以用更优雅的方式决定更新条件
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image6.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image7.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
> 这样看起来是不是更合理一些?另外 Component Class 的实现方式,也能在一定层度解决代码层级嵌套的问题,有时候实现一些 Component Class 的模版也可以成为 Flutter 里提高效率的工具,这个后面我们会聊到。
**当然使用 Component Class 在无形之中会需要你写更多的代码,同时控件之间的状态联动成本也会有所提高**,例如你需要在 B 控件关联 A 的 `count` 变化去改变高度,这时候可能就需要加入 `InheritedWidget` 或者 `ValueNotifier` 等方式来实现。
例如 Flutter 里 `DefaultTabController` 配合 `TabBar``TabBarView` 的实现就是一个很好的参考。
```dart
Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}
```
> 所以到这里我们理解一个小技巧:**在不偷懒的情况下,使用 Component Class 的方式实现子控件会比使用函数方式可能得到更好的性能和代码结构**。
当然,**使用 Component Class 实现的方式,在调试时也会比函数方式更方便**,如下图所示,当使用函数方式布局时,你在 Flutter Inspector 里看到的 Widget Tree 和 Details Tree 是完全铺平的情况,也没办法定制调试参数。
![](http://img.cdn.guoshuyu.cn/20221021_N13/image8.png)
**但是当你 Component Class 组织布局的时候,你就可以通过 `override debugFillProperties` 方法来可视化一些参数状态**,例如 `ItemA` 里可以把 count 添加到 `debugFillProperties` 里,这样在 Details Tree 里也可以直观看到目前的 `count` 状态信息。
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image9.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image10.png) |
| ------------------------------------------------------- | -------------------------------------------------------- |
> 所以这里又有一个小技巧:**通过 `override debugFillProperties` ,可以定制一些 Debug 时的可视化参数来帮助我们更好调试布局**。
既然讲到利用 Component Class 组织布局,那就不得不聊一个典型的控件:`AnimatedBuilder` 。
`AnimatedBuilder` 可以是最常说到的一个性能优化的例子, 一般情况下在页面的子控件里使用动画,特别是循环动画的话,我们都会建议使用前面介绍的 Component Class 方式,不然动画导致当前页面不停 rebuild 肯定会导致性能影响。
但是有时候我就不想用 Component Class 该怎么办?我就是想写在当前 Page 里,那就可以使用 `AnimatedBuilder` ,你只要把需要执行动画的部分放到 `builder` 方法里就好了。
**因为 `AnimatedBuilder` 的内部会有一个 `_AnimatedState` 用于独立触发 `setState`,从而执行外部 builder 方法执行动画效果**。
![](http://img.cdn.guoshuyu.cn/20221021_N13/image11.png)
类似 `AnimatedBuilder` 的模版实现,可以在一定程度上解决使用 Component Class 的痛点,当然,在使用 `AnimatedBuilder` 还是有一些需要注意, **比如 child 如果不需要跟随动画进行其他变化,一般是要放到 `AnimatedBuilder` 的 `child` 配置里**,因为如果直接放在 `builder` 方法里,那就会出现 child 也跟随动画重新 rebuild 的情况,但是如果是放到 `child` 配置项里,那就是调用了 `child` 的对象缓存。
| 不正确使用 | 正确使用 |
| -------------------------------------------------------- | ------------------------------------------------------------ |
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image12.png) | ![image-20221020175113790](http://img.cdn.guoshuyu.cn/20221021_N13/image13.png) |
> 如果对于这个缓存概念不理解,可以参考 [《MediaQuery 和 build 优化你不知道的秘密》](https://juejin.cn/post/7114098725600903175) 里的“**缓存区域不随帧变化,以便得到最小化的构建**”。
**当然类似 `AnimatedBuilder` 的构建方式还要注意 `context` 问题,不要拿错 `context`** ,这也是很多时候会犯的潜在错误,特别是在调用 `of(context)` 的时候。
*那有的人可能到这里会觉得,那你之前一直说 Widget 很轻Widget 不是真正的控件,那 rebuild 多几次有什么问题*
一般情况下确实不会有太大问题,但是当你的控件有 ` Opacity ` 、`ColorFilter` 、 `ShaderMash` 或者 `ClipRect``Clip.antiAliasWithSaveLayer`)时,就可能会有较大的性能影响,因为他们都是可能会触发 `saveLayer` 的操作。
> 为什么 `saveLayer` 对性能影响很大?因为需要在 GPU 绘制是需要增加额外的缓冲区域,粗俗点说就是需要做图层的保存和合成,这就会对 GPU 渲染时产生较大影响的耗时。
而这里面最常遇到的应该就是 ` Opacity ` 带来的性能问题,因为它看起来是那么的轻便,但是从官方的介绍里,除非真的有必要,不然可以使用效果类似的实现去做场景替代,例如:
**你需要对图片做透明度相关的动画是,那么使用 `AnimatedOpacity``FadeInImage` 代替 ` Opacity ` 会对性能更有帮助**。
> `AnimatedOpacity`` Opacity ` 不一样吗?某种程度上还真不大一样, ` Opacity ` 的内部是 `pushOpacity ` 的操作,而 `AnimatedOpacity` 里虽然有 `OpacityLayer` ,但是变动时是 `updateCompositedLayer` ;而 `FadeInImage` 会使用 GPU 的 fragment shader 去处理透明度的问题,所以性能也会更好一些。
或者在类似有颜色透明度的场景时,可以通过 `Color.fromRGBO` 来替代 `Opacity` **除非你需要将不透明度应用到一大组较为复杂的 child 里,你才会需要使用 `Opacity`** 。
```dart
/// no
Opacity(opacity: 0.5, child: Container(color: Colors.red))
/// yes
Container(color: Color.fromRGBO(255, 0, 0, 0.5))
```
另外还有 `IntrinsicHeight` / `IntrinsicWidth` 的场景,**因为它们是可以通过 child 的内部宽高来调整 child 的大小**,但是这个推算布局的过程会比较费时,可能会到 O虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。
这么说可能有点抽象,举一个官方介绍过的例子,如下代码所示,当你在 `ListView` 里对 `Row``children` 进行 `Align` 排列时,你可能会发现它没有效果,因为此时通过 `Border` 可以看到,绿色和蓝色方框的父容器大小一致。
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image14.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image15.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
但是在加上 `IntrinsicHeight` 之后, 因为通过 `IntrinsicHeight` 的测算之后再返回 size`Row` 里的三个 Item 现在高度一致,,这时候 `Align` 就可以生效了,但是正如前面所说,这个操作性对性能来说相对昂贵,虽然系统有缓存参数,但是如果出现动画 rebuild ,也会对性能造成影响。
| ![](http://img.cdn.guoshuyu.cn/20221021_N13/image16.png) | ![](http://img.cdn.guoshuyu.cn/20221021_N13/image17.png) |
| -------------------------------------------------------- | -------------------------------------------------------- |
> 对这部分感兴趣的可以看 [《带你了解不一样的 Flutter》](https://juejin.cn/post/7053777774707736613#heading-2)
**到这里我们就理解了 (函数方式) 和 Component Class 方式)组织布局的不同之处,同时也知道了 Component Class 方式可以帮助我们更好地调试布局代码,也举例了一些 UI 布局里常见的耗时场景**。
那本篇的小技巧到这里就结束了,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~

96
Flutter-N17.md Normal file
View File

@ -0,0 +1,96 @@
# Flutter 之快速理解混合开发里的手势事件传递
本篇我们聊聊 `PlatformView` 里的手势事件传递,为什么会有这么一篇?其实在此之前已经写过很多 Flutter 里关于混合开发里 `PlatformView` 的内容,而随着 Flutter 版本的迭代, `PlatformView` 的实现也出现了一定的历史包袱问题,恰好最近和[大佬](https://juejin.cn/user/4309694831660711)讨论了混合使用 `VirtualDisplay``HybirdComposition` 时手势事件有什么区别,就顺便把讨论结果梳理出来。
> 对历史包袱问题感兴趣的可以看 [《混合开发的摸爬滚打》](https://juejin.cn/post/7153184663077388295),之前写过 `PlatformView` 的文章最早的已经两年多前,关于事件处理经历过太多版本,如今可能会产生了一些误解或者错误引导,就在本篇一次性解释。
首先在当前 3.3 的版本里Flutter `PlatformView` 主要有 `VirtualDisplay``HybirdComposition``TextureLayer` 三种实现,而这三种实现在手势事件传递实现有差异,但是流程一致,所以本篇的目的就是快速梳理它们的异同。
**如果从当前的实现逻辑上总结,他们在流程上基本是一致的,事件都是从原生 -> Dart -> 原生这样的一个响应处理过程** ,也就是如下图所示,由原生的 `onTouchEvent` 产生手势事件,然后经过 dart 的统一的[事件竞技场](https://juejin.cn/post/6844903841742192648)处理后,最后回到原生层再去触发原生控件响应事件。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image1.png)
也就是在当前的设计里,**无论是哪种 `PlatformView` 的实现,原生控件都不会马上响应触摸事件,而是统一发送到 dart 进行处理,之后再返回触发 Native 控件进行响应**,这样处理的好坏在于:
- 好处是处理逻辑能在 dart 里统一,并且针对原生控件的事件处理也可以在 dart 层进行拦截处理
- 坏处是原生 Event 经历了多次转换,中间可能出现精度丢失和响应速度的问题,特别是在需要大量拖拽的场景
> 所以在 `PlatformView` 的 dart 实现里会有 `gestureRecognizers` 参数用于开发者处理自定义事件响应的支持,例如配置 `EagerGestureRecognizer` 可以用于获得所有手势,解决手势冲突问题。
那么它们在实现上有什么差异?其实这些差异不会直接影响你的使用,如果不感兴趣可以不关心,但是对于理解整个手势事件传递来说又是必不可缺。
# VirtualDisplay
`VirtualDisplay `可以说是老骥伏枥了,兜兜转转最后在 3.3 版本系还继续服役,我们都知道 `VirtualDisplay ` 的实现是采用 Android 上副屏的渲染逻辑,然后把控件渲染到内存,通过纹理 id 提取合成画面,也就是:
> **虽然你看到控件在那里,但是其实它并不是真的在那里,你看的是只是合成之后的纹理,所以 `VirtualDisplay ` 上原生端接受到的触摸事件,其实是来自于 `FluterView`**
`VirtualDisplay ` 里触摸事件的发起和普通 Flutter 控件一样,都是从 `FlutterView``onTouchEvent` 开始,经过统一的[事件竞技场](https://juejin.cn/post/6844903841742192648)处理后,最终回到 java 层去触发 NativeView 响应手势信息。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image2.png)
所以在 `VirtualDisplay ` 里所有的 Event 都是直接来自 `FlutterView` ,走的是 `AndroidTouchProcessor` 进行发送。
# `HybirdComposition`
对于 `HybirdComposition` 来说这个实现又不大一样,因为 `HybirdComposition` 是直接把原生 View 通过 `addView` 添加到 `FlutterView` 上面,中间通过 `FlutterMutatorView` 作为容器,大概效果如下图所示。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image3.png)
**那是不是 `HybirdComposition` 上用户的触摸点击事件是直接由原生控件进行响应呢?答案是否定的**。
其实[一开始](https://github.com/flutter/engine/commit/1832613e0961902d9d368b3b4b6541b858050eb4#diff-efdceec13b333498e1451586d96adc90030b07bc1b7818cc4dbb16b85f1aba32) `HybirdComposition` 的设定确实是这样,但是后来为了统一和方便处理, `FlutterMutatorView` 上添加了 `onInterceptTouchEvent` 进行了拦截,所以事件都无法传递到它的子控件上,而是在 `FlutterMutatorView` 通过 `AndroidTouchProcessor` 发送到 Dart 层。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image4.png)
当然,**事实上在坐标处理上也有差异**,因为这里的 `onTouchEvent``FlutterMutatorView` 上的触摸事件坐标,而为了能够匹配到 dart 里的坐标进行响应,还需要通过矩阵转化为屏幕坐标,而这部分换算在 `VirtualDisplay `里是不需要的。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image5.png)
![](http://img.cdn.guoshuyu.cn/20221026_N17/image6.png)
事实上 `HybirdComposition` 的实现在触摸事件响应上比较有迷惑性,特别是某些场景下会很有趣,例如在下面这个场景上:
> 红色的是 Flutter 控件,蓝色是 Native 控件,它们恰好有一部分重叠在一起。
| ![](http://img.cdn.guoshuyu.cn/20221026_N17/image7.png) | ![](http://img.cdn.guoshuyu.cn/20221026_N17/image8.png) |
| ------------------------------------------------------- | ------------------------------------------------------- |
我们知道在 `HybirdComposition` 里,如果 Flutter 控件需要覆盖在 Native 控件之上是,就会需要一个 `FlutterImageView` 来做新的图层承载,但是 `FlutterImageView` 本身并没有做触摸事件处理,所以如果这时候点击红色 RE ,就会有两种情况:
- 点击的是和蓝色 Native 控件相交的区域,因为事件穿透的影响,此时会是通过 `FlutterMutatorView` 触发事件发送到 Dart
- 点击的是没有相交的区域时,因为事件穿透的影响,此时会是通过 `FlutterView` 触发事件发送到 Dart
![](http://img.cdn.guoshuyu.cn/20221026_N17/image9.png)
> 虽然这个过程其实很诡异,但是实际上并不会影响最终结果,详细感兴趣可以看 [《Flutter 3.0下的混合开发演进》](https://juejin.cn/post/7113655154347343909)
# TextureLayer
其实 `TextureLayer` 的事件实现和 `HybirdComposition` 类似,不同之处在于它是通过 `PlatformViewWrapper` 做父容器来拦截事件。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image10.png)
`PlatformViewWrapper` 同样通过 `onInterceptTouchEvent` 进行了事件拦截,所以事件都无法传递到它的子控件上,而是通过 `AndroidTouchProcessor` 发送到 Dart 层,同时在对应的 `onTouchEvent` 上需要做事件转化。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image11.png)
> PS ,这里看到 ` TextView` 是空白的原因就是 `PlatformViewWrapper` 通过 Hook 了 Canvas 从而提取 Child 纹理的过程,详细感兴趣可见:[《Flutter 3.0下的混合开发演进》](https://juejin.cn/post/7113655154347343909)。
所以本质上 `TextureLayer``HybirdComposition` 在事件消费处理上类似,只是不会有像 `HybirdComposition` 一样会有 `FlutterImageView` 那样诡异的传递方式而已。
![](http://img.cdn.guoshuyu.cn/20221026_N17/image12.png)
# 最后
好了,本篇的内容其实并不复杂,**主要是帮助你理清 `PlatformView` 里手势事件传递和处理的相关逻辑**,理清这部分逻辑,在你使用 add-to-app 时针对一些手势冲突会更有帮助,如果还有什么想说的,欢迎留言讨论~

View File

@ -164,6 +164,10 @@
* [如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果](Flutter-N9.md)
* [给掘金 Logo 快速添加动画效果,并支持全平台开发框架](Flutter-N10.md)
* [Flutter 实现 “真” 3D 动画效果,用纯代码实现立体 Dash 和 3D 掘金 Logo](Flutter-N11.md)
* [Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它](Flutter-N12.md)
* [Flutter 小技巧之优化你的代码性能](Fluttter-N13.md)
* [Flutter 之快速理解混合开发里的手势事件传递](Flutter-N17.md)
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)

View File

@ -130,6 +130,10 @@
* [如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果](Flutter-N9.md)
* [给掘金 Logo 快速添加动画效果,并支持全平台开发框架](Flutter-N10.md)
* [Flutter 实现 “真” 3D 动画效果,用纯代码实现立体 Dash 和 3D 掘金 Logo](Flutter-N11.md)
* [Flutter 3.3 之 SelectionArea 好不好用?用 “Bug” 带你全面了解它](Flutter-N12.md)
* [Flutter 小技巧之优化你的代码性能](Fluttter-N13.md)
* [Flutter 之快速理解混合开发里的手势事件传递](Flutter-N17.md)
* [一文快速带你了解 KMM 、 Compose 和 Flutter 的现状](Flutter-CCK.md)