概述
iOS 和 Android 的原生开发模式是命令式编程模式。命令式编程要求开发者一步步描述整个构建过程,从而引导程序去构建用户界面。
Flutter 则采用了声明式编程模式,框架隐藏了具体的构建过程,开发者只需要声明状态,框架会自动构建用户界面。这也就意味着 Flutter 构建的用户界面就是当前的状态。
状态管理
App 在运行中总是会更新用户界面,因此我们需要对状态进行有效的管理。状态管理本质上就是 如何解决状态读/写的问题。对此,我们将从两个方面去评估状态管理方案:
- 状态访问
- 状态更新
此外,根据 Flutter 原生支持的情况,我们将 Flutter 状态管理方案分为两类:
- Flutter 内置的状态管理方案
- 基于 Pub 的状态管理方案
下文,我们将以 Flutter 官方的计数器例子来介绍 Flutter 中的状态管理方案,并逐步进行优化。
关于本文涉及的源码,见【Demo 传送门】。
Flutter 内置的状态管理方案
直接访问 + 直接更新
Flutter 模板工程就是【直接访问 + 直接更新】的状态管理方案。这种方案的状态访问/更新示意图如下所示。
很显然,【直接访问 + 直接更新】方案只适合于在单个 StatefulWidget
中进行状态管理。那么对于多层级的 Widget 结构该如何进行状态管理呢?
状态传递 + 闭包传递
对于多层级的 Widget 结构,状态是无法直接访问和更新的。因为 Widget 和 State 是分离的,并且 State 一般都是私有的,所以子 Widget 是无法直接访问/更新父 Widget 的 State。
对于这种情况,最直观的状态管理方案就是:【状态传递 + 闭包传递】。对于状态访问,父 Widget 在创建子 Widget 时就将状态传递给子 Widget;对于状态更新,父 Widget 将更新状态的操作封装在闭包中,传递给子 Widget。
这里存在一个问题:当 Widget 树层级比较深时,如果中间有些 Widget 并不需要访问或更新父 Widget 的状态时,这些中间 Widget 仍然需要进行辅助传递。很显然,这种方案在 Widget 树层级较深时,效率比较低,只适合于较浅的 Widget 树层级。
状态传递 + Notification
那么如何优化多层级 Widget 树结构下的状态管理方案呢?我们首先从状态更新方面进行优化。
【状态传递 + Notification】方案采用 Notification 定向地优化了状态更新的方式。
通知(Notification)是 Flutter 中一个重要的机制,在 Widget 树中,每个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过 NotificationListener
来监听通知。Flutter 中将这种由子向父的传递通知的机制称为 通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,而用户触摸事件无法中止。
下图所示为这种方案的状态访问/更新示意图。
具体的实现源码如下所示:
1 | // 与 父 Widget 绑定的 State |
InheritedWidget + Notification
【传递传递 + Notification】方案定向优化了状态的更新,那么如何进一步优化状态的访问呢?
【InheritedWidget + Notification】方案采用 InhertiedWidget
实现了在多层级 Widget 树中直接访问状态的能力。
InheritedWidget
是 Flutter 中非常重要的一个功能型组件,其提供了一种数据在 Widget 树中从上到下传递、共享的方式。这与 Notification 的传递方向正好相反。我们在父 Widget 中通过 InheritedWidget
共享一个数据,那么任意子 Widget 都能够直接获取到共享的数据。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
1 | /// 与父 Widget 绑定的 State |
InheritedWidget + EventBus
虽然【InheritedWidget + Notification】方案在状态访问和状态更新方面都进行了优化,但是从其状态管理示意图上看,状态的更新仍然具有优化空间。
【InheritedWidget + EventBus】方案则采用了 事件总线(Event Bus)的方式管理状态更新。
事件总线是 Flutter 中的一种全局广播机制,可以实现跨页面事件通知。事件总线通常是一种订阅者模式,其包含发布者和订阅者两种角色。
【InheritedWidget + EventBus】方案将子 Widget 作为发布者,父 Widget 作为订阅者。当子 Widget 进行状态更新时,则发出事件,父 Widget 监听到事件后进行状态更新。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
1 | /// 与父 Widget 绑定的状态 |
两种方案的对比
【InheritedWidget + Notification】和【InheritedWidget + EventBus】的区别主要在于状态更新。两者对于状态的更新其实并没有达到最佳状态,都是通过一种间接的方式实现的。
相比而言,事件总线是基于全局,逻辑难以进行收敛,并且还要管理监听事件、取消订阅。从这方面而言,【InheritedWidget + Notification】方案更优。
从状态管理示意图而言,显然【InheritedWidget + Notification】还有进一步的优化空间。这里,我们可能会想:状态能否直接提供更新方法,当子 Widget 获取到状态后,直接调用状态的更新方法呢?
对此,官方推荐了一套基于第三方 Pub 的 Provider 状态管理方案。
基于 Pub 的状态管理方案
Provider
【Provider】的本质是 基于 InheritedWidget
和 ChangeNotifier
进行了封装。此外,使用缓存提升了性能,避免不必要的重绘。
下图所示为这种方案的状态访问/更新示意图。
具体的源码实现如下所示:
1 | /// 与父 Widget 绑定的 State |
Flutter 社区早期使用的 Scoped Model 方案与 Provider 的实现原理基本是一致的。
Redux
对于声明式(响应式)编程中的状态管理,Redux 是一种常见的状态管理方案。【Redux】方案的状态管理示意图与【Provider】方案基本上是一致的。
在这个基础上,Redux 对于状态更新的过程进行了进一步的细分和规划,使得其数据的流动过程如下所示。
- 所有的状态都存储在 Store 中。一般会把 Store 放在 App 顶层。
- View 获取 Store 中存储的状态。
- 当事件发生时,发出一个 action。
- Reducer 接收到 action,遍历 action 表,找到匹配的 action,根据 action 生成新的状态存储到 Store 中。
- Store 存储新状态后,通知依赖该状态的 view 更新。
一个 Store 存储多个状态,适合用于全局状态管理。
具体的实现源码如下所示。
1 | /// 与父 Widget 绑定的 State |
BLoC
【BLoC】方案是谷歌的两位工程师 Paolo Soares 和 Cong Hui 提出的一种状态管理方案,其状态管理示意图同样与【Provider】方案是一致的。
【BLoC】方案的底层实现与【Provider】是非常相似的,也是基于 InheritedWidget
进行状态访问,并且对状态进行了封装,从而提供直接更新状态的方法。
但是,BLoC 的核心思想是 基于流来管理数据,并且将业务逻辑均放在 BLoC 中进行,从而实现视图与业务的分离。
- BLoC 使用 Sink 作为输入,使用 Stream 作为输出。
- BLoC 内部会对输入进行转换,产生特定的输出。
- 外部使用 StreamBuilder 监听 BLoC 的输出(即状态)。
具体的实现源码如下所示。
1 | /// 与父 Widget 绑定的 State |
总结
一般而言,对于普通的项目来说【Provider】方案是一种非常容易理解,并且实用的状态管理方案。
对于大型的项目而言,【Redux】 有一套相对规范的状态更新流程,但是模板代码会比较多;对于重业务的项目而言,【BLoC】能够将复杂的业务内聚到 BLoC 模块中,实现业务分离。
总之,各种状态管理方案都有着各自的优缺点,这些需要我们在实践中去发现和总结,从而最终找到一种适合自己项目的状态管理方案。