Flutter状态管理

1.初识状态

声明式UI-中文社区

Flutter是声明式编程,通过构建用户界面来反应状态的变化,如上图:

  • f对应着build()函数;
  • state是构建应用界面时所需的状态,即数据。

Flutter中通过修改状态,触发界面的重绘,而非直接修改界面对象本身。

1.1.原生-命令式

原生开发中,点击按钮并修改一个组件的属性从而改变其状态时,通常情况下,我们会在按钮回调中找到此组件的对象,直接修改其对应属性的值,由runloop在合适的时机会重新绘制。代码如下:

1
2
3
func onClick() {
myView.backgroundColor = UIColor.white;
}

当然,也可以结合RX以响应式编程实现此功能,这里不作展开~

1.2.Flutter-响应式

同样的功能,在 Flutter 这种响应式框架中,一般是在按钮组件的回调中修改某个变量,再以某种方式告诉框架:我已经修改了状态值,你去重绘对应的组件树从而响应状态的变化。以计算器为例:

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
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; // 1.定义变量

void _incrementCounter() {
setState(() {
_counter++; // 3.修改状态
});
}

@override
Widget build(BuildContext context) { // 4.重绘以响应状态变化
return Scaffold(
//省略。。。
body: Center(
child: Column(
children: <Widget>[
Text('$_counter'), // 5.获取最新_counter构建新Text
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // 2.执行回调函数
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

修改状态的流程是这样:

  1. 按钮回调函数中对状态_counter做自增运算;
  2. 通过setState告诉框架:我已修改,你去重新绘制整个组件树;
  3. 重绘工作交给了build(context)函数;
  4. build中新建所有子树,其中子组件Text使用最新_counter展示数值。

2.状态分类

Flutter 中状态可分两种:短时状态全局状态

2.1.短时状态

也称局部状态或临时状态,独立于某个组件中,只影响该组件自己的行为。

例子:

  • 前文计时器案例中的数值_counter
  • 一个 PageView 组件中的当前页面_index
  • 一个复杂动画中当前进度;
  • 一个 BottomNavigationBar 中当前被选中的 tab;

因为其他组件不需要访问此状态,也就无需状态管理架构去管理这种状态,你需要用的只是一个StatefulWidget

2.2.全局状态

在多组件甚至整个应用之间共享的、在用户会话期间保留的状态,就是全局状态或称共享状态。

例子:

  • 用户偏好设置;
  • 登录信息;
  • 电商应用中的购物车;

通常,我们可借助不同的技术实现跨组件共享状态,如传参、回调、控制器,也可以使用Flutter框架内置的InheritedWidgetChangeNotifierStreamBuilder等,还有一些优秀的三方库flutter_blocProvider等。

3.提升状态

原生开发中,兄弟组件通常以成员变量或属性的形式存在于共同的父组件中,想与对方通信(如传值或修改状态)时,一般是由前者在父组件中提供一个回调函数,在回调函数中获取对方组件的对象,通过对象.setxx的方式修改对方的属性值,或者调用对方提供的相关接口来处理具体业务。

而声明式框架中,通过这样的方式实现组件之间的通信是没有必要的。因为任何修改对方属性状态的行为,都相当于修改了对方的配置,会引起对方组件的重构,即对方组件会创建新的实例,以对象.setxx形式的修改就没有意义了,直接修改对方的 state 即可。

在声明式框架里,组件之间一般只能由上而下地传递数据,兄弟组件之间无法直接通信。Flutter 框架采用了 Facebook 在 React 中提出的Lift State Up理念:将需要传递的数据从子组件移到某个共同的父组件,在那里修改它,再将它传递给其他子组件。

示例:购物车

提升状态-中文社区

商品目录购物车两组件之间传递数据的思路:

  1. 两个组件之间需要共享CART(已加购商品)这个State
  2. 提升State到必要的高度,直到两组件都能读取到它;
  3. 最近最合适的节点是两者共同的父组件MyApp
  4. MyApp保存State对象,并分发给商品目录与购物车组件;
  5. 商品目录中通过回调等方式在MyApp中修改State
  6. 进入购物车页面时以入参形式接收MyApp中的State

代码实现1:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
void main() {
runApp(DakMyApp());
}

class DakMyApp extends StatelessWidget {
final _cart = DakCart(); //共同父组件中共享状态
DakMyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print('DakMyApp build');
return MaterialApp(
home: DakCatelogPage(
cart: _cart, // 以参数形式传递数据
),
);
}
}

// 购物车model
class DakCart {
// 已加购商品列表
final List<DakItem> _items = [];
List get items => _items;
//商品总价
int get totalPrice => _items.length * 42;

// 增加商品
void add(DakItem item) {
_items.add(item);
}

// 删除商品
void delete(DakItem item) {
int index = _items.indexOf(item);
_items.removeAt(index);
}
}

// 商品model
class DakItem {
//商品名
String name;
//是否已加购
bool selected;

DakItem({
required this.name,
required this.selected,
});
}

// 商品目录页面
class DakCatelogPage extends StatefulWidget {
final DakCart cart; // 传入共享的状态
const DakCatelogPage({
Key? key,
required this.cart,
}) : super(key: key);

@override
State<DakCatelogPage> createState() => _DakCatelogPageState();
}

class _DakCatelogPageState extends State<DakCatelogPage> {
static final items = [
'Apple',
'Banana',
'Cherry',
'Damson',
'Grape',
'Haw',
'Kiwifruit',
'Lemon',
'Mango',
'Orange'
].map((e) => DakItem(name: e, selected: false)).toList(); //模拟商品列表

@override
Widget build(BuildContext context) {
print('DakCatelogPage build');
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.yellow,
title: const Text(
'Catelog',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
actions: [
TextButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DakCartPage(provider: widget.cart),
),
);
},
icon: const Icon(
Icons.shopping_cart,
color: Colors.black,
),
label: DakCartCounter(provider: widget.cart),
),
],
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return DakListCell(
aItem: items[index],
callback: (item) {
// callback 处理状态变化
item.selected = !item.selected;
item.selected ? widget.cart.add(item) : widget.cart.delete(item);
setState(() {}); //刷新AppBar中的商品数量
},
);
},
),
);
}
}

//商品总数
class DakCartCounter extends StatelessWidget {
final DakCart provider;
const DakCartCounter({
Key? key,
required this.provider,
}) : super(key: key);

@override
Widget build(BuildContext context) {
print('DakCartCounter build');
return Text(
'共${provider.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
}
}

// 定义cell中按钮点击的回调函数
typedef DakCallback = Function(DakItem);

// Row cell
class DakListCell extends StatefulWidget {
final DakItem aItem;
final DakCallback callback;
const DakListCell({Key? key, required this.aItem, required this.callback})
: super(key: key);

@override
State<DakListCell> createState() => _DakListCellState();
}

class _DakListCellState extends State<DakListCell> {
_updateState() {
setState(() {
//item.selected = !item.selected; // Cell内部处理
widget.callback(widget.aItem); //执行回调,在父组件中处理数据逻辑
});
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
color: Colors.white,
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 50,
height: 50,
color: Colors.yellow,
),
Text(
widget.aItem.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
Builder(
builder: (context) {
return widget.aItem.selected
? IconButton(
onPressed: _updateState,
icon: const Icon(Icons.check),
)
: ElevatedButton(
onPressed: _updateState,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white),
elevation: MaterialStateProperty.all(0),
),
child: const Text(
'ADD',
style: TextStyle(color: Colors.black),
),
);
},
),
],
),
);
}
}

// 购物车页面
class DakCartPage extends StatelessWidget {
final DakCart provider; // 传入共享的状态
const DakCartPage({Key? key, required this.provider}) : super(key: key);

@override
Widget build(BuildContext context) {
print('DakCartPage build');
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.yellow,
title: const Text(
'Cart',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
body: Container(
color: Colors.yellow,
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: provider.items.length,
itemBuilder: (context, index) {
final item = provider.items[index];
return Container(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text(
'· ${item.name}',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
},
),
),
Container(
height: 2,
color: Colors.black,
),
Container(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'\$ ${provider.totalPrice}',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 50,
),
ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white),
),
child: const Text(
'BUY',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
),
),
],
),
),
);
}
}

示例中,

  • DakCatelogPage是商品目录页;
  • DakCartPage是购物车页;
  • DakMyApp为商品目录与购物车的共同父组件;
  • DakCart为已加购商品的实体类;

在商品目录页中选择或删除商品后,保存已加购商品信息到DakCart中;

为了让购物车页面获取已加购商品信息,状态控制类DakCart被提升到DakMyApp中。在商品目录与购物车页面初始化时,父组件DakMyApp以入参形式将此状态对象传递给二者。

注:此示例不是最优代码实现范例,还需要考虑局部刷新等问题~

4.读写状态

提升后的状态在父组件中,AB作为兄弟组件相互独立,访问或修改对方的状态需要以下方式:

  • 对方提供的回调函数;
  • 对方提供的处理状态的控制器;
  • 框架内置的传递状态的组件;
  • 社区提供的一些优秀三方库。

4.1.回调

  1. A组件访问B组件的状态:

将B的状态提升到AB共同的父组件中,在父组件build并创建A组件时,以参数形式将状态传给A;

  1. A组件修改B组件的状态:

将B的状态提升到AB共同的父组件中,A定义回调函数并由父组件在初始化A时传入;

在位于父组件中的回调函数中修改B组件的状态。

代码实现2:

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
// 父组件
class _MyHomePageState extends State<MyHomePage> {
int _count = 0; // 状态 在共同的父组件MyHome中

// 回调函数 修改状态
void _increment() {
setState(() {
_count++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: <Widget>[
// 组件A
DakUpdater(
count: _count, //访问外部状态
callback: _increment, // 传入回调函数,以便在父组件中修改外部状态_count
),
// 组件B
Text('$_count'), // 使用状态_count
],
),
),
);
}
}

// 组件A
class DakUpdater extends StatelessWidget {
final int count; //传入外部状态
final void Function() callback; //给外部用的回调函数

const DakUpdater({Key? key,required this.count,required this.callback}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
width: 100,
margin: const EdgeInsets.only(top: 20),
color: Colors.green,
child: Column(
children: [
Text(
'$count',
style: const TextStyle(color: Colors.white),
),
ElevatedButton(
onPressed: callback, // 修改外部状态
child: const Text('+1+'),
),
],
),
);
}
}

示例中DakUpdater在创建时以参数(值拷贝)形式访问了外部状态_count

DakUpdater组件中ElevatedButton按钮点击之后,执行外部传入的回调函数,在父组件中修改了_count状态值并在重新 build 时再次传给DakUpdater展示最新值。

4.2.控制器

对于内部状态比较复杂的组件,可将修改状态的业务封装成控制器,再将控制器对象提升到父组件中供外部调用。

很多Flutter框架内置组件都使用了这种方式,对外提供控制器以修改组件内部状态。

A组件想访问或修改B组件的状态,一般是由B组件提供一个controller处理自身状态变化的业务,再将此控制器提升到AB组件的共同父组件中,在那里调用控制器的接口触发状态变化。

代码实现3:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class DakBoxController {
Color color = Colors.pink;
DakBoxController({Key? key});
// 提供接口 修改状态
changeColor(Color value) {
color = value;
}
}

// 自定义的组件,主要是为了看颜色变化
class DakBox extends StatelessWidget {
final DakBoxController controller;
const DakBox({
Key? key,
required this.controller,
}) : super(key: key);

@override
Widget build(BuildContext context) {
print("DakBox rebuild"); // 打印日志
return Container(
height: 50,
width: 50,
color: controller.color,
); // 使用controller中的状态
}
}

//省略。。

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
// 1.框架内置的控制器
final _editController = TextEditingController(text: 'Hello world');
final _listController = ScrollController();
// 我们自定义的控制器
final _boxController = DakBoxController();

void _incrementCounter() {
// 3.修改状态
setState(() {
_counter++;
_boxController.changeColor(Colors.green); // 改颜色
_editController.clear(); // 清空文本
_listController.animateTo(100,
duration: const Duration(microseconds: 300), curve: Curves.bounceIn,); //滑动位置
});
}

@override
Widget build(BuildContext context) {
// 4.重绘以响应状态变化
return Scaffold(
//省略。。
body: Center(
child: Column(
children: <Widget>[
//计算器数值
Text('$_counter'),
//自定义控件
DakBox(controller: _boxController),//使用自定义的控制器
//输入框
TextField(controller: _editController), //使用内置控制器
//ListView
SizedBox(
height: 200,
child: ListView.builder(
controller: _listController, //使用内置控制器
itemCount: 100,
itemBuilder: (context, index) {
return Text('$index');
},
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // 2.执行回调函数
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

示例中,视图中间我们放置了数值Text、自定义的DakBox、文本输入框、列表4个组件,点击底部按钮修改它们的状态。其中数值Text的状态是直接传值,DakBox、TextField、ListView都是由控制器处理具体状态变化逻辑。可以看出,控制器实际上只是对修改状态的逻辑做了一层封装,外部组件要修改本组件内部状态时使用接口即可,属于命令式编程,本质上与Callback方式差不多。

5.InheritedWidget

理论上,访问状态使用传参,修改状态使用回调函数或控制器,通过这种方式已经可以实现几乎所有场景下的状态管理了。但现实项目中通常会有很多组件,且组件之间的树形关系可能会非常复杂,在使用状态提升后,如果只是依赖上述最简单的传参的方式,那么每个组件在构造函数中可能需要传入大量的参数才能将顶层的状态一层层传递到后续节点中。

那么有没有什么方法,能让底部的组件直接访问到被我们提升到顶部的状态呢?Flutter框架给出的方案是继承式组件InheritedWidget

1
2
3
4
5
6
7
/// Base class for widgets that efficiently propagate information down the tree.
///
/// To obtain the nearest instance of a particular type of inherited widget from
/// a build context, use [BuildContext.dependOnInheritedWidgetOfExactType].
///
/// Inherited widgets, when referenced in this way, will cause the consumer to
/// rebuild when the inherited widget itself changes state.

InheritedWidget也是一个组件,用于在组件树中高效的往下传递信息。

实际上这种机制的案例很常见:

  • Theme.of(context).primaryColor;
  • MediaQuery.of(context).size;
  • Navigator.of(context);

这些全局共享的状态,被提升到了整个应用组件树的最顶层,下面任意节点都能方便的访问到。

5.1.示例

我们使用InheritedWidget改造购物车案例中已加购商品的传值方式:

代码实现4:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
//注:此示例不是最优代码实现范例,还需要考虑局部刷新等问题~
void main() {
runApp(const DakMyApp());
}

class DakMyApp extends StatefulWidget {
const DakMyApp({Key? key}) : super(key: key);
@override
State<DakMyApp> createState() => _DakMyAppState();
}

class _DakMyAppState extends State<DakMyApp> {
//购物车实体在顶层
var cart = DakCart(items: []);
@override
Widget build(BuildContext context) {
print('DakMyAPP build');
return DakCartInheritedWidget(
cart: cart,
child: MaterialApp(
home: DakCatelogPage(
callback: (item) {
//重绘
setState(() {
if (item != null) {
final newItems =
item.selected ? cart.add(item) : cart.delete(item);
cart = DakCart(items: newItems);
} else {
print('单纯setState,未更新数据');
}
});
},
),
),
);
}
}

// 传递数据组件
class DakCartInheritedWidget extends InheritedWidget {
//状态
final DakCart cart;

const DakCartInheritedWidget({
Key? key,
required this.cart,
required Widget child, //必须传入child
}) : super(
key: key,
child: child,
);

// 便捷获取共享对象
static DakCartInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DakCartInheritedWidget>();
}

// 重写
@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
final update = oldWidget.cart != cart;
print('shoudl update? $update');
return update;
}
}

// 购物车Model
class DakCart {
// 已加购商品
List<DakItem> items = [];
// 商品总价
int get totalPrice => items.length * 42;
DakCart({required this.items});
//加入购物车
List<DakItem> add(DakItem item) {
items.add(item);
return items;
}

//移出购物车
List<DakItem> delete(DakItem item) {
int index = items.indexOf(item);
items.removeAt(index);
return items;
}

// 重写==操作符,用于后续判断购物车是否发生了变化
@override
bool operator ==(Object other) {
if (other is! DakCart) {
return false;
}
if (!identical(this, other)) {
return false;
}
bool same =
listEquals(items, other.items) && (other.totalPrice == totalPrice);
return same;
}

@override
int get hashCode => Object.hashAll([items, totalPrice]);
}

// 商品Model
class DakItem {
String name;
bool selected;

DakItem({
required this.name,
required this.selected,
});
}

// 商品列表页面
class DakCatelogPage extends StatelessWidget {
final DakCallback callback;
const DakCatelogPage({
Key? key,
required this.callback,
}) : super(key: key);
static final items = [
'Apple',
'Banana',
'Cherry',
'Damson',
'Grape',
'Haw',
'Kiwifruit',
'Lemon',
'Mango',
'Orange'
].map((e) => DakItem(name: e, selected: false)).toList();

@override
Widget build(BuildContext context) {
print('DakCatelogPage build');
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.yellow,
centerTitle: true,
title: const Text(
'Catelog',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
actions: [
TextButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DakCartPage(),
),
);
},
icon: const Icon(
Icons.shopping_cart,
color: Colors.black,
),
label: const DakCartCounter(),
),
],
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return DakListCell(
item: items[index],
callback: (item) {
callback(item); // 将已修改状态的商品回调给最顶层的DakCartInheritedWidget组件
},
);
},
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.yellow,
onPressed: () {
callback(null);
},
child: const Icon(
Icons.refresh,
color: Colors.black,
),
),
);
}
}

//商品总数
class DakCartCounter extends StatefulWidget {
const DakCartCounter({Key? key}) : super(key: key);

@override
State<DakCartCounter> createState() => _DakCartCounterState();
}

class _DakCartCounterState extends State<DakCartCounter> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('DakCartCounter didChangeDependencies');
}

@override
Widget build(BuildContext context) {
print('DakCartCounter build');
var provider = DakCartInheritedWidget.of(context); // 这里直接读取共享的数据 不再靠参数传递
return Text(
'共${provider?.cart.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
}
}

// Cell点击回调
typedef DakCallback = void Function(DakItem?);

// 商品cell
class DakListCell extends StatefulWidget {
final DakItem item;
final DakCallback callback;
const DakListCell({
Key? key,
required this.item,
required this.callback,
}) : super(key: key);

@override
State<DakListCell> createState() => _DakListCellState();
}

class _DakListCellState extends State<DakListCell> {
_updateState() {
setState(() {
widget.item.selected = !widget.item.selected;
});
widget.callback(widget.item); // 将修改状态后的item回调给商品列表组件
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
color: Colors.white,
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 50,
height: 50,
color: Colors.yellow,
),
Text(
widget.item.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
Builder(
builder: (context) {
return widget.item.selected
? IconButton(
// 已选中
onPressed: _updateState,
icon: const Icon(Icons.check),
)
: ElevatedButton(
//未选中
onPressed: _updateState,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white),
elevation: MaterialStateProperty.all(0),
),
child: const Text(
'ADD',
style: TextStyle(color: Colors.black),
),
);
},
),
],
),
);
}
}

// 购物车页面
class DakCartPage extends StatelessWidget {
const DakCartPage({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
print('DakCartPage build');
var provider = DakCartInheritedWidget.of(context); // 这里直接读取共享的数据 不再靠参数传递
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.yellow,
title: const Text(
'Cart',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
body: Container(
color: Colors.yellow,
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: provider?.cart.items.length,
itemBuilder: (context, index) {
final item = provider?.cart.items[index];
return Container(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text(
'· ${item?.name}',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
},
),
),
Container(
height: 2,
color: Colors.black,
),
Container(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'\$ ${provider?.cart.totalPrice}',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 50,
),
ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white),
),
child: const Text(
'BUY',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
),
),
],
),
),
);
}
}

示例中,我们增加了一个DakCartInheritedWidget类,它继承自InheritedWidget并重写了必要的方法,同时提供了一个全局便捷访问此对象的of()方法。

之前的方案中,已加购商品items保存在 DakMyApp 中,通过传参的方式传给 DakCatelogPage 与 DakCartPage;现在已加购商品状态仍保存在 DakMyApp 中,但不需要手动传递了,DakMyApp 的任意子节点内都可以直接通过以下两种方式获取到这个状态:

1
2
3
4
// 方式1
var provider = context.dependOnInheritedWidgetOfExactType<DakCartInheritedWidget>();
// 方式2:便捷方法
var provider = DakCartInheritedWidget.of(context);

后者是对前者的封装,它们会在组件树上返回当前节点之前且离当前节点【最近的】一个【指定类型的】共享组件,即 DakMyApp 中创建的DakCartInheritedWidget,接着读取其中的状态即可。

通过InheritedWidget组件,我们将状态提升到合适的顶部某一组件中,在其下面的组件可随时通过指定方法获取这一状态,从而省去了传参的麻烦~

5.2.机制原理

InheritedWidget是基于观察者模式实现的:

  • 注册:利用 BuildContext 注册监听;
  • 读取:通过 BuildContext 读取数据;
  • 通知:InheritedWidget发生改变,通知监听者重绘;
i.注册

使用InheritedWidget时,注册实际上是伴随着读取一起进行的,通过下面这种方式:

1
2
3
4
// 方式1
static DakCartInheritedWidget? of(BuildContext context) {
context.dependOnInheritedWidgetOfExactType<SomeInheritedWidget>();
}

与之对应的还有另一种读取方式:

1
2
3
4
// 方式2
static DakCartInheritedWidget? of(BuildContext context) {
context.getElementForInheritedWidgetOfExactType<SomeInheritedWidget>()?.widget as SomeInheritedWidget;
}

方式2是真正的读取,没有额外的注册监听操作。

dependOnxxgetxx都是 Element 根类中的成员方法,这两种方式都能读取到共享的数据。

但当InheritedWidget中的状态发生改变时,其下层依赖者能否感知到这种变化,在这二者上就有很大的不同了。

这是二者不同的实现逻辑导致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
//差别在以下部分
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

区别在于:

  • get直接返回一个InheritedElement,即_inheritedWidgets字典中与T类型对应的那个;
  • dependOn返回一个Widget对象并继续调用了dependOnInheritedElement()方法:
1
2
3
4
5
6
7
8
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}

方法中,查找祖先InheritedElement节点中的_dependencies字典,并将自己this加入进去。

  • this是调用dependOn方法的BuildContext,实质是BuildContext对应的Element
  • ancestor是指定InheritedWidget子类型对应的InheritedElement

继续跟踪updateDependencies方法进入InheritFromElement

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
class InheritedElement extends ProxyElement {
/// Creates an element that uses the given widget as its configuration.
InheritedElement(InheritedWidget widget) : super(widget);

@override
InheritedWidget get widget => super.widget as InheritedWidget;

final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
//省略...
@protected
void updateDependencies(Element dependent, Object? aspect) {
setDependencies(dependent, null);
}

@protected
void setDependencies(Element dependent, Object? value) {
_dependents[dependent] = value;
}

@protected
Object? getDependencies(Element dependent) {
return _dependents[dependent];
}

@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}

@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}

@override
void notifyClients(InheritedWidget oldWidget) {
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (final Element dependent in _dependents.keys) {
assert(() {
// check that it really is our descendant
Element? ancestor = dependent._parent;
while (ancestor != this && ancestor != null)
ancestor = ancestor._parent;
return ancestor == this;
}());
// check that it really depends on us
assert(dependent._dependencies!.contains(this));
notifyDependent(oldWidget, dependent);
}
}
}

_dependents是一个Map,存放着所有的依赖者,只有在这个字典中的依赖者,才有机会在InheritedWidget发生变化时获得更新通知。dependOn最终调用了setDependencies()将调用者context对应的Element加入_dependents字典中。即当依赖者使用of()方法中的dependOn函数获取自定义InheritedWidget时,会将自己加到依赖者集合中,而get则不会添加依赖和监听。

以购物车为例,通过dependOn读取数据,就是以DakCartInheritedWidget的类型为键,找到其对应的 InheritedElement 祖先节点对象,再把DakCartCounter的 BuildContext 对应的 Element 注册到祖先节点 InheritedElement 的_dependencies字典中,即右上角商品数量组件依赖和监听了 DakCartInheritedWidget,有机会获取后续更新通知。

ii.读取

子节点是如何读取InheritedWidget中数据的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class InheritedElement extends ProxyElement {
/// Creates an element that uses the given widget as its configuration.
InheritedElement(InheritedWidget widget) : super(widget);

@override
InheritedWidget get widget => super.widget as InheritedWidget;

final Map<Element, Object?> _dependents = HashMap<Element, Object?>();

@override
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets![widget.runtimeType] = this;
}
//省略。。。
}
  • Map<Type, InheritedElement>? _inheritedWidgets

这是从Element根类中继承的属性,保存着所有祖先节点中出现过的InheritedWidgetInheritedElement对象的映射关系。字典的键是InheritedWidget子类的Type,值是InheritedElement对象。

在使用getdependOn时,二者方法内部都会调用以下内容:

1
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];

ancestor是根据InheritedWidget类型从_inheritedWidgets中取出的InheritedElement对象,通过ancestor.widget就能获取对应的InheritedWidget,进而读取其内部数据。

这就是读取数据的原理~

需要注意的是,_inheritedWidgets中给相同的键赋值会覆盖原InheritedElement对象,注意这一点,后面会用到。

iii.传递

为什么InheritedWidget能一直向下传递数据呢?

1
2
3
4
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

这是Element根类中提供的函数,Element中在mount()的最后一步会调用此函数,以便更新_inheritedWidgets字段。

对于非InheritedWidget组件,调用的是上面的默认实现,即把父节点的_inheritedWidgets赋给自己,从而将父组件上的共享数据传递给自己。

对于InheritedElement,它重写了此函数:

1
2
3
4
5
6
7
8
9
10
11
@override
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null) {
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
}else{
_inheritedWidgets = HashMap<Type, InheritedElement>();
}
_inheritedWidgets![widget.runtimeType] = this;
}

同样保留了父节点中的_inheritedWidgets,但又多了一步:将当前InheritedWidget与其InheritedElement的映射关系加入进来。

这样在Element树中,InheritedElement中的数据会通过_inheritedWidgets字典,在其InheritedWidget或非InheritedWidget子节点中,层层往下传递~

iv.覆盖

还记得前面的提醒吗:

1
Map<Type, InheritedElement>? _inheritedWidgets;

这个Map的键是InheritedWidget的类型,值是InheritedElement对象。由于_inheritedWidgets会在组件树上层层往下传递,所以在遇到子组件也是InheritedWidget节点时,祖节点中的_inheritedWidgets会被继承下来并添加新的键值对。给键值对赋值时如果使用相同的键,那么后来的值就会替换前值,即下层的共享数据覆盖上一层的共享数据。

在组件树中传递数据时,可能会出现某个子节点A的上层有多个相同类型的InheritedWidget父节点,但携带的数据不同的情况,那A节点取到的是哪一层父节点共享的数据呢?以购物车案例为基础,我们稍作修改:

代码实现5:

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
65
66
67
68
69
70
71
72
73
74
void main() {
runApp(DakMyApp());
}

class DakMyApp extends StatefulWidget {
const DakMyApp({Key? key}) : super(key: key);
@override
State<DakMyApp> createState() => _DakMyAppState();
}

class _DakMyAppState extends State<DakMyApp> {
//顶级状态
final cart1 = DakCart(items: []);
//次级状态
var cart2 = DakCart(items: [DakItem(name: 'Pineapple', selected: true)]);
@override
Widget build(BuildContext context) {
print('DakMyAPP build');
return DakCartInheritedWidget( //看这里:父节点1
cart: cart1,
child: DakCartInheritedWidget( //看这里:父节点2
cart: cart2,
child: MaterialApp(
home: DakCatelogPage(
callback: (item) {
//重绘
setState(() {
if (item != null) {
final newItems =
item.selected ? cart2.add(item) : cart2.delete(item);
cart2 = DakCart(items: newItems);
} else {
print('单纯setState,未更新数据');
}
});
},
),
),
),
);
}
}
// 省略。。。
// 购物车
class DakCartPage extends StatelessWidget {DakCartInheritedWidget
// 省略。。。
@override
Widget build(BuildContext context) {
// 获取顶部最近的一份共享数据
var provider = DakCartInheritedWidget.of(context);
return Scaffold(
// 省略。。。
body: Container(
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: provider?.items.length,
itemBuilder: (context, index) {
final item = provider?.items[index];
return Container(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text('· ${item?.name}'),
);
},
),
),
// 省略。。。
],
),
),
);
}
}

我们在原DakCartInheritedWidget的下层,又套了一个DakCartInheritedWidget,其中上层共享的cart1是空的,下层的cart2则包含了一个已选水果“Pineapple”。此时我们直接进入购物车页面就会发现,购物车中列表中显示了“Pineapple”,即购物车组件获取到的是在它之前且离它最近的已经包含一个水果的cart2对象。

这是因为,两层继承式组件DakCartInheritedWidget是父子组件的关系,在构建时会各自执行一遍_updateInheritance()函数:

1
2
3
4
5
6
7
8
9
10
11
@override
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null) {
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
}else{
_inheritedWidgets = HashMap<Type, InheritedElement>();
}
_inheritedWidgets![widget.runtimeType] = this; //注意看,是这里
}

上层执行时,_inheritedWidgets中保存了DakCartInheritedWidgetInheritedElement的映射关系并往下传递,假设为[w:e1];下层执行时,_inheritedWidgets先保留了父节点的[w:e1],再将自己加入进去,但由于widget.runtimeType没变,即键w没变,导致字典的值e1被覆盖掉,替换成当前InheritedElement对象,从而变成[w:e2]

购物车页面是第二级DakCartInheritedWidget的子节点,所以它读取共享数据时,读到的是离自己最近的DakCartInheritedWidget中的cart2

这只是基于源码的一种推理验证,实际项目中应该不会真的有必要这么用~

v.更新

InheritedWidget来说,如果只是在某个依赖者里修改InheritedWidget中共享的数据,是不会触发其他依赖者更新的。只有满足以下条件才行:

  1. 子节点调用context.dependOnInheritedWidgetOfExactType注入依赖;
  2. 重写InheritedWidgetupdateShouldNotify()方法,比较新旧InheritedWidget中的共享数据是否发生了变化,最终返回 true 时依赖者才会同步更新;
  3. 修改InheritedWidget中共享的数据:对值类型的共享数据,可直接修改其值;对引用类型的共享数据,需要将其替换成新对象,而非在原指针的基础上修改其某个属性,必要时还需重写共享数据对象的==操作符与hashCode以定义新旧数据对象是否相同。只有新旧数据对象不相同,updateShouldNotify()中对二者做比较时才能返回 true;
  4. 调用setState(比如在在父节点中),触发InheritedWidget的重绘,从而调用 Element 的update()函数,走notifyClients流程;

这些都是InheritedWidget实现源码中相关逻辑要求的,主要是在InheritedElement中,而InheritedElement继承自ProxyElement

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
abstract class ProxyElement extends ComponentElement {

@override
ProxyWidget get widget => super.widget as ProxyWidget;

@override
Widget build() => widget.child;

@override
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget;
super.update(newWidget);
updated(oldWidget);
_dirty = true;
rebuild();
}

/// Called during build when the [widget] has changed.
///
/// By default, calls [notifyClients]. Subclasses may override this method to
/// avoid calling [notifyClients] unnecessarily (e.g. if the old and new
/// widgets are equivalent).
@protected
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}

/// Notify other objects that the widget associated with this element has
/// changed.
///
/// Called during [update] (via [updated]) after changing the widget
/// associated with this element but before rebuilding this element.
@protected
void notifyClients(covariant ProxyWidget oldWidget);
}

Element树已构建完成后,某个InheritedElement节点的配置发生变化时(通常是内部的共享数据变化了),在父节点调用setState重新构建widget树时,会复用当前位置上的Element,更新它的配置信息(newWidget),而非创建新的。此时会调用InheritedElementupdate()函数。

update内会调用updated方法,而InheritedElementProxyElement继承并重写了此方法:

1
2
3
4
5
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}

通常,我们会在自定义InheritedWidget时重写这里的updateShouldNotify()方法:

1
2
3
4
5
// 重写
@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
return (oldWidget.items != items); //自定义判断逻辑
}

这里的返回值可根据具体业务而定,返回 true 则继续调用上面所说的updated方法:

1
2
3
4
5
6
7
8
9
/// Called during build when the [widget] has changed.
///
/// By default, calls [notifyClients]. Subclasses may override this method to
/// avoid calling [notifyClients] unnecessarily (e.g. if the old and new
/// widgets are equivalent).
@protected
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}

updated里默认调用notifyClients方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// Notifies all dependent elements that this inherited widget has changed, by
/// calling [Element.didChangeDependencies].
///
/// This method must only be called during the build phase. Usually this
/// method is called automatically when an inherited widget is rebuilt, e.g.
/// as a result of calling [State.setState] above the inherited widget.
///
@override
void notifyClients(InheritedWidget oldWidget) {
for (final Element dependent in _dependents.keys) {
notifyDependent(oldWidget, dependent); //看这里
}
}

它会遍历_dependents字典,为每个依赖者调用notifyDependent方法:

1
2
3
4
5
6
7
8
9
10
11
/// Called by [notifyClients] for each dependent.
///
/// Calls `dependent.didChangeDependencies()` by default.
///
/// Subclasses can override this method to selectively call
/// [didChangeDependencies] based on the value of [getDependencies].
///
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}

notifyDependent最终调用依赖者(Element)的didChangeDependencies方法:

1
2
3
4
@mustCallSuper
void didChangeDependencies() {
markNeedsBuild();
}

ps:只有StatefulWidget才有didChangeDependencies回调,如果依赖者对应的组件是StatelessWidget类型,那么组件还是会重绘,只不过没有didChangeDependencies调用而已~

到这里,依赖者Element就已经能感知自己依赖的数据发生了变化,使用最新数据等待重绘即可。

由于getInheritedWidgetOfExactType不会往_dependents中注入依赖,也就不会调用notifyDependent方法,所以调用get时的组件没机会获得通知,也就不会重绘~

InheritedWidget中重写updateShouldNotify返回 false 时,就不会调用updated,不走notifyClients流程,依赖者也就不会获得通知和重绘。


以购物车为例,正常情况下选中并加购商品时,会输出以下日志:

1
2
3
4
DakMyAPP build
DakCatelogPage build
DakCartCounter didChangeDependencies
DakCartCounter build

其中最后两行是DakCartInheritedWidget的依赖者DakCartCounter组件的日志 ,setState时,因为DakCartCounter继承自StatefulWidget,所以它的didChangeDependencies函数会触发,并发生重绘。

当我们将 DakCartInheritedWidget 中updateShouldNotify的返回值始终设置为 false 时:

1
2
3
4
5
6
7
// 重写
@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
const update = false;
print('shoudl update? $update');
return update;
}

加购商品并在MyAppsetState时,会得到以下日志:

1
2
3
DakMyAPP build
shoudl update? false
DakCatelogPage build

即:虽然DakCartInheritedWidget组件发生了重绘,但其依赖者 Element 对应的DakCartCounter组件的didChangeDependencies()函数并未触发,组件本身也没重绘!

didChangeDependencies没触发可以理解,因为updateShouldNotify返回了 false,依赖者不走notifyClients流程;没重绘(build)是为啥呢?尤其是在其父节点都已发生重绘的情况下!

这里,看下我们使用DakCartCounter组件的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
actions: [
TextButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DakCartPage(),
),
);
},
icon: const Icon(
Icons.shopping_cart,
color: Colors.black,
),
label: const DakCartCounter(), // 注意看这里的const
),
]

代码标注处,我们使用的是const修饰依赖者组件,在完成第一次构建之后,它就不再参与重绘了,除非其依赖的共享数据发生了变化。你可以尝试将const关键字去掉并查看新的日志,你会发现依赖者这次会跟着重绘了,即使是updateShouldNotify返回了 false。因为非const组件在父节点 build 时,要跟着重新构建。

所以这就是节省性能的一个小技巧:必要时使用const关键字!

另外在本小结开头处,我们提到让依赖者同步更新时需要满足的条件3:修改共享数据时,数据对象本身要发生变化。这里变化的标准是以hashCode==操作符来定义的。以购物车为例:

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
class _DakMyAppState extends State<DakMyApp> {
var cart = DakCart(items: []);
@override
Widget build(BuildContext context) {
print('DakMyAPP build');
return DakCartInheritedWidget(
cart: cart,
child: MaterialApp(
home: DakCatelogPage(
callback: (item) {
//重绘
setState(() {
if (item != null) {
final newItems =
item.selected ? cart.add(item) : cart.delete(item);
//cart = DakCart(items: newItems); //看这里
} else {
print('单纯setState,未更新数据');
}
});
},
),
),
);
}
}

callback中返回的参数item不为空时,我们会去修改cart对象,即通过cart.addcart.delete增减响应商品。实际上到这一步,我们还只是对原共享数据对象cart内的items数组做了修改,并未改变cart对象的指针。如果此时注释掉“cart = DakCart(items: newItems)”这一行,直接调用setState,那么DakCartInheritedWidget会重绘,但其updateShouldNotify方法会触发并返回false,依赖者们不会重绘!

这是因为,我们在重写updateShouldNotify时设置的更新逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
final update = oldWidget.cart != cart; // 比对新旧cart对象,比的是二者的指针
return update;
}

class DakCart {
// 省略。。
// 重写==操作符,用于后续判断购物车是否发生了变化
@override
bool operator ==(Object other) {
if (other is! DakCart) {
return false;
}
if (!identical(this, other)) {
return false;
}
bool same =
listEquals(items, other.items) && (other.totalPrice == totalPrice);
return same;
}

@override
int get hashCode => Object.hashAll([items, totalPrice]);
}

setState 时会创建新的DakCartInheritedWidget对象this并调用其内部updateShouldNotify方法,此时this中持有的cart对象与oldWidget的相同。在没有重写DakCart类中==操作符与hashCode的情况下,!=操作符默认比对的是对象的内存地址,即新widget与oldWidget中cart对象的指针。而cart.addcart.delete只是修改了cart内部items数组,并未改变cart对象本身的指针,thisoldWidget持有的cart对象指向同一片内存地址,updateShouldNotify返回 false,依赖者们也就不会去重绘!

购物车示例中,我对DakCart类的==操作符与hashCode进行了重写,但也只是作为演示,仅仅比对了新旧对象的内存地址。在实际业务中,你可以设置自己的判断标准,比如当InheritedWidget中共享数据为指针类型的User对象时,只要name字段相同就可以认为两个User对象==;而对于简单的值类型,如int counter,直接给 counter 赋值就能让updateShouldNotify返回false,触发依赖者更新。

可以这么做个小结:依赖者能否同步更新,要根据updateShouldNotify中的判断逻辑、共享数据是否重写==hashCode等情况而定。如果根据我们设置的标准,数据确实发生变化则 setState 时依赖者们会跟随InheritedWidet重绘,否则仅InheritedWidet重绘而依赖者们不重绘。


5.3.总结

InheritedWidet的使用步骤总结:

1.自定义InheritedWidget子类,提供共享数据与of方法,重写updateShouldNotify方法;

2.使用依赖者作为InheritedWidet的子孙节点,在依赖者内调用of方法获取共享数据,同时将依赖者注入InheritedElement_dependents字典中;

3.InheritedWidget内共享数据变化时,在父节点中调用setState,触发自身重绘,通知依赖者们执行didChangeDependencies和重绘;

6.局部刷新

代码实现4的开头有个声明,这种实现不是最优方案,因为在InheritedWidget中的数据发生改变时,我们在MyApp这里调用了setState,这就导致几乎整个应用都执行了重绘!这是严重的性能浪费,所以需要考虑局部刷新问题,在小范围内只让依赖了InheritedWidget的组件重绘即可。那么接下来我们将使用 Flutter 框架内置的一些支持局部刷新的小组件继续完善代码~

6.1.ChangeNotifier

这是一个在所监听内容发生变化时,能产生通知的类,下面结合源码进行分析:

1.Listenable
1
2
3
4
5
6
7
8
9
10
abstract class Listenable {

const Listenable();

factory Listenable.merge(List<Listenable?> listenables) = _MergingListenable;

void addListener(VoidCallback listener);

void removeListener(VoidCallback listener);
}

Listenable这是一个抽象类,用于维护着监听者列表,对外提供了增、删、合并监听者的接口。

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 ChangeNotifier implements Listenable {
int _count = 0;
List<VoidCallback?> _listeners = List<VoidCallback?>.filled(0, null);

@protected
bool get hasListeners {
return _count > 0;
}

@override
void addListener(VoidCallback listener) {
//省略。。
}

void _removeAt(int index) {
//省略。。
}

@override
void removeListener(VoidCallback listener) {
//省略。。
}

@protected
void notifyListeners() {
if (_count == 0)
return;
_notificationCallStackDepth++;

final int end = _count;
for (int i = 0; i < end; i++) {
_listeners[i]?.call();
}
//省略。。
}
}

这是精简之后的ChangeNotifier源码,它实现了Listenable抽象类,提供了notifyListeners方法,在数据变化时供我们调用以便给监听者们发送通知。

看上去这个类并不复杂,下面就用它来改造购物车案例,实现局部刷新功能,再结合案例代码看看这个类的实现路径。

2.基本用法
  • 共享数据类继承ChangeNotifier
  • 依赖者中通过xxxBuilder注册数据变化的回调;
  • 触发共享数据的更新,执行notifyListeners()
  • 接收通知,局部重绘组件。
  1. 定义共享数据类
1
2
3
4
5
6
7
8
9
10
11
class DakCart extends ChangeNotifier { // 继承ChangeNotifier

final List<DakItem> _items = [];

UnmodifiableListView<DakItem> get items => UnmodifiableListView(_items);

add(DakItem item) {
_items.add(item);
notifyListeners(); // 修改数据后,发出通知
}
}
  1. 初始化共享数据实例
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
class DakMyApp extends StatelessWidget {
DakMyApp({Key? key}) : super(key: key);

final cart = DakCart(); //创建Model实例

@override
Widget build(BuildContext context) {
return DakCartInheritedWidget(
cart: cart,
child: const MaterialApp(
home: DakCatelogPage(),
),
);
}
}

class DakCartInheritedWidget extends InheritedWidget {
。。。

final DakCart cart;

static DakCartInheritedWidget? of(BuildContext context) {
return context.getElementForInheritedWidgetOfExactType<DakCartInheritedWidget>()
?.widget as DakCartInheritedWidget;
}

@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
return false;
}
}

3.读取共享的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DakCartCounter extends StatelessWidget {
。。。
@override
Widget build(BuildContext context) {

var cart = DakCartInheritedWidget.of(context)!.cart;

return AnimatedBuilder(
animation: cart, //使用AnimatedBuilder监听共享数据的变化
builder: (context, child) {
return Text(
'共${cart.items.length}件', // 数据变化时会重建此处组件
);
},
);
}
}
  1. 更新共享的数据
1
2
3
4
5
6
7
class _DakListCellState extends State<DakListCell> {
_updateState() {
var cart = DakCartInheritedWidget.of(context)!.cart;
// 调用接口修改共享数据
widget.item.selected ? cart.add(widget.item) : cart.delete(widget.item);
}
}
3.示例改造

代码实现6:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class DakMyApp extends StatelessWidget {
DakMyApp({Key? key}) : super(key: key);

final cart = DakCart();
@override
Widget build(BuildContext context) {
print('DakMyAPP build');
return DakCartInheritedWidget(
cart: cart,
child: const MaterialApp(
home: DakCatelogPage(),
),
);
}
}

class DakCartInheritedWidget extends InheritedWidget {
//状态
final DakCart cart;

const DakCartInheritedWidget({
Key? key,
required this.cart,
required Widget child,
}) : super(
key: key,
child: child,
);

// 便捷获取共享对象
static DakCartInheritedWidget? of(BuildContext context) {
// 这里变了
return context.getElementForInheritedWidgetOfExactType<DakCartInheritedWidget>()
?.widget as DakCartInheritedWidget;
}

// 重写
@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
return false; // 这里变了
}
}

// 购物车Model
class DakCart extends ChangeNotifier { // 这里变了
// 已加购商品
final List<DakItem> _items = [];

UnmodifiableListView<DakItem> get items => UnmodifiableListView(_items);

// 商品总价
int get totalPrice => items.length * 42;

//加入购物车
add(DakItem item) {
_items.add(item);
notifyListeners(); // 这里变了
}

//移出购物车
delete(DakItem item) {
int index = _items.indexOf(item);
_items.removeAt(index);
notifyListeners(); // 这里变了
}
}

// 商品Model
class DakItem {
String name;
bool selected;

DakItem({
required this.name,
required this.selected,
});
}

// 商品列表页面
class DakCatelogPage extends StatelessWidget {
const DakCatelogPage({
Key? key,
}) : super(key: key);
static final items = [
'Apple',
'Banana',
'Cherry',
'Damson',
'Grape',
'Haw',
'Kiwifruit',
'Lemon',
'Mango',
'Orange'
].map((e) => DakItem(name: e, selected: false)).toList();

@override
Widget build(BuildContext context) {
print('DakCatelogPage build');
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.yellow,
centerTitle: true,
title: const Text(
'Catelog',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
actions: [
TextButton.icon(
onPressed: () {},
icon: const Icon(
Icons.shopping_cart,
color: Colors.black,
),
label: const DakCartCounter(),
),
],
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return DakListCell(
item: items[index],
);
},
),
);
}
}

//商品总数
class DakCartCounter extends StatelessWidget {
const DakCartCounter({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
print('DakCartCounter build');
var cart = DakCartInheritedWidget.of(context)!.cart;
return AnimatedBuilder(
// 推荐使用最新的 ListenableBuilder
animation: cart,
builder: (context, child) {
print('AnimatedBuilder Go');
return Text(
'共${cart.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
},
);
}
}

// 商品cell
class DakListCell extends StatefulWidget {
final DakItem item;
const DakListCell({
Key? key,
required this.item,
}) : super(key: key);

@override
State<DakListCell> createState() => _DakListCellState();
}

class _DakListCellState extends State<DakListCell> {
_updateState() {
setState(() {
widget.item.selected = !widget.item.selected;
});
var cart = DakCartInheritedWidget.of(context)!.cart;
// 修改共享数据
widget.item.selected ? cart.add(widget.item) : cart.delete(widget.item);
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
color: Colors.white,
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 50,
height: 50,
color: Colors.yellow,
),
Text(
widget.item.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
Builder(
builder: (context) {
return widget.item.selected
? IconButton(
// 已选中
onPressed: _updateState,
icon: const Icon(Icons.check),
)
: ElevatedButton(
//未选中
onPressed: _updateState,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white),
elevation: MaterialStateProperty.all(0),
),
child: const Text(
'ADD',
style: TextStyle(color: Colors.black),
),
);
},
),
],
),
);
}
}

代码实现4相比,主要的变化是:

  • 现在 DakMyApp 与 DakCartCounter 是 StatelessWidget 了;
  • DakCartInheritedWidget 中updateShouldNotify()方法直接返回了 false,后续数据的更新通过 ChangeNotifier 实现,不再需要走notifyClients流程了;
  • 静态方法of()dependOn..变成了get..,也是因为后续数据的更新通过 ChangeNotifier 实现,不再需要注入依赖;
  • DakCatelogPage 与 DakListCell 中传递的 DakCallback 参数都不需要了,加购商品时直接在 DakListCell 的_updateState()回调里调用cart.add()cart.delete更新共享数据;
  • 共享数据使用者 DakCartCounter 中通过AnimatedBuilder来监听数据变化,并且在数据变化后使用已变化的数据仅局部重绘Text节点即可。

为了做个验证,加购一件商品,此时控制台输出日志:

1
AnimatedBuilder Go

共享数据变化后,仅局部重绘了依赖者中使用此数据的的Text节点。

4.实现原理
i.注册

实现代码6中,我在依赖者的Text节点使用了AnimatedBuilder注册监听并构建组件:

1
2
3
4
5
6
7
8
9
10
11
12
AnimatedBuilder(
animation: cart,
builder: (context, child) {
print('AnimatedBuilder Go');
return Text(
'共${cart.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
},
)

其实用它是因为我的MAC系统较老,暂时没有升级 Flutter 版本,如果你的系统比较新,这里最好是使用2.15版本之后新出的ListenableBuilder,不过它俩在功能上类似,我权且用它做演示了。

AnimatedBuilderAnimatedWidget的子类,用于在监听的数据变化时重绘依赖者组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AnimatedBuilder extends AnimatedWidget {
const AnimatedBuilder({
Key? key,
required Listenable animation,
required this.builder,
this.child,
}) : super(key: key, listenable: animation);

final TransitionBuilder builder;
final Widget? child;

@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
  • 参数1是个Listenable类型,对应购物车示例中的共享数据cart对象;
  • 参数2是构造组件的回调函数,每当监听的数据发生变化时,都会执行自己的build()函数,构建并返回我们自定义的组件,即购物车示例中DakCartCounterText组件。

构建AnimatedBuilder时,animation将传递给其父类AnimatedWidgetlistenable字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}

void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}

@override
Widget build(BuildContext context) => widget.build(context);
}

listenableinitState()阶段注册了通知的回调_handleChange,至此完成了注册这一步。

ii.修改

在商品分类页面,点击Cell中加购按钮后,先读取共享数据再修改它:

1
2
3
4
5
6
7
8
9
_updateState() {
setState(() {
widget.item.selected = !widget.item.selected;
// 1.读取共享数据
var cart = DakCartInheritedWidget.of(context)!.cart;
// 2.修改共享数据
widget.item.selected ? cart.add(widget.item) : cart.delete(widget.item);
});
}
iii.通知

cart对象在执行add()或者delete()的最后,都调用了notifyListeners()函数:

1
2
3
4
5
6
7
8
9
10
11
12
//加入购物车
add(DakItem item) {
_items.add(item);
notifyListeners();
}

//移出购物车
delete(DakItem item) {
int index = _items.indexOf(item);
_items.removeAt(index);
notifyListeners();
}
1
2
3
4
5
6
7
8
9
10
11
12
@protected
void notifyListeners() {
if (_count == 0)
return;
_notificationCallStackDepth++;

final int end = _count;
for (int i = 0; i < end; i++) {
_listeners[i]?.call(); // 重点在这里
}
//省略。。
}

即调用ChangeNotifier中的_listeners[i]?.call(),执行监听者的_handleChange回调。而这个回调正是前文在i.注册阶段里_AnimatedState中定义的:

1
2
3
4
5
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}

其默认实现是执行setState,即重绘AnimatedWidget组件,执行其build()函数,即执行我们在AnimatedBuilder中提供的第二个参数builder,也就是构建Text组件的回调。

这样,DakCartCounter就成功接收到通知,并且通过AnimatedBuilder局部重绘了Text组件。

6.2. ValueNotifier

ChangeNotifier已经很方便的帮我们实现局部重绘了,而Flutter框架想给你的还不止如此,它还提供了某些场景下更精简、方便的ValueNotifier

1.定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);

T _value;

@override
T get value => _value;

set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}

@override
String toString() => '${describeIdentity(this)}($value)';
}

它是ChangeNotifier的子类,可以很方便的帮我们在共享数据外包裹一层ChangeNotifier,为共享数据提供gettersetter,并在setter内帮我们叫notifyListeners()

单看这些介绍,是不是发现它与 Swift 中属性包装器@propertyWrapper很像!

2.基本用法
  1. 定义ValueNotifier数据及操作数据的接口
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
class DakCartInheritedWidget extends InheritedWidget {

// 定义ValueNotifier并指定其监听数据的类型
late final ValueNotifier<DakCart> _valueNotifier;

ValueNotifier<DakCart> get valueNotifier => _valueNotifier;

DakCartInheritedWidget(
DakCart cart, {Key? key, required Widget child,
}) : super(key: key, child: child,) {
_valueNotifier = ValueNotifier(cart);
}

// 更新监听的数据对象
void updateData(DakCart cart) {
_valueNotifier.value = cart;
}

static DakCartInheritedWidget? of(BuildContext context) {
return context
.getElementForInheritedWidgetOfExactType<DakCartInheritedWidget>()
?.widget as DakCartInheritedWidget;
}

@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
return false;
}
}
  1. 初始化共享的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DakMyApp extends StatelessWidget {
DakMyApp({Key? key}) : super(key: key);
//初始化共享数据
final cart = DakCart(items: []);
@override
Widget build(BuildContext context) {
return DakCartInheritedWidget(
cart,
child: const MaterialApp(
home: DakCatelogPage(),
),
);
}
}
  1. 读取共享的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DakCartCounter extends StatelessWidget {

@override
Widget build(BuildContext context) {

var valueNotifier = DakCartInheritedWidget.of(context)!.valueNotifier;

return ValueListenableBuilder(// 使用ValueListenableBuilder监听共享数据的变化
valueListenable: valueNotifier,
builder: (context, DakCart value, child) {

return Text(
'共${value.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
},
);
}
}
  1. 更新共享的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class _DakListCellState extends State<DakListCell> {
_updateState() {
setState(() {
widget.item.selected = !widget.item.selected;
});
// 修改共享数据
final inherit = DakCartInheritedWidget.of(context)!;
final valueNotifier = inherit.valueNotifier;
final newItems = widget.item.selected
? valueNotifier.value.add(widget.item)
: valueNotifier.value.delete(widget.item);
final newCart = DakCart(items: newItems);
inherit.updateData(newCart);
}
。。。
}
3.示例改造

接下来用它来继续改造代码实现4

代码实现7:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class DakMyApp extends StatelessWidget {
DakMyApp({Key? key}) : super(key: key);

final cart = DakCart(items: []); //初始化共享数据

@override
Widget build(BuildContext context) {
print('DakMyAPP build');
return DakCartInheritedWidget(
cart,
child: const MaterialApp(
home: DakCatelogPage(),
),
);
}
}

class DakCartInheritedWidget extends InheritedWidget {

late final ValueNotifier<DakCart> _valueNotifier; // 这里变了
ValueNotifier<DakCart> get valueNotifier => _valueNotifier;

DakCartInheritedWidget(
DakCart cart, {
Key? key,
required Widget child,
}) : super(
key: key,
child: child,
) {
_valueNotifier = ValueNotifier(cart);
}

// 更新监听的数据对象
void updateData(DakCart cart) {
_valueNotifier.value = cart;
}

static DakCartInheritedWidget? of(BuildContext context) {
return context
.getElementForInheritedWidgetOfExactType<DakCartInheritedWidget>()
?.widget as DakCartInheritedWidget;
}

@override
bool updateShouldNotify(DakCartInheritedWidget oldWidget) {
return false;
}
}

class DakCart {

List<DakItem> items = [];

int get totalPrice => items.length * 42;
DakCart({required this.items});

List<DakItem> add(DakItem item) {
items.add(item);
return items;
}

List<DakItem> delete(DakItem item) {
int index = items.indexOf(item);
items.removeAt(index);
return items;
}

@override
bool operator ==(Object other) {
if (other is! DakCart) {
return false;
}
if (!identical(this, other)) {
return false;
}
bool same =
listEquals(items, other.items) && (other.totalPrice == totalPrice);
return same;
}

@override
int get hashCode => Object.hashAll([items, totalPrice]);
}

// 商品Model
。。。
// 商品列表页面
。。。

//商品总数
class DakCartCounter extends StatelessWidget {
const DakCartCounter({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print('DakCartCounter build');

var valueNotifier = DakCartInheritedWidget.of(context)!.valueNotifier;

return ValueListenableBuilder(// 这里变了 使用共享数据
valueListenable: valueNotifier,
builder: (context, DakCart value, child) {
print('DakCartCounter build Text');
return Text(
'共${value.items.length}件',
style: const TextStyle(
color: Colors.black,
),
);
},
);
}
}

class DakListCell extends StatefulWidget {
final DakItem item;
const DakListCell({
Key? key,
required this.item,
}) : super(key: key);

@override
State<DakListCell> createState() => _DakListCellState();
}

class _DakListCellState extends State<DakListCell> {
_updateState() {
setState(() {
widget.item.selected = !widget.item.selected;
});
// 这里变了
final inherit = DakCartInheritedWidget.of(context)!;
final valueNotifier = inherit.valueNotifier;
final newItems = widget.item.selected
? valueNotifier.value.add(widget.item)
: valueNotifier.value.delete(widget.item);
final newCart = DakCart(items: newItems);
inherit.updateData(newCart); // 修改共享数据
}
。。。
}

主要的变化是:

  • 现在 DakMyApp 与 DakCartCounter 是StatelessWidget了;
  • DakCartInheritedWidget 中共享数据由 DakCart 类型变为了ValueNotifier<DakCart>类型,同时增加了更新数据的方法updateData()
  • updateShouldNotify()方法直接返回了 false,后续数据的更新通过ValueNotifier实现,不再需要走notifyClients流程;
  • 静态方法of()dependOn..变成了get..,也是因为后续数据的更新通过ValueNotifier实现,不再需要注入依赖;
  • DakCatelogPage 与 DakListCell 中的 DakCallback 参数都不需要了,加购商品时直接在 DakListCell 的_updateState()回调里调用inherit.updateData(newCart)更新数据;
  • 共享数据使用者 DakCartCounter 中通过ValueListenableBuilder来监听数据变化,并且在数据变化后直接使用回调中的value局部重绘Text节点即可。

为了做个验证,加购一件商品,此时控制台输出日志:

1
DakCartCounter build Text

这次,借助ValueNotifier的能力,MyApp、DakCatelogPage、DakCartCounter 也都没重绘,只有使用了共享数据的Text组件重绘了!

6.3. Provider

Provider是Flutter官方出的状态管理包,基于InheritedWidget实现,允许我们在应用的不同层级中传递和监听状态变化。Provider本身并不会自动更新依赖它的组件。因此,我们通常需要使用一些提供了更新机制的实现类,比如:

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider
1.基本用法

1.定义Model类

1
2
3
4
5
6
7
8
9
10
class ProviderModel with ChangeNotifier {
int _index = 0;
int get index => _index;
String text = "Hello";

add() {
_index++;
notifyListeners(); //数据变化时,通知依赖此值的地方rebuild
}
}

2.初始化状态数据

1
2
3
4
5
6
7
8
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => ProviderModel(), //初始化Model
child: const MyApp()
),
);
}

3.使用状态数据

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 ProviderWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1.使用 Consumer 监听状态
return Consumer<ProviderModel>(
builder: (context, model, child) {
return Text('${model.index}');
},
);
}
}

// 2.使用 Provider.of()读取状态:
Text('${Provider.of<ProviderModel>(context, listen: false).index}');

// 3.使用watch
Text("${context.watch<ProviderModel>().index}");

// 4.使用Selector监听「一个」或多个值的变化,更加节省性能
Selector<ProviderModel, String>(
selector: (_, model) => model.text,
builder: (_, text, __) {
return Text("$text");
},
)
2.示例演示
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import 'package:provider/provider.dart';

class DKProviderDemo extends StatelessWidget {
const DKProviderDemo({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => DKShareProviderModel()) //状态实例
],
child: const MaterialApp(
home: DKProviderHomeController(),
),
);
}
}

// 定义状态Model
class DKShareProviderModel with ChangeNotifier {
int _index = 0;
int get index => _index;
String text = "Hello";

add() {
_index++;
notifyListeners(); //状态变化时,通知依赖它的地方rebuild
}
}

// 主页面
class DKProviderHomeController extends StatelessWidget {
const DKProviderHomeController({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
//1.使用watch
Builder(
builder: (context) {
return Text("${context.watch<DKShareProviderModel>().index}");
},
),
//2.使用Consumer,监听「所有」属性的变化
Consumer<DKShareProviderModel>(
builder: (context, model, child) {
return Text("${model.text}");//任何属性的变化,都会导致此处重构,浪费性能
},
),
//3.使用Selector,只监听「一个」或多个值的变化,更加节省性能
Selector<DKShareProviderModel, String>(
selector: (_, model) => model.text,
builder: (_, text, __) {
return Text("$text");//其他属性的变化,不会引起此处的重构
},
),
MaterialButton(
color: Colors.blue,
child: const Text("-->进入"),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const DKProviderDetailController())
)
)
],
),
),
);
}
}

// 详情页
class DKProviderDetailController extends StatelessWidget {
const DKProviderDetailController({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("改变数据"),
),
body: const Center(
child:
DKTextIndexWidget(), //封装组件
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
/// 使用 `context.read` 而非 `context.watch`,这样在DKShareProviderModel变化时,此处不会重建
onPressed: () => context.read<DKShareProviderModel>().add(),
));
}
}

// 使用共享数据,同步其变化
class DKTextIndexWidget extends StatelessWidget {
const DKTextIndexWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
color: Colors.green,
child: Text('${context.watch<DKShareProviderModel>().index}',));
}
}

7.小结

本文介绍了原生开发与响应式编程中管理状态的不同方式,着重讲解了Flutter中对提升状态的实践及其产生的问题与解决方案。其中重点讲解了InheritedWidget在组件树中从上而下传递数据的机制原理,ChangeNotifierValueNotifierProvider实现局部重绘的原理与实践。其实除了这些,还有GetXBLoCMobX等三方库可以实现局部刷新,后面的文章里再接着介绍吧~


Flutter状态管理
https://davidlii.cn/2023/08/10/flutter-state.html
作者
Davidli
发布于
2023年8月10日
许可协议