【文末有惊喜!】带你深入理解不一样的 Flutter
本文字数: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 如今除了
Android
和iOS
平台之外,对于Web
、MacOS
、Linux
平台的支持也进入了beta
阶段,Win
进入了PreView
阶段,这也是得益于 Flutter 优秀的底层设计。
而对于 Flutter ,相信开发者们最常用也最熟悉的就是 Widget
,而 Widget
的设计理念也正是它的灵魂所在。
二、Widget,Flutter 中一切的开始
要理解 Flutter 就要理解 Flutter 中最灵魂的设计:
Flutter 内一切皆
Widget
,Widget
是不可变的(immutable),每个Widget
状态都代表了一帧。
理解这段话是非常重要的,因为 Flutter 中所有界面的展示效果,在代码层面都是通过 Widget
作为入口开始。 Widget
是不可变的,说明页面发生变化时 Widget
一定是被重新构建,所以 Widget
的固定状态代表了一帧静止的画面,当画面发生改变时,对应的 Widget
一定会变化。
举个例子,如下代码所示定义了一个 TestWidget
,TestWidget
接受传入的 title
和 count
参数显示到 Text
上,同时如果 count
大于 99,则只显示 99。
Warnning
/// This class is marked as '@immutable'
/// but one or more of its instance fields are not final
class 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
声明,从而在代码层面容易产生歧义。
因为前面说过
Widget
是immutable
,所以它的每次变化都会导致自身被重新构建,也就是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
就可以实现跨帧保存 ?State
和 Widget
是什么关系?这就需要介绍 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
这个 Widget
的 data
发生了改变,但是 StatePage
的 createState()
方法只在第一次被加载时调用,对应创建出来的 state
被保存在 Element
中,所以 _StatePageState
中的 data
只在第一次时被传递进来。
关键就在于
_StatePageState
创建时data
只被赋数一次。
如果这时候要让 StatePage
的 data
正常改变,就需要使用 widget.data
。看出来了没,如果把 State
放到 Element
的级别上来看,通过 widget.data
去更新的逻辑,是不是 Widget
看起来就很像是一个配置文件。
那最后再问一个问题:Widget
和 Element
的对应关系是怎么样?
四、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),
),
);
}
}
当然,当你给上面的 textUseAll
的 Text
配置上 GlobalKey
就是会是另外一个故事,因为 GlobalKey
会让一个 Widget
变成“全局单例” ,具体解释这里就不展开,感兴趣的可以自己尝试下。
最后补充一点:Flutter 中除了 Widget
、Element
之外,还有 RenderObject
、Layer
等才能组成 Dart 层的渲染闭环,这里主要介绍了 Widget
和 Element
的关系,是因为 Widget
和 Element
的理解是 Flutter 中入门时最容易忽略的, Flutter 对于 Widget
的设计让我们写代码时更像是在写配置文件。
五、最后
本篇文章以 Flutter 的 Widget
和 Element
为核心要点展开,为大家介绍了 Flutter 中最有意思的设计之一,事实上只有掌握了这些基础概念,才能在后续的学习里更好地理解 Flutter 的开发思想,当然更加完善的体系内容可以在 《Flutter开发实战详解》中找到。
本期赠书
积极参与留言板互动,写下你的想法和疑问,就有机会与该书的作者进行零距离学术交流。留言点赞数前五名的同学可以各获赠书一本哦~
获奖公布时间及位置:11月5日头条推送文末
特别提醒:兑奖截止至11月12日,请参与读者及时兑奖
加入搜狐技术作者天团
千元稿费等你来!
戳这里!👈
也许你还想看
(▼点击文章标题或封面查看)
Cocos Engine-节点与组件
2020-10-15
全面详细的java线程池解密,看我就够了!
2020-09-03
【周年福利Round3】Coroutines(协程)我是这样理解的!
2020-08-20
RecyclerView的曝光统计
2020-07-16
安卓自定义view中绘画几何图形和文字及圆角ImageView图片等api使用及举例
2020-07-02