Flutter状态管理之路(一)

背景

原生提供了StatefulWidget这个有状态组件来管理状态,对于多组件的状态交互可以选择由父组件进行统一管理分发,但是当业务一旦复杂,组件树的分支足够多,会出现状态下沉过深入,状态传递复杂的问题。

简单情况是这样的:


Flutter状态管理之路(一)_第1张图片
状态管理背景1.png

随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样:

Flutter状态管理之路(一)_第2张图片
状态管理背景2.png

上述实际就是多个页面需要共享状态和传递信息场景下出现的,直接的做法是:

  1. 通过父widget来分发通知,有嵌套层级深的问题,父层级的setState导致不必要build问题

  2. 通过回调传递,同样存在传递深的问题 ,回调也会出现漏调用的问题

面临的问题

  1. 如何获取数据源

  2. 如何更新数据源

  3. 如何通知组件数据源更新

  4. 跨组件数据源如何共享

数据流概念

状态管理里会出现基于单向数据流的情况,这里先介绍下数据流的概念

单向数据流

Flutter状态管理之路(一)_第3张图片
单向数据流.png
  • state:驱动应用的数据源。

  • view:以声明方式将 state 映射到视图 。

  • actions:响应在 view 上的用户输入导致的状态变化

单向数据流的状态管理:通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护

特点:
(1) 所有状态的改变可记录、可跟踪,源头易追溯;
(2) 所有数据只有一份,组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性;
(3) 一旦数据变化,就去更新页面(data->页面),但是没有(页面->data);
(4) 如果用户在页面上做了变动,那么就手动收集起来(双向是自动),合并到原有的数据中

双向数据流

Flutter状态管理之路(一)_第4张图片
双向数据流.png

双向数据绑定,带来双向数据流,数据(state)和视图(View)之间的双向绑定。

ng 里的 ng-model 和 vue 里的 v-model,以及Android的DataBinding

说到底就是 (value 的单向绑定 + onChange 事件侦听)的一个语法糖

特点:
(1)无论数据改变,或是用户操作,都能带来互相的变动,自动更新。适用于项目细节,如:UI控件中(通常是类表单操作)。
(2)状态的改变不可控

解决方案

StatefulWidget

官方自带有状态组件,其组件里的各种状态可以由自身管理,也可由父组件管理,哪个管理合适,一般遵循以下原则:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
  • 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。

一般来说,在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活,如果不知道是不是该由widget自身管理,则优先设计为由父widget管理并将其设计为StatelessWidget

问题:

功能单一,复杂多页面情况下,状态下沉过于深入,状态传递复杂,rebuild的范围过大等

InherityWidget

功能型组件,提供了一种数据在widget树中从上到下传递、共享的机制

比如在根Widget通过一个InherityWidget共享了一个状态,那么便可以在任意子widget中获取它,Theme,Navigator等都是通过这种机制来共享给整个应用的,它省去了逐级传递的麻烦

使用

  1. 用于存储共享数据的父Widget,该widget继承InheritedWidget
class FatherWidget extends InheritedWidget {
  final int data;

  FatherWidget({@required this.data, Widget child}) : super(child: child);

  //子树通过该方法获取共享数据
  static FatherWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FatherWidget);
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的widget
  @override
  bool updateShouldNotify(FatherWidget oldWidget) {
    return oldWidget.data != data;
  }
}
  1. 子widget,获取状态和处理依赖发生变化时的响应

    class ChildWidget extends StatefulWidget {
      @override
      _ChildWidgetState createState() => _ChildWidgetState();
    }
    
    class _ChildWidgetState extends State {
      @override
      Widget build(BuildContext context) {
        return new Text(FatherWidget.of(context).data.toString());
      }
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        //父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用
        //如果build中没有依赖InheritedWidget,则此回调不会被调用
        print("didChangeDependencies = " +
            FatherWidget.of(context).data.toString());
      }
    }
    
  2. 整合

    class ContainerWidget extends StatefulWidget {
      @override
      _ContainerWidgetState createState() => _ContainerWidgetState();
    }
    
    class _ContainerWidgetState extends State {
      int _data = 0;
      
      void _incrementCounter() {
        setState(() {               
          _data++;                  /// 改变状态
        });
      }
      
      @override
      Widget build(BuildContext context) {
        return FatherWidget(
          data: widget.data,
          child: ChildWidget(),
        );
      }
    
    }
    

图示

  1. 如何实现子树获取InheritedWidget
Flutter状态管理之路(一)_第5张图片
InheritedWidget机制1.png
  1. InheritedWidget和用of获取过它的子Widget如何建立联系的


    Flutter状态管理之路(一)_第6张图片
    InheritedWidget机制2.png

关键点

"didChangeDependencies":

State里的生命周期函数之一,表示依赖发生变化时由Framework调用通知,这里的依赖指的是子widget是否使用了InherityWidget的数据

另外,此回调紧跟initState执行,这里可以直接.context获取来使用

源码

如何实现子树直接获取InheritedWidget

新建build InheritedWidget时,对应的Element会调用如下_updateInheritance方法,从父Element复制 _inheritedWidgets并将此InheritedElement注册进 _inheritedWidgets里

class InheritedElement extends ProxyElement {
final Map _dependents = HashMap();
...
  @override
  void _updateInheritance() {
  ...
    final Map incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap();
    _inheritedWidgets[widget.runtimeType] = this;
  }
  ...
}

获取过程,如下

/// 调用of方法
static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
    final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
   ...
  }
  
///  Element class
 @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
  ...
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];   /// 根据类型 取出InheritedElement
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);  /// 此处建立子Element和InheritedElement的关系并返回InheritedWidget
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  
  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    ...
    _dependencies ??= HashSet();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

注意:还有个方法是只取InheritedElement而不注册依赖关系的

/// Element class
@override
  InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
    ...
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    return ancestor;
  }

总结

优点:

  1. 自动订阅

  2. 可跨组件获取状态

缺点:

  1. 每次促使inheritedWidget build重建 事实上都会触发所有子树的build,所以需要封装一个StatefulWidget来配合实现缓存加载

  2. 没有有效分离视图逻辑和业务逻辑。

  3. 无法定向通知/指向性通知。 事实上依赖InheriteWidget的子Widget,在调用State的didChangeDependencies前,在Element这一级会调用markNeedsBuild,所以都会rebuild一下

参考

  1. 单向数据流和双向数据流

你可能感兴趣的:(Flutter状态管理之路(一))