Flutter试用报告

4,845 阅读14分钟

目录

一、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. 架构简介

Flutter架构图

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的渲染管道:

Rendering Pipeline

在这个渲染过程中经历了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,体会一下吧。

flutter widgets (图片来自网上)

6. 套娃UI代码,揭开一层还有一层,喝完这杯还有三杯

由于Flutter基本上都是由widget实现,所以也就难以避免一层套一层的代码风格,颇有HTML风范。

  • 控件套一层
  • 容器修饰套一层(圆角,着色等)
  • 事件套一层
  • 布局套N层
  • 父级控件套N层 ……
  • 页面也来套一层

有些抽象,我们来看个实际的例子。

实现这样的一个cell

以下是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代码风格的笔者确实有些适应不过来(虽然已经磨合一段时间了):

image.png

也许,你和我一样想到了封装,好,我们就封装一下。

//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的有些遗憾,不过也已经挺强大。如下图:

Flutter Inspector select widget mode

更多工具
  • 性能监控(Performance Overlay)可以查看GPU和UI的帧率

    Performance Overlay

  • 绘制基线(Paint Baselines)

    image.png

  • Debug Paint 展示所有控件的绘制边界 绿色箭头表示可滚动内容,以及可滚动内容的初始到结束的方向

    Paint

因为widget树并不是最终绘制的UI树,所以Flutter 监察器还提供了真正的绘制树查看工具,如下Render Tree分栏。

Render Tree

Flutter新版本插件(32.0.1)提供了代码的同步定位功能,越来越好了呢

代码同步定位

这些工具在平常的开发中已经够用,其他细节留给读者们自己探索吧。

三、Flutter 与原生交互

1.混编依赖方案的抉择

1.1 Flutter默认的工程构建方式,native工程完全是Flutter构建产物

Flutter工程目录
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与原生的混编类似,沿用即可。

Flutter远程依赖

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提供。

image.png
Flutter和Native传输的数据载体是PixelBuffer,Native端的数据源(摄像头、播放器等)将数据写入PixelBuffer,Flutter拿到PixelBuffer以后转成OpenGLES Texture,交由Skia绘制。 通过这个方式,Flutter就可以容易的绘制出一切Native端想要绘制的数据,除了摄像头播放器等动态图像数据,也给其他诸如地图等视图的展示提供了另一种可能。