声明式和命令式构建UI的区别
在上一篇文章 Flutter 教程(二)Flutter 组件中,我们了解了在 Flutter 中构建UI的方式。可以看到,它们和在 Android 传统构建 UI 的方式完全不同。这是因为 Flutter 是基于声明式构建UI的,而在 Android ,是命令式构建UI的。
以 Flutter 默认的计数器例子为例:
在 Android 中,实现上面的效果的代码如下所示:
csharp
// 一、定义展示的内容
private int mCount = 0;
// 二、中间展示数字的控件 TextView
private TextView mTvCount;
// 三、关联 TextView 与 xml 中的组件
mTvCount = findViewById(R.id.tv_count)
// 四、点击按钮控制组件更新
private void increase( ){
mCount++;
mTvCounter.setText(mCount.toString());
}
可以看到在Android中实现计数更新的功能,需要获取对应的UI控件对象,然后设置该对象的值。而在 Flutter 中,声明 UI 布局之后,只需要通过 setState
来刷新对应的界面即可,而不需要进行繁琐的控制,代码示例如下:
scss
// 一、声明变量
int _counter =0;
// 二、展示变量
Text('$_counter')
// 三、变量增加,更新界面
setState(() {
_counter++;
});
声明式开发的优势
让开发者摆脱组件的繁琐控制,聚焦于状态处理
在开发 Android 原生时,你会发现当多个组件之间相互关联时,对于 View 的控制非常麻烦。而在 Flutter 中我们只需要处理好状态即可 (复杂度转移到了状态 -> UI 的映射,也就是 Widget 的构建)。
声明式开发的劣势
使用声明式开发主要遇到的问题有三个:
- 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
- 难以跨组件 (跨页面) 访问数据
- 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)
逻辑和页面 UI 耦合
一开始业务不复杂的时候,所有的代码都直接写到 widget 中,随着业务迭代,文件越来越大,其他开发者很难直观地明白里面的业务逻辑。这就导致了逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等问题。
这个问题在 Android 原生上同样存在,现在一般 MVP、MVVM、MVI等设计模式的思路去解决。
难以跨组件 (跨页面) 访问数据
如上图所示,在 Widget 结构中,一个子组件想要展示父组件中的 name
字段,可能需要层层进行传递。又或者是要在两个页面之间共享筛选数据,并没有一个很优雅的机制去解决这种跨页面的数据访问。
无法轻松的控制刷新范围
setState
会触发对你当前所在的小组件的重建。如果你的整个应用程序只包含一个widget,那么整个widget将被重建,这将使你的应用程序变得缓慢。
为什么需要状态管理
前面我们提到了声明式UI会造成的三个问题,而状态管理的目的就是解决「声明式」开发带来的问题。 Flutter 中常用的状态管理框架有 Get 和 Provider。
这两个框架各有优缺点,如果你或者你的团队刚接触 Flutter,使用 Provider 能帮助你们更快理解 Flutter 的核心机制。而如果已经对 Flutter 的原理有了解,Get 丰富的功能和简洁的 API,则能帮助你很好地提高开发效率。
组件基础知识
在介绍 Provider 和 Get 状态管理框架前,我们需要先了解一下组件的基础知识。这样才方便理解状态管理实现的原理。
Flutter 三棵树
Flutter 的渲染是通过三棵树实现的,三棵树分别为:
- Widget 树:Widget是Flutter的核心部分,它的定义就是
对一个 Element 配置的描述
,也就是说,widget 只是一个配置的描述,并不是真正的渲染对象,就相当于是 Android 里面的 xml,只是描述了一下属性,但他并不是真正的 View。 - Element 树:Element 是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,是在特定位置使用 Widget配置数据生成;
- RenderObject 树:用于应用界面的布局和绘制,保存了元素的大小,布局等信息;
StatelessWidget 和 StatefulWidget
在Flutter开发中,一切皆组件,我们展示给用户的界面也是一个组件。而组件 Widget 主要被划分为 StatelessWidget 和 StatefulWidget 两大类。StatelessWidget 就是一个无状态组件。由 StatelessWidget 设计出来的界面内容是无法使用setState()方法改变的。StatelessWidget 的代码示例如下:
scala
class MyStateLessWidget extends StatelessWidget{
final String title;
MyStateLessWidget({
Key key,
this.title,
});
@override
Widget build(BuildContext context){
return new ...;
}
}
而StatefulWidget 是有状态组件。当创建一个StatefulWidget组件的时候,肯定也会创建一个State对象。通过这个对象,我们可以与用户交互并刷新界面。StatefulWidget 的代码示例如下:
scala
// 主体部分
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
// State 部分
class _MyHomePageState extends State<MyHomePage> {
...
@override
Widget build(BuildContext context) {
return new ...;
}
}
我们需要改变StatefulWidget组件的界面内容,就需要使用setState(...),这个服务由Flutter框架层控制来更新UI。
BuildContext
从上面的代码示例,可以看到在实现 StatelessWidget 和 StatefulWidget 来创建组件时,会使用到 BuildContext
。而 BuildContext
就是 widget 对应的 Element
。BuildContext 的代码示例如下:
scss
Theme.of(context) //获取主题
Navigator.push(context, route) //入栈新路由
Localizations.of(context, type) //获取Local
context.size //获取上下文大小
context.findRenderObject() //查找当前或最近的一个祖先RenderObject
为什么不直接传入 Element,而是传入 BuildContext;这是因为 BuildContext 是 Element 的接口,传入 BuildContext 可以限制直接操作 Element
State 生命周期
我们将State的生命周期分为3个部分:第一部分是启动 App 的运行流程;第二部分是热重载的运行流程;第三部分是界面销毁时的运行流程。
State的生命周期中的几个非常重要的方法如下:
- initState。initState()方法是State的生命周期中创建运行的第一个方法。在Flutter项目中,如果需要初始化一些数据,或者绑定控制器(controller),就可以重写在这个方法中。需要注意的是,要重写该方法,就必须在该方法中加上super.initState()。不过,在initState()方法里,Flutter框架层还没有把Context与State关联在一起。因此,不能在这个方法中访问Context。另外,initState()方法在生命周期中只会运行一次。
- didChangeDependencies。didChangeDependencies()方法在State对象的依赖发生变化时被调用,在initState()方法之后运行,这个时候,就可以访问Context了。如果之前build()中包含一个InheritedWidget,而之后的Widget使用了InheritedWidget数据,并且发生了变化,那么Flutter框架层就会调用didChangeDependencies方法
- build。build()主要是用于构建Widget树,它在didChangeDependencies()和didUpdateWidget()之后被运行。基本上每次调用setState()都会运行build()方法。
- reassemble。reassemble()回调方法是专门为了开发调试而提供的,在热重载时会被调用。此回调方法在Release模式下永远不会被调用。
- didUpdateWidget。祖先节点重新构建Widget时会调用didUpdateWidget()方法,当组件的状态改变的时候也会调用didUpdateWidget()方法。需要注意的是,运行setState()方法并不会调用didUpdateWidget()方法,反而热重载的时候才会调用。这个方法一般用于监测新、旧Widget的属性,看看哪些属性值改变了,并对State做一些调整。
- deactivate。在dispose()方法之前会调用这个方法。实测在组件可见状态变化的时候会调用deactivate()方法,当组件卸载时也会先一步在dispose()前调用deactivate()。
- dispose。当State对象从树中被永久移除时会调用dispose()方法,通常在此回调方法中释放资源。一旦到这个阶段,组件就要被销毁了。这个方法一般用于移除监听,清理环境。
Key
在实现 StatelessWidget 和 StatefulWidget 时,都会使用一个 Key。它代表 Widget 的唯一标识。这个唯一标识在 build/rendering 阶段由框架定义。
关于 Key 的详情可以看 Flutter | Key 的原理和使用概述
InheritedWidget
InheritedWidget 是 Flutter 提供的一种在 widget 树中从上到下共享数据的方式,即在父widget 中通过InheritedWidget共享了一个数据,那么在任意子widget都能获取该共享的数据。
前面提到的 Provider 框架就是根据 InheritedWidget 来实现状态管理的
Provider 的使用
Provider 的使用具体看Flutter状态管理之 Provider 使用详解
GetX 的使用
GetX 的使用具体看Flutter GetX使用
参考
- [译] Flutter 核心概念详解: Widget、State、Context 及 InheritedWidget - 掘金
- Flutter | Key 的原理和使用概述
- 14.2 Element、BuildContext和RenderObject | 《Flutter实战·第二版》
- 在 Flutter 中使用 setState 时的 6 个简单技巧setState 函数是在 Flutter 应用程序中 - 掘金
- Flutter 状态管理框架 Provider 和 Get 分析 | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
- Flutter状态管理之 Provider 使用详解介绍 Provider是社区构建的状态管理工具,也是Flutter - 掘金
- 一文读懂Flutter的三棵树渲染机制和原理_flutter三棵树的渲染原理-CSDN博客