Flutter开发Provider详解

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 的共享。

注意:

由于flutter sdk版本的不同,有些demo跑步起来,可能需要调整参数名称才行

使用方式

ChangeNotifierProvider 方式

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

1. 绑定数据

ChangeNotifierProvider绑定数据有两种方式:

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

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

return ChangeNotifierProvider<User>(
        create: (BuildContext context) 
          print('ChangeNotifierProvider create');
          return User('赵云', 30);
        ,
        builder: (context, child) 
          return HomePage();
        ,
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(primarySwatch: Colors.blue,),
          home: HomePage(),
        )
    );

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

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

return ChangeNotifierProvider.value(
        value: User('赵云', 30),
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(primarySwatch: Colors.blue,),
          home: HomePage(),
        )
    );

2.ListenableProvider 方式

1. 数据绑定

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

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

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return ListenableProvider<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

return ListenableProvider.value(
        value: User('赵云', 30),
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(primarySwatch: Colors.blue,),
          home: HomePage(),
        )
    );

2. 获取数据

   Provider 需要在数据绑定的子 Widget 中进行获取;使用静态方法 Provider.of<T>(BuildContext context),此方法从 BuildContext 关联的 Widget Tree 中查找最近的相同类型的数据进行展示;没有则报异常;

Provider.of(context)方式

@override
  Widget build(BuildContext context) 
    
    User user = Provider.of<User>(context);
    
    print('Provider :  $user.name');

    return Scaffold(
      appBar: AppBar(title: const Text("一叶飘舟教学"),),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: widget._homeDataList.map((HomeData homeData) 
          return HomeListItem(homeData: homeData);
        ).toList(),
      ),
    );

Consumer Widget构造器方式

@override
  Widget build(BuildContext context) 
    
    return Consumer<User>(
        builder: (context, user, _) 
          return Text(user.name);
        
    );

小结

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

class ChangeNotifierProvider<T extends ChangeNotifier?>
    extends ListenableProvider<T> 
class ChangeNotifier implements Listenable 
class ValueListenableProvider<T> extends SingleChildStatelessWidget 
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> 

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

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

classUserwithChangeNotifierclassPersonextendsValueNotifier<User>

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

@override

void dispose()

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

provider新版本sdk发生了变化,已经不能很明显的看出InheritedProvider和InheritedWidget的关系:

class InheritedProvider<T> extends SingleChildStatelessWidget 
abstract class InheritedWidget extends ProxyWidget 

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 中调用的话,就会造成下面这个错误。

The following assertion was thrown while dispatching notifications for CounterModel:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<CounterModel> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. This exception is
flutter: allowed because the framework builds parent widgets before children, which means a dirty descendant
flutter: will always be built. Otherwise, the framework might not visit this widget during this build phase.

这里和 Flutter 的构建算法有关。简单来说,就是不能够在 State 的 build 期间调用 setState() 或者 markNeedsBuild(),在我们这里 didChangeDependence 的时候调用了此方法,导致出现这个错误。异步数据则会由于 event loop 的缘故不会立即执行。

首先 要保证初始化数据不能够产生副作用,我们需要找一个在 State 声明周期内一定只会运行一次的方法。initState 就是为此而生的。但是 initState 不是无法获取到 Inherit 吗。但是我们现在本身就在页面顶层啊,页面级别的 Model 就在顶层被创建,现在根本就不需要 Inherit。

class _HomeState extends State<Home> 
    final _myModel = MyModel();
      @override
  void initState() 
    super.initState();
    _myModel.init(); 
  

页面级别的 Model 数据都在页面顶层 Widget 创建并初始化即可。

 void initState() 
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((callback)
      Provider.of<CounterModel>(context).increment();
    );
  

我们通过 addPostFrameCallback 回调中在第一帧 build 结束时调用 increment 方法,这样就不会出现构建错误了。

provider 作者 Remi 给出了另外一种方式

This code is relatively unsafe. There's more than one reason for didChangeDependencies to be called.
You probably want something similar to:

MyCounter counter;
 
@override
void didChangeDependencies() 
  final counter = Provider.of<MyCounter>(context);
  if (conter != this.counter) 
    this.counter = counter;
    counter.increment();
  

This should trigger increment only once.

也就是说初始化数据之前判断一下这个数据是否已经存在。

cascade
你也可以在使用 dart 的级连语法 ..do() 直接在页面的 StatelessWidget 成员变量声明时进行初始化。

class FirstScreen extends StatelessWidget 
    CounterModel _counter = CounterModel()..increment();
    double _textSize = 48;
    ...


使用这种方式需要注意,当这个 StatelessWidget 重新运行 build 的时候,状态会丢失。这种情况在 TabBarView 中的子页面切换过程中就可能会出现。

性能问题

遵守其规范,做任何事情都考虑对性能的影响,要知道 Flutter 把更新算法可是优化到了 O(N)。
Provider 仅仅是对 InheritedWidget 的一个升级,不必担心引入 Provider 会对应用造成性能问题。

为什么选择 Provider

Provider 不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。
但是仅仅使用 Provider,Model 和 View 之间还是容易产生依赖。

只有通过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系,所以假如各位有组件化的需求,还需要另外处理。

不过对于大多数情况来说,Provider 足以优秀,它能够让你开发出简单、高性能、层次清晰 的应用。
 

源码分析

Flutter 中的 Builder 模式

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

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

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

class BuilderStateDelegate<T> extends ValueStateDelegate<T> 
  BuilderStateDelegate(this._builder, Disposer<T> dispose)
      : assert(_builder != null),
        _dispose = dispose;
 
  final ValueBuilder<T> _builder;
  final Disposer<T> _dispose;
 
  T _value;
  @override
  T get value => _value;
 
  @override
  void initDelegate() 
    super.initDelegate();
    _value = _builder(context);
  
 
  @override
  void didUpdateDelegate(BuilderStateDelegate<T> old) 
    super.didUpdateDelegate(old);
    _value = old.value;
  
 
  @override
  void dispose() 
    _dispose?.call(context, value);
    super.dispose();
  

以上是关于Flutter开发Provider详解的主要内容,如果未能解决你的问题,请参考以下文章

Flutter之跨组件共享状态Provider原理剖析

有时Provider在flutter中匿名注册后不会触发

Flutter状态管理-Provider的使用和源码解析

社区说|Flutter 主流状态管理框架 provider get 分析

Flutter 实现局部刷新 StreamBuilder 实例详解

Flutter 实现局部刷新 StreamBuilder 实例详解