跳到主要内容

基础

本篇笔记只涉及一些原理性底层知识,帮助更好的理解 Flutter,如需学习怎么使用组件:Flutter 实战

Widget

Widget 是 Flutter 功能的抽象描述,是视图的配置信息,同时也是数据的映射,是 Flutter 框架中最基本的概念

Flutter 的核心设计思想就是一切皆 Widget

创建第一个 Flutter 应用

计数器示例应用

使用 Android Studio 创建一个 Flutter 应用后,是一个示例项目

想要运行项目,需要先在 Device Manager 中创建一个虚拟机

20241224233155

创建虚拟机后,运行,只有虚拟机运行起来后,上面的运行选项中才有刚才创建的虚拟机

选择要在哪个虚拟机这运行项目后,点击运行按钮,结果如下:

20241224233204

Widget 渲染过程

进行 App 开发时,相当重要的一个问题就是:如何结构化的组织视图数据,提供给渲染引擎,最终完成界面显示

通常情况下,不同的 UI 框架会有不同的实现方式,但是无一例外的都会用视图树(VIew Tree)的概念

而 Flutter 将视图树的概念进行了拓展,把视图数据的组织和渲染抽象为三部分,即 Widget、Element和 RenderObject

三者之间的关系如下:

20241224233142

Widget

Widget 是 Flutter 里对视图的一种结构化描述,可以看作前端的一种“控件”

Widget 是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等

Flutter 将 Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 WIdget 树的方式进行数据更新,以数据驱动 UI 构建的方式简单高效

但是这样也有缺点,设计到大量对象的销毁和重建,会对垃圾回收造成很大压力,不过 Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建的成本很低

另外,由于 Widget 的不可变性,可以以较低的成本进行渲染节点复用,因此在一个真实的渲染树中可能存在不同 Widget 对应同一个渲染节点的情况,这又降低了重建 UI 的开销

Element

Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到最终完成渲染的桥梁

Flutter 的渲染流程是这样的:

  1. 根据 Widget 树生成一个 Element 树,树中的节点都继承自 Element 类
  2. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自 RenderObject 类并关联到Element.renderObject属性上
  3. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类

真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,类似一个中间人

为什么要增加中间这层 Element 呢?不能直接由 Widget 转化成 RenderObject 吗?

可以,但这样会极大的增加渲染带来的性能损耗

因为 Widget 是不可变的,但是 Element 是可变的。实际上 Element 树这一层将 Widget 树的变化(类似 React 的虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染消息,而不是销毁整个渲染视图树重建。

这就是 Element 树存在的意义

RenderObject

RenderObject 是负责视图渲染的对象

Flutter通过控件树(Widget 树)中每个控件(Widget)创建不同类型的渲染对象,组合成渲染对象树

而渲染对象树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染,其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中每个对象的位置和尺寸,并把它们渲染到不同的图层上

绘制完成后,合成和渲染的工作则交给 Skia 搞定,在 VSync 信号同步时直接从渲染树合并成 BitMap,然后提交给 GPU

State

Widget 有两种类型:

  • StatelessWidget:处理静态、无状态的视图展示
  • StatefulWidget:有交互、需要动态变化视觉状态的场景

为什么不全部使用 StatefulWidget 呢?下面回答了这个问题

UI编程范式

要理解 StatelessWidget 和 StatefulWidget 的使用场景,首先需要了解在 Flutter 中如何调整一个控件(Widget)的展示样式,即 UI 编程范式

Flutter 的视图开发是声明式的,其核心设计思想就是将视图和数据分离,与 React 的设计思路一致

命令式编程强调精确控制过程细节

声明式编程强调通过意图输出结果整体

对应到 Flutter 中,在 Widget 生命周期中,任何应用到 State 的更改都将触发 Widget 的重建

当要创建的用户界面不随任何状态信息的变化而变化时,需要使用 StatelessWidget,反之则使用 StatefulWidget

前者用于静态内容的展示,后者用于存在交互反馈的内容中

StatelessWidget

20241224233225

什么场景下使用 StatelessWidget 呢?

原则:**父 Widget是否能通过初始化参数完全控制其 UI 展示效果?**可以的话就使用 StatelessWidget

StatefulWidget

20241224233233

注意

不能滥用 StatefulWidget,滥用的话会影响 Flutter 应用的渲染性能

如果完全使用 StatefulWidget 的话:

Widget 是不可变的,更新意味着销毁+重建

StatelessWidget 是静态的,一旦创建就不需要更新;但是对于 StatefulWidget,在 State 类中调用 setState方法更新数据,会触发视图的销毁和重建,也将间接触发其每个子 Widget 的销毁和重建

这意味着,如果根布局是一个 StatefulWidget,在其 State 每调用一次更新 UI,都将是一整个页面所有 Widget的销毁和重建

正确评估你的视图展示需求,避免无谓的StatefulWidget 使用

生命周期

涵盖一个组件从加载到卸载的全过程,就是生命周期

Flutter 中的 Widget 也存在生命周期,并通过 State 来体现,而 App 是一个特殊的 Widget,除了要处理视图显示的各个阶段(即视图的生命周期)之前,还需要应对应用从启动到退出的各个阶段(App 的生命周期)

State生命周期

20241224233245

创建

State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染

  • 构造方法:State 生命周期的起点,Flutter 会通过调用StatefulWidget.createState()来创建一个 State。可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数 据。这些配置数据,决定了 Widget 最初的呈现效果
  • initState:在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中 只会被调用一次,所以可以在这里做一些初始化工作,比如为状态变量设定默认值
  • didChangeDependencies:用来专门处理 State 对象依赖关系变化,会在initState()调用结束后,被 Flutter 调用
  • build:作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是 调用 build。需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回

更新

Widget 的状态更新,主要由 3 个方法触发:setState、didchangeDependencies 与 didUpdateWidget

  • setState:当状态数据发生变化时,通过调用这个方法 告诉 Flutter:“我这儿的数据变啦,请使用更新后的数据重建 UI!”
  • didChangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。哪些情况下 State 对象的依赖关系会发生变化呢?典型的场景是,系统语言 Locale 或应用主题改变时,系统会通知 State 执行didChangeDependencies回调方法
  • didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数

一旦这三个方法被调用,Flutter 随后就会销毁老 Widget,并调用 build 方法重建 Widget

销毁

组件销毁相对比较简单。比如组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件

具体调用机制:

  • 当组件的可见状态发生变化时,deactivate 函数会被调用,这时 State 会被暂时从视图 树中移除。值得注意的是,页面切换时,由于 State 对象在视图树中的位置发生了变化,需要先暂时移除后再重新添加,重新触发组件构建,因此这个函数也会被调用
  • 当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 函数。而一旦到这个阶段,组件就要被销毁了,所以可以在这里进行最终的资源释放、移除监听、清理环境,等等

20241224233255

App 生命周期

在原生 Android、iOS 开发中,有时我们需要在对应的 App 生命周期事件中做相应处理, 比如 App 从后台进入前台、从前台退到后台,或是在 UI 绘制完成后做一些处理。

这样的需求,在原生开发中,我们可以通过重写 Activity、ViewController 生命周期回调方法,或是注册应用程序的相关通知,来监听 App 的生命周期并做相应的处理

而在 Flutter 中,可以利用WidgetsBindingObserver类,来实现同样的需求

回调函数如下:

abstract class WidgetsBindingObserver {
// 页面 pop
Future<bool> didPopRoute() => Future<bool>.value(false);
// 页面 push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
// 系统窗口相关改变回调,如旋转
void didChangeMetrics() { }
// 文本缩放系数变化
void didChangeTextScaleFactor() { }
// 系统亮度变化
void didChangePlatformBrightness() { }
// 本地化语言变化
void didChangeLocales(List<Locale> locale) { }
//App 生命周期变化
void didChangeAppLifecycleState(AppLifecycleState state) { }
// 内存警告回调
void didHaveMemoryPressure() { }
//Accessibility 相关特性回调
void didChangeAccessibilityFeatures() {}
}

生命周期回调

didChangeAppLifecycleState回调函数中,有一个参数类型为AppLifecycleState的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、 inactive、paused 这三个

  • resumed:可见的,并能响应用户的输入
  • inactive:处在不活动状态,无法处理用户响应
  • paused:不可见并不能响应用户的输入,但是在后台继续活动中

下面案例:在 initState 时注册了监听器,在 didChangeAppLifecycleState 回 调方法中打印了当前的 App 状态,最后在 dispose 时把监听器移除

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver{
...
@override
@mustCallSuper
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);// 注册监听器
}
@override
@mustCallSuper
void dispose(){
super.dispose();
WidgetsBinding.instance.removeObserver(this);// 移除监听器
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
print("$state");
if (state == AppLifecycleState.resumed) {
//do sth
}
}
}

从后台切入前台,控制台打印的 App 生命周期变化如下: AppLifecycleState.paused- >AppLifecycleState.inactive->AppLifecycleState.resumed; 从前台退回后台,控制台打印的 App 生命周期变化则变成了: AppLifecycleState.resumed->AppLifecycleState.inactive- >AppLifecycleState.paused

20241224233306

帧绘制回调

有时候我们需要在组件渲染之后做一些与显示安全相关的操作,依然使用万能的 WidgetsBinding 来实现

WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求:

1、单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完 成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次

WidgetsBinding.instance.addPostFrameCallback((_){
print(" 单次 Frame 绘制回调 ");// 只回调一次
});

2、实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次 绘制 Frame 结束后进行回调,可以用做 FPS 监测

WidgetsBinding.instance.addPersistentFrameCallback((_){
print(" 实时 Frame 绘制回调 ");// 每帧都回调
});