Flutter状态管理之路(一)

背景

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

简单情况是这样的:


状态管理背景1.png

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

状态管理背景2.png

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

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

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

面临的问题

  1. 如何获取数据源

  2. 如何更新数据源

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

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

数据流概念

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

单向数据流

单向数据流.png
  • state:驱动应用的数据源。

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

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

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

特点:
(1) 所有状态的改变可记录、可跟踪,源头易追溯;
(2) 所有数据只有一份,组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性;
(3) 一旦数据变化,就去更新页面(data->页面),但是没有(页面->data);
(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<ChildWidget> {
      @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<ContainerWidget> {
      int _data = 0;
      
      void _incrementCounter() {
        setState(() {               
          _data++;                  /// 改变状态
        });
      }
      
      @override
      Widget build(BuildContext context) {
        return FatherWidget(
          data: widget.data,
          child: ChildWidget(),
        );
      }
    
    }
    

图示

  1. 如何实现子树获取InheritedWidget
InheritedWidget机制1.png
  1. InheritedWidget和用of获取过它的子Widget如何建立联系的


    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<Element, Object> _dependents = HashMap<Element, Object>();
...
  @override
  void _updateInheritance() {
  ...
    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;
  }
  ...
}

获取过程,如下

/// 调用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<InheritedElement>();
    _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. 单向数据流和双向数据流
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容