Flutter 教程(三)状态管理

声明式和命令式构建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 的构建)。

声明式开发的劣势

使用声明式开发主要遇到的问题有三个:

  1. 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
  2. 难以跨组件 (跨页面) 访问数据
  3. 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)

逻辑和页面 UI 耦合

一开始业务不复杂的时候,所有的代码都直接写到 widget 中,随着业务迭代,文件越来越大,其他开发者很难直观地明白里面的业务逻辑。这就导致了逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等问题。

这个问题在 Android 原生上同样存在,现在一般 MVP、MVVM、MVI等设计模式的思路去解决。

难以跨组件 (跨页面) 访问数据

如上图所示,在 Widget 结构中,一个子组件想要展示父组件中的 name 字段,可能需要层层进行传递。又或者是要在两个页面之间共享筛选数据,并没有一个很优雅的机制去解决这种跨页面的数据访问。

无法轻松的控制刷新范围

setState 会触发对你当前所在的小组件的重建。如果你的整个应用程序只包含一个widget,那么整个widget将被重建,这将使你的应用程序变得缓慢。

为什么需要状态管理

前面我们提到了声明式UI会造成的三个问题,而状态管理的目的就是解决「声明式」开发带来的问题。 Flutter 中常用的状态管理框架有 GetProvider

这两个框架各有优缺点,如果你或者你的团队刚接触 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使用

参考

相关推荐
玖夜Kty6 小时前
国内环境修改 flutter.bat 来设置 flutter 的网络环境
flutter
LinXunFeng8 小时前
Flutter - GetX Helper 助你规范应用 tag
flutter·github·visual studio code
阅文作家助手开发团队_山神16 小时前
第五章:Flutter Quill渲染原理深度剖析:Delta到RichText的华丽转身
flutter
未来猫咪花16 小时前
# Flutter状态管理对比:view_model vs Riverpod
flutter·ios·android studio
阅文作家助手开发团队_山神1 天前
第四章(下) Delta 到 HTML 转换:块级与行内样式渲染深度解析
flutter
MaoJiu1 天前
Flutter造轮子系列:flutter_permission_kit
flutter·swiftui
阅文作家助手开发团队_山神2 天前
第四章(下):Delta 到 HTML 转换的核心方法解析
flutter
xiaoyan20152 天前
flutter3.32+deepseek+dio+markdown搭建windows版流式输出AI模板
flutter·openai·deepseek
阅文作家助手开发团队_山神2 天前
第四章(上):HTML 到 Delta 转换的核心方法解析
flutter
stringwu2 天前
Flutter高效开发利器:Riverpod框架简介及实践指南
flutter