iOSer 的 Flutter 快速入坑之道(二)
前言
本文是继上一篇的有关 flutter 的第二篇文章,由于合起来篇幅太长,所以考虑分开成多篇文章上传啦。
Flutter UI
a. Widget
在 iOS 中,大部分控件都是基于 UIView 的实例,所有的控件实例在添加到视图中后,会被作为节点添加到 UI 渲染树中。其实在 Flutter 中的 Widget 概念也是类似,都会由一棵渲染树来维护所有的 Widget 子节点。
但是不同之处在于,第一,Flutter 中所有的 widget 在它的生命周期中都是不可变的。在 iOS 中,假如我们要修改并重新渲染一个控件,可以调用 setNeedDisplay 方法,然后在这个控件的节点开始,重新跑一边所有子节点,渲染生成新的视图,但是在这个过程中,控件只是产生的属性的修改,实例却还是那个实例。然而在 Flutter 中就不同了,由于 Flutter 中的 Widget 都是不可变的,所以在当某个 Widget 需要修改的时候,Flutter 会创建一个新的 Widget 来做替换。
第二,其实在 Flutter 中,Widget 不同于 View 的另一个区别就是 Widget 并不负责任何渲染相关的事情。那么是谁在负责渲染呢?Flutter 是怎么样通过 Widget 来渲染出界面的?在这里我们就要了解 Flutter 中的 Widget、Element、RenderObject 之间的联系啦。
Widget 中包含属性 Element,Element 中又包含了 RenderObject 属性。Widget 就是一个对 Element 的配置或者描述,我们开发者也是只需要和 Widget 打交道,并不需要去维护更新渲染树。
Element 就是负责维护渲染树的实例,主要作用在 Flutter 的渲染管线 build 阶段,可以把 element 看作真正的渲染树子节点。
RenderObject 是用于渲染的对象,主要作用在 Flutter 的渲染管线 布局以及绘制 阶段,也就是它将我们的界面最终绘制在屏幕上。
关于 Flutter 的底层渲染机制,其实我认为作为每个初学 Flutter 的同学都需要了解,只有更深入的了解它的渲染机制,才能更好地去使用它。
b.如何使用 Widget 构建界面?
在上面我们大概了解了 Widget 是什么东西,以及 flutter 的渲染机制,那么实际开发中如何使用 Widget呢?
还是从 hello world 开始吧!
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}
所有的 Widget 都是通过这种嵌套的方式来表示其相互之间的关系以及配置,其实很容易懂,不懂的话多看几种 Widget 的例子就知道啦。
Flutter 中文网
c.StatelessWidget 和 StatefulWidget
StatelessWidget 是不存在中间状态的控件,也就是说当它被 new 出来之后,就不会被改变了,也无法被改变。如果需要更新展示的内容,就只能销毁掉重新 new 一个。
StatefulWidget 是可以保存中间状态的控件,控件的中间状态保存在 State 类中,通过调用 setState(){} 方法来触发更新机制,将此节点将其以下的整个子树重新更新绘制。
d.setState 机制介绍
setState 的作用是告诉框架这个 Object 的 state 已经被修改了,那么这个修改它可能会对 UI 有影响,所以需要重新 build 一遍该 Object。
首先我们来了解一下一个 [State]Object 的生命周期状态。
enum _StateLifeCycle {
created,
initialized,
ready,
defunct,
}
当这个[State]Object 刚被创建时,它的状态就是 created 的状态。紧接着 Object 会调用 didChangeDependencies 方法,这时候 Object 的状态会转变为 initialized,但是此时,object 并没有做好被 build 的准备。然后接下来,object 会转变为 ready 的状态,表示现在已经做好 build 的准备了。object 会保持这个状态,直到 dispose 方法被调用。一旦 object 调用了 dispose 方法,那么它的状态就会转变成 defunct 的状态。
然后我们可以看到 setState 的源码。首先 flutter 会判断 setState 是否在 Object 调用 dispose 销毁后再调用,若 state 为 defunct ,说明该 Object 已经被销毁了,报错。
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw error;
}
接下来我们看到 flutter 还判断了 object 的状态是否为 created 以及 是否非 mounted。那么 mounted 属性是什么呢?
mounted 属性代表的意思是这个 object 是否有被 buildContext 所持有。我们知道 mounted 有安装、加入的意思。在一个[state]Object 调用 initState 方法之前,flutter 会把该[state]Object 通过与 buildContext 关联的方式嵌入,直至 Object 调用了 dispose,也就是不会再调用 build 了,这个 Object 才不被继续持有。
所以这两个条件说明该 [state]Object 还未被加入到 widget 树中,有可能是在构造函数的时候调用了 setState,这个是没有必要的,因为 object 刚被创建的时候是被标记为 dirty 的,需要调用 build。
final dynamic result = fn() as dynamic;
接下来是判断如果 state 中的更新函数为一个异步函数,返回值是 Future 类型的,那么也报错。
最后,才是调用了 _element.markNeedsBuild 方法。那么 markNeedsBuild 函数做了什么事情呢。首先,他会判断 state 是否为 defunct,elementLifeStyle 是否为 active 以及 owner 是否为 nil。之后,将这些节点都标记为 dirty,然后加入到全局的 widget 队列中,在下一帧的时候进行重建。
那么什么是 owner,owner 的作用是什么呢?
我们可以看到 owner 其实是一个 BuildOwner 的实例,它主要负责跟踪需要 rebuild 的 widgets。 然后在 scheduleBuildFor 方法中,首先是判断 element 的 _inDirtyList 是否为 true,如果是,就说明它已经在 dirtyList 中了,于是把它的 dirtyElementsNeedsResorting 属性设置为yes,返回。
如果一切正常,那么接下来会调用到 onBuildScheduled()方法,这是一个方法回调,是 VoidCallback 类型。
那么接着,onBuildScheduled 的回调是在哪里呢。通过搜索我们发现在 WigetsBinding 的 initInstances 方法中有赋值 onBuildScheduled 代码块。
WigetsBinding 又是什么?! 从文档中我们可以了解到,文档说 WidgetsBinding 是 wigetsLayer 和 flutter 引擎的粘合剂。。
好的,不管他,先继续看内部实现,我们继续往下点追踪到一个叫
ensureVisualUpdate 的函数,在其中的一个 case 中,我们看到了 scheduleFrame 的方法。然后再往这个方法里面看,可以看到最终它调用了 window.scheduleFrame 方法。 window 的 scheduleFrame 方法会生成一个新的帧,在这个方法调用之后,flutter 引擎会最终调用 handleBeginFrame 强制刷新一个新帧,无论之前那一帧是否已经渲染结束了。
所以到这里差不多就是 setState 调用到屏幕渲染刷新的大致过程了。整理一下整个过程中调用的函数
State.setState() ->
Element.markNeedsBuild() ->
BuildOwner.scheduleBuildFor() ->
WidgetBinding._handleBuildScheduled() ->
SchedulerBinding.ensureVisualUpdate() ->
SchedulerBinding.scheduleFrame() ->
Window.scheduleFrame() ->
WidgetBinding.drawFrame()
e.如何做控件的显示和隐藏?
我们已经了解了 flutter 中 setState 的机制以及 widget 的一些特性了,那么实际开发中碰到对 widget 进行显示隐藏控制等需求的时候,应该怎么处理呢?
大概的简单思路就是通过一个变量 t 去表示 widget 的显示和隐藏,然后再封装一个函数根据这个变量来返回需要显示的 widget 或 Container()。在 flutter 中,如果需要表示返回空的一个 widget 的时候,不要返回 null,而是返回 Container()。最后在需要修改变量 t 的时候加上 setState 代码块,表示需要重新绘制。
class _TestState extends State<Test> {
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('Text');
} else {
return Container();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test App"),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: Icon(Icons.update),
),
);
}
}
以上是通过一个按钮点击控制控件显示与否的简单代码,蛮看下~
最后
这边主要讲有关 flutter UI 方面的东西,当然 flutter UI 相关的东西也远远不止这些,所以如果后续有补充的地方,我也会在本文中修改跟进~