[译] 极简主义者的 Flutter 状态管理(一)

原地址:https://suragch.medium.com/flutter-state-management-for-minimalists-4c71a2f2f0c1

Flutter 对我来说最大的挑战就是学习状态管理。人们都在讨论什么 ProviderBlocScopedModelReduxMobX 之类的,我实在不知道他们在说些什么。随着时间的推移,状态管理解决方案的列表却不断的在增长。

在我的学习旅程中,我写了不少文章,关于 StackedProviderRiverpod,也学习过 Bloc 模式和 Bloc Library 的教程,看了 CubitGetX,也看过 ReduxMobX 以及 Command 视频。但是这些库总是不能十分契合我的想法,他们要么有太多的选项,要么有太多的隐藏在背后的魔法。我的大脑需要简单和易于理解的极简。

这就是这篇文章的由来。我将要介绍一种方式来管理你的 App 状态,不需要引入任何第三方状态管理方案。唯一需要使用的第三方库是 GetIt,它不是用来管理状态的,只是用来提供一个引用,访问你将要管理的状态的 Dart 类。

对于状态变化时的 UI 重建,将使用 Flutter 内建的 ValueNotifierValueListenableBuilder 类。

这篇文章的目的不是让你远离你现在使用的状态管理方案,毕竟如果当前方案适合你,确实没有任何理由去改变。然而,这篇文章是给像我一样的不了解状态管理的人的引导,为那些脑子还在弯弯绕的人们提供一个直观的解释。

App 架构总览

尽管可以把所有的 UI 和逻辑混合在一起,写在一个大文件里来构造一个 Flutter App,但是那样实在是难以去搞清楚这个 App 是如何运作的。对于大部分 App 来说,它们都至少会使用一点架构。

我们先从一个传统的 App 架构总览开始,下面每个的方框代表一个类或文件,或者是一个文件夹的文件。用这个 App 中的两个页面代表你整个 App 中的 10 到 20 个页面。

这其中一共有三个层级:UI 层,状态管理层,以及 Service 层。我们将要在下面的几节中逐个讨论他们的细节。

UI 层

UI 层就是 Flutter 所在的层级,满是框架、按钮、列和文本 Widget 之类的这些用来合成用户看到的 App 布局。

UI 层

UI 层内的逻辑应当尽可能的少,这里所需要做的就只是拿到 App 当前的状态,把他们更好的展示给用户。

当我讨论 状态 时,是指 App 中能变化的部分,也就是 变量

  • 如果 App 的背景色是蓝色的但是用户将它变更为红色,那么颜色就是状态
  • 如果用户将一个列表向下滚动到一半,那么滚动的量就是状态
  • 如果从网络加载了一个图片,那么图片就是状态

再说一遍,UI 层唯一的职责就是把 App 的状态展示给用户,不要在这里做解析或者格式化之类的,那是逻辑,他们属于另一个层级。不要在这里做任何初始化、登录之类的工作,那些都是逻辑,都属于其他层级。也不要在这里保存数据,这也是逻辑,它们属于其他层级。

可以很肯定的是,UI 层唯一能看到的逻辑应当是 ifswitch 之类的,用来选择正确的 Widget 来展示当前App 的状态。

警告
当你在写一个自包含(self-contined)的 Widget 时(这意味着你可以在你想要的情况下发布自己的 package),它的内部会包含一些由参数用来完成的逻辑。。这种情况在 Flutter framework 层比比皆是,它们有可能是一个简单的 StatefulWidget 或者一个自定义渲染的 Widget。
关键在于,Widget 的状态是自持有在内部的,官方文档中称其为短期状态(Ephemeral State)。与其相对应的是 App 状态,是在 Widget 外部使用的。
我想说的是,App 的状态,以及你用来展示的逻辑应当远离 UI 层。

为了知道何时显示更新,UI 层需要监听 App 状态的变化。监听状态,并在状态变化时 rebuild 的 Widget 一般被称为 Builder Widget。

另一个重要的事情需要注意的是,尽管上面的例子提到登录页和首页,你也可以用其他更简单的页面去替代它们,比如一个表单,或者一个文本区域。毕竟所有的东西只是 Widget,哪怕是页面。

所有想要放在 UI 层里面做的逻辑,都应当放在状态管理层

状态管理层

这个层级有无数个名称,一些人叫它 ViewModel,另一些人叫它 Controller,还有一些人叫它 BLoC 或者 Model 或者 Reducer 或者 ChangeNotifier,这些基本都是一个意思。这个状态管理层是用来执行基于 UI 事件的逻辑,并更新 App 状态的。

状态管理层

事件,代表着 App 中发生的某件事,通常来说是用户操作,比如按下按钮,下拉刷新,点击图片或者输入文字。UI 层的职责就是将这些发生的事件通知状态管理层。简单来说,就是简单的调用状态管理类的方法。

一旦状态管理层收到来自 UI 层的事件,它便开始处理事件。举个例子,如果用户按下了你计算器 App 中的开根号按钮,状态管理层的职责就是实际的计算当前数字的平方根。这个数字是 App 的状态,并在以变量的形式储存在状态管理类中。当状态管理类完成了计算平方根的工作,它会用新的值去更新它。

在上面这个例子中,状态管理不仅应当进行开平方的计算工作,也应当包揽所有的字符串格式化工作,以便 UI 能够以它想要的形式展示它。举个例子,2 的平方根是 1.41421356237,但是 UI 只需要展示四位小数,那么状态管理类的职责就是将这个数字转换成字符串'1.4142'
当然,一些人可能不同意我这个观点,但是我想说的是,将逻辑放在状态管理层越多越好。这么做的一个原因是,状态管理层比 UI 层好测试的多。

状态管理层提供给 UI 一个监听 App 状态变化的途径,但是并不知道具体是哪个 UI 在监听。也正因为如此,你可以随意的重构 UI,完全不用担心去改变状态管理层。这是构建可伸缩的、可维护的 App 的重要建议。

当然,尽管状态管理层是一个执行逻辑的极佳位置,担并不是 App 所有的逻辑都应当放在这里,一些逻辑应当移动至服务层。

服务层

服务层,有时候也被称作数据仓库(data repository),是用来执行逻辑,以及存放可能会被 App 中多个地方使用的数据的。它也是为你的 App 逻辑和其他用来执行类似读取数据库、访问网络等 I/O 任务的第三方代码之间添加一个保护层的途径。这一层保护允许你切换服务的具体实现,而不用担心破坏你其他 App 的代码。

服务层

正如上图所示,存储服务在两个不同的页面通过状态管理层被调用。使用一个服务来执行任务,防止你在 App 中多个位置复制同样的代码,同样的,在你需要时,你会有一个单独的位置来升级这些逻辑。

从状态管理层的视角来看,服务层只是一个接口,也就是一个虚 Dart 类,定义了你能调用的方法。一旦你定义了接口 API,你在构建一个服务的时候就必须实现这些虚方法。举个例子,存储服务接口可以有两个实现,它们储存和接受数据,一个来自网络,一个老子本地数据库。选择哪一个都行,状态管理层并不在乎。有时候,实现一个只返回假数据的实现都是有用的。这允许你先创建一个 App 的可用原型出来,然后再去构建真正的服务实现。

尽管上面的例子中只展示了一个存储服务,但你的 App 中这场可能有多个不同的服务。比如在登录页,你可能需要一个授权验证服务。

如果你 App 中只有一个地方需要一个服务,你可以将代码直接放在状态管理层。但是,如果你想要做一些诸如切换数据库实现、切换授权提供方之类的,为了解耦就必须做很多工作了。有时候需要这种痛苦的经历来给自己上一课。所以,这都取决于你。

这些就是高层总览了,下一节我会更详细的描述如何实现上面我所描述的架构模式——完全不需要复杂的第三方状态管理库。