1.初识状态
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), ), ); } }
修改状态的流程是这样:
按钮回调函数中对状态_counter
做自增运算;
通过setState
告诉框架:我已修改,你去重新绘制整个组件树;
重绘工作交给了build(context)
函数;
build
中新建所有子树,其中子组件Text
使用最新_counter
展示数值。
2.状态分类 Flutter 中状态可分两种:短时状态
和全局状态
。
2.1.短时状态 也称局部状态或临时状态,独立于某个组件中,只影响该组件自己的行为。
例子:
前文计时器案例中的数值_counter
;
一个 PageView 组件中的当前页面_index
;
一个复杂动画中当前进度;
一个 BottomNavigationBar 中当前被选中的 tab;
因为其他组件不需要访问此状态,也就无需状态管理架构去管理这种状态,你需要用的只是一个StatefulWidget
。
2.2.全局状态 在多组件甚至整个应用之间共享的、在用户会话期间保留的状态,就是全局状态或称共享状态。
例子:
通常,我们可借助不同的技术实现跨组件共享状态,如传参、回调、控制器,也可以使用Flutter框架内置的InheritedWidget
、ChangeNotifier
、StreamBuilder
等,还有一些优秀的三方库flutter_bloc
、Provider
等。
3.提升状态 原生开发中,兄弟组件通常以成员变量或属性的形式存在于共同的父组件中,想与对方通信(如传值或修改状态)时,一般是由前者在父组件中提供一个回调函数,在回调函数中获取对方组件的对象,通过对象.setxx
的方式修改对方的属性值,或者调用对方提供的相关接口来处理具体业务。
而声明式框架中,通过这样的方式实现组件之间的通信是没有必要的。因为任何修改对方属性状态的行为,都相当于修改了对方的配置,会引起对方组件的重构,即对方组件会创建新的实例,以对象.setxx
形式的修改就没有意义了,直接修改对方的 state 即可。
在声明式框架里,组件之间一般只能由上而下地传递数据,兄弟组件之间无法直接通信。Flutter 框架采用了 Facebook 在 React 中提出的Lift State Up
理念:将需要传递的数据从子组件移到某个共同的父组件,在那里修改它,再将它传递给其他子组件。
示例:购物车
商品目录
与购物车
两组件之间传递数据的思路:
两个组件之间需要共享CART
(已加购商品)这个State
;
提升State
到必要的高度,直到两组件都能读取到它;
最近最合适的节点是两者共同的父组件MyApp
;
MyApp
保存State
对象,并分发给商品目录与购物车组件;
商品目录中通过回调等方式在MyApp
中修改State
;
进入购物车页面时以入参形式接收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.回调
A组件访问
B组件的状态:
将B的状态提升到AB共同的父组件中,在父组件build
并创建A组件时,以参数形式将状态传给A;
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
方式差不多。
理论上,访问
状态使用传参,修改
状态使用回调函数或控制器,通过这种方式已经可以实现几乎所有场景下的状态管理了。但现实项目中通常会有很多组件,且组件之间的树形关系可能会非常复杂,在使用状态提升后,如果只是依赖上述最简单的传参的方式,那么每个组件在构造函数中可能需要传入大量的参数才能将顶层的状态一层层传递到后续节点中。
那么有没有什么方法,能让底部的组件直接访问到被我们提升到顶部的状态呢?Flutter框架给出的方案是继承式组件InheritedWidget
。
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 static DakCartInheritedWidget? of(BuildContext context) { context.dependOnInheritedWidgetOfExactType<SomeInheritedWidget>(); }
与之对应的还有另一种读取方式:
1 2 3 4 static DakCartInheritedWidget? of(BuildContext context) { context.getElementForInheritedWidgetOfExactType<SomeInheritedWidget>()?.widget as SomeInheritedWidget; }
方式2是真正的读取,没有额外的注册监听操作。
dependOnxx
与getxx
都是 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 { 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 (() { Element? ancestor = dependent._parent; while (ancestor != this && ancestor != null ) ancestor = ancestor._parent; return ancestor == this ; }()); 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 { 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
根类中继承的属性,保存着所有祖先节点中出现过的InheritedWidget
与InheritedElement
对象的映射关系。字典的键是InheritedWidget
子类的Type
,值是InheritedElement
对象。
在使用get
或dependOn
时,二者方法内部都会调用以下内容:
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
中保存了DakCartInheritedWidget
与InheritedElement
的映射关系并往下传递,假设为[w:e1]
;下层执行时,_inheritedWidgets
先保留了父节点的[w:e1]
,再将自己加入进去,但由于widget.runtimeType
没变,即键w
没变,导致字典的值e1
被覆盖掉,替换成当前InheritedElement
对象,从而变成[w:e2]
。
购物车页面是第二级DakCartInheritedWidget
的子节点,所以它读取共享数据时,读到的是离自己最近的DakCartInheritedWidget
中的cart2
。
这只是基于源码的一种推理验证,实际项目中应该不会真的有必要这么用~
v.更新 对InheritedWidget
来说,如果只是在某个依赖者里修改InheritedWidget
中共享的数据,是不会触发其他依赖者更新的。只有满足以下条件才行:
子节点调用context.dependOnInheritedWidgetOfExactType
注入依赖;
重写InheritedWidget
的updateShouldNotify()
方法,比较新旧InheritedWidget
中的共享数据是否发生了变化,最终返回 true 时依赖者才会同步更新;
修改InheritedWidget
中共享的数据:对值类型的共享数据,可直接修改其值;对引用类型的共享数据,需要将其替换成新对象,而非在原指针的基础上修改其某个属性,必要时还需重写共享数据对象的==
操作符与hashCode
以定义新旧数据对象是否相同。只有新旧数据对象不相同,updateShouldNotify()
中对二者做比较时才能返回 true;
调用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(); } @protected void updated(covariant ProxyWidget oldWidget) { notifyClients(oldWidget); } @protected void notifyClients(covariant ProxyWidget oldWidget); }
在Element
树已构建完成后,某个InheritedElement
节点的配置发生变化时(通常是内部的共享数据变化了),在父节点调用setState
重新构建widget树时,会复用当前位置上的Element
,更新它的配置信息(newWidget),而非创建新的。此时会调用InheritedElement
的update()
函数。
update
内会调用updated
方法,而InheritedElement
从ProxyElement
继承并重写了此方法:
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 @protected void updated(covariant ProxyWidget oldWidget) { notifyClients(oldWidget); }
updated
里默认调用notifyClients
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @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 @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; }
加购商品并在MyApp
中setState
时,会得到以下日志:
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.add
或cart.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; 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.add
或cart.delete
只是修改了cart
内部items
数组,并未改变cart
对象本身的指针,this
与oldWidget
持有的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 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 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 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
节点即可。
为了做个验证,加购一件商品,此时控制台输出日志:
共享数据变化后,仅局部重绘了依赖者中使用此数据的的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
,不过它俩在功能上类似,我权且用它做演示了。
AnimatedBuilder
是AnimatedWidget
的子类,用于在监听的数据变化时重绘依赖者组件:
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()
函数,构建并返回我们自定义的组件,即购物车示例中DakCartCounter
的Text
组件。
构建AnimatedBuilder
时,animation
将传递给其父类AnimatedWidget
的listenable
字段:
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(() { }); } @override Widget build(BuildContext context) => widget.build(context); }
listenable
在initState()
阶段注册了通知的回调_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(() { }); }
其默认实现是执行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 class ValueNotifier <T > extends ChangeNotifier implements ValueListenable <T > { ValueNotifier(this ._value); T _value; @override T get value => _value; set value(T new Value ) { if (_value == new Value ) return ; _value = new Value ; notifyListeners(); } @override String toString() => '${describeIdentity(this)} ($value)'; }
它是ChangeNotifier
的子类,可以很方便的帮我们在共享数据外包裹一层ChangeNotifier
,为共享数据提供getter
、setter
,并在setter
内帮我们叫notifyListeners()
。
单看这些介绍,是不是发现它与 Swift 中属性包装器@propertyWrapper
很像!
2.基本用法
定义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 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 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 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
在组件树中从上而下传递数据的机制原理,ChangeNotifier
、ValueNotifier
与Provider
实现局部重绘的原理与实践。其实除了这些,还有GetX
、BLoC
、MobX
等三方库可以实现局部刷新,后面的文章里再接着介绍吧~