1

Flutter 中,我们经常会看到这样的代码: Theme.of(context), MediaQuery.of(context),这背后其实就和我们今天的主角 InheritedWidget 有关。

介绍

InheritedWidgetFlutter 中的一个非常重要的功能性 widget,适用于在 widget 树中共享数据的场景。通过它,可以方便地将数据在 widget 树不同层级间进行传递。

Base class for widgets that efficiently propagate information down the tree.

基本使用

我们先看官方给的例子。

class FrogColor extends InheritedWidget {
  const FrogColor({
    super.key,
    required this.color,
    required super.child,
  });

  final Color color;

  static FrogColor? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<FrogColor>();
  }
  
  static FrogColor of(BuildContext context) {
    final FrogColor? result = maybeOf(context);
    assert(result != null, 'No FrogColor found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color;
}

FrogColor 可以理解为数据仓库,通过继承 InheritedWidget,将要共享的数据保存在 color 属性中,并提供一个 of 方法方便子 widget 通过 widget 树找到它。代码中重写的 updateShouldNotify 方法,决定当共享的数据发生变化时,是否通知依赖 color 的子 widget 重新 build
接着我们看看子 widget 中如何获取 color

class TestInheritedWidget extends StatelessWidget {
  const TestInheritedWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return FrogColor(
      color: Colors.green,
      child: Builder(
        builder: (BuildContext innerContext) {
          return Text(
            'Hello Frog',
            style: TextStyle(color: FrogColor.of(innerContext).color),
          );
        },
      ),
    );
  }
}

FrogColor 中的 Text 通过 of 方法就可以找到 color。需要注意的是,context 必须是 InheritedWidget 的后代,这意味着必须处于 InheritedWidget 树中。上述代码中,context 来自 Builder,而 BuilderFrogColor 的子 widget,因此可以找到 color

// ...
return FrogColor(
  color: Colors.green,
  child: Text(
    'Hello Frog',
    style: TextStyle(color: FrogColor.of(innerContext).color),
  ),
);
// ...

如果去掉 Builder,则使用的是 TestInheritedWidgetcontext,会抛出 No FrogColor found in context 异常。

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building TestInheritedWidget(dirty):
No FrogColor found in context
'package:flutter_wiki/inherit_widget.dart':
Failed assertion: line 18 pos 12: 'result != null'

上面的是只读的情况,接下来,我们看看修改数据的情况。

class FrogWidget extends InheritedWidget {
  const FrogColor({
    super.key,
    required this.color,
    required super.child,
    required this.onColorChange,
  });

  final Color color;
  final VoidCallback onColorChange;
  // ...
}

我们给 FrogWidget 增加一个 onColorChange 的方法,同时让TestInheritedWidget 继承 StatefulWidget

class TestInheritedWidget extends StatefulWidget {
  const TestInheritedWidget({super.key});

  @override
  State<TestInheritedWidget> createState() => _TestInheritedWidgetState();
}

class _TestInheritedWidgetState extends State<TestInheritedWidget> {
  Color color = Colors.green;

  void onColorChange() {
    setState(() {
      color = Colors.red;
    });
  }

  @override
  Widget build(BuildContext context) {
    return FrogColor(
      color: color,
      onColorChange: onColorChange,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Builder(
            builder: (BuildContext innerContext) => Text(
              'Hello Frog',
              style: TextStyle(color: FrogColor.of(innerContext).color),
            ),
          ),
          ElevatedButton(
            onPressed: () => onColorChange(),
            child: const Text("Change Color"),
          ),
        ],
      ),
    );
  }
}

当点击按钮时,会发现 Hello Frog 的颜色变为红色了。
在上述例子中,不管 FrogColor 中的 updateShouldNotify 是否返回 true,都会触发更新。因为执行了 setState 之后,build 方法会重新执行,Text 中直接使用最新的 color。我们需要 Text 单独抽出来成 classwidget

class ColoredWidget extends StatefulWidget {
  const ColoredWidget({super.key});

  @override
  State<ColoredWidget> createState() => _ColoredWidgetState();
}

class _ColoredWidgetState extends State<ColoredWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(
      'Hello Frog',
      style: TextStyle(color: FrogColor.of(context).color),
    );
  }
}

// ...
return FrogColor(
  // ...
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      // 子 widget
      const ColoredWidget(),
      ElevatedButton(
        // ..
      ),
    ],
  ),
);
// ...

这时,如果手动修改 shouldUpdateNotify 返回 falseColoredWidget 就不会更新了。

深入了解

我们回头在看看这行代码:FrogColor.of(context).color

  • 在这行代码中,我们获取到位于 widget 树顶部的 FrogColor 的引用
  • 然后,当我们书写 .of(context) 时,框架会将 widget (这里是 ColoredWidget)会注册为 FrogColor 的侦听器
  • 因此,每当 FrogColor 中的值发生变化时,依赖它的 widget 就会被重建

InheritedWidget 访问数据时需要遵循的一些原则:

  • StatefulWidget 中,不能在 initState 中调用 .of(context) 方法,会直接报错
  • 可以在 build, didChangeDependencies, didUpdate 等钩子函数中调用 .of(context)
class _ColoredWidgetState extends State<ColoredWidget> {
  late Color textColor;
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    textColor = FrogColor.of(context).color;
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(
      'Hello Frog',
      style: TextStyle(color: textColor),
    );
  }
}

在子 widget 中,我们已经知道了该在哪里调用 .of(context) 方法,接下来我们来看看它是如何工作的。
要回答这个问题,我们需要先理解 didChangeDependencies 的“依赖”变化指的是什么依赖?
我们来看一下源码。

/// Called when a dependency of this [State] object changes.
///
/// For example, if the previous call to [build] referenced an
/// [InheritedWidget] that later changed, the framework would call this
/// method to notify this object about the change.
///
/// This method is also called immediately after [initState]. It is safe to
/// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method.
///
/// Subclasses rarely override this method because the framework always
/// calls [build] after a dependency changes. Some subclasses do override
/// this method because they need to do some expensive work (e.g., network
/// fetches) when their dependencies change, and that work would be too
/// expensive to do for every build.
@protected
@mustCallSuper
void didChangeDependencies() { }

从源码中,可以清楚地看到,这里的依赖指的是:是否使用了父 widgetInheritedWidget 的数据。如果使用了,则表示子 widget 存在依赖,反之就没有。这种机制可以保证是子 widget 在依赖发生变化时进行更新。
那是在哪里注册了依赖关系呢?我们来看 dependOnInheritedWidgetOfExactType 的源码。

/// Once a widget registers a dependency on a particular type by calling this
/// method, it will be rebuilt, and [State.didChangeDependencies] will be
/// called, whenever changes occur relating to that widget until the next time
/// the widget or one of its ancestors is moved (for example, because an
/// ancestor is added or removed).
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });

上面是 dependOnInheritedWidgetOfExactType 的声明,从注释(删减了部分注释)可以看到,widget 通过调用此方法注册了依赖关系,当依赖发生变化时,调用 State.didChangeDependencies 回调。

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
  if (ancestor != null) {
    /// 这里注册了依赖关系
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

上面 dependOnInheritedWidgetOfExactType 的实现,当祖先元素存在 InheritedElement 时,注册依赖关系。
到这里我们总算弄清楚了 InheritedWidget 的原理了,总结一下:

  • 当在子 widget 中调用 .of(context) 时,会触发dependOnInheritedWidgetOfExactType 函数进行依赖注册
  • 当依赖发生变化时,重建子 widget,并且也会调用didChangeDependencies 回调

注意:

  • 前面说过,我们也可以在 build 中调用 .of(context) 方法,与在 didChangeDependencies 中调用相比,性能差异不会那么显著,但还是推荐在 didChangeDependencies 中进行调用。
  • widget 很少重写 didChangeDependencies 方法,因为依赖有变化,Flutter 框架会重新 build。但如果需要再依赖变化后执行一些昂贵的操作如网络请求,最好是在该方法中执行,避免每次 build 时执行昂贵的操作。

最后,我们来想一个问题,如果我们只想子 widget 获取 InheritedWidget 中的数据,但又不想数据变化时更新子 widget,这时该怎么办呢?
除了前面所说,将 updateShouldNotify 直接返回 false,我们还可以再访问 InheritedWidget 数据时不建立依赖关系。在实现 of(contex) 方法时,改为调用 getElementForInheritedWidgetOfExactType 方法。

class FrogColor extends InheritedWidget {
  // ...
  static FrogColor? maybeOf(BuildContext context) {
    // return context.dependOnInheritedWidgetOfExactType<FrogColor>();
    return context.getElementForInheritedWidgetOfExactType<FrogColor>()!.widget
        as FrogColor;
  }
  // ...
}

dependOnInheritedWidgetOfExactType 相比, getElementForInheritedWidgetOfExactType 没有进行依赖注册。

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
  if (ancestor != null) {
    /// 这里注册了依赖关系
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
  return ancestor;
}

见贤思齐
66 声望8 粉丝

写代码的