Flutter provider
Provider 是社区构建的状态管理工具,也是 Flutter Favorite 一员。基于 InheritedWidget 组件进行封装,使其更简单易用。它拥有着一套完整的解决方案,能够解决开发者们遇到的绝大多数情况,它能够让你开发出简单、高性能、层次清晰的应用。
- 简化的资源分配与处置
- 懒加载
- 创建新类时减少大量的模板代码
- 支持 DevTools
- 更通用的调用 InheritedWidget 的方式(参考 Provider.of/Consumer/Selector)
- 提升类的可扩展性,整体的监听架构时间复杂度以指数级增长(如 ChangeNotifier, 其复杂度为 O(N))
Provider 中提供了几种不同类型的 provider(暂且称为提供者)和几种使用模式。
Provider
最基础的 provider 组成,接收一个任意值并暴露它,但是并不会更新 UI。
- Provider.create(create, child)
新创建的对象模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
return Provider<Person>(
create: (ctx)=> Person(),
child: const MaterialApp(
home: ProviderDemo(),
),
);
class ProviderDemo extends StatelessWidget {
const ProviderDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer<Person>( /// 在程序任何地方都可以拿到person对象,读取数据
builder: (_,person,child){
return Text(person.name);
}
),
),
);
}
}
class Person {
String name = "新create的Provider";
}
- Provider.value(value, child)
使用 .value
是将已经存在的对象实例暴露出来,如果是新的模型对象,务必使用 create
1
2
3
4
Provider.value(
value: value, /// value是已经存在的模型对象
child: ...
)
在使用 provider 的 create 和 update 回调时,回调函数默认是延迟调用的。也就是说,变量被读取时,create 和 update 函数才会被调用。开发者也可以使用 lazy 参数来禁用这一行为:
1
2
3
4
Provider(
create: (_) => Something(),
lazy: false,
)
还有个 dispose 的回调 :
1
typedef Dispose<T> = void Function(BuildContext context, T value);
当 Provider 所在节点被移除的时候,它就会启动 Disposer<T>
,然后我们便可以在这里释放对应的资源。
ChangeNotifierProvider
ChangeNotifierProvider
配合 ChangeNotifier
一起使用来实现状态的管理与 Widget 的更新。其中 ChangeNotifier 是系统提供的,用来负责数据的变化通知。ChangeNotifierProvider 本质上其实就是 Widget,它作为父节点 Widget,可将数据共享给其所有子节点 Widget 使用或更新。
监听模型对象的变化,而且当数据改变时,它也会重建 Consumer,更新 UI。
- 类需要继承、混入 ChangeNotifier
- 调用了 notifyListeners()
它不会重复实例化模型,除非在个别场景下。如果该实例已经不会再被调用,ChangeNotifierProvider 也会自动调用模型的 dispose() 方法。
使用步骤:
- 创建混合或继承 ChangeNotifier 的 Model,用来实现数据更新的通知并监听数据的变化
- 创建 ChangeNotifierProvider,用来声明 Provider,实现跨组建的数据共享
- 接收共享数据
ChangeNotifier
它类似于一个 Observable,继承自 Listenable,内部维护了一个 ObserverList _listeners
,提供添加和删除 listener 的方法,有一个 notify 方法,用来通知所有的观察者(在 Provider 或者称为消费者 Consumer)。
ChangeNotifier 只有在需要动态更新时候,也就是 watch 的时候才需要使用
Provider 和 ChangeNotifierProvider
同步更新不代表同步更新 UI,也可能只是值更新了。是否同步更新 UI 取决了使用了哪一种依赖的 provider
- 使用最基础的 Provider 值已经改变了(通过热更新或 debug 可知),但是不会更新 UI
- 使用 ChangeNotifierProvider 更新值的同时会同步更新 UI
案例
案例 1
- 创建 Model
1
2
3
4
5
6
7
8
9
10
class ProviderViewModel with ChangeNotifier {
int _number = 0;
get number => _number;
void addNumber() {
_number++;
notifyListeners();
}
}
- 创建 ChangeNotifierProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ChangeNotifierProviderDemoPage extends StatelessWidget {
final _providerViewModel = ProviderViewModel();
ChangeNotifierProviderDemoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("ChangeNotifierProviderDemoPage Test "),
),
body: ChangeNotifierProvider.value(
value: _providerViewModel,
builder: (context, child) {
print("parent build");
return Column(
children: [
const Text("我是父节点"),
Text(
"Parent number is: ${Provider.of<ProviderViewModel>(context, listen: true).number}"),
const ChildA(),
// const ChildB(),
// const ChildC()
],
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_providerViewModel.addNumber();
}, //使用context.read不会调用rebuild
),
);
}
}
ChangeNotifierProvider 将父布局包裹,在父或子节点 ChildA 通过 Provider.of<T>(BuildContext context, {bool listen = true})
进行数据操作,可同步更新父与子的数据与 UI,listen 为 false 的时候,数据变更 UI 不会变
- ChildA 接收数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ChildA extends StatelessWidget {
const ChildA({super.key});
@override
Widget build(BuildContext context) {
print("childA build");
return Container(
width: double.infinity,
color: Colors.amberAccent,
child: Column(
children: [
Text(
"Child A number: ${Provider.of<ProviderViewModel>(context).number}"),
MaterialButton(
color: Colors.white,
onPressed: () {
Provider.of<ProviderViewModel>(context, listen: false)
.addNumber();
},
child: const Text("Add Number"))
],
),
);
}
}
效果:
上面操作与读取时使用的是 Provider.of<T>(BuildContext context, {bool listen = true})
的方式,为了可以更明确对于 Provider 的操作,我们可将它替换为 context.watch<>()
和 context.read<>()
方式;context.watch<>()
和 context.read<>()
方法其实都是调用 Provider.of<T>(BuildContext context, {bool listen = true})
来实现的:
1
2
3
4
5
6
T watch<T>() {
return Provider.of<T>(this);
}
T read<T>() {
return Provider.of<T>(this, listen: false);
}
- ChildB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ChildB extends StatelessWidget {
const ChildB({super.key});
@override
Widget build(BuildContext context) {
print("childB build");
return Container(
width: double.infinity,
color: Colors.red,
child: Column(
children: [
const Text("我是子节点"),
Text("Child B number: ${context.watch<ProviderViewModel>().number}"),
MaterialButton(
color: Colors.white,
onPressed: () {
context.read<ProviderViewModel>().addNumber();
},
child: const Text("Add Number"))
],
),
);
}
}
每点击一次父 Widget 右下角的加号或子 Widget 的 Add Number 按钮,我们看一下 Log 打印的结果:
childA build childB build
我们会发现每一次的操作,都会导致 ChildA 与 ChildB 整体重新 build。但实际上从代码中我们可知,在 ChildA 和 ChildB 中,只有以下的 Text() 会监听 ProviderViewModel 的数据更新:
1
2
3
4
5
//ChildA:
Text("Child A number: ${Provider.of<ProviderViewModel>(context).number}")
//ChildB:
Text("Child B number: ${context.watch<ProviderViewModel>().number}")
怎么实现实现局部的更新,Flutter 提供了 Consumer<>()
来进行支持。下面我们来看一下 Consumer<>() 的用法:
- ChildC
由于我们只希望 Text() 来监听 ProviderViewModel 的数据更新,我们用 Consumer<>() 包裹住 Text()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ChildC extends StatelessWidget {
const ChildC({super.key});
@override
Widget build(BuildContext context) {
print("not const childC build");
return Container(
width: double.infinity,
color: Colors.blue,
child: Column(
children: [
const Text("我是子节点"),
Consumer<ProviderViewModel>(builder: (context, value, child) {
print("ChildC Consumer builder");
return Text("Child C number: ${value.number}");
}),
MaterialButton(
color: Colors.white,
onPressed: () {
context.read<ProviderViewModel>().addNumber();
},
child: const Text("Add Number"))
],
),
);
}
}
再看 log 输出:
childA build childB build ChildC Consumer builder
引用时要加个 const,const ChildC(),不然 ChildC 还是会 build
从 Log 中我们可以得知,ChildC 并没有被 rebuild,而是由 Consumer 调用内部的 builder 来实现局部更新的
ProxyProvider
我们日常开发中会遇到一种模型嵌套另一种模型、或一种模型的参数用到另一种模型的值、或是需要几种模型的值组合成一个新的模型的情况,在这种情况下,就可以使用 ProxyProvider 。它能够将多个 provider 的值聚合为一个新对象,将结果传递给 Provider(注意是 Provider 而不是 ChangeNotifierProvider),这个新对象会在其依赖的任意一个 provider 更新后同步更新值。
1
ProxyProvider<T, R> /// R依赖T或用到T的值,T发生改变会通知R
- Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person extends ChangeNotifier {
String name = "小虎牙";
void changName() {
name = "更新的小虎牙";
notifyListeners();
}
}
// EatModel 需要用到 Person 的 name 值,才知道到底是谁在吃饭
class EatModel {
EatModel({required this.name});
final String name;
String get whoEat => "$name正在吃饭";
}
- ProxyProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class ProxyProviderDemo extends StatelessWidget {
const ProxyProviderDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text("ProxyProvider Demo"),
),
body: MultiProvider(
providers: [
ChangeNotifierProvider<Person>(
create: (ctx) => Person(),
),
ProxyProvider<Person, EatModel>(
update: (ctx, person, eatModel) => EatModel(name: person.name),
)
],
child: const MaterialApp(
home: ProxyProviderStateless(),
),
)),
);
}
}
class ProxyProviderStateless extends StatelessWidget {
const ProxyProviderStateless({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Consumer<EatModel>(builder: (context, eatModel, child) {
return Text(eatModel.whoEat ?? "");
}),
Consumer<Person>(
// 拿到person对象,调用方法
builder: (context, person, child) {
return ElevatedButton(
/// 点击按钮更新Person的name,eatModel.whoEat会同步更新
onPressed: () => person.changName(),
child: const Text("点击修改"),
);
},
),
],
);
}
}
**结果: ** 页面显示小虎牙正在吃饭;点击按钮后:内容更新为更新的小虎牙正在吃饭。
ProxyProvider 还有其他不同的形式:ProxyProvider
、ProxyProvider2
、ProxyProvider3…ProxyProvider6
。类名后的数字是 ProxyProvider 依赖的 provider 的数量。一般很难用到 6 个或以上的模型糅合一个新的模型。
ChangeNotifierProxyProvider
MultiProvder
在实际开发中,程序肯定存在多种 Provider,如果我们还是用嵌套的方式来解决,但是这样无疑是混乱的,可读性级差。
1
2
3
4
5
6
7
8
9
10
return ProvderA(
child: ProvderB(
child: ProvderC(
child: ProvderD(
...
child: MaterialApp()
)
)
)
)
为了解决这样的嵌套地狱,MultiProvder 应运而生。它实际上是多种 Provider 的集合,且仅仅是改变了代码的书写方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class MultiProviderPage extends StatelessWidget {
const MultiProviderPage({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text("MultiProviderPage Demo"),
),
body: MultiProvider(
providers: [
ChangeNotifierProvider<Person1>(
create: (ctx) => Person1(),
),
ChangeNotifierProvider<Person2>(
create: (ctx) => Person2(),
)
],
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Consumer<Person1>(
builder: (ctx, person1, child) => Text(person1.name)),
Consumer<Person2>(
builder: (_, person2, child) => Text(person2.name)),
Consumer<Person1>(
/// 拿到person1对象,调用方法,也可以修改person2
builder: (ctx, person1, child) {
return ElevatedButton(
onPressed: () {
person1.changName();
},
child: const Text("点击修改"),
);
},
),
],
),
)));
}
}
class Person1 with ChangeNotifier {
String name = "MultiProvider --- 1";
void changName() {
name = "更新MultiProvider --- 1";
notifyListeners();
}
}
class Person2 with ChangeNotifier {
String name = "MultiProvider --- 2";
void changName(String desc) {
name = "更新MultiProvider --- 2 $desc";
notifyListeners();
}
}