5

重走Flutter状态管理之路—Riverpod入门篇

 2 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/124811671
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

重走Flutter状态管理之路—Riverpod入门篇

b7aa0e22a02a74b05bbff15c716d1f06.png

点击上方蓝字关注我,知识会给你力量

6c828980328767f8cf9d6755ece8febd.png

熟悉我的朋友应该都知道,我好几年前写过一个「Flutter状态管理之路」系列,那个时候介绍的是Provider,这也是官方推荐的状态管理工具,但当时没有写完,因为写着写着,觉得有很多地方不尽人意,用着很别扭,所以在写了7篇文章之后,就暂时搁置了。

一晃时间过了这么久,Flutter内部依然没有一个能够碾压一切的状态管理框架,GetX可能是,但是我觉得不是,InheritedWidget系的状态管理,才应该是正统的状态管理。

最近在留意Provider的后续进展时,意外发现了一个新的库——Riverpod,号称是新一代的状态管理工具,仔细一看,嘿,居然还是Provider的作者,好家伙,这是搬起石头砸自己的脚啊。

就像作者所说,Riverpod就是对Provider的重写,可不是吗,字母都没变,就换了个顺序,这名字也是取的博大精深。

其实Provider在使用上已经非常不错了,只不过随着Flutter的更加深入,大家对它的需求也就越来越高,特别是对Provider中因为InheritedWidget层次问题导致的异常和BuildContext的使用这些问题诟病很多,而Riverpod,正是在Provider的基础上,探索出了一条心的状态管理之路。

大家可以先把官方文档看一看 https://riverpod.dev ,看完之后发现还是一脸懵逼,那就对了,Riverpod和Provider一样,有很多类型的Provider,分别用于不同的场景,所以,理清这些Provider的不同作用和使用场景,对于我们用好Riverpod是非常有帮助的。

官网的文档,虽然是作者精心编写的,但它的教程,站在的是一个创作者的角度,所以很多入门的初学者看上去会有点摸不清方向,所以,这才有了这个系列的文章。

我将在这个系列中,带领大家对文档进行一次精读,进行一次赏析,本文不全是对文档的翻译,而且讲解的顺序也不一样,所以,如果你想入门Riverpod进行状态管理,那么本文一定是你的最佳选择。

Provider第一眼

首先,我们为什么要进行状态管理,状态管理是解决申明式UI开发,关于数据状态的一个处理操作,例如Widget A依赖于同级的Widget B的数据,那么这个时候,就只能把数据状态上提到它们的父类,但是这样比较麻烦,Riverpod和Provider这样的状态管理框架,就是为了解决类似的问题而产生的。

将一个state包裹在一个Provider中可以有下面一些好处。

  • 允许在多个位置轻松访问该状态。Provider可以完全替代Singletons、Service Locators、依赖注入或InheritedWidgets等模式

  • 简化了这个状态与其他状态的结合,你有没有为,如何把多个对象合并成一个而苦恼过?这种场景可以直接在Provider内部实现

  • 实现了性能优化。无论是过滤Widget的重建,还是缓存昂贵的状态计算;Provider确保只有受状态变化影响的部分才被重新计算

  • 增加了你的应用程序的可测试性。使用Provider,你不需要复杂的setUp/tearDown步骤。此外,任何Provider都可以被重写,以便在测试期间有不同的行为,这可以轻松地测试一个非常具体的行为

  • 允许与高级功能轻松集成,如logging或pull-to-refresh

首先,我们通过一个简单的例子,来感受下,Riverpod是怎么进行状态管理的。

Provider是Riverpod应用程序中最重要的部分。Provider是一个对象,它封装了一个state并允许监听该state。Provider有很多变体形式,但它们的工作方式都是一样的。

最常见的用法是将它们声明为全局常量,例如下面这样。

不要被Provider的全局变量所吓倒。Provider是完全final的。声明一个Provider与声明一个函数没有什么不同,而且Provider是可测试和可维护的。

这段代码由三个部分组成。

  • final myProvider,一个变量的声明。这个变量是我们将来用来读取我们Provider的状态的。Provider应该始终是final的

  • Provider,我们决定使用的Provider类型。Provider是所有Provider类型中最基本的。它暴露了一个永不改变的对象。我们可以用其他Provider如StreamProvider或StateNotifierProvider来替换Provider,以改变值的交互方式

  • 一个创建共享状态的函数。该函数将始终接收一个名为ref的对象作为参数。这个对象允许我们读取其他Provider,在我们Provider的状态将被销毁时执行一些操作,以及其它一些事情

传递给Provider的函数返回的对象的类型,取决于所使用的Provider。例如,一个Provider的函数可以创建任何对象。另一方面,StreamProvider的回调将被期望返回一个Stream。

你可以不受限制地声明你想要的多个Provider。与使用package:provider不同的是,Riverpod允许创建多个暴露相同 "类型 "的状态的provider。

两个Provider都创建了一个字符串,但这并没有任何问题。

为了使Provider发挥作用,您必须在Flutter应用程序的根部添加ProviderScope。

以上就是Riverpod最简单的使用,我们看下完整的示例代码。

可以发现,Riverpod的使用比package:Provider还要简单,申明一个全局变量来管理状态数据,然后就可以在任意地方获取数据了。

如何读取Provider的状态值

在有了一个简单的了解后,我们先来了解下关于状态中的「读」。

在Riverpod中,我们不像package:Provider那样需要依赖BuildContext,取而代之的是一个「ref」变量。这个东西,就是联系存取双方的纽带,这个对象允许我们与Provider互动,不管是来自一个Widget还是另一个Provider。

从Provider中获取ref

所有Provider都有一个 "ref "作为参数。

这个参数可以安全地传递给其它Provider或者类,来获取所需要的值。

例如,一个常见的用例是将Provider的 "ref "传递给一个StateNotifier。

这样做,可以使我们的Counter类能够读取Provider。

这种方式是联系组件和Provider的一个重要方式。

从Widget中获取ref

Widgets自然没有一个ref参数。但是Riverpod提供了多种解决方案来从widget中获得这个参数。

扩展ConsumerWidget

在widget树中获得一个ref的最常见的方法是用ConsumerWidget代替StatelessWidget。

ConsumerWidget在使用上与StatelessWidget相同,唯一的区别是它的构建方法上有一个额外的参数:"ref "对象。

一个典型的ConsumerWidget看起来像这样。

扩展ConsumerStatefulWidget

与ConsumerWidget类似,ConsumerStatefulWidget和ConsumerState相当于一个带有状态的StatefulWidget,不同的是,state有一个 "ref "对象。

这一次,"ref "不是作为构建方法的参数传递,而是作为ConsumerState对象的一个属性。

通过ref来获取状态

现在我们有了一个 "ref",我们可以开始使用它。

ref "有三个主要用途。

  • 获得一个Provider的值并监听变化,这样,当这个值发生变化时,这将重建订阅该值的Widget或Provider。这是通过ref.watch完成的

  • 在一个Provider上添加一个监听器,以执行一个action,如导航到一个新的页面或在该Provider发生变化时执行一些操作。这是通过 ref.listen 完成的

  • 获取一个Provider的值,同时忽略它的变化。当我们在一个事件中需要一个Provider的值时,这很有用,比如 "点击操作"。这是通过ref.read完成的

只要有可能,最好使用 ref.watch 而不是 ref.read 或 ref.listen 来实现一个功能。通过依赖ref.watch,你的应用程序变得既是反应式的又是声明式的,这使得它更容易维护。

通过ref.watch观察Provider的状态

ref.watch在Widget的构建方法中使用,或者在Provider的主体中使用,以使得Widget/Provider可以监听另一个Provider。

例如,Provider可以使用 ref.watch 来将多个Provider合并成一个新的值。

一个例子是过滤一个todo-list,我们需要两个Provider。

  • filterTypeProvider,一个暴露当前过滤器类型的Provider(None,表示只显示已完成的任务)

  • todosProvider,一个暴露整个任务列表的Provider

通过使用ref.watch,我们可以制作第三个Provider,结合这两个Provider来创建一个过滤后的任务列表。

有了这段代码,filteredTodoListProvider现在就可以管理过滤后的任务列表。

如果过滤器或任务列表发生变化,过滤后的列表也会自动更新。同时,如果过滤器和任务列表都没有改变,过滤后的列表将不会被重新计算。

类似地,一个Widget可以使用ref.watch来显示来自Provider的内容,并在该内容发生变化时更新用户界面。

这段代码显示了一个Widget,它监听了一个存储计数的Provider。如果该计数发生变化,该Widget将重建,用户界面将更新以显示新的值。

ref.watch方法不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。在这些情况下,考虑使用 ref.read 来代替。

通过ref.listen监听Provider的变化

与ref.watch类似,可以使用ref.listen来观察一个Provider。

它们之间的主要区别是,如果被监听的Provider发生变化,使用ref.listen不会重建widget/provider,而是会调用一个自定义函数。

这对于在某个变化发生时执行某些操作是很有用的,比如在发生错误时显示一个snackbar。

ref.listen方法需要2个参数,第一个是Provider,第二个是当状态改变时我们要执行的回调函数。回调函数在被调用时将被传递2个值,即先前状态的值和新状态的值。

ref.listen方法也可以在Provider的体内使用。

或在一个Widget的Build方法中使用。

ref.listen也不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。

通过ref.read来读取Provider的状态

ref.read方法是一种在不监听的情况下获取Provider的状态的方法。

它通常用于由用户交互触发的函数中。例如,当用户点击一个按钮时,我们可以使用ref.read来增加一个计数器的值。

应该尽可能地避免使用ref.read,因为它不是响应式的。

它存在于使用watch或listen会导致问题的情况下。如果可以的话,使用watch/listen几乎总是更好的,尤其是watch。

关于ref.read到底什么时候用

首先,永远不要在Widget的build函数中直接使用ref.read。

你可能很想使用ref.read来优化一个Widget的性能,例如通过下面的代码来实现。

但这是一种非常糟糕的做法,会导致难以追踪的错误。

以这种方式使用 ref.read 通常与这样的想法有关:"Provider所暴露的值永远不会改变,所以使用'ref.read'是安全的"。这个假设的问题是,虽然今天该Provider可能确实从未更新过它的值,但不能保证明天也是如此。

软件往往变化很大,而且很可能在未来,一个以前从未改变的值需要改变。

如果你使用ref.read,当这个值需要改变时,你必须翻阅整个代码库,将ref.read改为ref.watch--这很容易出错,而且你很可能会忘记一些情况。

如果你一开始就使用ref.watch,你在重构时就会减少问题。

但是如果我想用ref.read来减少我的widget重构的次数呢?

虽然这个目标值得称赞,但需要注意的是,你可以用ref.watch代替来达到完全相同的效果(减少构建的次数)。

Provider提供了各种方法来获得一个值,同时减少重建的次数,你可以用这些方法来代替。

例如下面的代码(bad)。

我们可以这样改。

这两个片段代码都达到了同样的效果:当计数器增加时,我们的按钮将不会重建。

另一方面,第二种方法支持计数器被重置的情况。例如,应用程序的另一部分可以调用。

ref.refresh(counterProvider);

这将重新创建StateController对象。

如果我们在这里使用ref.read,我们的按钮仍然会使用之前的StateController实例,而这个实例已经被弃置,不应该再被使用。

而使用ref.watch则可以正确地重建按钮,使用新的StateController。

关于ref.read可以读哪些值

根据你想监听的Provider,你可能有多个可能的值可以监听。

作为一个例子,考虑下面的StreamProvider。

final userProvider = StreamProvider<User>(...);

当读取这个userProvider时,你可以像下面这样。

  • 通过监听userProvider本身同步读取当前状态。

  • 通过监听userProvider.stream来获得相关的Stream。

  • 通过监听userProvider.future获得一个Future,该Future以最新发出的值进行解析。

其他Provider可能提供不同的替代值。

欲了解更多信息,请查阅API参考资料,参考每个Provider的API文档。

通过select来控制精确的读范围

最后要提到的一个与读取Provider有关的功能是,能够减少Widget/Provider从ref.watch重建的次数,或者ref.listen执行函数的频率的功能。

这一点很重要,因为默认情况下,监听一个Provider会监听整个对象的状态。但有时,一个Widget/Provider可能只关心一些属性的变化,而不是整个对象。

例如,一个Provider可能暴露了一个User对象。

但一个Widget可能只使用用户名。

如果我们简单地使用ref.watch,当用户的年龄发生变化时,这将重建widget。

解决方案是使用select来明确地告诉Riverpod我们只想监听用户的名字属性。

更新后的代码将是这样。

通过使用select,我们能够指定一个函数来返回我们关心的属性。

每当用户改变时,Riverpod将调用这个函数并比较之前和新的结果。如果它们是不同的(例如当名字改变时),Riverpod将重建Widget。然而,如果它们是相等的(例如当年龄改变时),Riverpod将不会重建Widget。

这个场景也可以使用select和ref.listen。

这样做也将只在名称改变时调用listener。

另外,你不一定要返回对象的一个属性。任何覆盖==的值都可以使用。例如,你可以这样做。

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));

读取状态,是一个非常重要的部分,什么时候用什么样的方式来读,都会有不同的效果。

ProviderObserver

ProviderObserver可以监听一个ProviderContainer的变化。

要使用它,你可以扩展ProviderObserver类并覆盖你想使用的方法。ProviderObserver有三个方法。

  • didAddProvider:在每次初始化一个Provider时被调用

  • didDisposeProvider:在每次销毁Provider的时候被调用

  • didUpdateProvider:每次在Provider更新时都会被调用

ProviderObserver的一个简单用例是通过覆盖didUpdateProvider方法来记录Provider的变化。

现在,每当我们的Provider的值被更新时,logger将记录它。

对于诸如StateController(StateProvider.state的状态)和ChangeNotifier等可改变的状态,previousValue和newValue将是相同的。因为它们引用的是同一个StateController / ChangeNotifier。

这些是对Riverpod的最基本了解,但是却是很重要的部分,特别是如何对状态值进行读取,这是我们用好Riverpod的核心。

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK