Flutter:如何正确使用Inherited Widget?

180
什么是使用InheritedWidget的正确方法?到目前为止,我理解它为您提供了在Widget树中传播数据的机会。如果将其作为RootWidget放置,它将可以从树上所有Widget及其所有路由中访问,这很好,因为我必须使我的ViewModel / Model对我的Widgets可访问,而无需使用全局变量或单例。
但是InheritedWidget是不可变的,那么如何更新它?更重要的是,我的有状态的小部件如何触发重建它们的子树?
不幸的是,文档在这里非常不清楚,在与大量人讨论后,似乎没有人真正知道使用它的正确方式。
我引用Brian Egan的一句话:
“是的,我认为这是一种向下传播数据的方法。我从API文档中发现令人困惑的是:'在这种方式引用时,继承的小部件将在继承的小部件本身发生更改时导致使用者重建。' 当我第一次看到这个时,我想:我可以将一些数据塞入InheritedWidget并稍后突变它。当突变发生时,它会重建所有引用我的InheritedWidget的小部件。但我发现:为了突变InheritedWidget的State,您需要将其包装在一个StatefulWidget中。然后您实际上突变了StatefulWidget的状态,并将此数据向下传递给InheritedWidget,后者将数据传递给其所有子代。但是,在这种情况下,它似乎会重建StatefulWidget下方的整个树,而不仅仅是引用InheritedWidget的小部件。这是正确的吗?还是如果updateShouldNotify返回false,它会如何跳过引用InheritedWidget的小部件?”

5
好问题!谢谢您的提问。 - Pratik Butani
4个回答

169

问题在于您的引用有误。

正如您所说,InheritedWidget像其他小部件一样是不可变的。因此它们不会更新,而是每次创建新的InheritedWidget实例。

事实是:InheritedWidget只是一个简单的小部件,仅保存数据。它没有任何更新逻辑或其他逻辑。

但是,就像其他小部件一样,它与Element相关联。

这个东西是可变的,Flutter将尽可能重用它!

正确的引用应该是:

当引用到与InheritedElement相关联的InheritedWidget时,消费者将在InheritedElement更改时进行重建。

关于widgets/elements/renderbox如何连接的演讲非常精彩。简单来说,它们就像这样(左边是典型的小部件,中间是“元素”,右边是“渲染框”):

enter image description here

问题在于:当您实例化新小部件时,Flutter将将其与旧小部件进行比较。重用其“Element”,该“Element”指向一个RenderBox。并且改变RenderBox属性。


好的,但这怎么回答我的问题呢?

当实例化InheritedWidget,然后调用context.inheritedWidgetOfExactType(或MyClass.of,基本上是相同的)时;意味着它将侦听与您的InheritedWidget相关联的Element。每当该Element获得新小部件时,它都会强制刷新调用先前方法的任何小部件。

简而言之,在用全新的InheritedWidget替换现有的InheritedWidget时,Flutter将看到其已更改。并且会通知绑定的小部件可能已被修改。

如果您已经理解了所有内容,您应该已经猜到了答案:

将您的InheritedWidget包装在一个StatefulWidget中,该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可能与以前完全相同的子孙节点。而且可以通过确切相同的实例来实现这一点。 具有与以前相同实例的Widget不会重新构建。

在大多数情况下(将InheritedWidget放在应用程序的根部),Inherited Widget是常量。因此没有不必要的重建。


2
但是创建新的InheritedWidget不会重建整个树吗?那么为什么需要Listerners呢? - Thomas
2
对于您的第一个评论,我在我的答案中添加了第三部分。至于是否繁琐:我不同意。一个代码片段可以很容易地生成这个。访问数据就像调用 MyInherited.of(context) 一样简单。 - Rémi Rousselet
3
不确定您是否感兴趣,但是使用该技术已更新示例:https://github.com/brianegan/flutter_architecture_samples/tree/master/example/inherited_widget现在重复代码少了一些!如果您对此实现有任何其他建议,如果您有多余的时间,我很乐意进行代码审查 :)仍在尝试找到跨平台共享此逻辑的最佳方法(Flutter和Web),并确保它可以进行测试(特别是异步部分)。 - brianegan
4
updateShouldNotify 测试始终引用相同的 MyInheritedState 实例,那么它不会一直返回 false 吗?确实,MyInheritedStatebuild 方法创建了新的 _MyInherited 实例,但是 data 字段总是引用 this,对吗? 我遇到了问题......如果我只是硬编码为 true,则可以工作。 - cdock
3
没错,我犯了错。不记得为什么要那么做,因为显然行不通。通过编辑为true已经修复,谢谢。 - Rémi Rousselet
显示剩余20条评论

31

TL;DR

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


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

  1. Render对象是实际渲染在屏幕上的内容。它们是可变的,包含绘制和布局逻辑。渲染树与Web中的文档对象模型(DOM)非常相似,您可以将渲染对象视为树中的DOM节点。
  2. Widget是描述应该渲染什么的一种方式。它们是不可变的且廉价的。因此,如果Widget回答“什么?”这个问题(声明式方法),那么Render对象就回答“如何?”这个问题(命令式方法)。Web上的类比是“虚拟DOM”。
  3. Element/BuildContextWidgetRender对象之间的代理。它包含有关小部件在树中位置的信息*,以及如何在对应小部件更改时更新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 - 仅是一个小部件,我们的情况下实现了一个重要的方法 - updateShouldNotify

updateShouldNotify - 一个函数,接受一个参数oldWidget并返回一个布尔值:true或false。

像任何小部件一样,InheritedWidget都有相应的元素对象。它是InheritedElement。 InheritedElement在每次构建新小部件(在祖先上调用setState)时调用小部件上的updateShouldNotify。当updateShouldNotify返回true时,InheritedElement会遍历其dependencies,并在其上调用方法didChangeDependencies

InheritedElement从哪里获取dependencies?在这里,我们应该看看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. 我们以祖先元素的widget作为inheritFromWidgetOfExactType方法的返回值 (return ancestor.widget)

现在我们知道InheritedElement从哪里获取其依赖关系了。

现在让我们看一下didChangeDependencies方法。每个Element都有这个方法:

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

如我们所见,这种方法只是标记一个元素为dirty,并且这个元素应该在下一帧中重建。重建意味着在相应的小部件元素上调用build方法。

那么,“当我重建InheritedWidget时整个子树都会被重建”怎么办呢? 在这里,我们应该记住,小部件是不可变的,如果您创建了新的小部件,Flutter将重新构建子树。我们该如何解决呢?

  1. 手动缓存小部件(手动)
  2. 使用const,因为const只创建值/类的一个实例

2
很好的解释,maksimr。最让我困惑的是,当InheritedWidget被替换时,整个子树都会被重建,那么updateShouldNotify()有什么意义呢? - Panda World
所以在这里,如果值发生变化,Inherited widget 可以更新其监听器,而 Provider widget 正是这样做的。那么它们之间有什么区别呢?如果我说错了,请纠正我。 - Omar Essam El-Din
清理工作在哪里进行?例如,当小部件被移除时,从HashSet中删除所有依赖项? - Kernel James

5

根据文档:

[BuildContext.dependOnInheritedWidgetOfExactType] 获取给定类型的最近的widget,该类型必须是一个具体的InheritedWidget子类,并将该构建上下文注册到该widget中,使得当该widget发生更改(或引入该类型的新widget或该widget消失)时,该构建上下文将被重建,以便从该widget获取新的值。

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

正如OP所指出的,一个InheritedWidget实例并不会改变... 但它可以被放置在widget树的相同位置使用新实例替换。当发生这种情况时,已经注册的widgets需要被重新构建。 InheritedWidget.updateShouldNotify 方法负责做出这一决定。(参见:文档

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


-8
InheritedWidget 管理应用程序的集中数据并将其传递给子级,就像我们可以在此处存储购物车计数一样,如 此处 所述:

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接