Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod

lxf2023-04-25 20:46:01

Provider 是做什么的?

Provider 在做的事情其实并不是状态管理

和 InheritedWidget 一样,Provider 最主要的功能就只是把我们要更新的状态传递下去,还是需要依靠 ChangeNotifier。然而状态管理通常是跟整个程序架构有关的,无论是 BLoC、Redux 等等,选择了其中一个基本上就决定了我们 App 的表现层架构,而且我们也不太可能同时混用两个以上的状态管理框架。但 Provider 却很神奇的可以和这些的任意一个并存。这正是 Provider 并非状态管理,而是一个工具的证明。

Provider 在做的事情其实也不是依赖注入

我们来看一看,Flutter 社群中,Dependency Injection 代表着什么意义吧:

"Flutter Dependency Injection a Beginners Guide"

Motivation Passing dependencies through a constructor is perfectly fine for accessing data one level down, maybe even two. What if you're four levels deep in the widget tree and you suddenly need the data from an object in your code? Imagine these are all widgets in separate files with their own logic. HomeView -> MyCustomList -> PostItem -> PostMenu -> PostActions -> LikeButton

"What is Dependency Injection in the context of Flutter development?"

In the Flutter world, DI is being able to pass a value deep down the widget tree without using globals.

This is all about that BuildContext object, mostly combined with Inheritedwidget.

这些人认为依赖注入要解决的问题就是,让你能够把上层提供的依赖注入到好几层深的 Widget 中。虽然这里的四个字似乎用的很通顺,但这个多层传参的问题,是因为是 Flutter 才有的问题,而不是因为做了依赖注入才产生的问题。如果按照新的 Flutter 专属的依赖注入定义,那么 Provider 是依赖注入工具。

关于 Provider 的误会

Provider 为什么会被误认为是依赖注入/状态管理工具呢?我想这只是因为它所解决的问题,刚好是我们在 widget tree 里进行依赖注入和状态管理时,因为 Flutter 的 积极的复合性而产生的问题。以依赖注入而言,是深层的 widget 该如何拿到它的依赖。以状态管理而言,是深层的 widget 如何拿到它的状态。Provider 可以解决依赖注入和状态管理的变量存取问题,但我们没办法只靠 Provider 就实现依赖注入和状态管理。

就像扳手可以用来修理汽车,也可以用来修理人。但我们并不会说扳手是一个混合汽车和人的工具一样。Provider 终究只是一个工具,既不是状态管理,也不是依赖注入。

所有工具,甚至说每一行代码,都是为了解决某个问题而存在的。所以接下来我们看一下 InheritedWidget 有什么问题,Provider 解决了哪些,又没有解决哪些;Riverpod 又解决和没解决哪些。

InheritedWidget 与 Provider 与 Riverpod

InheritedWidget

让我们来看一个最简单的 InheritedWidget 使用的例子

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 除了我们必须把继承从常用的 StatelessWidget / StatefulWidget 改为 InheritedWidget 之外,还必须重写一个 updateShouldNotify 方法,再调用 dependOnInheritedWidgetOfExactType 方法来获取我们的实例,其他看着也不算太麻烦。

不过,一般我们使用的都是 StatefulWidget,而 InheritedWidget 却不是 StatefulWidget,所以我们需要用一个 StatefulWidget 来管理状态,以便更新,然后把状态传给 InheritedWidget,再由 InheritedWidget 传递给子树上的 Widget,便有了如下代码:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 我可以理解 Flutter 技术团队为了单一职责原则而选择这样设计,让 InheritedWidget 唯一的责任就是把参数提供给子树上的 Widget。不过我们想传递下去的参数,绝大多数就是我们提升上来的状态,也就需要更新。

InheritedWidget 的问题,简单来说:

  1. 写着麻烦,读起来复杂;
  2. 状态多的时候会造成更深的嵌套;
  3. 因为是根据类型向上寻找 InheritedWidget,无法提供相同类型的参数;
  4. 找不到对应参数的时候可能会发生不可预估的错误;
  5. 依赖 Widget,无法解决 Widget 之外的问题。

关于第 5 条补充说明一下:除了 Widget 会不断复合之外,项目中还有很多地方会发生复合,比如BloC/ViewModel,Interactor/UseCase,Service...如果我们想在这些地方获取状态,显然不能使用 InheritedWidget。

Provider

Provider Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 上面的例子,改为用 Provider 如下:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod Provider 对于上述问题的改善有哪些呢?不只是行数减少了,逻辑也清楚了许多。嵌套的问题可以靠 MultiProvider。不仅如此,Provider 提供了 lazy-loading。然后就是可以通过 context.watchcontext.readcontext.selectConsumer提供了 InheritedWidget 难以实现的功能。

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 当然 Provider 也不是完美的。虽然解决了问题1、2,但它背后依旧依赖 InheritedWidget,问题3、4、5依旧没变,于是有了如下问题:

  1. 写着麻烦,读起来复杂;
  2. 状态多的时候会造成更深的嵌套;
  3. 因为是根据类型向上寻找 Provider,无法提供相同类型的参数;
  4. 找不到对应参数的时候可能会发生不可预估的错误;
  5. 依赖 Widget,无法解决 Widget 之外的问题;
  6. 无法简单的组合多个 Provider;
  7. 无法在不使用 state 的时候 dispose state(只能在 dispose widget 的时候 dispose state);

来自评论区 Dreamer2q 用户的补充:Provider 无法在不使用 Consumer 消费 state 的时候,无法 dispose state,只能在 dispose Provider 的时候 dispose state。

  1. 无法建立 private Provider。

显然3、4、5是不可能解决的,那么 Riverpod 便出现了。

Riverpod

Riverpod

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod

上面的例子,改为用 Riverpod 如下:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 程序上看起来更加清晰简洁一些,因为 Riverpod 里的 Provider 不再是 Widget,所以不需要把它塞进 Widget tree 中,自然也就没有了嵌套问题。所以 Riverpod 同样可以解决 InheritedWidget 的第1、2个问题,又因为我们现在是直接通过变量来存取的 Riverpod 的 provider,问题3、4自然不复存在,问题8也可以解决。

对于问题5,下面展示了如何在非 Flutter 环境下使用 Riverpod:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 这里的 ProviderContainer 就像上面的 ProviderScope 一样,存放我们所有的 state。我们可以看到它其实有点像观察者模式了,我相信它在纯 Dart 环境下能做得到的所有事,用 stream/rxDart 都能做到,但至少它跟 Provider 库相比起来,多了很多可以发挥的地方。而且作者将 Riverpod 拆成了 riverpodflutter_riverpodhooks_riverpod

对于问题6,可以用如下的方法解决:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 可以看到代码简洁明了。

对于问题7,需要靠autoDispose来解决了:

Flutter - 为什么我把项目的状态管理框架从 Provider 升级到了 Riverpod 任何种类的 Provider 都可以搭配 autoDispose 来使用,只要在没有地方 watch 的时候就会自动被 dispose 掉。

到这里,Riverpod 可以把列出来的所有问题都解决了。

结语

再一次确认了 Riverpod 真的有解决了从 InheritedWidget 到 Provider 所无法解决的问题。如果你确实遇过这些问题,那么 Riverpod 很值得你试试看。