Flutter:如何正确使用继承的窗口小部件?


103

使用InheritedWidget的正确方法是什么?到目前为止,我了解到它为您提供了沿Widget树向下传播数据的机会。在极端情况下,如果您将其作为RootWidget放置,则可以从所有Route的树中的所有Widget进行访问,这很好,因为以某种方式我必须使Widget可以访问ViewModel / Model,而不必诉诸于globals或Singletons。

但是InheritedWidget是不可变的,那么如何更新它?更重要的是,如何触发我的有状态小部件来重建其子树?

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

我引用了Brian Egan的话:

是的,我将其视为在树上传播数据的一种方式。我从API文档中发现了令人困惑的地方:

“以这种方式引用继承的小部件时,当继承的小部件自身更改状态时,将导致使用者重建。”

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

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

为了使InheritedWidget的状态发生变化,您需要将其包装在StatefulWidget中,然后实际上使StatefulWidget的状态发生变化,然后将此数据传递给InheritedWidget,该InheritedWidget将数据传递给所有子项。但是,在那种情况下,似乎要重建StatefulWidget之下的整个树,而不仅仅是引用InheritedWidget的Widget。那是对的吗?还是以某种方式知道如果updateShouldNotify返回false时如何跳过引用InheritedWidget的Widget?

Answers:


107

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

就像您说的那样,InheritedWidgets和其他小部件一样是不可变的。因此,它们不会更新。它们是重新创建的。

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

更正后的报价为:

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

关于如何将小部件/元素/渲染框插入一起进行了精彩的演讲。简而言之,它们就像这样(左边是您的典型小部件,中间是“元素”,右边是“渲染框”):

在此处输入图片说明

事情是:实例化一个新的小部件时;扑将它与旧的比较。重用它的“元素”,它指向一个RenderBox。并更改 RenderBox属性。


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

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

简而言之,当您用InheritedWidget全新的产品替换现有产品时;扑扑会看到它变了。并且会通知绑定的小部件可能的修改。

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

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

实际代码的最终结果将是:

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),继承的Widget是恒定的。因此,无需进行任何重建。


1
但是,创建新的InheritedWidget不会重建整个树吗?为什么然后需要侦听器?
托马斯

1
对于您的第一条评论,我在回答中添加了第三部分。至于乏味:我不同意。代码段可以很容易地生成此代码。访问数据就像调用一样简单MyInherited.of(context)
雷米Rousselet

3
不知道您是否感兴趣,但是使用此技术更新了样本:github.com/brianegan/flutter_architecture_samples/tree/master/… 现在肯定可以减少重复!如果您对该实现有任何其他建议,如果您有空余时间,不妨进行代码复审:)仍在尝试找出共享此逻辑跨平台(Flutter和Web)并确保其可测试的最佳方法(尤其是异步内容)。
brianegan '18

4
由于updateShouldNotify测试总是引用相同的MyInheritedState实例,因此它不会总是返回false吗?当然创建新实例的build方法,但是该字段始终引用否?我遇到了问题。。。如果我只是硬编码,那就可以了。MyInheritedState_MyInheriteddatathistrue
cdock

2
@cdock是的,我不好。不记得为什么我这样做了,因为它显然行不通。修正为true,谢谢。
罗米·罗素(RémiRousselet)

20

TL; DR

创建小部件时,请勿在updateShouldNotify方法内部使用大量计算,而应使用const而不是 new


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

  1. 渲染对象是屏幕上实际渲染的对象。它们是可变的,包含绘画和布局逻辑。渲染树与网络中的文档对象模型(DOM)非常相似,您可以在此树中将渲染对象视为DOM节点
  2. 小部件 -是应呈现内容的描述。他们是一成不变的,便宜的。因此,如果Widget回答问题“ What?”(声明性方法),则Render对象回答问题“ How?”(命令性方法)。网络上的类比是“虚拟DOM”。
  3. Element / BuildContext-WidgetRender对象之间的代理。它包含有关小部件在tree *中的位置以及更改相应小部件时如何更新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也具有相应的Element对象。它是InheritedElement。每次我们构建一个新的小部件时,InheritedElement 都会在小部件上调用updateShouldNotify(在祖先上调用setState)。当updateShouldNotify返回true时, InheritedElement遍历依赖项(?),并对其调用方法didChangeDependencies

InheritedElement在哪里获得依赖关系?在这里,我们应该看一下InheritedFromWidgetOfExactType方法。

InheritFromInheritOfExactType-在BuildContext中定义的此方法, 每个元素都实现BuildContext接口(Element == BuildContext)。因此,每个元素都有此方法。

让我们看一下InheritFromFromWidgetOfExactType的代码:

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

在这里,我们尝试在按类型映射的_inheritedWidgets中找到祖先。如果找到祖先,那么我们将调用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. 我们返回继承祖先的小部件,这是InheritFromFromWidgetOfExactType的结果(返回ancestor.widget)

现在,我们知道了InheritedElement从何处获取依赖项。

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

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

如我们所见,此方法只是将元素标记为元素,并且应在下一帧上重建该元素。重建意味着调用方法建立在coresponding小部件元素上。

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

  1. 手动缓存小部件(手动)
  2. 使用const是因为const 仅创建 value / class的一个实例

1
很好的解释maksimr。最让我感到困惑的是,如果在替换了InheritedWidget时重建了整个子树,那么updateShouldNotify()的意义是什么?
熊猫世界

3

文档

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

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

如OP所述,InheritedWidget实例不会更改...但是可以用小部件树中相同位置的新实例替换它。发生这种情况时,可能需要重建已注册的窗口小部件。该InheritedWidget.updateShouldNotify方法进行该确定。(请参阅:文档

那么如何替换实例?一个InheritedWidget实例可以被包含StatefulWidget,这可能与一个新的实例替换旧的实例。


-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.