flutter...

Flutter 之 数据共享(InheritedWidget)(

2022-05-06  本文已影响0人  maskerII

1. 前言

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

InheritedWidget和 React 中的 context 功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget的在 widget 树中数据传递方向是从上到下的,这和通知Notification的传递方向正好相反。

2. InheritedWidget 示例

“计数器”示例应用程序的InheritedWidget版本

  1. 自定义MSShareDataWidget类继承于InheritedWidget,将需要共享的数据保存在MSShareDataWidget的data属性中
class MSShareDataWidget extends InheritedWidget {
  // 1.共享数据
  final int data;

  // 2.定义构造方法
  MSShareDataWidget({Key? key, required this.data, required Widget child})
      : super(key: key, child: child);

  // 3.获取组件最近的当前InheritedWidget
  static MSShareDataWidget? of(BuildContext context) {
    // 沿着Element树,去找到最近的MSShareDataWidget,从Element中取出Widget对象
    return context.dependOnInheritedWidgetOfExactType<MSShareDataWidget>();
  }

  // 4.决定要不要回调State中的didChangeDependencies
  // 如果true,执行依赖当前的InheritedWidget的State中的didChangeDependencies
  @override
  bool updateShouldNotify(covariant MSShareDataWidget oldWidget) {
    return data != oldWidget.data;
  }
}

  1. 使用InheritedWidget共享的数据
    实现一个子组件MSShowDataWidget,在其build方法中引用MSShareDataWidget中的数据,同时,在其didChangeDependencies() 回调中打印日志

class MSShowDataWidget extends StatefulWidget {
  const MSShowDataWidget({Key? key}) : super(key: key);

  @override
  State<MSShowDataWidget> createState() => _MSShowDataWidgetState();
}

class _MSShowDataWidgetState extends State<MSShowDataWidget> {
  @override
  void didChangeDependencies() {
    print("MSShowDataWidget didChangeDependencies");
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    int _counter = MSShareDataWidget.of(context)!.data;
    return Text("$_counter", textScaleFactor: 1.5);
  }
}

完整代码


class MSInheritedWidgetDemo1 extends StatefulWidget {
  const MSInheritedWidgetDemo1({Key? key}) : super(key: key);

  @override
  State<MSInheritedWidgetDemo1> createState() => _MSInheritedWidgetDemo1State();
}

class _MSInheritedWidgetDemo1State extends State<MSInheritedWidgetDemo1> {
  int _counter = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidgetDemo1")),
      body: Center(
        child: MSShareDataWidget(
          child: MSShowDataWidget(),
          data: _counter,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          _counter++;
          setState(() {});
        },
      ),
    );
  }
}

class MSShowDataWidget extends StatefulWidget {
  const MSShowDataWidget({Key? key}) : super(key: key);

  @override
  State<MSShowDataWidget> createState() => _MSShowDataWidgetState();
}

class _MSShowDataWidgetState extends State<MSShowDataWidget> {
  @override
  void didChangeDependencies() {
    print("MSShowDataWidget didChangeDependencies");
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    int _counter = MSShareDataWidget.of(context)!.data;
    return Text("$_counter", textScaleFactor: 1.5);
  }
}

class MSShareDataWidget extends InheritedWidget {
  // 1.共享数据
  final int data;

  // 2.定义构造方法
  MSShareDataWidget({Key? key, required this.data, required Widget child})
      : super(key: key, child: child);

  // 3.获取组件最近的当前InheritedWidget
  static MSShareDataWidget? of(BuildContext context) {
    // 沿着Element树,去找到最近的MSShareDataWidget,从Element中取出Widget对象
    return context.dependOnInheritedWidgetOfExactType<MSShareDataWidget>();
  }

  // 4.决定要不要回调State中的didChangeDependencies
  // 如果true,执行依赖当前的InheritedWidget的State中的didChangeDependencies
  @override
  bool updateShouldNotify(covariant MSShareDataWidget oldWidget) {
    return data != oldWidget.data;
  }
}

105.gif

3. didChangeDependencies

在之前介绍StatefulWidget生命周期时,我们提到State对象有一个didChangeDependencies回调,它会在“依赖”发生变化时被Flutter 框架调用。而这个“依赖”指的就是子 widget 是否使用了父 widget 中InheritedWidget的数据!如果使用了,则代表子 widget 有依赖;如果没有使用则代表没有依赖。这种机制可以使子组件在所依赖的InheritedWidget变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子 widget 的didChangeDependencies方法将会被调用。

在上例中,我们每点击一次按钮,计数器就会自增,控制台就会打印一句日志:

MSShowDataWidget didChangeDependencies

可见依赖发生变化后,其didChangeDependencies()会被调用。

但要注意,如果MSShowDataWidget的build方法中没有使用MSShareDataWidget的数据,那么它的didChangeDependencies()将不会被调用,因为它并没有依赖MSShareDataWidget。

例如,我们将_MSShowDataWidgetState代码改为下面这样,didChangeDependencies()将不会被调用:

class _MSShowDataWidgetState extends State<MSShowDataWidget> {
  @override
  void didChangeDependencies() {
    print("MSShowDataWidget didChangeDependencies");
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    // int _counter = MSShareDataWidget.of(context)!.data;
    // return Text("$_counter", textScaleFactor: 1.5);
    return Text("text");
  }
}

上面的代码中,我们将build()方法中依赖MSShareDataWidget的代码注释掉了,然后返回一个固定Text,这样一来,当点击add按钮后,MSShareDataWidget的data虽然发生变化,但由于_MSShowDataWidgetState并未依赖MSShareDataWidget,所以_MSShowDataWidgetState的didChangeDependencies方法不会被调用。其实,这个机制很好理解,因为在数据发生变化时只对使用该数据的Widget更新是合理并且性能友好的。

应该在didChangeDependencies()中做什么?

一般来说,子 widget 很少会重写此方法,因为在依赖改变后 Flutter 框架也都会调用build()方法重新构建组件树。但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()都执行这些昂贵操作。

4. 深入了解InheritedWidget

现在来思考一下,在上面的例子中,如果我们只想在_MSShowDataWidgetState中引用MSShareDataWidget数据,但却不希望在MSShareDataWidget发生变化时调用_MSShowDataWidgetState的didChangeDependencies()方法应该怎么办?

  1. MSShareDataWidget 的updateShouldNotify 返回false

MSShareDataWidget 的 updateShouldNotify 返回false,就不会执行依赖当前的InheritedWidget的State中的didChangeDependencies。

  1. 修改MSShareDataWidget.of()的实现
    如下:
  static MSShareDataWidget of(BuildContext context) {
    // 沿着Element树,去找到最近的MSShareDataWidget,从Element中取出Widget对象
    // return context.dependOnInheritedWidgetOfExactType<MSShareDataWidget>()!;

    return context.getElementForInheritedWidgetOfExactType<MSShareDataWidget>()!.widget as MSShareDataWidget;
  }

唯一的改动就是获取MSShareDataWidget对象的方式,把dependOnInheritedWidgetOfExactType()方法换成了context.getElementForInheritedWidgetOfExactType<MSShareDataWidget>().widget

那么他们到底有什么区别呢,我们看一下这两个方法的源码

  InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    return ancestor;
  }
  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    // 多出部分
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

我们可以看到,dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了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方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会。所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。

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

那么,现在就带来了一个问题:实际上,我们只想更新子树中依赖了MSShareDataWidget的组件,而现在只要调用_MSInheritedWidgetDemo1State的setState()方法,所有子节点都会被重新build,这很没必要,那么有什么办法可以避免呢?答案是缓存!一个简单的做法就是通过封装一个StatefulWidget,将子Widget树缓存起来,具体做法下一节我们将通过实现一个Provider Widget 来演示如何缓存,以及如何利用InheritedWidget 来实现 Flutter 全局状态共享。

https://book.flutterchina.club/chapter7/inherited_widget.html

上一篇下一篇

猜你喜欢

热点阅读