大神教你举一反三---用实战讲述Flutter-跨平台框架应用(1)
注意,Flutter 中常见的,其实就是Element的抽象,通过,我们一般情况就可以对应获得Element,也就是拿到了“仓库的钥匙” ,通过context就可以去获取Element内持有的东西,比如前面所说的,还有后面我们会谈到State等。

注意,Flutter 中常见的
BuildContext,其实就是Element的抽象,通过BuildContext,我们一般情况就可以对应获得Element,也就是拿到了“仓库的钥匙” ,通过context就可以去获取Element内持有的东西,比如前面所说的RenderObject,还有后面我们会谈到State等。
1.2 Widget 的分类
这里我们将 Widget 分为如下图所示分类:是否存在 State 、是否存在RenderObject 。

其实还可以按照
RenderBox和RenderSliver分类,但是篇幅原因以后再介绍。
1.2.1 是否存在 State
Flutter 中我们常用的 Widget 有: StatelessWidget 和 StatefulWidget 。
如下图, StatelessWidget 的代码很简单,因为 Widget 是不可变的,传入的 text 决定了它显示的内容,并且 text 也算是 final 的。

注意图中
DemoPage有个黄色警告,这是因为我们定义了int i = 0不是 final 导致的,在StatelessWidget中, 非 final 的变量起始容易产生误解,因为Widget本事就是不可变的。
前面我们说过 Widget 都是不可变的,在这个基础上, StatefulWidget 的 State ,帮我们实现了 Widget 的跨帧绘制 ,也就是在每次 Widget 重构时,可以通过 State 重新赋予 Widget 需要的配置信息,而这里的 State 对象,就是存在每个 Element 里的。
同时,前面我们说过,Flutter 内的
BuildContext其实就是Element的抽象,这说明我们可以通过context去获取Element内的东西,比如State、RenderObject、Widget。
Widget ancestorWidgetOfExactType
State ancestorStateOfType
State rootAncestorStateOfType
RenderObject ancestorRenderObjectOfType
如下图所示,保存在 State 中的 text ,当我们点击按键时,setState 时它被标志为 "变化了" , 它可以主动发生改变,保存变量,不再只是“只读”状态了。

1.2.2、容器 Widget/渲染 Widget
在 Flutter 中还有 容器 Widget 和 渲染Widget 的区别,一般情况下:
-
Text、Slider、ListTile等都是属于渲染Widget,其内部主要是RenderObjectElement,对应有RenderObject参数。 -
StatelessWidget/StatefulWidget等属于容器Widget,其内部使用的是ComponentElement,ComponentElement本身是不存在RenderObject的。
所以作为容器 Widget, 获取它们的 RenderObject 时,获取到的是 build 后的树结构里,最上面渲染 Widget的 RenderObject 。

如上图所示
findRenderObject的实现,最终就是获取renderObject,在遇到ComponentElement时,执行的是element.visitChildren(visit);, 递归直到找到RenderObjectElement,再返回它的renderObject。
获取 RenderObject 在 Flutter 里很重要的,因为获取控件的位置和大小等,都需要通过 RenderObject 获取。
1.3、RenderObject
Flutter 中各类 RenderObject 的实现,大多都是颗粒度很细,功能很单一的存在 :

然而接触过 Flutter 的同学应该知道 Container 这个 Widget ,Container 的功能却不显单一,这是为什么呢?
如下图,因为 Container 其实是容器 Widget ,它只是把其他“单一”的 Widget 做了二次封装,然后通过配置参数来达到 “多功能的效果” 而已。

所以 Flutter 开发中,我们经常会根据功能定义出各类如 Continer、Scaffold 等脚手架模版,实现灵活与复用的界面开发。
回归到 RenderObject ,事实上 RenderObject 还属于比较“低级”的阶段,因为绘制到屏幕上我们还需要坐标体系和布局协议等,所以 大部分 Widget 的 RenderObject 会是子类 RenderBox (RenderSliver 例外) ,因为 RenderObject 本身只实现了基础的 layout 和 paint ,而绘制到屏幕上,我们需要的坐标和大小等,这些内容是在 RenderBox 中开始实现。
RenderSliver主要是在滚动控件中继承使用。
比如控件被绘制在 x=10,y=20 的位置,然后大小由 parent 对它进行约束显示,RenderBox 继承了 RenderObject,在其基础上实现了 笛卡尔坐标系 和布局协议。
这里我们通过 Offstage 这个 Widget ,看下其 RenderBox 子类的实现逻辑, Offstage 是用于控制 child 是否显示的作用,如下图,可以看到 RenderOffstage 对于 offstage 标志位的内部逻辑:
[图片上传失败…(image-6da7e5-1571570507182)]
那么 Flutter 中的布局协议是什么呢?
简单来说就是 child 和 parent 之间的大小应该怎么显示,由谁决定显示区域。 相信从 Android 到接触 Flutter 的同学有这样的疑惑, Flutter 中的 match_parent 和 wrap_content 逻辑需要怎么设置?
就我们从一个简单的代码分析,如下图所示,Row 布局我们没有设置任何大小,它是怎么确定自身大小的呢?

我们翻阅源码,可以发现其实 Flutter 中常用的 Row 、Column 等其实都是 Flex 的子类,只是对 Flex 做了简单默认配置。

那按照我们前面的理解,看一个 Widget 的实现逻辑,就应该看它的 RenderObject ,而在 Flex 布对应的 RenderFlex 中,我们可以看到如下一段代码:

可以看到在布局的时候,RenderFlex 首先要求 constraints != null ,Flex 布局的上层中必须存在约束,不然肯定会报错。
之后,在布局时,Row 布局的 direction 是横向的,所以 maxMainSize 为上层布局的最大宽度,然后根据我们配置的 mainAxisSize 的参数:
- 当
mainAxisSize为max时,我们Row的横向布局就是maxMainSize; - 当
mainAxisSize为min时,我们Row的横向布局就是allocatedSize;
前面 maxMainSize 我们知道了是父布局的最大宽度,而 allocatedSize 其实就是 child 的宽度之和。所以结果很明显了:
对于 Row 来说, mainAxisSize 为 max 时就是 match_parent ;mainAxisSize 为 min 时就是 wrap_content 。
而高度 crossSize ,其实是由 math.max(crossSize, _getCrossSize(child)); 决定,也就是 child 中最高的一个作为其高度。
最后小结一个知识点:
布局一般都是由上层往下传递 Constraints ,然后由下往上返回 Size。

那如何直接自定义 RenderObject 布局?
抛开 Flutter 为我们封装的好的,三大金刚 Widget 、Element 、RednerObject 一个不少,当然, Flutter 内置了很多封装帮我们节省代码。
一般情况下自定义 RenderObject 布局:
- 我们会继承
MultiChildRenderObjectWidget和RenderBox这两个abstract类,实现自己的Widget和RenderObject对象; - 然后利用
MultiChildRenderObjectElement关联起它们; - 除此之外,还有几个关键的类:
ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin和ContainerBoxParentData等可以帮你减少代码量。

总结起来, 对于 Flutter 而言,整个屏幕都是一块画布,我们通过各种 Offset 和 Rect 确定了位置,然后通过 Canvas 绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是重新绘制。
这里没有介绍
RenderSliver相关,它的输入和输出和Renderbox又不大一样,有机会我们后面再详细介绍。
三、Flutter 的实战技巧
3.1、InheritedWidget
InheritedWidget 是 Flutter 的灵魂设定之一。
InheritedWidget 共享的是 Widget ,只是这个 Widget 是一个 ProxyWidget ,它自己本身并不绘制什么,但共享这个 Widget 内保存有的数据,从而到了共享状态的目的。
如下图所示,是 Flutter 中常见的 Theme ,其内部就是使用了 _InheritedTheme 这个 InheritedWidget 来实现主题的全局共享的。那么 InheritedWidget 是如何实现全局共享的呢?

其实在 Element 的内部有一个 Map<Type, InheritedElement> _inheritedWidgets; 参数,_inheritedWidgets 一般情况下是空的,只有当父控件是 InheritedWidget 或者本身是 InheritedWidget 时,它才会被初始化,而当父控件是 InheritedWidget 时,这个 Map 会被一级一级往下传递与合并。
所以当我们通过 context 调用 inheritFromWidgetOfExactType 时,就可以通过这个 Map 往上查找,从而找到这个上级的 InheritedWidget 。(毕竟 context is Element)

如我们的 Theme/ThemeData 、Text/DefaultTextStyle、Slider / SliderTheme 等,如下代码所示,我们可以定义全局的 ThemeData 或者局部的 DefaultTextStyle ,从而实现全局的自定义和局部的自定义共享等。


其实,Flutter 中大部分的状态管理控件,其状态共享方法,也是基于
InheritedWidget去实现的。
3.2、支持原生控件
前面我们说过, Flutter 既然不依赖于原生控件,那么如何集成一些平台已有的控件呢?比如 WebView 和 Map ?
我们这里以 WebView 为例子:
在官方 WebView 控件支持出来之前 ,第三方是直接在 FlutterView 上覆盖了一个新的原生控件,利用 Dart 中的占位控件传递位置和大小。
如下图,在 Flutter 端 push 出一个 设定好位置和大小 的 SingleChildRenderObjectWidget ,从而得到需要显示的大小和位置,将这些信息通过 MethodChannel 传递到原生层,在原生层 addContentView 一个指定大小和位置的 WebView 。
这时候 WebView 和 SingleChildRenderObjectWidget 处于一样的大小和位置,而空白部分则用 FLutter 的 Appbar 显示。

这样看起来就像是在 Flutter 中添加了 WebView ,但实际这脱离了 Flutter 的渲染树,其中一个问题就是,当你跳转 Flutter 其他页面的时候,会被 WebView 挡住;并且打开页面的动画,Appbar 和 WebView 难以保持一致。

后面 官方 WebView 控件支持出来后,这时候官方是利用 PlatformView 的设计,完成了不脱离 Flutter 渲染堆栈,也能集成平台原生控件的功能。
以 Android 为例,Android 上是利用了副屏显示的底层逻辑,使用 VirtualDisplay 类,创建一个虚拟显示器,需要调用 DisplayManager 的 createVirtualDisplay() 方法,将虚拟显示器的内容渲染在一个内存的 Surface 上 ,生成一个唯一的 textureId 。
如下图,之后渲染时将 textureId 传递给 Dart 层,渲染引擎会根据 textureId , 获取到内存里已渲染数据,绘制到 AndroidView 上进行显示。

3.3、错误处理
Flutter 中比较有趣的情况是,在 Dart 中的一些错误,并不会导致应用闪退,而是通过如下的红色堆栈 UI ,错误区域不同,可能是全屏红,也可能局部红,这种状态就和传统 APP 的“崩溃”状态不大一样了。

在开发过程中这样的显示没太大问题,但事实发布线上版本就不合适了,所以我们一般会选择自定义错误显示。
如下图所示,一般我们可以通过如下处理,自定义我们的错误页面,并且收集错误信息。

重写 ErrorWidget 的 builder 方法,然后将信息收集到 Zone 中,返回自己的自定义错误显示,最后在 Zone 内利用 onError 统一处理错误。
ps 图中的
Zone等概念这里就不展开了,有兴趣的可以去以前的文章详细查看。
四、Flutter Web
最后简单说下 Flutter Web ,Flutter 在支持 Web 平台上的优势在于 Flutter UI 与平台的耦合度很低,而 Dart 起初就是为了 Web 而生,一拍即合下 Flutter 支持 Web 并不是什么意外。
但是 Web 平台就绕不过 JS ,在 Web 平台,实际上 Image 控件最后会通过 dart2js 转化为 <img> 标签并通过 src 赋值显示。

同时,多了一个平台就多了需要兼容的,目前 Flutter 的 issue 仍然不少,而 Web 支持虽然已经合并到主项目中,但是在兼容、性能等问题上还需要继续优化,比如 Flutter Web 中 canvas.drawColor(Colors.black, BlendMode.clear); 是会出现运行错误的,因为不支持 BlendMode.clear
作者:恋猫月亮
链接:https://www.jianshu.com/p/f79534f5efd0
文末
那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试
- 技能

- 面试技巧+面试题

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。*
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
更多推荐



所有评论(0)