Flutter 之 InheritedWidget

2,852 阅读8分钟

我们在进行Flutter开发时,经常会遇到数据传递的问题。我们可能会逐级逐级一层一层传递,由于Widget 树的关系,Widget层级可以做得非常深,在这些层级间传递数据会让效率变得很低。也可能在某一层widget不需要这些数据,但是在下一层会使用的这些数据,造成了不需要数据的widget却也持有数据,显得冗余且不优雅。

如:

return A(
  data:data
  child:B(
    data:data
    child:C(
      data:data
      child:D(
        data:data
      )   
    )
  )
);

其中可能B和C都没用到data,只是D使用到了data,却也不得不传递data,就显得很呆!

为了解决以上问题,Flutter提供了InheritedWidget这样一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式,即在父widget 中通过InheritedWidget共享了一个数据,那么在任意子widget都能获取该共享的数据!

例如我平时使用到的MediaQuery.of(context)Theme.of(context),都是使用InheritedWidget来实现数据共享的。

实际上Flutter中使用setState()InheritedWidget足以解决绝大部分的状态管理问题,现有的很多状态管理框架也是基于InheritedWidget封装的。

计时器的简单使用

首先通过继承InheritedWidget,将当前计数器点击数据保存在CounterInheritedWidget中:

  • CounterInheritedWidget
class CounterInheritedWidget extends InheritedWidget {
  /// 构造方法
  const CounterInheritedWidget({Key? key, required this.count,
    required Widget child}):super(key:key, child: child);

  /// 需要共享的数据
  final int count;
  
  /// 默认的约定:如果状态是希望暴露出的,应当提供一个`of`静态方法来获取其对象,开发者便可直接通过该方法来获取
  /// 返回实例对象,方便子树中的widget获取共享数据
  static CounterInheritedWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
  }

  /// 是否通知widget树中依赖该共享数据的子widget
  /// 这里当count发生变化时,是否通知子树中所有依赖count的Widget重新build
  /// 这里判断注意:是值改变还是内存地址改变。
  @override
  bool updateShouldNotify(covariant CounterInheritedWidget oldWidget) {
    return count != oldWidget.count;
  }
}

我们自定义2个Widget(分别继承StatelessWidgetStatefulWidget)来模拟多个子Widget共享数据:

  • CounterOneWidget :
class CounterOneWidget extends StatelessWidget {
  const CounterOneWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100, height: 100,
      color: Colors.redAccent,
      alignment: Alignment.center,
      child: Text(CounterInheritedWidget.of(context)!.count.toString(),style: const TextStyle(fontSize: 20),),
    );
  }
}
  • CounterTwoWidget:
class CounterTwoWidget extends StatefulWidget {
  const CounterTwoWidget({Key? key}) : super(key: key);
  @override
  State<CounterTwoWidget> createState() => _CounterTwoWidgetState();
}

class _CounterTwoWidgetState extends State<CounterTwoWidget> {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100, height: 100,
      color: Colors.greenAccent,
      alignment: Alignment.center,
      child: Text(CounterInheritedWidget.of(context)!.count.toString(),style: const TextStyle(fontSize: 20),),
    );
  }
}
  • page页面
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("InheritedWidget"),),
      body: CounterInheritedWidget( /// 父节点使用 
        count: _count,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: const [ /// 所有子节点均可以共享数据
              CounterOneWidget(),
              CounterTwoWidget(),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          if(!mounted) return;
          setState(() {
            _count += 1;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 效果:

counter.gif

进阶使用

日常开发中,我们经常会遇到跨页面共享数据。且在某一个页面更改数据,所有依赖页面的数均会改变,例如Theme - 改变主题功能。

场景:

我们来使用InheritedWidget来实现这样的场景:两个页面共享数据,且在一个页面更改数据,两个页面均会发生改变。

分析:

  • 跨页面 : InheritedWidget需要在MaterialApp的上层节点
  • 更新状态 : 需要使用StatefulWidgetsetState()

实现:

  • Person类:
/// 我们使用更加贴合日常开发的模型类作为需要共享的数据来类比。
class Person{
  String name;
  int age;
  Person({required this.name, required this.age});
}
  • PersonInheritedWidget:
class PersonInheritedWidget extends InheritedWidget {
  /// 构造方法
  const PersonInheritedWidget({Key? key, required this.person, 
    required this.updateCallback,required Widget child}): super(key:key, child: child);

  /// 需要共享的数据
  final Person person;
  /// 我们使用回调方法来更新数据
  final Function(Person person) updateCallback;

  /// 定义一个便捷方法,获取对象,方便子树中的widget获取共享数据
  static PersonInheritedWidget? of(BuildContext context) {
      return context.dependOnInheritedWidgetOfExactType<PersonInheritedWidget>();
  }
  /// 提供一个刷新方法
  void updateInfo(Person person){
    updateCallback(person);
  }

  /// 是否通知依赖该树共享数据的子widget
  @override
  bool updateShouldNotify(covariant PersonInheritedWidget oldWidget) {
    return person != oldWidget.person;
  }
}
  • PersonStateWidget
class PersonStateWidget extends StatefulWidget { 
  const PersonStateWidget({Key? key,required this.person,required this.child}) : super(key: key);
  final Person person;
  final Widget child;
  @override
  _PersonStateWidgetState createState() => _PersonStateWidgetState();
}

class _PersonStateWidgetState extends State<PersonStateWidget> {
  late Person _person;
  
  @override
  void initState() {
    // TODO: implement initState
    _person = widget.person;
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return PersonInheritedWidget(person: _person, updateCallback: _updateCallback, child: widget.child);
  }
  /// 使用setState更新数据
  void _updateCallback(Person person){
    if(!mounted) return;
    setState(() {
      _person = person;
    });
  }
}
  • MyApp
/// 由于跨页面,`InheritedWidget`需要在`MaterialApp`的上层节点
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return PersonStateWidget(
      person: Person(name: "张三",age: 18),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.orange,
        ),
        home: const FirstPage(),
      ),
    );
  }
}
  • FirstPage
class FirstPage extends StatefulWidget {
  const FirstPage({Key? key}) : super(key: key);
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("FirstPage"),),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Text("姓名:${PersonInheritedWidget.of(context)!.person.name}",style: const TextStyle(fontSize: 20),),
            Text("年龄:${PersonInheritedWidget.of(context)!.person.age}",style: const TextStyle(fontSize: 20),),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){ /// 跳转下一页
          Navigator.push(context, MaterialPageRoute(builder: (context) {
            return const PersonTwoPage();
          }));
        },
        child: const Icon(Icons.chevron_right_outlined),
      ),
    );
  }
}
  • SecondPage
class SecondPage extends StatefulWidget {
  const SecondPage({Key? key}) : super(key: key);
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("SecondPage"),),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Text("姓名:${PersonInheritedWidget.of(context)!.person.name}",style: const TextStyle(fontSize: 20),),
            Text("年龄:${PersonInheritedWidget.of(context)!.person.age}",style: const TextStyle(fontSize: 20),),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){ /// 点击更改数据
          PersonInheritedWidget.of(context)?.updateCallback(Person(name: "李四",age: 28));
        },
        child: const Icon(Icons.change_circle_outlined),
      ),
    );
  }
}
  • 效果:

demo.gif

didChangeDependencies

额外提一下这个方法:didChangeDependencies

我们知道在StatefulWidgetState对象有一个didChangeDependencies回调,它会在“依赖”发生变化时被Flutter 框架调用。而这个“依赖”指的就是子 widget 是否使用了父 widget 中InheritedWidget的数据!如果使用了,则代表子 widget 有依赖。这种机制可以使子组件在所依赖的InheritedWidget变化时来更新自身!

一般来说子 widget 很少会重写此方法,因为在依赖发送改变后,会调用build()方法构建组件树。如果我们需要在重构后做一些耗时、昂贵的操作,如网络请求,就可以在此方法中执行,这也是 didChangeDependencies 生命周期存在的意义。

源码分析

我们能读取共享数据的前提是通过一个自定义的静态方法of来获取InheritedWidget对象。of静态方法调用了dependOnInheritedWidgetOfExactType()方法。其实我们还可以通过另外一个方法context.getElementForInheritedWidgetOfExactType().widget来获取对象:

/// 定义一个便捷方法,获取对象,方便子树中的widget获取共享数据
static PersonInheritedWidget? of(BuildContext context) {
  return context.getElementForInheritedWidgetOfExactType<PersonInheritedWidget>()!.widget as PersonInheritedWidget;
}

对比两者实现源码:

  • dependOnInheritedWidgetOfExactType()源码:
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  /// 断言,用于监测在调试模式下是否有正在使用的父Widget
  assert(_debugCheckStateIsActiveForAncestorLookup());
  /// 获取到_inheritedWidgets数组数据
 final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) {
    /// 返回并调用更新方法
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}
  • getElementForInheritedWidgetOfExactType()源码:
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  return ancestor;
}

对比之后我们最直观的发现就是dependOnInheritedWidgetOfExactType()源码中,多调用了一个dependOnInheritedElement()方法,查看其源码:

  • dependOnInheritedElement()
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
 assert(ancestor != null);
 _dependencies ??= HashSet<InheritedElement>();
 _dependencies!.add(ancestor);
 ancestor.updateDependencies(this, aspect);
 return ancestor.widget;
}

可以看到dependOnInheritedElement()方法中主要是注册了依赖关系!

两个方法的区别就是前者会注册依赖关系,而后者不会。所以当前者 InheritedWidget 发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的 didChangeDependencies() 方法和 build() 方法。而当调用的是  getElementForInheritedWidgetOfExactType() 时,由于没有注册依赖关系,所以之后当 InheritedWidget 发生变化时,就不会更新相应的子孙Widget。

其次在两者源码中,首先在 _inheritedWidgets 查找对应的 InheritedElement

其中 _inheritedWidgets

Map<Type, InheritedElement> _inheritedWidgets;

查找其赋值的源码:

@override
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();
  _inheritedWidgets![widget.runtimeType] = this;
}

其中_updateInheritance()Element也调用了:

void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  _inheritedWidgets = _parent?._inheritedWidgets;
}

上述源码说明,每个Element 中都含有 _inheritedWidgets 集合。

InheritedElementElement_inheritedWidgets 等于父组件的 _inheritedWidgets,而 InheritedElement 会将自身添加到 _inheritedWidgets 中,系统通过此方式将组件和 InheritedWidgets 的依赖关系层层向下传递。

再深入,可以看到在mountactivate 中均调用了 _updateInheritance 。说明在当前组件mount和activate阶段,系统调用 _updateInheritance 方法将 InheritedWidget类型的父组件添加到 _inheritedWidgets 集合中。

当依赖子组件通过自定义静态方法of调用 ancestor.updateDependencies 时:

  • ancestor.updateDependencies源码:
@protected
void updateDependencies(Element dependent, Object aspect) {
  setDependencies(dependent, null);
}

@protected
  void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
  }

向  _dependents 中添加注册,InheritedWidget 组件更新时可以更具此列表通知子组件。

我们知道widget发生变化时会调用 update方法:

  • update源码:
@override
void update(ProxyWidget newWidget) {
  final ProxyWidget oldWidget = widget;
  assert(widget != null);
  assert(widget != newWidget);
  super.update(newWidget);
  assert(widget == newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();
}

其中 InheritedElement 重写了 updated 方法:

@override
void updated(InheritedWidget oldWidget) {
  if (widget.updateShouldNotify(oldWidget))
    super.updated(oldWidget);
}

当 updateShouldNotify 返回 true时,执行更新操作。而其父类的 updated方法如下:

@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

在看看 notifyClients中实现了什么:

  • notifyClients源码:
@override
void notifyClients(InheritedWidget oldWidget) {
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  for (final Element dependent in _dependents.keys) {
    assert(() {
      // check that it really is our descendant
      Element? ancestor = dependent._parent;
      while (ancestor != this && ancestor != null)
        ancestor = ancestor._parent;
      return ancestor == this;
    }());
    // check that it really depends on us
    assert(dependent._dependencies!.contains(this));
    notifyDependent(oldWidget, dependent);
  }
}

一眼就看到遍历依赖的子组件集合: _dependents,并调用 notifyDependent

  • notifyDependent源码:
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  dependent.didChangeDependencies();
}

这里看到了熟悉的方法:didChangeDependencies()。再次说明了依赖的子组件发生变化,重建时候会调用 didChangeDependencies

总结

  • widget在 mount 和 activate 阶段,系统调用  _updateInheritance 方法,将 InheritedWidget 类型的父组件添加到 _inheritedWidgets 集合中。
  • 子组件调用 dependOnInheritedWidgetOfExactType 时,从_inheritedWidgets 集合中获取指定的 InheritedWidget 类型的父组件,并将当前组件注册到 InheritedWidget 类型父组件的  _dependents 集合中。
  • InheritedWidget 组件数据发生变化(updateShouldNotify 方法返回true),重建时,InheritedWidget 组件遍历  _dependents 集合中所有依赖的子组件,执行子组件的 didChangeDependencies 的方法。