provider状态管理

引入provider

打开pubspec.yaml,在dependecies下添加provider版本:

dependencies:
  provider: ^3.1.0

其中最新版本查看官方更新文档:

https://pub.dev/packages/provider#-changelog-tab-

创建数据Model

原理

从provider中取存储值时,会向上寻找最近存储的指定类型值。也就是说,对一个具体的Widget而言,从provider中每种Model类型只能取到一个对象,就是上级中最近的那个。
例如,有如下组件关系:widget1→widget2→widget3。其中widget1存入provider中1个int值1,widget2存入provider中1个int值2。

  • widget1从provider中取int值,会抛出异常。
  • widget2从provider中取int值,取到的值为1。
  • widget3从provider中取int值,取到的值为2。

这是因为:

  • widget1虽然向provider中存入了int值,但会从父节点向上查找,因此对widget1而言provider是找不到int值的,故而会抛出异常。
  • widget2从父节点向上查找,会找到widget1存入provider中的int值1。
  • widget3从父节点向上查找,会找到widget2存入provider中的int值2。

现在去掉widget2存入provider中的int值,则:

  • widget1从provider中取int值,会抛出异常。
  • widget2从provider中取int值,取到的值为1。
  • widget3从provider中取int值,取到的值为1。

widget1与widget2原理不变。对widget3而言,从父节点向上查找,会找到widget1存入provider中的int值1。

定义方式

Model的定义方式有几个要点,以网上常见的CounterModel为例:

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get value => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

其中有以下几个要点:

  1. 定义的class必须使用with关键字来混入ChangeNotifier
  2. 定义数据,get,以及修改接口。
  3. 在修改数据时,同时调用notifyListeners()来通知所有监听该数据的对象。

ChangeNotifier的作用是管理所有监听者。当调用notifyListeners()时,该类会通知所有监听者进行刷新。

初始化

根据使用的Provider构造函数不同,其初始化方式也不同。

常用命名构造函数

常用的初始化方式为XXXProvider<T>.value(),有两个步骤:

  1. 创建共享Model对象。
  2. 调用XXXProvider<T>.value()来包装组件,同时将Model对象添加到Provider中,而令子Widget可以方便地获取存储的Model。

XXXProvider<T>.value()的结构为:

XXXProvider<T>.value(
    value: model,
    child: widget组件
)

其中:

  • value传入定义的Model对象。
  • child传入原本要build的widget组件,也就是需要包装的对象。

例如,原本要创建的Widget结构为:

class ProviderTestMain extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyWidget();
  }
}

使用Provider包装后:

class ProviderTestMain extends StatelessWidget {
  CounterModel cm = CounterModel();

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CounterModel>.value(
      value: cm,
      child: MyWidget(),
    );
  }
}

这里使用了ChangeNotifierProvider来进行包装,这是因为希望CounterModel改变时,子组件的显示内容随之更改。
注意XXXProvider<T>.value()中的<T>是可省的。例如上面的:
ChangeNotifierProvider<CounterModel>.value()
实际等价于:
ChangeNotifierProvider.value()
不过考虑到可读性,建议还是不要省略。

默认构造函数

默认构造函数相对烦琐,故而使用较少。但对于某些场景是必须的:

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

相比于XXXProvider<T>.value()

  • 没有value属性。使用builder来设置value值。
  • 多了一个dispose属性。这是一个回调函数,当节点被移除时会触发。对于一些手动清理工作(例如清除缓存),可以放在这里。

builder需要返回一个value值。传入一个返回value的函数即可:

Provider(
    builder: (context) => myValue,
    ...
)

由于使用了Builder模式,必须要传入context。但实际上返回的value与context并没有关系。于是可以进行替换:

Provider(
    builder: (_) => myValue,
    ...
)

使用

Provider.of<T>方式

对于其所有的子Widget,可以通过Provider.of<CounterModel>(context)来直接获取Provider中存储的全局对象。

CounterModel cm = Provider.of<CounterModel>(context);
cm.increment();
print('result is ' + cm._count.toString());

然而,这种方式存在一个问题,那就是若Model对象更改,则所有使用该Model对象的widget都会被刷新,包括这些组件的子组件,以及这些组件所在的class。
于是,对于那些可能仅仅是调用一下Model的数据接口,而不需要根据Model内容显示的组件,即使Model对其UI显示没有影响,但Model更改依然会导致这些组件的刷新。

Consumer<T>方式(推荐)

Consumer方式的格式为:

Consumer<T1,T2,T3>(
  builder: (context, T1 t1, T2 t2, T3 t3, child) =>
    FloatingActionButton(
      onPressed: () => {
        t1.action();
        t2.action();
        t3.action();
      },
      child: child,
    ),
  child: Icon(Icons.add),
),

如上。

  • Consumer()可以一次性引入多个Model,在<>中使用逗号将Model类型隔开即可。上面引入了3个Model。
  • Consumer()中最多一次性引入6个Model。若需要引入更多的Model,需要自行定义。实际上,常规做法是将复杂数据放在一个Model中。
  • 使用builder方式,其参数为(context, T t, child)。其中只有t是需要指定类型的,其类型与Consumer()中的T一致。
  • 在builder的函数中传入多个T参数后,直接在逻辑中调用即可。
  • builder有个child参数,该参数传入的是外层Consumer()的child属性。这样做是为了规定无需刷新的属性。当t更改时,传入的child是不会刷新的,尽管这个child会作为显示内容的一部分。实际应用中,child往往用于构建与Model无关的部分。
  • 由于child属性的存在,会极大地缩小控件刷新范围。 同时Consumer会将刷新限制在所有Consumer范围内,而非整个class(每次改动Model会刷新所有使用该Model的Consumer,而非class)。因此推荐多使用Consumer。特别是在页面级别的Widget中。

存储多个类型数据

对于一个子组件而言,上级存入Provider中的对象,每种Model只能取到最近的一个。这也这意味着上级可以将不同类型的Model都分别存一个对象到Provider中,且子组件可以取到这些不同Model的对象。

嵌套方式

嵌套存储多种Model的方式为:

XXXProvider<T1>.value(
    value: t1,
    child: XXXProvider<T2>.value(
        value: t2,
        child: child: XXXProvider<T3>.value(
            value: t3,
            child: widget组件
        )
    )
)

如上,只要利用child属性无限迭代下去即可将每种Model的对象都存入Provider中。
以上面例子为例,本来只需要存储一个CounterModel对象cm,现在需要添加一个int对象myInt:

class ProviderTestMain extends StatelessWidget {
  CounterModel cm = CounterModel();
  int myInt = 1;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CounterModel>.value(
      value: cm,
      child: Provider<int>.value(
        value: myInt,
        child: MyWidget(),
      ),
    );
  }
}

如上,嵌套添加即可。

MultiProvider方式

上面嵌套方式层级过多,且比较繁琐。可以使用MultiProvider方式直接用数组添加:

class ProviderTestMain extends StatelessWidget {
  CounterModel cm = CounterModel();
  int myInt = 1;

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<CounterModel>.value(value: cm),
        Provider<int>.value(value: myInt)
      ],
      child: MyWidget(),
    );
  }
}

如上。将所有需要添加到provider中的对象直接放到数组中,封装一层即可添加全部的Model对象。结构更清晰。
该方式与嵌套方式的效果完全等价。

provider种类

  • Provider<T>.value():基本类型数据。
  • ListenableProvider<T>.value():用于复杂数据变化,可更改界面。需手动实现addListener/removeListener,自定义监听者的管理。
  • ChangeNotifierProvider<T>.value():用于复杂数据变化,可更改界面。当需要通知引用界面更改时,调用notifyListeners()。默认实现addListener/removeListener,不需要用户管理监听者。注意若Model中定义了dispose,在节点释放时会被调用。
  • ValueListenableProvider<T>.value():用于单一数据变化,可更改界面。数据变化即会导致引用的Widget更改,不再需要调用notifyListeners()
  • StreamProvider<T>.value():流。
  • FutureProvider<T>.value():延迟刷新,当用户定义的Future完成时才会通知组件进行刷新。

一般来说,常用的只有两种:Provider<T>.value()ChangeNotifierProvider<T>.value()

发布了215 篇原创文章 · 获赞 51 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/fyyyr/article/details/100884508
今日推荐