146

使用 InheritedWidget 的正确方法是什么?到目前为止,我了解到它使您有机会将数据沿 Widget 树传播。在极端情况下,如果您将其设置为 RootWidget,它将可以从所有路由上的树中的所有 Widget 访问,这很好,因为我必须以某种方式使我的 ViewModel/Model 可用于我的 Widget,而不必求助于全局变量或单例。

但是 InheritedWidget 是不可变的,那么我该如何更新它呢?更重要的是,我的 Stateful Widgets 是如何被触发来重建它们的子树的?

不幸的是,这里的文档非常不清楚,在与很多人讨论之后,似乎没有人真正知道使用它的正确方法是什么。

我添加了 Brian Egan 的一句话:

是的,我认为它是一种沿树传播数据的方式。我发现令人困惑的是,来自 API 文档:

“以这种方式引用继承的小部件时,将导致消费者在继承的小部件本身更改状态时进行重建。”

当我第一次读到这篇文章时,我想:

我可以在 InheritedWidget 中填充一些数据,然后再对其进行变异。当这种突变发生时,它将重建所有引用我的 InheritedWidget 我发现的小部件:

为了改变 InheritedWidget 的状态,您需要将其包装在 StatefulWidget 中。然后实际改变 StatefulWidget 的状态并将此数据传递给 InheritedWidget,后者将数据传递给它的所有子级。但是,在这种情况下,它似乎重建了 StatefulWidget 下的整个树,而不仅仅是引用 InheritedWidget 的 Widget。那是对的吗?或者,如果 updateShouldNotify 返回 false,它是否会以某种方式知道如何跳过引用 InheritedWidget 的 Widget?

4

4 回答 4

137

问题来自您的报价,这是不正确的。

正如您所说,InheritedWidgets 与其他小部件一样,是不可变的。因此他们不更新。它们被重新创建。

问题是:InheritedWidget 只是一个简单的小部件,除了保存数据之外什么都不做。它没有任何更新或任何逻辑。但是,与任何其他小部件一样,它与Element. 你猜怎么着?这个东西是可变的,flutter 会尽可能地重用它!

更正后的报价是:

InheritedWidget,当以这种方式引用时,将导致消费者在与 InheritedElement 关联的InheritedWidget发生更改时重建。

有一个关于如何将小部件/元素/渲染框插入在一起的精彩讨论。但简而言之,它们是这样的(左边是您的典型小部件,中间是“元素”,右边是“渲染框”):

在此处输入图像描述

事情是:当你实例化一个新的小部件时;颤振会将其与旧的进行比较。重用它的“元素”,它指向一个渲染框。并改变 RenderBox属性。


好的,但这如何回答我的问题?

当实例化一个InheritedWidget,然后调用context.inheritedWidgetOfExactType(或者MyClass.of说基本一样);暗示的是它会听Element与你的关联InheritedWidget。并且每当Element获得一个新的小部件时,它都会强制刷新任何调用前一个方法的小部件。

简而言之,当您用InheritedWidget全新的替换现有的时;flutter 会看到它发生了变化。并将通知绑定的小部件潜在的修改。

如果您了解所有内容,那么您应该已经猜到了解决方案:

将您的InheritedWidget内部包裹起来,每当发生变化时StatefulWidget都会创建一个全新的InheritedWidget

实际代码的最终结果是:

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  _MyInheritedState createState() => _MyInheritedState();
}

class _MyInheritedState extends State<MyInherited> {
  String myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

但是创建一个新的 InheritedWidget 不会重建整个树吗?

不,不一定。因为您的新 InheritedWidget 可能具有与以前完全相同的孩子。确切地说,我的意思是同一个例子。具有与以前相同的实例的小部件不会重建。

在大多数情况下(在您的应用程序的根目录下有一个inheritedWidget),继承的小部件是常量。所以没有不必要的重建。

于 2018-03-26T13:20:20.150 回答
28

TL;博士

不要在updateShouldNotify方法中使用繁重的计算,并在创建小部件时使用const而不是 new


首先,我们应该了解什么是Widget、Element和Render对象。

  1. 渲染对象是实际在屏幕上渲染的对象。它们是可变的,包含绘画和布局逻辑。渲染树与 Web 中的文档对象模型 (DOM) 非常相似,您可以将渲染对象视为该树中的 DOM 节点
  2. 小部件- 是对应该呈现的内容的描述。它们是不可变的且便宜的。因此,如果 Widget 回答问题“什么?”(声明性方法),则 Render 对象回答问题“如何?”(命令式方法)。来自网络的类比是“虚拟 DOM”。
  3. Element/BuildContext - 是WidgetRender对象之间的代理。它包含有关小部件在树中的位置* 以及在相应小部件更改时如何更新 Render 对象的信息。

现在我们准备深入研究InheritedWidget和 BuildContext 的方法inheritFromWidgetOfExactType

作为一个例子,我建议我们从 Flutter 的 InheritedWidget 文档中考虑这个例子:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget - 只是一个小部件,在我们的例子中实现了一个重要的方法 - updateShouldNotifyupdateShouldNotify - 一个接受一个参数oldWidget并返回一个布尔值的函数:true 或 false。

与任何小部件一样,InheritedWidget具有相应的 Element 对象。它是InheritedElement。每次我们构建新的小部件时,InheritedElement都会在小部件上调用updateShouldNotify (在祖先上调用setState )。updateShouldNotify返回true时,InheritedElement 会遍历依赖项(?) 并在其上调用方法didChangeDependencies

InheritedElement 在哪里获取依赖项?这里我们应该看一下inheritFromWidgetOfExactType方法。

inheritFromWidgetOfExactType - 此方法在 BuildContext 中定义, 每个Element 都实现 BuildContext 接口(Element == BuildContext)。所以每个Element都有这个方法。

让我们看一下inheritFromWidgetOfExactType的代码:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

这里我们尝试在按类型映射的_inheritedWidgets中找到一个祖先。如果找到了祖先,我们就调用inheritFromElement

inheritFromElement的代码:

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. 我们添加祖先作为当前元素的依赖项 (_dependencies.add(ancestor))
  2. 我们将当前元素添加到祖先的依赖项(ancestor.updateDependencies(this, aspect))
  3. 我们返回祖先的小部件作为inheritFromWidgetOfExactType的结果(返回祖先.widget)

所以现在我们知道了 InheritedElement 从哪里获取它的依赖项。

现在让我们看看didChangeDependencies方法。每个元素都有这个方法:

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

正如我们所看到的,这个方法只是将一个元素标记为,这个元素应该在下一帧重建。重建意味着调用方法构建在对应的小部件元素上。

但是“当我重建 InheritedWidget 时会重建整个子树吗?”。在这里,我们应该记住 Widget 是不可变的,如果您创建新的 Widget,Flutter 将重建子树。我们该如何解决?

  1. 手动缓存小部件(手动)
  2. 使用const因为 const创建唯一的值/类实例
于 2018-12-11T22:44:05.223 回答
4

文档

[BuildContext.dependOnInheritedWidgetOfExactType] 获取给定类型的最近小部件,该类型必须是具体 InheritedWidget 子类的类型,并将此构建上下文注册到该小部件,以便当该小部件更改(或引入该类型的新小部件时,或小部件消失),此构建上下文将被重建,以便它可以从该小部件获取新值。

这通常从 of() 静态方法中隐式调用,例如 Theme.of。

正如 OP 所指出的,一个InheritedWidget实例不会改变......但它可以用小部件树中相同位置的新实例替换。发生这种情况时,可能需要重建已注册的小部件。该InheritedWidget.updateShouldNotify方法做出该确定。(见:文档

那么如何替换实例呢?实例InheritedWidget可以包含在 中StatefulWidget,它可以用新实例替换旧实例。

于 2019-03-03T19:45:14.000 回答
-3

InheritedWidget 管理应用程序的集中数据并将其传递给孩子,就像我们可以在这里存储购物车计数一样,如下所述

于 2020-01-05T05:50:46.587 回答