【问题标题】:Flutter: How to correctly use an Inherited Widget?Flutter:如何正确使用 Inherited Widget?
【发布时间】:2018-09-04 14:54:54
【问题描述】:

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

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

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

我添加了 Brian Egan 的一句话:

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

"继承的小部件,当以这种方式引用时,将导致 当继承的小部件本身改变状态时,消费者可以重建。”

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

我可以在 InheritedWidget 中填充一些数据,然后再对其进行变异。 当这种突变发生时,它将重建所有 参考我的 InheritedWidget 我发现了什么:

为了改变 InheritedWidget 的状态,您需要包装 它在一个 StatefulWidget 你然后实际上改变了状态 StatefulWidget 并将这些数据传递给 InheritedWidget,后者 将数据交给它所有的孩子。然而,在这种情况下,它 似乎在 StatefulWidget 下重建整个树,而不是 只是引用 InheritedWidget 的小部件。那是对的吗? 或者它会以某种方式知道如何跳过引用 如果 updateShouldNotify 返回 false,则 InheritedWidget?

【问题讨论】:

  • 好问题!感谢您的提问。

标签: flutter dart flutter-widget


【解决方案1】:

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

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

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

更正后的报价是:

InheritedWidget,当以这种方式引用时,当与 InheritedElement 关联的 InheritedWidget 发生变化时,将导致使用者重新构建。

There's a great talk about how widgets/elements/renderbox are pluged together。 但简而言之,它们是这样的(左边是您的典型小部件,中间是“元素”,右边是“渲染框”):

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


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

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

简而言之,当您用全新的InheritedWidget 替换现有的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),继承的小部件是常量。所以没有不必要的重建。

【讨论】:

  • 但是创建一个新的 InheritedWidget 不会重建整个树吗?那么为什么需要监听器呢?
  • 对于您的第一条评论,我在答案中添加了第三部分。至于乏味:我不同意。一个代码 sn-p 可以很容易地生成这个。访问数据就像调用MyInherited.of(context)一样简单。
  • 不确定您是否感兴趣,但使用此技术更新了示例:github.com/brianegan/flutter_architecture_samples/tree/master/… 现在肯定会少一些重复!如果您对该实现有任何其他建议,如果您有空闲时间,会喜欢代码审查 :) 仍在尝试找出共享此逻辑跨平台(Flutter 和 Web)并确保其可测试的最佳方式(尤其是异步的东西)。
  • 既然updateShouldNotify 测试总是引用同一个MyInheritedState 实例,它不会总是返回false 吗?当然MyInheritedStatebuild 方法正在创建新的_MyInherited 实例,但是data 字段总是引用this 不是吗?我遇到了问题...如果我只是硬编码 true 就可以工作。
  • @cdock 是的,我的错。不记得为什么我这样做了,因为它显然行不通。已通过编辑为 true 修复,谢谢。
【解决方案2】:

TL;DR

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


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

  1. 渲染 对象是实际在屏幕上渲染的对象。它们是可变的,包含绘画和布局逻辑。渲染树与 Web 中的文档对象模型 (DOM) 非常相似,您可以将渲染对象视为该树中的 DOM 节点
  2. Widget - 是对应该呈现的内容的描述。它们不可变而且便宜。因此,如果 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 会遍历 dependencies(?) 并在其上调用方法 didChangeDependencies

InheritedElement 从哪里获得依赖项?这里我们应该看看 inheritFromWidgetOfExactType 方法。

inheritFromWidgetOfExactType - 此方法在 BuildContext 和 每个元素都实现了 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();
  }

我们可以看到这个方法只是将一个元素标记为 dirty 并且这个元素应该在下一帧重建。 Rebuild 表示在对应的小部件元素上调用方法build

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

  1. 手动缓存小部件(手动)
  2. 使用 const 因为 const create the only one instance 的值/类

【讨论】:

  • 很好的解释 maksimr。最让我困惑的是,如果在继承的Widget被替换时,整个子树还是重建了,那么 updateShouldNotify() 的意义何在?
  • 所以这里继承的小部件可以在值发生变化时更新其侦听器,而这正是提供者小部件这样做,它们之间有什么区别。如果我错了,请纠正我
  • 清理在哪里进行?即当小部件被删除时,从 HashSet 中删除所有依赖项?
【解决方案3】:

来自docs

[BuildContext.dependOnInheritedWidgetOfExactType] 获取最近的widget 给定类型的,它必须是混凝土的类型 InheritedWidget 子类,并用它注册这个构建上下文 小部件,以便当该小部件更改(或该小部件的新小部件 类型被引入,或者小部件消失),这个构建上下文是 重建,以便它可以从该小部件获取新值。

这通常是从 of() 静态方法中隐式调用的,例如 主题。

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

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 2019-06-12
      • 2018-12-26
      • 2019-04-01
      • 2021-11-24
      • 2019-10-24
      • 2021-08-28
      • 1970-01-01
      • 2021-09-29
      • 2018-01-19
      相关资源
      最近更新 更多