使用 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?
问题来自您的报价,这是不正确的。
正如您所说,InheritedWidgets 与其他小部件一样,是不可变的。因此他们不更新。它们被重新创建。
问题是:InheritedWidget 只是一个简单的小部件,除了保存数据之外什么都不做。它没有任何更新或任何逻辑。但是,与任何其他小部件一样,它与 Element
相关联。你猜怎么着?这个东西是可变的,flutter 会尽可能地重用它!
更正后的报价是:
InheritedWidget,当以这种方式引用时,将导致消费者在与 InheritedElement 关联的 InheritedWidget 发生更改时重建。
There's a great talk about how widgets/elements/renderbox are pluged together。但简而言之,它们是这样的(左边是您的典型小部件,中间是“元素”,右边是“渲染框”):
https://i.stack.imgur.com/ZhPdS.png
事情是:当你实例化一个新的小部件时;颤振会将其与旧的进行比较。重用它的“元素”,它指向一个渲染框。并改变 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),继承的小部件是恒定的。所以没有不必要的重建。
TL;博士
不要在 updateShouldNotify 方法中使用繁重的计算,并在创建小部件时使用 const 而不是 new
首先,我们应该了解什么是Widget、Element和Render对象。
渲染对象是实际在屏幕上渲染的对象。它们是可变的,包含绘画和布局逻辑。渲染树与 Web 中的文档对象模型 (DOM) 非常相似,您可以将渲染对象视为此树中的 DOM 节点 Widget - 是对应该渲染的内容的描述。它们是不可变的且便宜的。因此,如果 Widget 回答问题“什么?”(声明性方法),则 Render 对象回答问题“如何?”(命令式方法)。来自网络的类比是“虚拟 DOM”。 Element/BuildContext - 是 Widget 和 Render 对象之间的代理。它包含有关小部件在树中的位置* 以及在相应小部件更改时如何更新 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 都会在小部件上调用 updateShouldNotify(在祖先上调用 setState)。当 updateShouldNotify 返回 true 时,InheritedElement 会遍历 dependencies(?) 并在其上调用方法 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;
}
我们将祖先添加为当前元素的依赖项 (_dependencies.add(ancestor)) 我们将当前元素添加到祖先的依赖项 (ancestor.updateDependencies(this, aspect)) 我们返回祖先的小部件作为 inheritFromWidgetOfExactType 的结果(返回祖先.widget)
所以现在我们知道了 InheritedElement 从哪里获取它的依赖项。
现在让我们看看 didChangeDependencies 方法。每个元素都有这个方法:
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
正如我们所看到的,这个方法只是将一个元素标记为脏,这个元素应该在下一帧重建。重建意味着调用方法构建在对应的小部件元素上。
但是“当我重建 InheritedWidget 时会重建整个子树吗?”。在这里,我们应该记住 Widget 是不可变的,如果您创建新的 Widget,Flutter 将重建子树。我们该如何解决?
手动缓存小部件(手动)使用 const 因为 const 创建唯一的值/类实例
从 docs:
[BuildContext.dependOnInheritedWidgetOfExactType] 获取给定类型的最近小部件,该类型必须是具体 InheritedWidget 子类的类型,并将此构建上下文注册到该小部件,以便当该小部件更改(或引入该类型的新小部件时,或小部件消失),此构建上下文将被重建,以便它可以从该小部件获取新值。这通常从 of() 静态方法中隐式调用,例如 Theme.of。
正如 OP 所指出的,InheritedWidget
实例不会改变......但它可以用小部件树中相同位置的新实例替换。发生这种情况时,可能需要重建已注册的小部件。 InheritedWidget.updateShouldNotify
方法进行此确定。 (见:docs)
那么如何替换实例呢? InheritedWidget
实例可能包含在 StatefulWidget
中,它可以用新实例替换旧实例。
MyInherited.of(context)
一样简单。updateShouldNotify
测试总是引用同一个MyInheritedState
实例,它不总是返回false
吗?当然MyInheritedState
的build
方法是创建新的_MyInherited
实例,但data
字段总是引用this
no?我遇到了问题...如果我只是硬编码true
就可以了。