Flutter开发 Provider详解

一、为什么会有 Provider ?

因为 Flutter 与 React 技术栈的相似性,所以在 Flutter 中涌现了诸如flutter_redux 、flutter_dva 、 flutter_mobx 、 fish_flutter等前端式的状态管理,它们大多比较复杂,而且需要对框架概念有一定理解。

而作为 Flutter 官方推荐的状态管理 scoped_model ,又因为其设计较为简单,有些时候不适用于复杂的场景。

所以在经历了一端坎坷之后,今年 Google I/O 大会之后, Provider 成了 Flutter 官方新推荐的状态管理方式之一。

它的特点就是: 不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度 , 而原 Google 官方仓库的状态管理 flutter-provide 已宣告GG , provider 成了它的替代品。


二、Provider知识点

1、 Provider 的内部 DelegateWidget 是一个 StatefulWidget ,所以可以更新且具有生命周期。

2、状态共享是使用了 InheritedProvider 这个 InheritedWidget 实现的。

3、巧妙利用 MultiProvider 和 Consumer 封装,实现了组合与刷新颗粒度控制。


三、Provider的工作原理

1、Delegate

既然是状态管理,那么肯定有 StatefulWidget 和 setState 调用。

在 Provider 中,一系列关于  StatefulWidget 的生命周期管理和更新,都是通过各种代理完成的,如下图所示,上面代码中我们用到的 ChangeNotifierProvider 大致经历了这样的流程:

设置到 ChangeNotifierProvider 的 ChangeNotifer 会被执行 addListener 添加监听 listener。

listener 内会调用 StateDelegate 的 StateSetter 方法,从而调用到  StatefulWidget 的 setState。

当我们执行 ChangeNotifer 的 notifyListeners 时,就会最终触发 setState 更新。


2、InheritedProvider

状态共享肯定需要 InheritedWidget ,InheritedProvider 就是InheritedWidget 的子类,所有的 Provider 实现都在 build 方法中使用 InheritedProvider 进行嵌套,实现 value 的共享。


使用方式

ChangeNotifierProvider 方式

通过调用ChangeNotifier.notifyListenersChangeNotifier进行监听,将其公开给它的子Widget并重建依赖项;

1. 绑定数据

ChangeNotifierProvider绑定数据有两种方式:

ChangeNotifierProvider({Key key, @required ValueBuilder<T> builder, Widget child })

通过构造器创建一个ChangeNotifier,在ChangeNotifierProvider移除时自动处理;

classMyAppextendsStatelessWidget{@overrideWidgetbuild(BuildContext context){returnChangeNotifierProvider<User>(builder:(_)=>User('Flutter',0),child:MaterialApp(title:'Flutter Demo',theme:ThemeData(primarySwatch:Colors.blue),home:MyHomePage(title:'Peovider Demo')));}}

ChangeNotifierProvider.value({Key key, @required T notifier, Widget child })

通过监听通知给子Widget并重建依赖项;

classMyAppextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnChangeNotifierProvider<User>.value(notifier:User('Flutter',0),child:MaterialApp(title:'Flutter Demo',theme:ThemeData(primarySwatch:Colors.blue),home:MyHomePage(title:'Peovider Demo')));}}

2.ListenableProvider 方式

1. 数据绑定

ListenableProvider({Key key, @required ValueBuilder<T> builder, Disposer<T> dispose, Widget child })

通过构造器绑定数据并进行监听,当从Widget Tree中删除时dispose要销毁;注意:构造器builder不可为空;

classMyAppextendsStatelessWidget{@overrideWidgetbuild(BuildContext context){returnListenableProvider<User>(builder:(_)=>User('Flutter',0),child:MaterialApp(title:'Flutter Demo',theme:ThemeData(primarySwatch:Colors.blue),home:MyHomePage(title:'Peovider Demo')));}}

ListenableProvider.value({Key key, @required T listenable, Widget child })

通过.value方式对数据进行监听listenable

classMyAppextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnListenableProvider<User>.value(listenable:User('Flutter',0),child:MaterialApp(title:'Flutter Demo',theme:ThemeData(primarySwatch:Colors.blue),home:MyHomePage(title:'Peovider Demo')));}}

2. 获取数据

Provider.of(context)方式

classProviderTextextendsStatelessWidget{@overrideWidgetbuild(BuildContext context){finaluser=Provider.of<User>(context);returnText('${user.getName}');}}

Consumer Widget构造器方式

classProviderTextextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnConsumer<User>(builder:(context,user,_){returnText(user.getName);});}}


小结

为方便理解,结合上一节的ChangeNotifierProvider,发现与ListenableProviderValueListenableProvider的使用基本相同;

classChangeNotifierProvider<TextendsChangeNotifier>extendsListenableProvider<T>implementsSingleChildCloneableWidget{}classChangeNotifierimplementsListenable{}classValueListenableProvider<T>extendsAdaptiveBuilderWidget<ValueListenable<T>,ValueNotifier<T>>implementsSingleChildCloneableWidget{}classValueNotifier<T>extendsChangeNotifierimplementsValueListenable<T>{}

分析源码:ChangeNotifierProvider继承自ListenableProvider且对应的ChangeNotifier继承自listenable;算是ListenableProvider的子类;ValueNotifier继承自ChangeNotifier也与ChangeNotifierProvider相似;

使用ChangeNotifierProviderValueListenableProvider绑定实体类时需要注意分别继承对应的ChangeNotifierValueNotifier

classUserwithChangeNotifier{}classPersonextendsValueNotifier<User>{}

无论使用那种.value方式,均建议在dispose中进行listener的关闭;

@overridevoiddispose(){stream.dispose();super.dispose();}



Provider分类

你也可以在 main 方法中通过下面这行代码来禁用此提示。 Provider.debugCheckInvalidValueType = null;

这是由于 Provider 只能提供恒定的数据,不能通知依赖它的子部件刷新。提示也说的很清楚了,假如你想使用一个会发生 change 的 Provider,请使用下面的 Provider。

ListenableProvider

ChangeNotifierProvider

ValueListenableProvider

StreamProvider

这几个 Provider 有什么异同。

先关注 ListenableProvider / ChangeNotifierProvider 这两个类。

ListenableProvider 提供(provide)的对象是继承了 Listenable 抽象类的子类。由于无法混入,所以通过继承来获得 Listenable 的能力,同时必须实现其 addListener / removeListener 方法,手动管理收听者。显然,这样太过复杂,我们通常都不需要这样做。

class ChangeNotifier implements Listenable

而混入了 ChangeNotifier 的类自动帮我们实现了听众管理,所以 ListenableProvider 同样也可以接收混入了 ChangeNotifier 的类。

ChangeNotifierProvider 则更为简单,它能够对子节点提供一个 继承 / 混入 / 实现 了 ChangeNotifier 的类。通常我们只需要在 Model 中 with ChangeNotifier ,然后在需要刷新状态的时候调用 notifyListeners 即可。

那么 ChangeNotifierProvider 和 ListenableProvider 究竟区别在哪呢,ChangeNotifierProvider 会在你需要的时候,自动调用其 _disposer 方法。

static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose();

我们可以在 Model 中重写 ChangeNotifier 的 dispose 方法,来释放其资源。

ValueListenableProvider。

ValueListenableProvider 用于提供实现了 继承 / 混入 / 实现 了 ValueListenable 的 Model。它实际上是专门用于处理只有一个单一变化数据的 ChangeNotifier。

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>

通过 ValueListenable 处理的类不再需要数据更新的时候调用 notifyListeners。

StreamProvider

StreamProvider 专门用作提供(provide)一条 Single Stream。我在这里仅对其核心属性进行讲解。

T initialData:你可以通过这个属性声明这条流的初始值。

ErrorBuilder<T> catchError:这个属性用来捕获流中的 error。在这条流 addError 了之后,你会能够通过 T Function(BuildContext context, Object error) 回调来处理这个异常数据。实际开发中它非常有用。

updateShouldNotify:和之前的回调一样,这里不再赘述。

除了这三个构造方法都有的属性以外,StreamProvider 还有三种不同的构造方法。

StreamProvider(...):默认构造方法用作创建一个 Stream 并收听它。

StreamProvider.controller(...):通过 builder 方式创建一个 StreamController<T>。并且在 StreamProvider 被移除时,自动释放 StreamController。

StreamProvider.value(...):监听一个已有的 Stream 并将其 value 提供给子孙节点。



注意事项

不要所有状态都放在全局

开发者为了图方便省事,经常把所有东西都放在顶层 MaterialApp 之上。严格区分你的全局数据与局部数据,资源不用了就要释放!否则将会严重影响你的应用 performance。

尽量在 Model 中使用私有变量“_”,减少耦合

控制你的刷新范围

在 Flutter 中,组合大于继承的特性随处可见。常见的 Widget 实际上都是由更小的 Widget 组合而成,直到基本组件为止。为了使我们的应用拥有更高的性能,控制 Widget 的刷新范围便显得至关重要。尽量使用 Consumer 来获取祖先 Model,以维持最小刷新范围。

Provider 是如何做到状态共享的

这个问题实际上得分两步。

获取顶层数据

实际上在祖先节点中共享数据这件事我们已经在之前的文章中接触过很多次了,都是通过系统的 InheritedWidget 进行实现的。Provider 也不例外,在所有 Provider 的 build 方法中,返回了一个 InheritedProvider。

class InheritedProvider<T> extends InheritedWidget

Flutter 通过在每个 Element 上维护一个 InheritedWidget 哈希表来向下传递 Element 树中的信息。通常情况下,多个 Element 引用相同的哈希表,并且该表仅在 Element 引入新的 InheritedWidget 时改变。时间复杂度为 O(1) 。

通知刷新

通知刷新这一步实际上就是使用了 Listener 模式。Model 中维护了一堆听众,然后 notifiedListener 通知刷新。

全局状态需要放在顶层 MaterialApp 之上,优先初始化,以便在 Navigator 以及 BuildContex控制全局状态

数据初始化

全局数据

当需要获取全局顶层数据,并需要做一些会产生额外结果的时候,main 函数是一个很好的选择。在 main 方法中创建 Model 并进行初始化的工作,这样就只会执行一次。

单页面

如果我们的数据只是在这个页面中需要使用,那么你有这两种方式可以选择。

StatefulWidget

在InitState()中使用 Provider.of<T>(context)是错误的。

/// If [listen] is `true` (default), later value changes will trigger a new

/// [State.build] to widgets, and [State.didChangeDependencies] for

/// [StatefulWidget].

源码中的注释解释了,如果这个 Provider.of<T>(context) listen 了的话,那么当 notifyListeners 的时候,就会触发 context 所对应的 State 的 [State.build] 和 [State.didChangeDependencies] 方法。也就是说,如果你使用了非 Provider 提供的数据,例如 ChangeNotifierProvider 这样会改变依赖的类,并且获取数据时 Provider.of<T>(context, listen: true) 选择 listen (默认就为 listen)的话,数据刷新时会重新运行 didChangeDependencies 和 build 两个方法。这样一来对 didChangeDependencies 也会产生副作用。假如在这里请求了数据,当数据到来的时候,又回触发下一次请求,最终无限请求下去。

这里除了副作用以外还有一点,假如数据改变是一个同步行为,例如这里的 counter.increment 这样的方法,在 didChangeDependencies 中调用的话,就会造成下面这个错误。



源码分析

Flutter 中的 Builder 模式

在 Provider 中,各种 Provider 的原始构造方法都有一个 builder 参数,这里一般就用 (_) => XXXModel() 就行了。感觉有点多次一举,为什么不能像 .value() 构造方法那样简洁呢。

实际上,Provider 为了帮我们管理 Model,使用到了 delegation pattern。

builder 声明的 ValueBuilder 最终被传入代理类 BuilderStateDelegate / SingleValueDelegate。 然后通过代理类才实现的 Model 生命周期管理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345