3.2 状态管理
响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在React/Vue(两者都是支持响应式编程的Web开发框架)还是Flutter中,他们讨论的问题和解决的思想都是一致的。所以,如果你对React/Vue的状态管理有了解,可以跳过本节。言归正传,我们想一个问题,StatefulWidget的状态应该被谁管理?Widget本身?父Widget?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:
- Widget管理自己的状态。
- Widget管理子Widget状态。
- 混合管理(父Widget和子Widget都管理状态)。
如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
- 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父widget中管理(灵活会显得更重要一些)。
接下来,我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的 ——创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active确定颜色:绿色为true ,灰色为false,如图3-4所示。
下面的例子将使用GestureDetector来识别点击事件,关于该GestureDetector的详细内容我们将在后面“事件处理”一章中介绍。
3.2.1 Widget管理自身状态
_TapboxAState 类:
- 管理TapboxA的状态。
- 定义
_active:确定盒子的当前颜色的布尔值。 - 定义
_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。 - 实现widget的所有交互式行为。
1// TapboxA 管理自身状态. 2 3//------------------------- TapboxA ---------------------------------- 4 5class TapboxA extends StatefulWidget { 6 TapboxA({Key key}) : super(key: key); 7 8 9 _TapboxAState createState() => new _TapboxAState(); 10} 11 12class _TapboxAState extends State<TapboxA> { 13 bool _active = false; 14 15 void _handleTap() { 16 setState(() { 17 _active = !_active; 18 }); 19 } 20 21 Widget build(BuildContext context) { 22 return new GestureDetector( 23 onTap: _handleTap, 24 child: new Container( 25 child: new Center( 26 child: new Text( 27 _active ? 'Active' : 'Inactive', 28 style: new TextStyle(fontSize: 32.0, color: Colors.white), 29 ), 30 ), 31 width: 200.0, 32 height: 200.0, 33 decoration: new BoxDecoration( 34 color: _active ? Colors.lightGreen[700] : Colors.grey[600], 35 ), 36 ), 37 ); 38 } 39} 40
3.2.2 父Widget管理子Widget的状态
对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。 例如,IconButton是一个图标按钮,但它是一个无状态的Widget,因为我们认为父Widget需要知道该按钮是否被点击来采取相应的处理。
在以下示例中,TapboxB通过回调将其状态导出到其父组件,状态由父组件管理,因此它的父组件为StatefulWidget。但是由于TapboxB不管理任何状态,所以TapboxB为StatelessWidget。
ParentWidgetState 类:
- 为TapboxB 管理
_active状态。 - 实现
_handleTapboxChanged(),当盒子被点击时调用的方法。 - 当状态改变时,调用
setState()更新UI。
TapboxB 类:
- 继承
StatelessWidget类,因为所有状态都由其父组件处理。 - 当检测到点击时,它会通知父组件。
1// ParentWidget 为 TapboxB 管理状态. 2 3//------------------------ ParentWidget -------------------------------- 4 5class ParentWidget extends StatefulWidget { 6 7 _ParentWidgetState createState() => new _ParentWidgetState(); 8} 9 10class _ParentWidgetState extends State<ParentWidget> { 11 bool _active = false; 12 13 void _handleTapboxChanged(bool newValue) { 14 setState(() { 15 _active = newValue; 16 }); 17 } 18 19 20 Widget build(BuildContext context) { 21 return new Container( 22 child: new TapboxB( 23 active: _active, 24 onChanged: _handleTapboxChanged, 25 ), 26 ); 27 } 28} 29 30//------------------------- TapboxB ---------------------------------- 31 32class TapboxB extends StatelessWidget { 33 TapboxB({Key key, this.active: false, this.onChanged}) 34 : super(key: key); 35 36 final bool active; 37 final ValueChanged<bool> onChanged; 38 39 void _handleTap() { 40 onChanged(!active); 41 } 42 43 Widget build(BuildContext context) { 44 return new GestureDetector( 45 onTap: _handleTap, 46 child: new Container( 47 child: new Center( 48 child: new Text( 49 active ? 'Active' : 'Inactive', 50 style: new TextStyle(fontSize: 32.0, color: Colors.white), 51 ), 52 ), 53 width: 200.0, 54 height: 200.0, 55 decoration: new BoxDecoration( 56 color: active ? Colors.lightGreen[700] : Colors.grey[600], 57 ), 58 ), 59 ); 60 } 61}
3.2.3 混合状态管理
对于一些组件来说,混合管理的方式会非常有用。在这种情况下,组件自身管理一些内部状态,而父组件管理一些其他外部状态。
在下面TapboxC示例中,手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。 TapboxC将其_active状态导出到其父组件中,但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState和_TapboxCState。
_ParentWidgetStateC 类:
- 管理
_active状态。 - 实现
_handleTapboxChanged(),当盒子被点击时调用。 - 当点击盒子并且
_active状态改变时调用setState()更新UI。
_TapboxCState 对象:
- 管理
_highlight状态。 GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。- 当按下、抬起、或者取消点击时更新
_highlight状态,调用setState()更新UI。 - 当点击时,将状态的改变传递给父组件。
1//---------------------------- ParentWidget ---------------------------- 2 3class ParentWidgetC extends StatefulWidget { 4 5 _ParentWidgetCState createState() => new _ParentWidgetCState(); 6} 7 8class _ParentWidgetCState extends State<ParentWidgetC> { 9 bool _active = false; 10 11 void _handleTapboxChanged(bool newValue) { 12 setState(() { 13 _active = newValue; 14 }); 15 } 16 17 18 Widget build(BuildContext context) { 19 return new Container( 20 child: new TapboxC( 21 active: _active, 22 onChanged: _handleTapboxChanged, 23 ), 24 ); 25 } 26} 27 28//----------------------------- TapboxC ------------------------------ 29 30class TapboxC extends StatefulWidget { 31 TapboxC({Key key, this.active: false, this.onChanged}) 32 : super(key: key); 33 34 final bool active; 35 final ValueChanged<bool> onChanged; 36 37 38 _TapboxCState createState() => new _TapboxCState(); 39} 40 41class _TapboxCState extends State<TapboxC> { 42 bool _highlight = false; 43 44 void _handleTapDown(TapDownDetails details) { 45 setState(() { 46 _highlight = true; 47 }); 48 } 49 50 void _handleTapUp(TapUpDetails details) { 51 setState(() { 52 _highlight = false; 53 }); 54 } 55 56 void _handleTapCancel() { 57 setState(() { 58 _highlight = false; 59 }); 60 } 61 62 void _handleTap() { 63 widget.onChanged(!widget.active); 64 } 65 66 67 Widget build(BuildContext context) { 68 // 在按下时添加绿色边框,当抬起时,取消高亮 69 return new GestureDetector( 70 onTapDown: _handleTapDown, // 处理按下事件 71 onTapUp: _handleTapUp, // 处理抬起事件 72 onTap: _handleTap, 73 onTapCancel: _handleTapCancel, 74 child: new Container( 75 child: new Center( 76 child: new Text(widget.active ? 'Active' : 'Inactive', 77 style: new TextStyle(fontSize: 32.0, color: Colors.white)), 78 ), 79 width: 200.0, 80 height: 200.0, 81 decoration: new BoxDecoration( 82 color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], 83 border: _highlight 84 ? new Border.all( 85 color: Colors.teal[700], 86 width: 10.0, 87 ) 88 : null, 89 ), 90 ), 91 ); 92 } 93}
另一种实现可能会将高亮状态导出到父组件,但同时保持_active状态为内部状态,但如果你要将该TapBox给其它人使用,可能没有什么意义。 开发人员只会关心该框是否处于Active状态,而不在乎高亮显示是如何管理的,所以应该让TapBox内部处理这些细节。
3.2.4 全局状态管理
当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,APP中依赖应用语言的组件能够重新build一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
- 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的
initState方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。 - 使用一些专门用于状态管理的包,如Provider、Redux,读者可以在pub上查看其详细信息。
本书将在"功能型组件"一章中介绍Provider包的实现原理及用法,同时也将会在"事件处理与通知"一章中实现一个全局事件总线,读者有需要可以直接翻看。
