当App的复杂性发展到一定程度,经常会出现一个页面中不同深度的子Widget需要共享访问同一个数据状态,甚至不同页面要共享同一个状态。这时我们就会想到InheritedWidget。InheritedWidget是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据。而Provider就是对InheritedWidget组件的上层封装,使其更易用,更易复用。
Provider的类型
为我们提供了多个类型便于不同的场景下使用:
名称 | 描述 |
Provider | 最基础的 provider 组成,接收一个任意值并暴露它。由于它接收任何类型,所以它并不会自动帮你对暴露的数据进行addListener,所以数据变化后,也不会自动触发相关的通知和UI更新。 |
ListenableProvider | 供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。 |
ChangeNotifierProvider | 为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose。 |
ValueListenableProvider | 监听 ValueListenable,并且只暴露出 ValueListenable.value。 |
StreamProvider | 监听流,并暴露出当前的最新值。 |
FutureProvider | 接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。 |
可以看到,Provider支持的共享数据类型涉及到多个类,在这里也梳理一下:
Listenable:抽象类。子类需实现addListener和removeListener。
ValueListenable:Listenable的子类,同样是个抽象类,增加了一个属性value,用于存放值。目的在于当这个值改变时,执行所有的listener回调。注意,它是个抽象类,所以并不能直接工作,需要其他类来实现。
ChangeNotifier:Listenable的一个实现,也就是给出了addListener和removeListener的具体实现,并且多提供了一个notifyListeners的方法,用于通知listener。但是它本身没有数据,所以如果你想保存数据,需要自己写一个类保存数据,混入ChangeNotifier接口拥有它的能力,然后编写代码来确定在何时通知所有的listener。
例如:
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
ValueNotifier:继承ChangeNotifier,并且实现了ValueListenable,也就是说它可以存放value,又不需要自己实现addListener、notifyListeners之类的方法。所以这个类我们会经常看到。这个类自己的实现里最主要的工作就是将value的变化和notifyListeners联系起来。
Provider的使用
Provider放置的位置一般是在相应的widget的外层,也就是数据状态的共享都是在该层widget内部进行。需要使用多个Provider的话,可以选择MultiProvider来完成放置。以下代码将所有的providers放置到了app的最外层:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
至于如何在代码中使用共享状态,使用 BuildContext
上的扩展属性。也可以直接使用Provider.of<T>(context)
来代替watch和read,因为他们就是对Provider.of<T>(context)
的封装,通过传入不同的参数值来告知Provider是否需要监听数据的变化:static T of<T>(BuildContext context, {bool listen = true})
。
context.watch<T>()
,widget 能够监听到T
类型的 provider 发生的改变。context.read<T>()
,直接返回T
,不会监听改变。context.select<T,R>(R cb(T value))
,允许 widget 只监听T
上的一部分内容的改变。
以实现一个简单的计数器为例,下面给出相关的实现:
// 实现一个基于ChangeNotifier的类,内部维护一个计数,当计数有变化时,同时通知所有监听者
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
// String _description = 'Counter';
// String get description => _description;
void increment() {
_count++;
notifyListeners();
}
}
// 显示计数器的次数
class Count extends StatelessWidget {
const Count({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
/// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headline4);
}
}
由于我们需要实时刷新计数器的数据,所以上面的Count类使用了context.watch。之后实现一个简单的页面显示计数器的数值,并通过一个按钮的点击来增加计数。由于按钮点击时,并不关心计数变化的值,所以使用context.read。需要注意,context.read不能在 StatelessWidget.build
和 State.build
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('You have pushed the button this many times:'),
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
什么是Consumer和Selector
从上面的例子可以看到,我们在Column中放了两个child,一个Text,以及一个Count实例。可以看到Count这个类非常简单,那么可不可以在使用Count()
的地方直接替换为Text('${context.watch<Counter>().count}')
呢?从最终的实验效果看,肯定是可以的。但是使用Count这个类将对计数器的值的变化的监听封装到自己的build方法中,会有一个重要的优点,即使用Count()
后,每次计数器变化时,前面的Text('You have pushed the button this many times:')
不会每次重新build,所以在一些需要性能优化的场景,把对共享状态的使用和监听封装到一个新的widget中,会很有用。
此外,这种写法还有一个好处,即当 widget 很难获取到 provider 所在层级以下的 BuildContext
时,可以通过封装,轻松拿到buildContext。来看一个例子:
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Foo(),
child: Text(Provider.of<Foo>(context).value),
);
}
在上面的例子中,通过context调用Provider.of会找不到Provider,因为这个context在ChangeNotifierProvider的层级之上。
Consumer其实就是类似Count的类,将对共享数据的监听和使用进行了封装,使用Consumer来重新实现上面的例子:
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Foo(),
child: Consumer<Foo>(
builder: (_, foo, __) => Text(foo.value),
),
);
}
总结一下Consumer,Consumer简单封装了[Provider.of],Consumer 有两个优点:
- 有时我们无法拿到合适的BuildContext,比如provider是在buildContext之下构建的,需要Consumer帮我们拿到它。
- Consumer可提升性能,避免不需要rebuild的Widget更新。
最后提一下,如果指向监听共享数据的部分变化,可以使用Selector,用法和Consumer类似,不过要指定具体想监听的是哪部分的变化:
Selector<Counter, String>(
builder: (_, value, __) => Text(value),
selector: (_, counter) => counter.description);