Flutter如何状态管理

【Flutter如何状态管理】少年辛苦终身事,莫向光阴惰寸功。这篇文章主要讲述Flutter如何状态管理相关的知识,希望能为你提供帮助。
目录介绍

  • 01.什么是状态管理
  • 02.状态管理方案分类
  • 03.状态管理使用场景
  • 04.Widget管理自己的状态
  • 05.Widget管理子Widget状态
  • 06.简单混合管理状态
  • 07.全局状态如何管理
  • 08.Provider使用方法
  • 09.订阅监听修改状态
推荐
  • fluter Utils 工具类库:https://github.com/yangchong211/YCFlutterUtils
  • flutter 混合项目代码案例:https://github.com/yangchong211/YCHybridFlutter
01.什么是状态管理
  • 响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”
    • 在Flutter中,想一个问题,StatefulWidget的状态应该被谁管理?
    • Widget本身?父Widget?都会?还是另一个对象?答案是取决于实际情况!
  • 以下是管理状态的最常见的方法:
    • Widget管理自己的状态。
    • Widget管理子Widget状态。
    • 混合管理(父Widget和子Widget都管理状态)。
    • 不同模块的状态管理。
  • 如何决定使用哪种管理方法?下面给出的一些原则可以帮助你做决定:
    • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
    • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
    • 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
    • 如果是多个模块需要公用一个状态,那么该怎么处理呢,那可以用Provider。
    • 如果修改了某一个属性,需要刷新多个地方数据。比如修改用户城市id数据,那么则刷新首页n处的接口数据,这个时候可以用订阅监听修改状态
02.状态管理方案分类
  • setState状态管理
    • 优点:
      • 简单场景下特别适用,逻辑简单,易懂易实现
      • 所见即所得,效率比较高
    • 缺点
      • 逻辑与视图耦合严重,复杂逻辑下可维护性很差
      • 数据传输基于依赖传递,层级较深情况下不易维护,可读性差
  • InheritedWidget状态管理
    • 优点
      • 方便数据传输,可以基于InheritedWidget达到逻辑和视图解耦的效果
      • flutter内嵌类,基础且稳定,无代码侵入
    • 缺点
      • 属于比较基础的类,友好性不如封装的第三方库
      • 对于性能需要额外注意,刷新范围如果过大会影响性能
  • Provider状态管理
    • 优点
      • 功能完善,涵盖了ScopedModel和InheritedWidget的所有功能
      • 数据逻辑完美融入了widget树中,代码结构清晰,可以管理局部状态和全局状态
      • 解决了多model和资源回收的问题
      • 对不同场景下使用的provider做了优化和区分
      • 支持异步状态管理和provider依赖注入
    • 缺点
      • 使用不当可能会造成性能问题(大context引起的rebuild)
      • 局部状态之前的数据同步不支持
  • 订阅监听修改状态
    • 有两种:一种是bus事件通知(是一种订阅+观察),另一个是接口注册回调。
    • 接口回调:由于使用了回调函数原理,因此数据传递实时性非常高,相当于直接调用,一般用在功能模块上。
    • bus事件:组件之间的交互,很大程度上降低了它们之间的耦合,使得代码更加简洁,耦合性更低,提升我们的代码质量。
03.状态管理使用场景
  • setState状态管理
    • 适合Widget管理自己的状态,这种很常见,调用setState刷新自己widget改变状态。
    • 适合Widget管理子Widget状态,这种也比较常见。不过这种关联性比较强。
04.Widget管理自己的状态
  • _TapboxAState 类:
    • 管理TapboxA的状态。
    • 定义_active:确定盒子的当前颜色的布尔值。
    • 定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。
    • 实现widget的所有交互式行为。
  • 代码如下
    // TapboxA 管理自身状态.//------------------------- TapboxA ----------------------------------class TapboxA extends StatefulWidget { TapboxA({Key key}) : super(key: key); @override _TapboxAState createState() => new _TapboxAState(); }class _TapboxAState extends State< TapboxA> { bool _active = false; void _handleTap() { setState(() { _active = !_active; }); }Widget build(BuildContext context) { return new GestureDetector( onTap: _handleTap, child: new Container( child: new Center( child: new Text( _active ? \'Active\' : \'Inactive\', style: new TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: new BoxDecoration( color: _active ? Colors.lightGreen[700] : Colors.grey[600], ), ), ); } }

05.Widget管理子Widget状态
  • 先看一下下面这些是什么
    typedef ValueChanged< T> = void Function(T value);

  • 对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。
    • 例如,IconButton是一个图标按钮,但它是一个无状态的Widget,因为我们认为父Widget需要知道该按钮是否被点击来采取相应的处理。
    • 在以下示例中,TapboxB通过回调将其状态导出到其父组件,状态由父组件管理,因此它的父组件为StatefulWidget
  • ParentWidgetState 类:
    • 为TapboxB 管理_active状态。
    • 实现_handleTapboxChanged(),当盒子被点击时调用的方法。
    • 当状态改变时,调用setState()更新UI。
  • TapboxB 类:
    • 继承StatelessWidget类,因为所有状态都由其父组件处理。
    • 当检测到点击时,它会通知父组件。
    // ParentWidget 为 TapboxB 管理状态.class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState() => new _ParentWidgetState(); }class _ParentWidgetState extends State< ParentWidget> { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); }@override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Widget管理子Widget状态"), ), body: new ListView( children: [ new Text("Widget管理子Widget状态"), new TapboxB( active: _active, onChanged: _handleTapboxChanged, ), ], ), ); } }//------------------------- TapboxB ----------------------------------class TapboxB extends StatefulWidget{final bool active; final ValueChanged< bool> onChanged; TapboxB({Key key , this.active : false ,@required this.onChanged }); @override State< StatefulWidget> createState() { return new TabboxBState(); }}class TabboxBState extends State< TapboxB> {void _handleTap() { widget.onChanged(!widget.active); }@override Widget build(BuildContext context) { return new GestureDetector( onTap: _handleTap, child: new Container( child: new Center( child: new Text( widget.active ? \'Active\' : \'Inactive\', ), ), width: 100, height: 100, decoration: new BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[850], ), ), ); } }

06.简单混合管理状态
  • 对于一些组件来说,混合管理的方式会非常有用。
    • 在这种情况下,组件自身管理一些内部状态,而父组件管理一些其他外部状态。
  • 在下面TapboxC示例中
    • 手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。
    • TapboxC将其_active状态导出到其父组件中,但在内部管理其_highlight状态。
    • 这个例子有两个状态对象_ParentWidgetState_TapboxCState
  • _ParentWidgetStateC类:
    • 管理_active 状态。
    • 实现 _handleTapboxChanged() ,当盒子被点击时调用。
    • 当点击盒子并且_active状态改变时调用setState()更新UI。
  • _TapboxCState 对象:
    • 管理_highlight 状态。
    • GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
    • 当按下、抬起、或者取消点击时更新_highlight状态,调用setState()更新UI。
    • 当点击时,将状态的改变传递给父组件。
    //---------------------------- ParentWidget ----------------------------class ParentWidgetC extends StatefulWidget { @override _ParentWidgetCState createState() => new _ParentWidgetCState(); }class _ParentWidgetCState extends State< ParentWidgetC> { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); }@override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("简单混合管理状态"), ), body: new Container( child: new ListView( children: [ new Text("_ParentWidgetCState状态管理"), new Padding(padding: EdgeInsets.all(10)), new Text( _active ? \'Active\' : \'Inactive\', ), new Padding(padding: EdgeInsets.all(10)), new Text("_TapboxCState状态管理"), new TapboxC( active: _active, onChanged: _handleTapboxChanged, ) ], ), ), ); } }//----------------------------- TapboxC ------------------------------class TapboxC extends StatefulWidget { TapboxC({Key key, this.active: false, @required this.onChanged}) : super(key: key); final bool active; final ValueChanged< bool> onChanged; @override _TapboxCState createState() => new _TapboxCState(); }class _TapboxCState extends State< TapboxC> { bool _highlight = false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight = true; }); }void _handleTapUp(TapUpDetails details) { setState(() { _highlight = false; }); }void _handleTapCancel() { setState(() { _highlight = false; }); }void _handleTap() { widget.onChanged(!widget.active); }@override Widget build(BuildContext context) { // 在按下时添加绿色边框,当抬起时,取消高亮 return new GestureDetector( onTapDown: _handleTapDown, // 处理按下事件 onTapUp: _handleTapUp, // 处理抬起事件 onTap: _handleTap, onTapCancel: _handleTapCancel, child: new Container( child: new Center( child: new Text(widget.active ? \'Active\' : \'Inactive\', style: new TextStyle(fontSize: 32.0, color: Colors.white)), ), width: 200.0, height: 200.0, decoration: new BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], border: _highlight ? new Border.all( color: Colors.teal[700], width: 10.0, ) : null, ), ), ); } }

07.全局状态如何管理
  • 当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。
    • 比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,APP中依赖应用语言的组件能够重新build一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。
    • 这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。
  • 目前主要有两种办法:
    • 1.实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。
    • 2.使用一些专门用于状态管理的包,如Provider、Redux,读者可以在pub上查看其详细信息。
  • 举一个简答的案例来实践
    • 本实例中,使用Provider包来实现跨组件状态共享,因此我们需要定义相关的Provider。
    • 需要共享的状态有登录用户信息、APP主题信息、APP语言信息。由于这些信息改变后都要立即通知其它依赖的该信息的Widget更新,所以我们应该使用ChangeNotifierProvider,另外,这些信息改变后都是需要更新Profile信息并进行持久化的。
    • 综上所述,我们可以定义一个ProfileChangeNotifier基类,然后让需要共享的Model继承自该类即可,ProfileChangeNotifier定义如下:
      class ProfileChangeNotifier extends ChangeNotifier { Profile get _profile => Global.profile; @override void notifyListeners() { Global.saveProfile(); //保存Profile变更 super.notifyListeners(); //通知依赖的Widget更新 } }

    • 用户状态
      • 用户状态在登录状态发生变化时更新、通知其依赖项,我们定义如下:
      class UserModel extends ProfileChangeNotifier { User get user => _profile.user; // APP是否登录(如果有用户信息,则证明登录过) bool get isLogin => user != null; //用户信息发生变化,更新用户信息并通知依赖它的子孙Widgets更新 set user(User user) { if (user?.login != _profile.user?.login) { _profile.lastLogin = _profile.user?.login; _profile.user = user; notifyListeners(); } } }

08.Provider使用方法
8.1 正确地初始化 Provider
  • 如下所示,create是必须要传递的参数
    ChangeNotifierProvider( create: (_) => MyModel(), child: ... )

  • 实际开发中如何应用
    builder: (BuildContext context, Widget child) { return MultiProvider(providers: [ ChangeNotifierProvider(create: (context) => BusinessPattern()), ]); },

  • 然后看一下BusinessPattern是什么?
    class BusinessPattern extends ChangeNotifier { PatternState currentState = PatternState.none; void updateBusinessPatternState(PatternState state) { if (currentState.index != state.index) { LogUtils.d(\'当前模式:$currentState\'); LogUtils.d(\'更新模式:$state\'); currentState = state; notifyListeners(); } } }

8.2 如何获取Provider取值
  • 一种是 Provider.of(context) 比如:
    Widget build(BuildContext context) { final text = Provider.of< String> (context); return Container(child: Text(text)); }

    • 遇到的问题:由于 Provider 会监听 Value 的变化而更新整个 context 上下文,因此如果 build 方法返回的 Widget 过大过于复杂的话,刷新的成本是非常高的。那么我们该如何进一步控制 Widget 的更新范围呢?
    • 解决办法:一个办法是将真正需要更新的 Widget 封装成一个独立的 Widget,将取值方法放到该 Widget 内部。
    Widget build(BuildContext context) { return Container(child: MyText()); }class MyText extends StatelessWidget { @override Widget build(BuildContext context) { final text = Provider.of< String> (context); return Text(text); } }

  • Consumer 是 Provider 的另一种取值方式
    • Consumer 可以直接拿到 context 连带 Value 一并传作为参数传递给 builder ,使用无疑更加方便和直观,大大降低了开发人员对于控制刷新范围的工作成本。
    Widget getWidget2(BuildContext context) { return Consumer< BusinessPattern> (builder: (context, businessModel, child) { switch (businessModel.currentState) { case PatternState.none: returnText("无模式"); break; case PatternState.normal: return Text("正常模式"); break; case PatternState.small: return Text("小屏模式"); break; case PatternState.overview: return Text("全屏模式"); break; default: return Text("其他模式"); return SizedBox(); } }); }

  • Selector 是 Provider 的另一种取值方式
    • Selector 是 3.1 推出的功能,目的是更近一步的控制 Widget 的更新范围,将监听刷新的范围控制到最小
    • selector:是一个 Function,传入 Value ,要求我们返回 Value 中具体使用到的属性。
    • shouldRebuild:这个 Function 会传入两个值,其中一个为之前保持的旧值,以及此次由 selector 返回的新值,我们就是通过这个参数控制是否需要刷新 builder 内的 Widget。如果不实现 shouldRebuild ,默认会对 pre 和 next 进行深比较(deeply compares)。如果不相同,则返回 true。
    • builder:返回 Widget 的地方,第二个参数 定义的参数,就是我们刚才 selector 中返回的 参数。
    Widget getWidget4(BuildContext context) { return Selector< BusinessPattern, PatternState> ( selector: (context, businessPattern) => businessPattern.currentState, builder: (context, state, child) { switch (state) { case PatternState.none: returnText("无模式"); break; case PatternState.normal: return Text("正常模式"); break; case PatternState.small: return Text("小屏模式"); break; case PatternState.overview: return Text("全屏模式"); break; default: return Text("其他模式"); return SizedBox(); } } );

8.3 修改Provider状态
  • 如何调用修改状态管理
    BusinessPatternService _patternService = serviceLocator< BusinessPatternService> (); //修改状态 _patternService.nonePattern(); _patternService.normalPattern();

  • 然后看一下normalPattern的具体实现代码
    class BusinessPatternServiceImpl extends BusinessPatternService {final BuildContext context; BusinessPatternServiceImpl(this.context); PatternState get currentPatternState => _getBusinessPatternState(context).currentState; BusinessPattern _getBusinessPatternState(BuildContext context) { return Provider.of< BusinessPattern> (context, listen: false); }@override void nonePattern() { BusinessPattern _patternState = _getBusinessPatternState(context); _patternState.updateBusinessPatternState(PatternState.none); }@override void normalPattern() { BusinessPattern _patternState = _getBusinessPatternState(context); _patternState.updateBusinessPatternState(PatternState.normal); } }

8.4 关于Provider刷新
  • 状态发生变化后,widget只会重新build,而不会重新创建(重用机制跟key有关,如果key发生变化widget就会重新生成)
09.订阅监听修改状态
  • 首先定义抽象类。还需要写上具体的实现类
    typedef LocationDataChangedFunction = void Function(double); abstract class LocationListener { /// 注册数据变化的回调 void registerDataChangedFunction(LocationDataChangedFunction function); /// 移除数据变化的回调 void unregisterDataChangedFunction(LocationDataChangedFunction function); /// 更新数据的变化 void locationDataChangedCallback(double angle); }class LocationServiceCenterImpl extends LocationListener {List< LocationDataChangedFunction> _locationDataChangedFunction = List(); @override void locationDataChangedCallback(double angle) { _locationDataChangedFunction.forEach((function) { function.call(angle); }); }@override void registerDataChangedFunction(LocationDataChangedFunction function) { _locationDataChangedFunction.add(function); }@override void unregisterDataChangedFunction(LocationDataChangedFunction function) { _locationDataChangedFunction.remove(function); } }

  • 那么如何使用呢?在需要用的页面添加接口回调监听
    _locationListener.registerDataChangedFunction(_onDataChange); void _onDataChange(double p1) { //监听回调处理 }

  • 那么如何发送事件,这个时候
    LocationListener _locationListener = locationService(); _locationListener.locationDataChangedCallback(520.0);

fluter Utils 工具类库:https://github.com/yangchong211/YCFlutterUtils
flutter 混合项目代码案例:https://github.com/yangchong211/YCHybridFlutter

    推荐阅读