引入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();
}
}
其中有以下几个要点:
- 定义的class必须使用
with
关键字来混入ChangeNotifier
。 - 定义数据,get,以及修改接口。
- 在修改数据时,同时调用
notifyListeners()
来通知所有监听该数据的对象。
ChangeNotifier
的作用是管理所有监听者。当调用notifyListeners()
时,该类会通知所有监听者进行刷新。
初始化
根据使用的Provider构造函数不同,其初始化方式也不同。
常用命名构造函数
常用的初始化方式为XXXProvider<T>.value()
,有两个步骤:
- 创建共享Model对象。
- 调用
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()
。