目录
一、Flutter 为何使用Dart开发语言
二、Flutter的UI系统
1.特点
2.架构简介
2.1 Flutter Engine
2.2 Framework(Dart)
3.Flutter如何通过widget构建UI
4.Flutter是响应式的框架,但是推崇能不变就不变
5.庞大的widget体系,带来方便的同时也带来了高昂的学习成本
6.套娃UI代码,揭开一层还有一层,喝完这杯还有三杯
7.优秀的跨平台UI框架必须要有优秀的UI调试工具
三、Flutter与Native的交融
1.混编依赖方案的抉择
2.通不通且看武功
2.1 打通事件通讯:平台通道(Platform Channel)
2.2 打通跨层渲染:外接纹理(Texture)
一、Flutter 为何使用Dart开发语言
- Dart运行时和编译器支持Flutter的两个关键特性:在开发阶段采用,采用JIT模式,改动无需编译,极大的节省了开发时间;发布时可以通过AOT生成高效的ARM代码以保证应用性能。
- 另外Dart还支持静态类型检查,相比JavaScript在开发时有很大优势。
- Flutter框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器,而Dart使用Chrome V8引擎来做内存分配,使得内存分配可以得到保证。
- Dart使Flutter不需要单独的声明式布局语言,如JSX或XML,或单独的可视化界面构建器,因为Dart的声明式编程布局易于阅读和可视化。所有的布局使用一种语言,聚集在一处,Flutter很容易提供高级工具,使布局更简单
- 由于Flutter应用程序被编译为本地代码,因此它们不需要在领域之间建立缓慢的桥梁(例如,RN需要在JavaScript和Native之间通信),它的启动速度也快得多。
二、Flutter 的UI系统
1. 特点
Flutter不使用webView,也不使用操作系统的原生控件
Flutter使用自己的高性能渲染引擎
Skia
来绘制widget。
这样不仅可以保证在Android和iOS上UI的一致性,而且也可以避免对原生控件依赖而带来的限制及高昂的维护成本。组合大于继承
控件本身通常由许多小型、单用途的控件组成,结合起来产生强大的效果,类的层次结构是扁平的,以最大化可能的组合数量
2. 架构简介
2.1 Flutter Engine
Flutter引擎是托管Flutter应用程序的可移植运行时。它实现了Flutter的核心库,包括动画和图形、文件和网络I/O、可访问性支持、插件架构以及Dart运行时和编译工具链。大多数开发人员将通过Flutter框架与Flutter进行交互,该框架提供了一个现代的、可响应的框架,以及一组丰富的平台、布局和基础小部件。
2.2 Framework(Dart)
这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
底下两层(Foundation和Animation、Painting、Gestures)
在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。Rendering层
这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。Widgets层是Flutter提供的的一套基础组件库
在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。
在Flutter中,几乎一切都是widget。应用程序,页面,布局,视图,事件,通知,甚至是具体的文本样式。统一化为widget的方式,使得Flutter的代码更加统一。
Flutter的widget是对页面UI的一种描述,类似于web中的html,iOS中的xib,android中的xml。Flutter在构建UI过程中也是形成一个widget树,就如iOS的视图树。但是不同的是这个树并不是最终渲染的树。
3. Flutter如何通过widget构建UI
先来看一下Flutter的渲染管道:
在这个渲染过程中经历了widget树转化成element树再到最终渲染的renderObject树的过程,如下:
Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable(如何更新数据呢?查看后续内容)
Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject
RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容
element相比于widget增加了上下文的信息。element是对应widget,在渲染树的实例化节点。同一个widget可以对应渲染树中的多个element,就像是一个视图模板。
widget都是不可变的,初始状态设置以后就不可再变化。也就是说,每次视图的更新都会重新构建widget本身和子widget(具体表现为重新执行widget的build方法)。
针对视图在运行时可能变化的情况,Flutter引入了State来管理视图的状态。在修改数据之后,需要主动调用setState()
来触发视图状态的更新。不像普通的双向绑定,数据一修改就会触发视图的变化,容易造成视图在短时间内多次更新渲染。从这里也能看得出Flutter的设计者并不希望你频繁地去更新视图状态,毕竟重新构建widget树的代价也是蛮大,尤其是相对复杂的页面。
另外,Flutter在视图描述widget和真实渲染的RenderObject的中间设计的Element层,对某一时刻的事件做了汇总和比对,只对真正需要修改的部分同步到真实渲染的RenderObject树上面,做到最小程度的修改,以提高渲染效率。
4. Flutter是响应式的框架,但是推崇能不变就不变
拥有响应式框架的以下特点
- 不直接操作UI,改为通过修改数据然后更新视图的状态来驱动视图变化
- 通过视图事件的绑定来操作数据并最终将结果反作用于视图
Flutter在页面渲染上面的核心思想是simple is fast
,所以相对于可变状态的StatefulWidget
还设计了状态不可变的StatelessWidget
,也就更加强调了能不变就不变的理念。
5. 庞大的widget体系带来方便的同时也带来了高昂的学习成本
Flutter有一个庞大的组件体系,有很多iOS风格(Cupertino)和安卓风格(Material)的现成widget可以使用,使得UI的构建变得相对容易。但是,庞大的组件体系带来方便的同时也带来了高昂的学习成本(单单记下这些widget的大体功能都要花不少时间)。
不过值得庆幸的是,你常用的widget并不会这么多。上面说过widget只是界面描述,同一个界面实现的方式都会有很多种,每个人都会使用自己熟悉和擅长的方式去构建界面,但是经过转换成Element树,最终到达的RenderObject树可能是一样的。有一种殊途同归的感觉。
下图是Flutter常见的widget,体会一下吧。
6. 套娃UI代码,揭开一层还有一层,喝完这杯还有三杯
由于Flutter基本上都是由widget实现,所以也就难以避免一层套一层的代码风格,颇有HTML风范。
- 控件套一层
- 容器修饰套一层(圆角,着色等)
- 事件套一层
- 布局套N层
- 父级控件套N层
…… - 页面也来套一层
有些抽象,我们来看个实际的例子。
以下是Cell的视图代码:
Column(//纵向分栏
children: <Widget>[
Padding(//边距
padding: EdgeInsets.all(10),
child: Row(//横向分栏
children: <Widget>[
ClipRRect(//切圆角
borderRadius: BorderRadius.circular(10.0),
child: Image.asset('images/icon.png',width: 80,height: 80),
),
Padding(//边距
padding: const EdgeInsets.only(left: 10),
child: Column(//纵向分栏
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(//文本控件
'香草拿铁',
style: TextStyle(//文本风格
fontSize: 18,
color: Colors.black
),
),
Text(
'Vanilla Latte',
style: TextStyle(
fontSize: 14,
color: Color(0xffcccccc)
),
),
Text(
'默认:大/单糖/热',
style: TextStyle(
fontSize: 14,
color: Color(0xffcccccc)
),
),
Text(
'¥27',
style: TextStyle(
fontSize: 17,
color: Colors.black
),
),
],
),
),
Expanded(//充满父容器剩余空间
child: Row(
mainAxisAlignment: MainAxisAlignment.end,//右对齐
children: <Widget>[
GestureDetector(
onTap: (){//点击事件触发
},
child: ClipRRect(//切圆角
borderRadius: BorderRadius.circular(10.0),
child: Container(//容器修饰,用于添加蓝色背景
color:Colors.blue ,
child: Icon(//图标
Icons.add,
size: 20,
color: Colors.white
)
),
),
)
],
),
)
],
),
),
Container(
color: Color(0xffcccccc),
height: 0.5
)
],
);
在IDE中有个辅助线和尾部的备注会稍微好一点,但是已经习惯iOS代码风格的笔者确实有些适应不过来(虽然已经磨合一段时间了):
也许,你和我一样想到了封装,好,我们就封装一下。
//cell整体
Column(
children: <Widget>[
getContent(),/*cell内容*/
getBottomLine()/*分割线*/
],
);
/*cell内容*/
Widget getContent(){
return Padding(
padding: EdgeInsets.all(10),
child: Row(
children: <Widget>[
getHeadIcon(),/*头像*/
Padding(/*中间文本列*/
padding: const EdgeInsets.only(left: 10),
child: getMiddleWidget(),
),
Expanded(/*充满父容器剩余空间*/
child: getRightButton()/*按钮*/
)
],
),
);
}
/*分割线*/
Widget getBottomLine(){
return Container(
color: Color(0xffcccccc),
height: 0.5
);
}
/*头像*/
Widget getHeadIcon(){
return ClipRRect(//切圆角
borderRadius: BorderRadius.circular(10.0),
child: Image.asset('images/icon.png',width: 80, height: 80),
);
}
/*中间的文本列*/
Widget getMiddleWidget() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
getText('香草拿铁', 18, Colors.black),
getText('Vanilla Latte', 14, Color(0xffcccccc)),
getText('默认:大/单糖/热', 14, Color(0xffcccccc)),
getText('¥27', 17, Colors.black)
],
);
}
/*右侧按钮*/
Widget getRightButton(){
return Row(
mainAxisAlignment: MainAxisAlignment.end,//右对齐
children: <Widget>[
GestureDetector(
onTap: (){//点击事件触发
},
child: ClipRRect(//切圆角
borderRadius: BorderRadius.circular(10.0),
child: Container(
color:Colors.blue ,
child: Icon(
Icons.add,
size: 20,
color: Colors.white
)
),
),
)
],
);
}
/*文本*/
Text getText(text, double fontSize, Color color){
return Text(
text,
style: TextStyle(
fontSize: fontSize,
color: color
),
);
}
以上已经是较为详细的封装了,当然因为层级很多,要再继续拆分下去也不是不可以,这就涉及封装粒度以及封装的最终成果能不能成正比了。看一下结构:
- cell整体
- cell内容
- 头像
- 文本列
- 文本
- 按钮
- 分割线
以上,拆分后相对好一些,但是一层套一层的诟病还是无法避免。
(比如,cell内容这个函数,右侧按钮这个函数不考虑,因为这个是非常规的按钮实现方式,一般用Flutter的按钮widget即可)
笔者认为,造成这个结果的最主要原因即是其最大的特点:一切皆widget,widget包widget。
如果要让代码够优雅,布局粒度的划分是值得思量的。
7.优秀的跨平台UI框架必须要有优秀的UI调试工具
在Flutter Inspector之中提供了,可视化视图树查看工具,虽然与xcode的 界面调试工具相比是2D的有些遗憾,不过也已经挺强大。如下图:
更多工具
-
性能监控(Performance Overlay)可以查看GPU和UI的帧率
-
绘制基线(Paint Baselines)
-
Debug Paint 展示所有控件的绘制边界
绿色箭头表示可滚动内容,以及可滚动内容的初始到结束的方向
因为widget树并不是最终绘制的UI树,所以Flutter 监察器还提供了真正的绘制树查看工具,如下Render Tree分栏。
Flutter新版本插件(32.0.1)提供了代码的同步定位功能,越来越好了呢
这些工具在平常的开发中已经够用,其他细节留给读者们自己探索吧。
三、Flutter 与原生交互
1.混编依赖方案的抉择
1.1 Flutter默认的工程构建方式,native工程完全是Flutter构建产物
默认的方式,无法在已有原生工程的基础上引入Flutter,必须完整重新创建整个工程,这个是致命的。问题还有很多,比如:
- native反向依赖Flutter父目录,耦合严重
- 代码库难以拆分管理
- 对纯native开发的团队成员造成入侵,需要完备的Flutter开发环境,和相应的构建步骤
1.2 三个代码库独立,修改 Flutter 构建流程将构建产物直接提供给native作为依赖
这个方式中,以iOS为例,将Flutter.framework及相关插件等做成本地的pod依赖,资源也复制到本地进行维护。这样Flutter就被打包成了pod库,
在native团队成员那边,Flutter就是黑盒,只管用就行了。Flutter pod库的引用内容需要各个团队成员走Flutter构建流程去生成。虽说代码仓库比较好分开了,但是缺点还是有的:
- 需要对Flutter原有的构造流程进行稍嫌复杂的改动
- Native工程与Flutter的内容还是耦合在本地
- Native开发者仍然需要完备的Flutter开发环境
1.3 将Flutter本地依赖修改成远程依赖,native开发完全脱离Flutter
这个方案是将Flutter所有依赖内容都放在独立的远端仓库中,native如同引用公开三方库一样去引用Flutter。这时候,native就不需要Flutter开发环境了。
要说这个方案的缺点就是同步的流程变得更繁琐,Flutter内容的变动需要先同步到远程仓库再同步到native依赖。极端情况,在native与Flutter频繁交互的时候,就需要频繁更新依赖库。Flutter依赖库的版本和native代码版本的对应管理也是需要额外耗费精力。不过,这个不算大问题,与以往的H5与原生的混编类似,沿用即可。
2 通不通且看武功
Flutter基于SKIA使用Dart搭建了自己的UI框架,而底层最终都是调用OpenGL绘制,在Native和Flutter Engine上实现了UI的隔离。那么开发者书在写UI代码时就不用再关心平台实现,从而实现了跨平台。这层隔离在Flutter Engine和Native之间竖立了一座大山,想要实现通讯就得另辟蹊径。
2.1 打通事件通讯:平台通道(Platform Channel)
Flutter与原生之间的通信依赖灵活的消息传递方式:平台通道(Platform Channel)。
平台指的就是指Flutter运行的平台,如Android或iOS,可以认为就是应用的原生部分,平台通道正是Flutter和原生之间通信的桥梁。
当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。值得注意的是消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。
平台通道的能力
- 传递小量数据:基本数据类型,数组,字典,二进制数据;
- 通过定制可传递大数据块,但是用于如图像,视频等大数据的传输必然引起内存和CPU的巨大消耗
- 非线程安全,native的回调必须在主线程执行,故应该在Native端的Handler中处理耗时操作
平台通道的设计初衷并不是用来传递大数据的,从本质上说是提供了一个消息传送机制。
2.2 打通图像渲染:外接纹理(Texture)
纹理(Texture):可以理解为GPU内代表图像数据的一个对象。
Flutter提供了一个Texture控件,这个控件上显示的数据,需要由Native提供。
Flutter和Native传输的数据载体是PixelBuffer,Native端的数据源(摄像头、播放器等)将数据写入PixelBuffer,Flutter拿到PixelBuffer以后转成OpenGLES Texture,交由Skia绘制。
通过这个方式,Flutter就可以容易的绘制出一切Native端想要绘制的数据,除了摄像头播放器等动态图像数据,也给其他诸如地图等视图的展示提供了另一种可能。