Flutter状态管理系列之InheritedWidget

4,921 阅读6分钟

简介

对于flutter这种大量依靠组合的widget的形式来实现用户界面的框架来说,跨组件之间的数据传递其实是比较麻烦的。比如简单的widget之间,我们可以通过构造函数来传递数据,但是当组合方式过于复杂的情况,页面层级很多,一个属性就要跨越很多层的widget来做数据传递。这样就会导致很多冗余的字段,和不必要的耦合,写起来也非常的麻烦。 所以对于这种跨层的数据传递,flutter为我们提供了三种解决方式,分别是:

  • InheritedWidget
  • Notifcation
  • eventbus

后面的文章,会逐个分析介绍每一种方式的使用和原理。本文先聊一下InheritedWidget

InheritedWidget

对于开发过flutter,写过dart代码的同学,一定都已经用过InheritedWidget了,只是可能你都不知道是在什么时候用到过

其实flutter中有很多的代码,也是通过InheritedWidget的方式来实现的。 比如说最常用的获取屏幕的宽高,获取主题等,他们都是继承自InheritedWidget的;

MediaQuery.of(context);


Theme.of(context);

简而言之,InheritedWidget 允许在 widget 树中有效地向下传播(和共享)信息。

InheritedWidget 是一个特殊的 Widget,它将作为另一个子树的父节点放置在 Widget 树中。该子树的所有 widget 都必须能够与该 InheritedWidget 暴露的数据进行交互。

使用

下面来看下 InheritedWidget如何使用,以一个小demo为例: 我们有一个用户信息的类 UserBean,当在某一个页面改了用户信息中的数据,在其他页面如何及时刷新用户信息的数据呢?

class UserBean {String name;String address;
UserBean({this.name, this.address});
@overrideString toString() { return 'UserBean{name: $name, address:$address}';}
}

首先创建一个用户信息的UserinfoInheritedWidget 继承自InheritedWidget,目的在于为子树中的所有 widget 提供用户信息数据:

class UserinfoInheritedWidget extends InheritedWidget {


  UserBean userBean;

  UserinfoInheritedWidget({this.userBean, Key key, Widget child}):
  super(key:key, child:child);


  @override
  bool updateShouldNotify(UserinfoInheritedWidget oldWidget) {
    // TODO: implement updateShouldNotify
    if (oldWidget.userBean.name != userBean.name || oldWidget.userBean.address != userBean.address) {
      return true;
    }
    return false;
  }
}

为了能够传播/共享数据,需要将 InheritedWidget 放置在 widget 树的顶部,这解释了传递给 InheritedWidget 基础构造函数的 @required Widget child 参数

static UserinfoInheritedWidget of(BuildContext context) 方法允许所有子 widget 通过包含的 context 获得最近的 UserinfoInheritedWidget实例(参见后面的内容)。

最后重写 updateShouldNotify 方法用来告诉 InheritedWidget 如果对数据进行了修改,是否必须将通知传递给所有子 widget(已注册/已订阅)。

然后在main.dart中 我们需要将UserinfoInheritedWidget放在树节点级别,并初始化一个用户信息UserBean:

Widget build(BuildContext context) {
    return UserinfoInheritedWidget (
    
      userBean: UserBean(name: 'flutter', address: 'China'),
      child: MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    )
    );
  }

子节点如何访问 InheritedWidget 的数据?

然后在页面上取到用户信息,做一个简单的展示,那用户信息如何获取呢,和上面提到的获取屏幕宽高,主题是一样的,用 of函数

static UserinfoInheritedWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);
  }

在构建子节点时,后者将获得 InheritedWidget 的引用

class _MyHomePageState ...{
  ...
  @override
  Widget build(BuildContext context) {
   
   
    return Scaffold(
      appBar: AppBar(
        
        title: Text(widget.title),
      ),
      body: Center(
       
        child: Column(
          
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              UserinfoWidget.of(context).userBean.name,
            ),
            Text(
              UserinfoWidget.of(context).userBean.address,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
    );
  }
}

如何在 Widget 之间进行交互?

InheritedWidget继承自ProxyWidget,ProxyWidget继承自Widget,可以单独使用,但是没有状态,为了有状态,一般和StatefulWidget搭配使用

class UserinfoWidget extends StatefulWidget {
  UserBean userBean;
  Widget child;


  UserinfoWidget({this.userBean, Key key, Widget child}):
  super(key:key);

  static UserinfoInheritedWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);
  }
  
  @override
  _UserinfoWidgetState createState() => _UserinfoWidgetState();
}

class _UserinfoWidgetState extends State<UserinfoWidget> {
  void _update(String name, String address) {
    widget.userBean = UserBean(name: name, address: address);
    setState(() {
      
    });
  }

  @override
  Widget build(BuildContext context) {
    return UserinfoInheritedWidget(
      updateInfo: _update,
      userBean: widget.userBean,
      child: widget.child,
    );
  }
}

继续在UserinfoInheritedWidget中添加更新的方法

class UserinfoInheritedWidget extends InheritedWidget {


  UserBean userBean;
  Function updateInfo;

  UserinfoInheritedWidget({this.userBean, Key key, Widget child, this.updateInfo}):
  super(key:key, child:child);

  

  void updateUserBean (String name, String address) {
    updateInfo(name, address);
  }
  @override
  bool updateShouldNotify(UserinfoInheritedWidget oldWidget) {
    // TODO: implement updateShouldNotify
    if (oldWidget.userBean.name != userBean.name || oldWidget.userBean.address != userBean.address) {
      return true;
    }
    return false;
  }
}

至此,就是InheritedWidget的全部使用方法,上面有一个最主要的问题,就是 context.inheritFromWidgetOfExactType(UserinfoInheritedWidget) 在内部,除了简单地返回 UserinfoInheritedWidget实例外,它还订阅消费者 widget 以便用于通知更改。

在幕后,对这个静态方法的简单调用实际上做了 2 件事:

  • 消费者 widget 被自动添加到订阅者列表中,从而当对 InheritedWidget应用修改时,该 widget 能够重建
  • InheritedWidget中引用的数据将返回给消费者

源码分析

这个方法为什么就可以获取到UserinfoInheritedWidget的实例呢?

InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

Map<Type, InheritedElement> _inheritedWidgets; _inheritedWidgets是这样定义的一个map,以type为key,所以用我们传入的UserinfoInheritedWidget这个type去找与他对应的InheritedElement 再看下这个map是如何赋值的:

void _updateInheritance() {
    assert(_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;
  }

通过分析源码就可以明白,这里就是把type作为可以 自身this作为value,存储在这个map中, 假设一个子节点InheritedWidget的实例,持有_inheritedWidgets数组,这个数组的值,首先是把这个InheritedWidget父节点的_inheritedWidgets值赋值给子节点,然后子节点在把自己的实例添加到这个数组,所以你也就可以明白为啥InheritedWidget的作用域是自己及自己的子节点了

那_updateInheritance方法又是在何处调用呢,就是下图所示的mount方法,查看mount方法的注释可以知道在创建element的时候就会被调用

当然说到这里,还需要我们去好好了解一下,widget、element,renderobject之间的关系

阻止重新绘制

在继续访问 Inherited Widget 的同时阻止某些 Widget 重建 上面的整个介绍说明了InheritedWidget的工作原理,但是还有一个需要考虑的问题,就是,在InheritedWidget的子树中,如果某一个节点,可能只是发出一个更新的指令,但他本身并不需要重新绘制,如果我们不去处理,肯定是会降低绘制的效率的,因为每次更新都要重新绘制本不需要绘制的节点。

如前所述,调用 context.inheritFromWidgetOfExactType() 方法实际上会自动将 Widget 订阅到消费者列表中。 避免自动订阅,同时仍然允许 Widget 访问 InheritedWidget 的解决方案是通过以下方式改造 UserinfoInheritedWidget 的静态方法:

static UserinfoInheritedWidget of([BuildContext context, bool rebuild = true]) {
    // return context.inheritFromWidgetOfExactType(UserinfoInheritedWidget);


    return (rebuild ? context.inheritFromWidgetOfExactType(UserinfoInheritedWidget) as UserinfoInheritedWidget
                    : (context.ancestorWidgetOfExactType(UserinfoInheritedWidget) as UserinfoInheritedWidget).userBean);
  }

通过添加一个 boolean 类型的额外参数

如果 rebuild 参数为 true(默认值),我们使用普通方法(并且将 Widget 添加到订阅者列表中) 如果 rebuild 参数为 false,我们仍然可以访问数据,但不使用 InheritedWidget 的内部实现

因此,要完成在继续访问 Inherited Widget 的同时阻止某些 Widget 重建,还需要再修改一下使用InheritedWidget的代码

UserinfoWidget.of(context, false).updateUserBean('name', 'address');

就是这样,做更新的widget,它不会再重建了。

下面附上demo代码的链接,因为代码是后面整理的,代码中最后又做了一些细节的优化,所以跟文章稍微有一些出入,但是对InheritedWidget的使用是没问题的 demo代码传送门