Flutter数据共享的利器——InheritedWidget

247 阅读5分钟

一、前言

在flutter中一切皆为widget,许许多多的widget组成一个庞大的widget树。每个widget除了各司其职外,彼此之间还需要相互协作,相互通讯。所谓的通讯也即是一种数据共享与传递的手段。那么flutter中有哪些数据共享的手段呢?

flutter为我们提供了四种数据共享的方案:

  1. InheritedWidget:层级传递,适用于父组件传递给子组件的场景, 可跨层级。
  2. Notification:消息通知,适用于子组件通知父组件数据改变的场景。
  3. Event Bus:事件广播,可适用于各种类型的数据通知同步。
  4. Provider:状态管理,适用于复杂应用,可以方便的实现三种类型的数据同步。

这四种数据共享方案没有利弊之说,主要看适用的场景。

今天,我们重点看下InheritedWidget。它能够提供一种自上而下的数据共享方式,并且能够做到跨越层级。

二、InheritedWidget的使用

InheritedWidget是一个功能型组件(flutter中的widget分为功能型、组合型、绘制型)。功能型组件一般用于实现数据间的共享,如Theme、MediaQuery都是基于InheritedWidget实现的组件。

InheritedWidget使用起来也比较简单,它本身是一个抽象类,只需编写一个实现类即可。

/// InheritedWidget的实现类
class CountInheritedWidget extends InheritedWidget {
  const CountInheritedWidget({Key? key, this.data, required Widget child})
      : super(key: key, child: child);
  /// 共享的数据
  final int? data;
  
  /// 返回当前组件的实例
  static CountInheritedWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CountInheritedWidget>();
  }
  
  /// 数据更新的开关,返回true才能通知子组件更新
  @override
  bool updateShouldNotify(covariant CountInheritedWidget oldWidget) {
    return oldWidget.data != data;
  }
}
  • CountInheritedWidget构造函数需要增加一个child必选属性,并传给父类。
  • 同时需要定义一个of静态方法,通过context.dependOnInheritedWidgetOfExactType返回当前实例,以此来获取组件内部共享的数据data。
  • updateShouldNotify是重写父类方法,它的作用是为我们提供一个数据更新的开关,返回false,data属性状态变化将不会同步到其它子组件上去。
/// 定义子类
class ChildWidget extends StatefulWidget {
  const ChildWidget({Key? key}) : super(key: key);

  @override
  State<ChildWidget> createState() => _ChildWidgetState();
}

class _ChildWidgetState extends State<ChildWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// 通过CountInheritedWidget的静态方法of获取共享数据data
      '共享数据:${CountInheritedWidget.of(context)?.data}',
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint('更新了!');
  }
}
/// 定义父类
class ParentWidget extends StatefulWidget {
  const ParentWidget({Key? key}) : super(key: key);

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  /// 共享数据
  int data = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedWidget demo'),
      ),
      /// CountInheritedWidget的用法
      body: CountInheritedWidget(
        data: data,
        child: const ChildWidget(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            /// data数据改变,会同步通知到子组件
            data++;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

屏幕录制2023-02-01 09.24.30.gif

InheritedWidget为单向数据流,数据只能自上而下,因此共享的数据需要定义到最顶层上。

当data数据改变时,子组件凡是使用到共享的数据,都会进行更新,并且会触发子组件的didChangeDependencies函数。

三、InheritedWidget的原理

InheritedWidget的使用比较简单,那么组件之间的数据是如何做到共享的呢?

带着这个问题,我们来分析下背后的实现原理。

widget树在首次构建的过程中,加载了InheritedWidget组件,会对应的创建InheritedElement。

abstract class InheritedWidget extends ProxyWidget {
  /// ...
  @override
  InheritedElement createElement() => InheritedElement(this);
  
  /// ...
}

所以,InheritedElement才是InheritedWidget核心所在,我们要重点分析它。

class InheritedElement extends ProxyElement {
  /// ...
  @override
  void _updateInheritance() {
    /// 获取父级inheritedWidgets集合
    final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      /// 将父级inheritedWidgets保存到当前_inheritedWidgets属性中
      _inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
    else
      /// 为null再创建新的集合
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    /// 将自身添加到_inheritedWidgets中
    _inheritedWidgets![widget.runtimeType] = this;
  }
  /// ...
}

updateInheritance方法在InheritedElement首次挂载的时候会调用。它的作用就是把所有的InheritedElement都进行收集,并存储在每一个InheritedElement中。

这样的设计可以保证每一层Element都包含同样的inheritedWidgets集合,这便是inheritedWidgets能够一直向下传递的原因。

那么它又是如何获取InheritedWidget中的数据呢?

还记得我们获取共享数据的时候调用了of静态方法,它实际调用的是context.dependOnInheritedWidgetOfExactType。context由外部传入,它代表了当前组件的上下文,即widget所对应的Elememt。所以dependOnInheritedWidgetOfExactType方法是在Element中定义的。

/// Element
  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies!.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

通过分析,调用dependOnInheritedWidgetOfExactType方法会从_inheritedWidgets获取到当前的InheritedWidget,并将它加入_dependencies依赖列表中,最后返回widget对象。

当我们更新共享的数据时,它又是如何通知到依赖组件呢?

在StatefulWidget中进行数据刷新都需要调用setState方法,然后会更新widget树中对应的组件。InheritedWidget也在更新的列表,它最终会触发InheritedElement的updated方法。

  /// InheritedElement
  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      /// 调用父级的updated方法
      super.updated(oldWidget);
  }
  
  /// ComponentElement
  @protected
  void updated(covariant ProxyWidget oldWidget) {
    /// 子类InheritedElement重写了notifyClients,所以实际调用的是子类的notifyClients
    notifyClients(oldWidget);
  }
  
  /// InheritedElement
  @override
  void 
  (InheritedWidget oldWidget) { 
    /// 遍历依赖列表进行更新
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
  
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }

在updated中调用widget.updateShouldNotify判断是否需要更新数据,也即是我们重写的updateShouldNotify方法。

notifyClients遍历_dependents依赖列表进行InheritedElement更新,并且调用didChangeDependencies方法。

总结

1、flutter中有四种数据共享方案InheritedWidget、Notification、Event Bus、Provider。

2、InheritedWidget的使用需要编写一个实现类,除了提供child属性,还需要定义获取数据的方法,通常需要调用context.dependOnInheritedWidgetOfExactType方法返回当前widget对象,最后重写updateShouldNotify可以用于控制数据刷新时是否需要通知子组件。

3、InheritedWidget的核心原理在于:

  • widget树构建时收集所有InheritedElement对象并添加在其_inheritedWidgets属性中,这样就可以实现数据层层传递,达到共享的目的。

  • 而通过dependOnInheritedWidgetOfExactType方法获取数据主要是从_inheritedWidgets属性中获取当前InheritedWidget返回,并将其加入dependents依赖列表。

  • 共享数据改变时,先是判断updateShouldNotify是否为true,再进行更新。更新的方式则是遍历dependents依赖列表进行逐项调用widget的didChangeDependencies方法。因此也能解释了didChangeDependencies只有在子组件依赖父组件数据变化时才会调用。