老生常谈的几个问题
一.为什么需要状态管理?
二.什么是Provider?
三.Provider如何使用(最关心的问题)?
四.Provider 是如何做到状态共享的?
五.为什么选择 Provider?
回答第一个问题,先看两张图
图一
这个图相信大家也很常见,就是创建flutter工程的时的默认demo,页面也很简单视图中间有个text文本,点击按钮时文本数字自增,单个页面状态很可控,没有setState无法实现的功能。
图二如果不是单个页面呢,是多个页面,其中一些页面之间相互关联,正常开发中不可能遇到每个页面独立互不影响的,如果有效的管理这些多页面共用的状态成为一个头痛的问题,也解释了为什么要多界面状态管理。
现在回答第二个问题,Provider 从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个 app 都有它自己的解决方案,我们可以很方便的管理状态。
如何使用Provider
第一步:添加依赖
在pubspec.yaml中添加Provider的依赖(provider: ^3.1.0)
第二步:创建数据 Model
这里的 Model 实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据,如图三
图三
这个类意图非常清晰,我们的数据就是一个 int 类型的 _count,下划线代表私有。通过 get value 把 _count 值暴露出来。并提供 increment 方法用于更改数据。
这里使用了 mixin 混入了 ChangeNotifier,这个类能够帮驻我们自动管理所有听众。当调用 notifyListeners() 时,它会通知所有听众进行刷新。
第三步:创建顶层共享数据
我们在 main 方法中初始化全局数据,如图4,这个是在我已有项目中建的所以有一些别的引用类.
标红的代码意思,runApp是优化的后的代码,通过 Provider<T>.value 能够管理一个恒定的数据,并提供给子孙节点使用。我们只需要将数据在其 value 属性中声明即可。在这里我们将 textSize 传入。
而 ChangeNotifierProvider<T>.value 不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 notifyListeners)
此处的 <T> 范型可省略。但是我建议大家还是进行声明,这会使你的应用更加健壮。
除了上述几个属性之外 Provider<T>.value 还提供了 UpdateShouldNotify Function,用于控制刷新时机。
typedef UpdateShouldNotify<T> = bool Function(T previous, T current);
我们可以在这里传入一个方法 (T previous, T current){...} ,并获得前后两个 Model 的实例,然后通过比较两个 Model 以自定义刷新规则,返回 bool 表示是否需要刷新。默认为 previous != current 则刷新。
图4第四步:在子页面中获取状态
获取顶层数据最简单的方法就是 Provider.of<T>(context); 这里的范型 <T> 指定了获取 FirstScreen 向上寻找最近的储存了 T 的祖先节点的数据。
我们通过这个方法获取了顶层的 CounterModel 及 textSize。并在 Text 组件中进行使用,如图5使用起来也容易,一般在无状态组件中放入build中。
图五在有状态组件中最好使用,有些界面一进入会行网络请求,如果放入图五的build中会无限调用,所以根据自己的需求来决定调用方式。
图六如果只是应用的话上面几个介绍就可以使用Provider了。
下面讲一下提升的方案,比如我只是点击触发了一个按钮,不希望当前界面重新绘制,要怎么办呢?
图五中,floatingActionButton出现了Consumer,如果你细心看的话发会现。
Consumer 使用了Builder模式,收到更新通知就会通过 builder 重新构建。Consumer<T> 代表了它要获取哪一个祖先中的 Model。
Consumer 的 builder 实际上就是一个 Function,它接收三个参数 (BuildContext context, T model, Widget child)。
context: context 就是 build 方法传进来的 BuildContext 在这里就不细说了.
T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。
然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
在这个浮动按钮的例子中,我们通过 Consumer 获取到了顶层的 CounterModel 实例。并在浮动按钮 onTap 的 callback 中调用其 increment 方法。
而且我们成功抽离出 Consumer 中不变的部分,也就是浮动按钮中心的 Icon 并将其作为 child 参数传入 builder 方法中。
图七可以发现,Consumer 就是通过 Provider.of<T>(context) 来实现的。但是从实现来讲 Provider.of<T>(context) 比 Consumer 简单好用太多,为啥我要搞得那么复杂捏。
实际上 Consumer 非常有用,它的经典之处在于能够在复杂项目中,极大地缩小你的控件刷新范围。Provider.of<T>(context) 将会把调用了该方法的 context 作为听众,并在 notifyListeners 的时候通知其刷新。
举个例子来说,我们的 FirstScreen 使用了 Provider.of<T>(context) 来获取数据,SecondScreen 则没有。
你在 FirstScreen 中的 build 方法中添加一个 print('first screen rebuild');
然后在 SecondScreen 中的 build 方法中添加一个 print('second screen rebuild');
点击第二个页面的浮动按钮,那么你会在控制台看到这句输出。
first screen rebuild
首先这证明了 Provider.of<T>(context) 会导致调用的 context 页面范围的刷新。
那么第二个页面刷新没有呢? 刷新了,但是只刷新了 Consumer 的部分,甚至连浮动按钮中的 Icon 的不刷新我们都给控制了。你可以在 Consumer 的 builder 方法中验证,这里不再啰嗦
假如你在你的应用的 页面级别 的 Widget 中,使用了 Provider.of<T>(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,你都会重新刷新整个页面。虽然你有 Flutter 的自动优化算法给你撑腰,但你肯定无法获得最好的性能。
所以在这里我建议各位尽量使用 Consumer 而不是 Provider.of<T>(context) 获取顶层数据。
以上便是一个最简单的使用 Provider 的例子。Consumer最多支持取六个Modal不能无限所有Modal,通常情况下是够用了。
控制你的刷新范围
在 Flutter 中,组合大于继承的特性随处可见。常见的 Widget 实际上都是由更小的 Widget 组合而成,直到基本组件为止。为了使我们的应用拥有更高的性能,控制 Widget 的刷新范围便显得至关重要。
我们已经通过前面的介绍了解到了,在 Provider 中获取 Model 的方式会影响刷新范围。所有,请尽量使用 Consumer 来获取祖先 Model,以维持最小刷新范围。
Provider 是如何做到状态共享的
这个问题实际上得分两步。
获取顶层数据
实际上在祖先节点中共享数据这件事我们已经在之前的文章中接触过很多次了,都是通过系统的 InheritedWidget 进行实现的。
Provider 也不例外,在所有 Provider 的 build 方法中,返回了一个 InheritedProvider。
class InheritedProvider<T> extends InheritedWidget
Flutter 通过在每个 Element 上维护一个 InheritedWidget 哈希表来向下传递 Element 树中的信息。通常情况下,多个 Element 引用相同的哈希表,并且该表仅在 Element 引入新的 InheritedWidget 时改变。
通知刷新
通知刷新这一步实际上在讲各种 Provider 的时候已经讲过了,其实就是使用了 Listener 模式。Model 中维护了一堆听众,然后 notifiedListener 通知刷新。
为什么选择 Provider
Provider 不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。就连 BLoC 未解决的那个棘手的 dispose 问题,和 ScopedModel 的侵入性问题,它也都解决了。
然而它就是完美的吗,并不是,至少现在来说。Flutter Widget 构建模式很容易在 UI 层面上组件化,但是仅仅使用 Provider,Model 和 View 之间还是容易产生依赖。
我们只有通过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系,所以假如各位有组件化的需求,还需要另外处理。
不过对于大多数情况来说,Provider 足以优秀,它能够让你开发出简单、高性能、层次清晰 的应用。
我应该如何选择状态管理
介绍了这么多状态管理,你可能会发现,一些状态管理之间职责并不冲突。例如 BLoC 可以结合 RxDart 库变得很强大,很好用。而 BLoC 也可以结合 Provider / ScopedModel 一起使用。那我应该选择哪种状态管理方式呢。
我的建议是遵守以下几点:
使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。
选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。
在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。
附上我的github学习地址:FlutterStudy,希望能帮到一起前行的你,有问题提issue。
网友评论