深入探索Flutter的独特之处

发表时间: 2020-10-30 11:18

作者:GSYTech@微信公众号 搜狐技术产品

出处
:https://mp.weixin.qq.com/s/wETMBKmswBrXhuK5ZP20sg


本文字数: 2608

预计阅读时间: 6 分钟

本文主要介绍 Flutter 的现状和 Widget 的灵魂设计,用不一样的角度带你领略 Flutter 的魅力所在,相信本篇文章会给你带来 Flutter 上不一样的理解。

一、Flutter 的现状

Flutter 作为谷歌的新一代跨平台技术方案,它使用 自带的绘制引擎 ,优雅地解决了以前跨平台开发兼容性能 的痛点,通过成熟的 Skia 引擎去实现控件的绘制,让控件与平台 解耦 ,在提高性能的同时也增强了代码在不同平台的 复用率

Flutter 如今在国内如:字节跳动阿里巴巴、腾讯、京东、网易、美团等企业都相继投入使用,甚至阿里巴巴已经成立了 Alibabab-Flutter 相关事业组用于集团开发维护,今年 Alibabab-Flutter 更是战略上升到集团移动委员会四大战略之一,得到了资源上的重点投入,由多 BU 合作共建。

从实际使用角度出发,Flutter 如今已经在各大企业的产品投入使用,在 《国内大厂在移动端跨平台的框架接入分析》 中分析了 52 款大厂应用,其中就有 19 款应用集成了 Flutter 的开发能力。

Flutter 最大的特点就在于它的渲染引擎,Flutter 中提供的控件和 Canvas 能力都是通过 Flutter Engine 实现。

不同于 ReactNative 通过转化为原生控件再去渲染,Flutter Engine 在 Android 和 iOS 平台只需要提供对应的 Surface ,剩下都由 Flutter Engine 来完成绘制,这大大降低了 Flutter 和平台的耦合度,同时也让渲染出来的控件在不同平台能有一致性的表现。

Flutter 如今除了 AndroidiOS 平台之外,对于 WebMacOSLinux 平台的支持也进入了 beta 阶段, Win 进入了 PreView 阶段,这也是得益于 Flutter 优秀的底层设计。

而对于 Flutter ,相信开发者们最常用也最熟悉的就是 Widget ,而 Widget 的设计理念也正是它的灵魂所在。

二、Widget,Flutter 中一切的开始

要理解 Flutter 就要理解 Flutter 中最灵魂的设计:

Flutter 内一切皆 WidgetWidget 是不可变的(immutable),每个 Widget 状态都代表了一帧。

理解这段话是非常重要的,因为 Flutter 中所有界面的展示效果,在代码层面都是通过 Widget 作为入口开始。 Widget 是不可变的,说明页面发生变化时 Widget 一定是被重新构建,所以 Widget 的固定状态代表了一帧静止的画面,当画面发生改变时,对应的 Widget 一定会变化。

举个例子,如下代码所示定义了一个 TestWidgetTestWidget 接受传入的 titlecount 参数显示到 Text 上,同时如果 count 大于 99,则只显示 99。

Warnning/// This class is marked as '@immutable'/// but one or more of its instance fields are not finalclass TestWidget extends StatelessWidget {  final String title;  int count;  TestWidget({this.title, this.count});  @override  Widget build(BuildContext context) {    this.count = (count > 99) ? 99 : count;    return Container(      child: new Text("$title $count"),    );  }}

这段代码看起来没有什么问题,也可以正常运行,但是在编译器上会有 “This class is marked as '@immutable',but one or more of its instance fields are not final” 的提示警告,这是因为 TestWidget 内的 count 成员变量没有加上 final 声明,从而在代码层面容易产生歧义。

因为前面说过 Widgetimmutable ,所以它的每次变化都会导致自身被重新构建,也就是 TestWidget 内的 count 成员变量其实是不会被保存且二次使用。

如上所示代码中 count 成员没有 final 声明,所以理论是可以对 count 进行二次修改赋值,造成 count 成员好像被保存在 TestWidget 中被二次使用的错觉,容易产生歧义。

那这时候可能有人会问: 每次 Widget 都重构,那怎么保存状态?这样 Widget 的设计会不会性能很差?

三、Element,Flutter 的背后大佬

前面说过, Flutter 中的 Widget 是不可变的,比如前面用的 StatelessWidget ,一个 Widget 状态代表了一帧,而在 Flutter 里提供了 StatefulWidget 实现跨帧保存状态的功能。

那什么是 StatefulWidget ?如下代码所示,只需要让 TestWidget 继承 StatefulWidget ,然后将 build 方法和 count 写到 State 内,就可以实现 count 状态的跨帧保存。

当然这里的 title 参数还是定义在 Widget 层面,所以依旧需要 final

class TestWidget extends StatefulWidget {  final String title;  TestWidget({this.title});  @override  _TestWidgetState createState() => _TestWidgetState();}class _TestWidgetState extends State<TestWidget> {  int count;  @override  Widget build(BuildContext context) {    this.count = (count > 99) ? 99 : count;    return InkWell(      onTap: () {        setState(() {          count++;        });      },      child: Container(        child: new Text("${widget.title} $count"),      ),    );  }}

可以看到 StatefulWidget 本身依旧是不可变的,所以 title 属性需要定义为 final ,但是 build 方法被放到了 State 中, count 也被移动到 State 中,所以 count 可以支持跨帧保存。

为什么写到 State 就可以实现跨帧保存 ? StateWidget 是什么关系?这就需要介绍 Flutter 中另一个概念: Element

事实上如果查看 Flutter 对于 Widget 的定义,可以看到官方对于 Widget 的定义是 Describes the configuration for an [Element]. ,也就是 Element 才是 Widget 真正的体现。

对于 Element 大家可能会比较陌生,因为 Flutter 开发中很少直接使用 Element 。但是如果说到 BuildContext 相信大家就不会陌生,因为 BuildContext 在开发中经常被使用到,而 Element 恰好就是 BuildContext 的实现类,你使用的 BuildContext 其实就是 Element

所以在 Flutter 的开发过程中, Element 其实扮演着十分重要的角色:一个 Widget 被首次加载时,会先创建出它对应的 Element ,就比如上面介绍的 StatefulWidget ,会对应先创建出 StatefulElement ;而 StatefulElement 被调用的时候,会先通过 widget.createState() 创建出 State ,之后 _state 就被保存在 Element ,所以 _state 可以跨 Widget 保存。

理解了吗? Element 才是一个真正的工作节点,而 Widget 仅仅是一个“配置信息” 。

因为 Widget 仅仅是“配置信息” ,所以它可以每次改变时都被重构,而 Element 才是真正的节点,它每次读取 Widget 的状态去配置和绘制,但是 Element 并不是每次都重新创建,而是在 Widget 首次被加载时通过 createElement 创建出来。

什么是首次加载呢?通俗点来说就是这个 Element 在 mount 的时候。

举一个例子,如下面代码所示:

  • DemoPage Scaffold body StatePage
  • StatePage data “init” _StatePageState data
  • floatingActionButton setState _DemoPageState内 data “Change By setState”
  • 但是最终结果 StatePage 界面依然显示 “init”
class DemoPage extends StatefulWidget {  @override  _DemoPageState createState() => _DemoPageState();}class _DemoPageState extends State<DemoPage> {    String data = "init";    @override  Widget build(BuildContext context) {    return Scaffold(      /// 将data数据传递给StatePage      body: StatePage(data),      floatingActionButton: FloatingActionButton(        onPressed: (){          /// 改变data数据          setState(() {            data = "Change By setState";          });        },      ),    );  }}class StatePage extends StatefulWidget {  final String data;  StatePage(this.data);  @override  _StatePageState createState() => _StatePageState(data);}class _StatePageState extends State<StatePage> {  String data;   //// data是从StatePageState的构造方法传入  _StatePageState(this.data);  @override  Widget build(BuildContext context) {    return Container(      child: new Center(          child: new Text(data ?? "")      ),    );  }}

为什么 StatePage 界面依然显示 “init” ?因为虽然 StatePage 这个 Widgetdata 发生了改变,但是 StatePagecreateState() 方法只在第一次被加载时调用,对应创建出来的 state 被保存在 Element 中,所以 _StatePageState 中的 data 只在第一次时被传递进来。

关键就在于 _StatePageState 创建时 data 只被赋数一次。

如果这时候要让 StatePagedata 正常改变,就需要使用 widget.data 。看出来了没, 如果把 State 放到 Element 的级别上来看,通过 widget.data 去更新的逻辑,是不是 Widget 看起来就很像是一个配置文件 。

那最后再问一个问题: WidgetElement 的对应关系是怎么样?

四、Element 和 Widget 的对应关系

从官方的注释里可以看出, Widget 做为配置文件,和 Element 是应该是一对多的关系,因为真实工作的其实是 Element 树,而 Widget 作为配置文件,可能会在多个地方被复用。

举个例子,如下代码所示,通过运行后可以看到 textUseAll 在多个地方被 inflated 并且正常渲染出 3333333 的效果。

这是因为 textUseAll 仅仅是作为配置文件,所以被多个 Element 加载并不会出现问题,因为这并不是一个真正的 "View" 被多个地方加载。

final textUseAll = new Text(  "3333333",  style: new TextStyle(fontSize: 18, color: Colors.red),);class MyHomePage extends StatelessWidget {  void goNext(context) {    Navigator.of(context).push(MaterialPageRoute(builder: (context) {      return ShowPage();    }));  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(),      body: Center(          child: Container(        color: Colors.blue,        height: 80,        child: Stack(          children: <Widget>[            new Center(              child: Row(                crossAxisAlignment: CrossAxisAlignment.center,                textBaseline: TextBaseline.alphabetic,                mainAxisAlignment: MainAxisAlignment.center,                children: <Widget>[                  textUseAll,                  Text(                    ' GSY ',                    style: TextStyle(fontSize: 36, fontFamily: "Heiti"),                  ),                  textUseAll,                ],              ),            ),          ],        ),      )),      floatingActionButton: FloatingActionButton(        onPressed: () {          goNext(context);        },        tooltip: 'Next',        child: Icon(Icons.add),      ),    );  }}class ShowPage extends StatelessWidget {  void goNext(context) {    Navigator.of(context).push(MaterialPageRoute(builder: (context) {      return ShowPage();    }));  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(),      body: Container(        child: new Center(          child:          textUseAll,        ),      ),      floatingActionButton: FloatingActionButton(        onPressed: () {          goNext(context);        },        tooltip: 'goNext',        child: Icon(Icons.add),      ),    );  }}

当然,当你给上面的 textUseAllText 配置上 GlobalKey 就是会是另外一个故事,因为 GlobalKey 会让一个 Widget 变成“全局单例” ,具体解释这里就不展开,感兴趣的可以自己尝试下。

最后补充一点:Flutter 中除了 WidgetElement 之外,还有 RenderObjectLayer 等才能组成 Dart 层的渲染闭环,这里主要介绍了 WidgetElement 的关系,是因为 WidgetElement 的理解是 Flutter 中入门时最容易忽略的, Flutter 对于 Widget 的设计让我们写代码时更像是在写配置文件。

五、最后

本篇文章以 Flutter 的 WidgetElement 为核心要点展开,为大家介绍了 Flutter 中最有意思的设计之一,事实上只有掌握了这些基础概念,才能在后续的学习里更好地理解 Flutter 的开发思想,当然更加完善的体系内容可以在 《Flutter开发实战详解》 中找到。


作者:GSYTech@微信公众号 搜狐技术产品

出处
:https://mp.weixin.qq.com/s/wETMBKmswBrXhuK5ZP20sg