来到Flutter时,我面临的最大挑战是学习状态管理。人们说Provider和Bloc、ScopedModel、Redux和MobX......我不知道他们在说什么。随着时间的推移,状态管理解决方案的清单继续增加。

在我的学习旅程中,我写了Stacked这里)和Provider这里)以及Riverpod这里这里)的文章。我浏览了Bloc模式和BlocLibrary教程,查看了CubitsGetX,并观看了有关ReduxMobXCommand的视频。但对于每一个,总有一些我无法完全包裹我的大脑的东西。幕后要么有太多的选择,要么有太多的魔法。我的大脑需要简单易懂。它需要最低限度。

这就是这篇文章的来源。我将介绍一种管理您的应用程序状态的方法,该方法不使用任何第三方状态管理解决方案。它使用的唯一第三方软件包是GetIt,不是为了管理状态,而是为了提供对普通Dart类的引用,您将在那里管理自己的状态。要在状态更改时重建用户界面,您将使用Flutter自己的内置ValueNotifier和ValueListenableBuilder类。

本文的目的不是说服您放弃使用您当前可能使用的任何状态管理解决方案。如果它对你有用,那么就没有理由改变。相反,这篇文章是像我这样不了解状态管理的人的指南,他们只是想得到直截了当的解释,以至于可以让他们可以全神贯注。


1. 应用程序架构概述

虽然可以在一个大文件中构建一个Flutter应用程序,并将所有UI和逻辑混合在一起,但这真的很难理解该应用程序的工作原理。对于大多数应用程序,您会希望给自己设计一点结构。

首先,让我们来看看典型应用程序的架构概述。下面的每个彩色框代表一个类或一个文件,甚至可能代表一个文件夹。此应用程序中的两个页面作为示例,可以表示您在完整应用程序中可能有的10或20页

avatar

有三个层:UI层、state management层和service层。我们将在以下部分更详细地研究其中的每一部分。


2. UI层

用户界面层是Flutter所在的位置。这些是您在编写用户看到的应用程序布局时使用的所有scaffolds, buttons, columns, 和 text widgets

avatar

UI层的逻辑应该尽可能少。您在这里所做的只是获取应用程序的当前状态,使其对用户来说看起来不错。

注意:当我谈论状态时,我只是指您的应用程序中可能有所变化的部分。也就是说,我说的是变量。如果应用程序背景颜色为蓝色,但用户将其更改为红色,则该颜色为状态。如果用户向下滚动列表一半,则该滚动量为状态。如果您从互联网加载图像,则该图像为状态。

再说一遍,UI层的唯一工作是向用户显示应用程序状态。不要在这里解析或格式化字符串。那是逻辑。它属于不同的层。不要在这里完成所有Firebase初始化和登录工作。那是逻辑。它属于不同的层。不要将数据保存到此处的shared preferences中。那是逻辑。它属于不同的层。

在UI层中,您应该看到的唯一逻辑是if或switch语句,用于选择为当前应用程序状态显示的正确小部件。

Caveat: 当您制作自包含的小部件时,可以将逻辑包含在小部件中(这意味着如果您愿意,您可以将其作为自己的软件包发布)。整个Flutter框架充满了这些。这可以是一个简单的有状态小部件自定义渲染对象小部件。然而,关键是状态在小部件内是自包含的。 文档称这种短暂状态(ephemeral state)。 与此相反的是app state,该应用程序在小部件本身之外使用。我想说的是,应用程序状态和准备演示所需的逻辑应该排除在UI层之外。

为了知道何时显示新的更新,UI层应该监听app state的变化。监听状态并在更改时重建的小部件通常称为builder widgets

需要注意的另一件重要事情是,尽管上面的例子是登录页面和主页,但您也可以用表单或文本区域等小得多的UI小部件替换单词页面。毕竟,一切都只是一个widget,甚至是一个页面。

您在UI层中想做的所有逻辑,您都应该放在state management层中。


3. 状态管理层

这个层有一百万个名字。有些人称它为ViewModel。其他人称它为Controller。还有一些人称它为Bloc、Model、Ceducor或ChangeNotifier。不过,这个想法基本上是一样的。此状态管理层是根据UI事件执行逻辑,然后更新应用程序状态的地方。

avatar

event只是应用程序中发生的事情。通常,这是一种用户操作,例如按下按钮或滑动以刷新或点击图像或输入文本。UI层的工作是将发生的事件通知状态管理层。在极简主义方法中,这只需在状态管理类上调用方法即可完成。

一旦状态管理层从UI层收到有关事件的通知,它就会处理该事件。例如,如果用户在您的计算器应用程序中按下平方根按钮,那么状态管理层的工作就是实际计算当前数字的平方根。该数字是应用程序状态,并作为变量存储在状态管理类中。在状态管理类完成平方根的计算工作后,它会用新值更新变量。

Note: 在上面的示例中,状态管理不仅应该进行平方根计算,还应该进行所有字符串格式化,以便仅以UI需要显示的形式。 例如,2的平方根是1.41421356237,但如果UI只需要小数点后四位,那么状态管理类的工作就是将该数字转换为字符串“1.4142”。当然,有些人可能不同意我的观点,但我只是说,你写入状态管理层的逻辑越多越好。其中一个原因是,状态管理层比UI层更容易测试。

状态管理层为UI提供了一种监听应用程序状态更改的方式,但对展示的实际UI一无所知。由于状态管理层对UI层一无所知,您可以随时重构UI,而不会破坏状态管理层中的代码。这是构建灵活且可维护的应用程序的关键优势。

虽然状态管理层是执行逻辑的好地方,但并非所有应用程序逻辑都应该在那里完成。一些逻辑应该转移到service上。


4. 服务层

服务层有时被称为data repository,是执行逻辑并提供应用程序周围多个地方可能需要的数据的地方。这也是在应用程序逻辑与您用于I/O任务(如读取数据库或联系远程服务器)的第三方代码之间添加一层保护的方法。该保护层允许您在不破坏应用程序代码其余部分的情况下交换服务实现。

avatar

如上图所示,存储服务从状态管理层的两个不同页面调用。拥有一项服务可以防止您在应用程序周围的许多地方复制相同的代码。这也为您提供了一个位置,以便在您需要时进行更新。

从状态管理层的角度来看,该服务只是一个接口,即定义您可以调用它的方法的抽象Dart类。定义了接口API后,您必须使用具体方法实现抽象方法。例如,存储服务接口可以通过从远程服务器或本地数据库保存和检索数据来实现。无论哪种选择都可以,状态管理层也不在乎。有时,制作一个只返回虚假数据的服务实现甚至是有用的。这允许您在开始构建真正的服务实现之前拥有应用程序其余部分的工作原型。

虽然上面的示例仅显示存储服务,但您的应用程序很可能有多个不同的服务。例如,您可能还需要登录页面的身份验证服务。

Note: 如果您在应用程序的一个地方只需要服务,您可以将其代码直接放在状态管理层中。但是,如果您想做一些事情,例如交换数据库实现或身份验证提供商,解开代码将需要做很多工作。不过,有时你可能只有经历过痛苦的经历才能更好的吸取教训。

这是一个高层次的概述。在下一节中,我将更详细地写下如何实现我上面描述的架构模式——所有这些都不使用复杂的第三方状态管理包。


5. 极简的实现

状态管理解决方案需要能够做以下事情:

  1. 为UI层提供对状态管理层的引用
  2. 将UI事件通知状态管理层
  3. 给UI层一种监听状态变化的方法
  4. 状态更改后重建用户界面

5.1. 为UI层提供对状态管理层的引用

当您将逻辑从UI移动到另一个类时,您需要向UI提供该状态管理类的引用。许多状态管理解决方案的方式是使用基于InheritedWidget构建的provider package

然而,我发现使用GetIt来引用状态管理类要简单得多:

final homePageManager = getIt<HomePageManager>();

GetIt没有任何复杂之处。它只是创建状态管理类的单个实例(如果您将其注册为单例),然后随时随地为您提供。

有些人可能会对此感到不舒服,因为从本质上讲,您有一个全局变量,您可以从应用程序的任何地方访问。就我个人而言,只要我遵循以下准则,我就不会遇到麻烦:

  • 不要直接从UI层修改状态管理中的状态。相反,在状态管理类上调用方法,并让该类修改自己的状态。

其他人担心测试,因为单例是众所周知的很难测试。然而,GetIt提供了一种测试这些类的方法,因此这并不是一个真正的问题。

这里的另一个简单优势是,您可以使用GetIt以同样的方式设置服务层。稍后再详细讨论。

5.2. 将UI事件通知状态管理层

一旦UI引用了状态管理类,您只需调用方法即可通知状态管理类UI事件。

以下是用户按下登录按钮时如何做到这一点的示例:

loginPageManager.submitUserInfo(username, password);

现在,状态管理层可以使用用户名和密码做任何需要做的事情

5.3. 给UI层一种监听状态变化的方法

如果您在状态管理类中有一些状态,例如:

var myState = 1;

UI无法直接知道myState的值何时发生变化。

但是,如果您使用ValueNotifier或ChangeNotifier,您可以将状态更改通知UI。这两个类都是Flutter自带的,因此您不需要任何第三方软件包。

我更喜欢使用ValueNotifier,因为它支持不可变状态,并且是仅隔离实际变化状态的好方法。我发现,当我使用ChangeNotifier时,我倾向于构建非常大的状态管理类。
此外,当我通知监听者状态更改时,重建的用户界面个数将超过必要的个数。

以下是您将如何使该整数示例可观察:

final myStateNotifier = ValueNotifier<int>(1);

1只是notifier的初始值。每当您在通知符中更改值时,它都会通知任何正在收听它的对象。以下是您将如何更改值:

myStateNotifier.value = 2;

value属性存储状态。

上面的例子是一个简单的int。当我有更复杂的状态时,我会创建一个扩展ValueNotifier的新类。这样做的另一个好处是保持主状态管理类的清洁,并将逻辑隔离到实际使用的地方。

以下是扩展ValueNotifier的示例:

class FavoriteNotifier extends ValueNotifier<bool> {
  // set initial value to false
  FavoriteNotifier() : super(false);
  // get reference to service layer
  final _storageService = getIt<StorageService>();
  // a method to call from the outside
  void toggleFavoriteStatus(Song song) {
    value = !value;
    _storageService.updateFavoriteSong(song, value);
  }
}

虽然状态本身,是一个布尔值,足够简单,但每当toggleFavoriteStatus被调用时,它都需要一些额外的逻辑来保存歌曲最喜欢的状态。请注意,保存状态的实际工作由服务层完成。value变量属于ValueNotifier,每当value发生变化时,任何监听此ValueNotifier的对象都将收到有关新值的通知。

您在上面的FavoriteNotifier类中看到的所有逻辑都是不需要扰乱页面主状态管理类的逻辑。在页面的状态管理类中,您只有以下内容。

final favoriteNotifier = FavoriteNotifier();

将逻辑提取到这样的离散类中,使您的状态管理变得简单明了。

除了ValueNotifierChangeNotifier外,您的状态管理类还可以将future或stream暴露到UI层。原因是UI层有builder widget,可以直接收听Future或Stream的更新。根据我自己的经验,我倾向于将futures隐藏在ValueNotifier后面,有些人也喜欢用streams这样做,但这完全取决于你。

这把我们带到了最后一步。

5.4. 状态更改后重建用户界面

Flutter附带的小部件,可以监听状态变化,并在发生改变时重建自己。

如果UI需要监听ValueNotifier中的状态(这是我推荐的方法),那么您应该使用ValueListenableBuilder小部件。这个长名字相当不幸,因为它的工作很简单。只需使用新值重建一小部分用户界面。以下是官方视频:

您所要做的就是用ValueListenableBuilder小部件包装要重建的小部件。例如,以下是上面FavoriteNotifier的UI小部件:

class FavoriteButton extends StatelessWidget {
  const FavoriteButton({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final playPage = getIt<PlayPageManager>();
    return ValueListenableBuilder<bool>(
      valueListenable: playPage.favoriteNotifier,
      builder: (context, value, child) {
        return IconButton(
          icon: Icon(
            (value) 
              ? Icons.favorite 
              : Icons.favorite_border,
          ),
          onPressed: playPage.onFavoritePressed,
        );
      },
    );
  }
}

注意以下事项:

  • getIt:如上一节所述,您可以使用GetIt获取此页面状态管理类的引用。
  • ValueListenableBuilder:此小部件需要两个参数:valueListenable和builder
  • valueListenable:这是您从状态管理类中给它ValueNotifier的地方。
  • builder:此参数接受一个函数,每当状态值发生变化时,该函数都会被调用。该功能返回新的UI小部件,在这种情况下,根据最喜欢的状态,一个带有不同图标的按钮。构建器函数有三个参数:context、value和child。
  • context:构建器函数的上下文只是当前的BuildContext
  • value:这是ValueNotifier的新值。由于ValueNotifier在本例中包装了一个布尔值,因此值类型为布尔值。我通常将值重命名为更具描述性的东西,在这种情况下可能是isFavorite
  • child:如果您在此构建器功能中重建的小部件有一个子小部件,并且该子小部件不受状态变化的影响(即它不需要自己重建),那么您可以将子小部件放在这里。例如,如果您正在使用新颜色重建容器,但容器的子容器是一个没有变化的Text小部件,则适用。在构建器函数中使用子函数只是一个优化。如果您不需要它,您可以将子项替换为_(如果您将_用于未使用的上下文,则将其替换为__)。
builder: (_, value, __) {...}

Tip:有时用ValueListenableBuilder包装小部件很痛苦。但是,如果您使用VS Code或Android Studio中的快捷键来显示上下文菜单,您可以选择用StreamBuilder包装。然后只需将StreamBuilder修改为ValueListenableBuilder

avatar

我建议保持简单,并在所有事情上都使用ValueNotifier和ValueListenableBuilder。但是,如果您决定公开Stream、Future或ChangeNotifier,它们也有构建器小部件:

  • Stream: Use a StreamBuilder.
  • Future: Use a FutureBuilder.
  • ChangeNotifier: Use an AnimatedBuilder. (Yes, AnimatedBuilder is normally used for animation, but it takes a ChangeNotifier, too.) Here is a minimal example.

同样,如果您选择极简主义和简单,则不需要StreamBuilder、FutureBuilder或ChangeNotifier。只需使用ValueNotifier和ValueListenableBuilder

这涵盖了极简主义状态管理方法背后的理论。然而,看到一个真实的例子对许多人来说是有帮助的。


6. 计时器app示例项目

在本文的初稿中,我开始编写一个完整的教程,介绍如何像我之前为Riverpod所做的那样创建计时器应用程序(这反过来又受到Bloc库计时器教程的启发)。然而,我决定更清楚地指出相关部分,然后为您提供完整的源代码供您自己探索。

avatar

它只是一个简单的10秒计时器,您可以启动、暂停和重置。然而,为了演示服务层,我还添加了本地存储,如果您关闭应用程序并恢复下次打开应用程序的时间,则可以节省剩余时间。

以下是应用程序的架构:

avatar

这是我用来匹配该架构的文件结构:

avatar

在一个普通的应用程序中,我会有多个页面和服务,这就是为什么我制作了一个页面文件夹和一个服务文件夹。

现在让我们看看与您在文章前面学到的理论相关的一些关键领域

6.1. UI层

文件中main.dart中几乎没有什么。保持此文件的干净性并使用它来启动您的应用程序是件好事。页面的所有小部件都保存在pages文件夹中。

pages文件夹中的文件timer_page.dart具有UI布局代码。

avatar

这里有一个浓缩的摘要:

class TimerPage extends StatefulWidget {...}
class _TimerPageState extends State<TimerPage> {
  @override
  void initState() {
    ...
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My Timer App')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TimerTextWidget(),
            SizedBox(height: 20),
            ButtonsContainer(),
          ],
        ),
      ),
    );
  }
}
class TimerTextWidget extends StatelessWidget {...}
class ButtonsContainer extends StatelessWidget {...}
class StartButton extends StatelessWidget {...}
class PauseButton extends StatelessWidget {...}
class ResetButton extends StatelessWidget {...}

以下是一些要点:

  • 您应该将用户界面分解为许多小型独立小部件。这使您的UI代码更容易阅读,因为您可以给每个组件一个有意义的名称,而不仅仅是看到一堆容器和填充物。
  • 这里的小部件在一个文件中,但没有什么能阻止你把它们放在自己的文件中并把它们整理在文件夹中。如果您这样做,您只需将小部件导入任何需要的地方。这带来了从UI中提取小部件的另一个优势:小部件是可重用的,并减少了代码重复。
  • 您无法从上面的代码中分辨出来,但所有这些无状态小部件也是const小部件。尽可能让你的小部件保持不变。Flutter不需要重建常量小部件,因此这是一个性能优化。注意StartButton构造函数中的const关键字,例如:
class StartButton extends StatelessWidget {
  const StartButton({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {...}
}
  • 最后,每当您需要在页面首次加载时初始化某些内容时,请使用有状态小部件。有状态小部件的状态类具有initState方法。许多其他状态管理解决方案都有特殊的方法来做到这一点,但我发现这些方法令人困惑或太神奇了。initState很容易。它首次创建小部件状态时运行一次,然后就不再运行。这是在状态管理类上调用方法的好地方。稍后再详细讨论。

6.2. 状态管理类

我喜欢把我的状态管理文件放在我的UI文件旁边。在这种情况下,我调用了文件timer_page_logic.dart

avatar

或者,你可以叫它别的东西

  • timer_page_model.dart
  • timer_page_controller.dart
  • timer_page_manager.dart

或者别的什么。

在这个状态管理文件中,我添加了一个普通的Dart类:

class TimerPageManager {
  
}

然后,我查看用户界面,并分析用户界面的哪些部分可能会独立于其他部分更改。对于计时器应用程序,我可以看到两个不同的状态单元。一个是剩余时间,另一个是按钮配置。

avatar

剩下的时间是一个字符串,按钮配置可以用具有以下四个值的枚举表示:

avatar

由于UI需要一种方法来监听这两种状态的变化,下一步是为每个状态添加一个ValueNotifier。要做到这一点,只需在状态管理类中添加两个变量:

class TimerPageManager {
  final timeLeftNotifier = TimeLeftNotifier();
  final buttonNotifier = 
    ValueNotifier<ButtonState>(ButtonState.initial);
}
enum ButtonState {
  initial,
  started,
  paused,
  finished,
}

您可以在页面的状态管理类中执行所有逻辑,但当逻辑复杂时,最好将ValueNotifier分解为一个单独的类。在这种情况下,这就是我为TimeLeftNotifier所做的,因为它必须管理由stream控制的计时器。不过,按钮notifier逻辑非常简单,所以我直接使用ValueNotifier

提取时间left notifier逻辑后,文件结构如下所示:

avatar

页面状态管理类的工作是管理notifiers,而notifiers的工作是处理自己的状态。您越能将状态管理隔离到小的离散块中,您的工作就越容易。

我已经单独讨论了UI层和状态管理层,但现在让我们看看它们是如何相互作用的。我将再次回顾我在理论部分涵盖的四点。

6.2.1. 为UI层提供状态管理层的引用

如前所述,这里的答案是使用GetIt。我在pubspec.yaml中添加了该软件包:

dependencies:
  get_it: ^6.0.0

然后在service_locator.dart中注册了状态管理页面:

final getIt = GetIt.instance;
void setupGetIt() {
  getIt.registerLazySingleton<TimerPageManager>(
    () => TimerPageManager()
  );
}

在应用程序运行之前,在main.dart中调用setupGetIt方法。

void main() {
  setupGetIt();
  runApp(MyApp());
}

设置GetIt后,我可以从timer_page.dart的UI端访问计时器页面状态管理类:

final stateManager = getIt<TimerPageManager>();

我不仅在主**_TimerPageState类中这样做,而且在StartButton和PauseButton**等较小的小部件中也这样做。由于它被注册为懒惰的单例,它只会在您第一次收到它时创建一个新对象。之后,它只会返回与以前相同的对象。

6.2.2. 通知状态管理层有关UI事件

一旦UI引用了状态管理类,当发生某些事件时,UI可以在状态管理类上调用方法(例如点击开始计时器按钮)。这意味着您必须向状态管理类添加适当的方法。我在timer_page_logic.dart中向TimerPageManager添加了以下方法:

class TimerPageManager {
  final timeLeftNotifier = ...
  final buttonNotifier = ...
  void initTimerState() {...}
  void start() {...}
  void pause() {...}
  void reset() {...}
  void dispose() {...}
}

然后,我可以在timer_page.dart中从UI端调用方法,如下所示:

onPressed: () {
  final stateManager = getIt<TimerPageManager>();
  stateManager.pause();
},

那是在PauseButton小部件里面。

6.2.3. 为UI层提供一种监听状态更改的方法

当您将两个ValueNotifier变量添加到状态管理类时,您已经为UI层提供了监听状态更改的方法:

class TimerPageManager {
  final timeLeftNotifier = ...
  final buttonNotifier = ...
  
  ...
}

使这些notifiers通知任何侦听器的方法是更改其值。以下是暂停方法的实现:

class TimerPageManager {
  final timeLeftNotifier = ...
  final buttonNotifier = ...
  
  void pause() {
    timeLeftNotifier.pause();
    buttonNotifier.value = ButtonState.paused;
  }
  ...
}

buttonNotifier会直接地更新值,而 timeLeftNotifier 将调用转发到自己的notifier类以自行处理。

下一步是UI监听和响应这些状态更改。

6.2.4. 状态更改后重建用户界面

由于状态管理器类有两个值notifiers

class TimerPageManager {
  final timeLeftNotifier = ...
  final buttonNotifier = ...
  ...
}

您可以使用ValueListenableBuilder小部件收听它们。计时器应用程序UI使用其中两个,一个用于TimerTextWidget,一个用于ButtonContainer。这是TimerTextWidget的构建方法

Widget build(BuildContext context) {
  final stateManager = getIt<TimerPageManager>();
  return ValueListenableBuilder<String>(
    valueListenable: stateManager.timeLeftNotifier,
    builder: (context, timeLeft, child) {
      return Text(
        timeLeft,
        style: Theme.of(context).textTheme.headline2,
      );
    },
  );
}

这些是重要的部分:

  • 当您设置valueListenable参数时,ValueListenableBuilder会监听状态管理类中的notifier
  • timeLeftstateManager.timeLeftNotifier中的当前值。这是一条以'00:10'、'00:09'、'00:08'等形式出现的字符串。
  • 每当stateManager.timeLeftNotifier中的值发生变化时,此小部件(仅此小部件)都会重建。由于TimeLeftNotifier的内部实现在计时器滴答时每秒更新一次值,因此此小部件将每秒显示一个新的时间字符串

这就是UI和状态管理层交互,但让我也涵盖服务层。

6.2.5. 服务层

当用户离开应用程序时,计时器需要将其状态保存到本地存储,所以我创建了一个StorageService界面来定义状态管理层可以使用的API。它在storage_service.dart中:

avatar
abstract class StorageService {
  Future<int?> getTimeLeft();
  Future<void> saveTimeLeft(int seconds);
}

虽然该接口可以通过数据库或Web API实现,但我决定使用共享首选项实现它。它在shared_preferences_storage.dart中:

import 'package:shared_preferences/shared_preferences.dart';
import 'storage_service.dart';
class SharedPreferencesStorage extends StorageService {
  static const time_left_key = 'time_left';
  @override
  Future<int?> getTimeLeft() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt(time_left_key);
  }
  @override
  Future<void> saveTimeLeft(int seconds) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setInt(time_left_key, seconds);
  }
}

然后,我通过将其添加到service_locator.dart中的setupGetIt方法,在GetIt中注册了SharedPreferencesStorage

void setupGetIt() {
  ...
  getIt.registerLazySingleton<StorageService>(
    () => SharedPreferencesStorage()
  );
}

之后,我可以在time_left_notifier.dartTimeLeftNotifier类中访问和使用它:

class TimeLeftNotifier extends ValueNotifier<String> {
  final _storageService = getIt<StorageService>();
  
  void pause() {
    ...
    _storageService.saveTimeLeft(_currentTimeLeft);
  }
  ...
}

由于TimeLeftNotifiershared preferences一无所知,我可以用SQLite或其他方式交换存储服务实现,而TimeLeftNotifier甚至不知道。

6.2.6. 关于服务locators与依赖项注入的说明

我在本文中使用了服务locator模式(尽管有些人称之为“反模式”)。另一种选择是使用依赖注入。

这是服务locator模式:

class MyStateManager {
  final myService = getIt<MyService>();
}

这是依赖注入模式:

class MyStateManager {
  MyStateManager(this.myService);
  final MyService myService;
}

通过依赖注入,您可以为状态管理类提供作为构造函数参数所需的任何服务。依赖注入的支持者认为,这使得依赖性更加明确。我可以同意这一点。如果你愿意,欢迎你走那条路。在StorageService中作为参数传递给TimerPageManager和TimeLeftNotifier,只需对计时器应用程序进行小幅调整。但是,如果您想在UI小部件树中注入TimerPageManager,则必须修改许多小部件构造函数。那将是一种痛苦。这就是InheritedWidget要解决的问题。如果您想使用InheritedWidget,那么您应该使用Provider或Riverpod

不过,我不相信服务locators是Flutter应用程序的反模式。到目前为止,我发现它们易于使用且有帮助。服务locators和依赖注入都解决了抽象服务与其具体实现脱钩的更大问题。此时此刻,这对我来说已经足够了。不过,你必须自己做决定。以下是Martin Fowler关于该主题的一篇有用的文章(请参阅标题为服务定位器与依赖注入的部分)。

6.3. 关于初始化的说明

有状态小部件的状态类是执行初始化任务的好地方。对于计时器应用程序,我需要在页面首次加载时获得保存的剩余时间值。为此,我在timer_page.dart中向initState方法添加了初始化调用:

class _TimerPageState extends State<TimerPage> {
  final stateManager = getIt<TimerPageManager>();
  @override
  void initState() {
    stateManager.initTimerState();
    super.initState();
  }
  @override
  void dispose() {
    stateManager.dispose();
    super.dispose();
  }
  ...
}

如您所见,还有一个dispose方法。这对于取消计时器中的流非常有用。不过,请注意,我不会在用户界面中直接从这里访问流。我只是在状态管理类上调用一种方法,该类又在计时器流所在的notifier类上调用一个方法。

如果您想更早地开始一些初始化(例如联系Web服务器),您可以在主函数中的runApp之前添加它。

void main() {
  setupGetIt();
  //             <-- add here
  runApp(MyApp());
}

在这种情况下,您希望有一个处理应用程序范围状态的状态管理类(例如用户的登录状态)

6.4. 关于重建的说明

运行计时器应用程序,并在倒计时中途按下暂停和开始按钮。观察小部件重建时显示的打印语句

building MyHomePage
building time left state: 00:10
building button state: ButtonState.initial
building button state: ButtonState.started
building time left state: 00:09
building time left state: 00:08
building time left state: 00:07
building time left state: 00:06
building time left state: 00:05
building button state: ButtonState.paused
building button state: ButtonState.started
building time left state: 00:04
building time left state: 00:03
building time left state: 00:02
building time left state: 00:01
building time left state: 00:00
building button state: ButtonState.finished

由于timer_page.dart中的ValueListenableBuilder小部件只包装需要重建的小部件,因此您无需重新构建UI树中不必要的部分。您可以从上面的打印语句中看到,计时器更新没有导致按钮配置被重建。同样,按下按钮不会导致文本小部件被重建。

要点:隔离你的状态!


6.5. Summary

以下是基于我上面描述的计时器应用程序的更详细的架构模型:

avatar

Notes:

  • 用户界面层中有两个独立的状态单元可以更改:剩余时间和按钮配置。
  • 在状态管理层中将状态映射更改为两个值notifiers的两种方法。
  • 在这两个notifiers中,只有剩余时间的notifier需要访问存储服务,该服务是通过shared preferences实现的。
  • 事件来自用户按下按钮,来自时间左侧notifier内的计时器,以及从存储服务中获取保存值的结果
  • 状态管理层处理这些事件并更新状态。
  • 用户界面正在监听状态更改,并在发生这种情况时重建自己。

7. 结论

到目前为止,我所看到的所有状态管理解决方案(setState除外)的工作方式基本相同。他们采用应用程序状态和逻辑,并通过将其放入新类将其与UI分离。除此之外,它们还为状态变化时的UI提供了重建方式。我在这里介绍的极简主义方法做到了这一切,但没有第三方状态管理包。GetIt是一个第三方软件包,但我不称它为状态管理解决方案,因为您仅使用它来获取Dart类的引用。如果你愿意,你甚至可以用单例替换GetIt。然而,与单例相比,GetIt的一个优势是GetIt更容易测试。

正如我在文章开头所说(没有双关语),如果您理解它并可以让它做您需要做的事情,就没有理由放弃您当前正在使用的任何状态管理解决方案。然而,如果你对这一切的复杂性感到困惑,那么也许这种极简主义方法适合你。

然而,仅仅因为这种方法是“极简主义的”,并不意味着你不能用它构建一个大型应用程序。关键是将状态和逻辑隔离到只担心自己的小值的notifiers and services中。