Flutter 理解数据共享组件 InheritedWidget

标签: 编程学习 Flutter学习

InheritedWidget 是Flutter中非常重要的一个功能型组件,它提供了一种数据在widget 树中从上到下传递、共享的方式,比如我们在应用的根 widget 中通过 InheritedWidget 共享了一个数据,那么我们便可以在任意子 widget 中来获取该共享的数据!这个特性在一些需要在widget树中共享数据的场景中非常方便!如 Flutter SDK中 正是通过 InheritedWidget 来共享应用主题(Theme)和语言环境 (Locale)信息的。

先来看一个案例,下面的案例也可以实现 Flutter 的官方 demo,点击按钮实现数字增加的操作,不同的是这里实现的是点击父组件上面的按钮,子组件上面的数据进行变化,这里就是使用了 InheritedWidget 来实现父子组件的数据共享操作

class InheritedDemo extends InheritedWidget {
  final int data; //需要在子树中共享的数据,保存点击次数
  InheritedDemo(this.data, {Widget child}) : super(child: child);

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static InheritedDemo of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedDemo>();
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(InheritedDemo old) {
    //如果返回true,则子树中依赖(build函数中有调用)本widget
    //的子widget的`state.didChangeDependencies`会被调用
    return old.data != data;
  }
}

子组件代码,主要是获取共享数据并展示

class InheritedTest extends StatefulWidget {
  @override
  _InheritedTestState createState() => new _InheritedTestState();
}

class _InheritedTestState extends State<InheritedTest> {
  @override
  Widget build(BuildContext context) {
    //使用InheritedWidget中的共享数据,如果这里没有依赖 InheritedDemo 的话再怎么点击按钮这里的 didChangeDependencies 也不会被调用
    return Text(InheritedDemo.of(context).data.toString());
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
    //如果build中没有依赖InheritedWidget,则此回调不会被调用。
    print("Dependencies change");
  }
}

父组件,这里主要进行数据的操作

class InheritedTestWidget extends StatefulWidget {
  @override
  _InheritedTestWidgetState createState() => new _InheritedTestWidgetState();
}

class _InheritedTestWidgetState extends State<InheritedTestWidget> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('测试'),
      ),
      body: Center(
        //外层使用 Inherited 组件进行包裹,并设置共享数据字段
        child: InheritedDemo(
          count,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(bottom: 20.0),
                child: InheritedTest(),//子 widget 中依赖 InheritedDemo
              ),
              RaisedButton(
                child: Text("点我"),
                //每点击一次,将count自增,然后重新build, InheritedDemo 的 data 将被更新
                onPressed: () => setState(() => ++count),
              )
            ],
          ),
        ),
      ),
    );
  }
}

注意这里的重点,为什么这样就能让子组件获取值了呢?最主要的是子组件中获取数据的那句代码,来看一下具体实现

@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //注意这里
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

来看一下 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 方法中主要是注册了依赖关系!看到这里也就清晰了,调用 dependOnInheritedWidgetOfExactType() 会注册依赖关系,所以在调用 dependOnInheritedWidgetOfExactType() 时,InheritedWidget 和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的 didChangeDependencies() 方法和 build() 方法。

如果子组件只是想获取 值,并不想子组件调用 didChangeDependencies ,我们只需要吧 dependOnInheritedWidgetOfExactType() 换为 getElementForInheritedWidgetOfExactType() 即可,唯一的区别就是没有注册子组件的依赖关系。

但是注意,如果将上面示例中 InheritedDemo.of() 方法实现改成调用 getElementForInheritedWidgetOfExactType(),运行示例后,点击 "点我" 按钮,发现子组件的didChangeDependencies() 方法确实不会再被调用,但是其 build() 仍然会被调用!造成这个的原因其实是,点击 "点我" 按钮后,会调用父组件的 setState() 方法,此时会重新构建整个页面,由于示例中,子组件并没有任何缓存,所以它也都会被重新构建,所以也会调用 build() 方法。

那么,现在就带来了一个问题:实际上,我们只想更新子树中依赖了 InheritedDemo 的组件,而现在只要调用父组件的setState()方法,所有子节点都会被重新 build,这很没必要,那么有什么办法可以避免呢?答案是缓存!一个简单的做法就是通过封装一个 StatefulWidget,将子Widget树缓存起来。