Flutter:如何正确使用 Inherited Widget?
Flutter: How to correctly use an Inherited Widget?
InheritedWidget 的正确使用方法是什么?到目前为止,我了解到它为您提供了将数据向下传播到 Widget 树的机会。在极端情况下,如果你把它作为 RootWidget,它可以从所有路径上的树中的所有 Widget 访问,这很好,因为我必须以某种方式让我的 ViewModel/Model 可以被我的 Widget 访问,而不必求助于全局变量或 Singletons。
但是 InheritedWidget 是不可变的,那么我该如何更新它呢?更重要的是,如何触发我的有状态小部件来重建它们的子树?
不幸的是,这里的文档非常不清楚,经过大量讨论后似乎没有人真正知道正确的使用方法。
我引用 Brian Egan 的话:
Yes, I see it as a way to propagate data down the tree. What I find
confusing, from the API docs:
"Inherited widgets, when referenced in this way, will cause the
consumer to rebuild when the inherited widget itself changes state."
When I first read this, I thought:
I could stuff some data in the InheritedWidget and mutate it later.
When that mutation happens, it will rebuild all the Widgets that
reference my InheritedWidget What I found:
In order to mutate the State of an InheritedWidget, you need to wrap
it in a StatefulWidget You then actually mutate the state of the
StatefulWidget and pass this data down to the InheritedWidget, which
hands the data down to all of it's children. However, in that case, it
seems to rebuild the entire tree underneath the StatefulWidget, not
just the Widgets that reference the InheritedWidget. Is that correct?
Or will it somehow know how to skip the Widgets that reference the
InheritedWidget if updateShouldNotify returns false?
问题出在你的引用上,不正确。
如您所说,InheritedWidget 与其他小部件一样是不可变的。因此他们不更新。它们是重新创建的。
问题是:InheritedWidget 只是一个简单的小部件,除了保存数据外什么都不做。它没有任何更新逻辑或任何东西。
但是,与任何其他小部件一样,它与 Element
相关联。
你猜怎么着?这个东西是可变的,flutter 会尽可能重用它!
更正后的报价为:
InheritedWidget, when referenced in this way, will cause the consumer to rebuild when InheritedWidget associated to an InheritedElement changes.
There's a great talk about how widgets/elements/renderbox are pluged together。
但简而言之,它们是这样的(左边是你的典型小部件,中间是 'elements',右边是 'render boxes'):
事情是:当你实例化一个新的小部件时; flutter 会将其与旧版本进行比较。重用它的“Element”,它指向一个 RenderBox。并且改变 RenderBox 属性。
好的,但这如何回答我的问题?
当实例化一个InheritedWidget,然后调用context.inheritedWidgetOfExactType
(或MyClass.of
,基本相同);这意味着它会监听与您的 InheritedWidget
关联的 Element
。每当那个 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 可能具有与以前完全相同的 child。确切地说,我指的是同一个实例。
具有与之前相同实例的小部件不会重建。
并且在大多数情况下(在您的应用程序的根目录下有一个继承的小部件),继承的小部件是常量。所以没有不必要的重建。
TL;DR
不要在 updateShouldNotify 方法中使用大量计算并使用 const 而不是 new创建小部件时
首先,我们应该了解什么是Widget、Element和Render对象。
- Render 对象是实际呈现在屏幕上的对象。它们是 可变的 ,包含绘画和布局逻辑。渲染树与 Web 中的文档对象模型 (DOM) 非常相似,您可以将渲染对象视为此树中的 DOM 节点
- Widget - 是对应该渲染的内容的描述。它们 不可变 并且便宜。因此,如果 Widget 回答了问题 "What?"(声明式方法),那么 Render 对象回答了问题 "How?"(命令式方法)。来自网络的类比是 "Virtual DOM".
- Element/BuildContext - 是 Widget 和 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 和 returns 一个布尔值:true 或 false。
与任何小部件一样,InheritedWidget 具有相应的 Element 对象。它是 InheritedElement。每次我们构建一个新的小部件时,InheritedElement 都会在小部件上调用 updateShouldNotify(在祖先上调用 setState)。当 updateShouldNotify returns 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;
}
- 我们将祖先添加为当前元素的依赖项 (_dependencies.add(ancestor))
- 我们将当前元素添加到祖先的依赖项中 (ancestor.updateDependencies(this, aspect))
- 我们 return 祖先的小部件是 inheritFromWidgetOfExactType (return ancestor.widget)
的结果
现在我们知道 InheritedElement 从哪里获取它的依赖项了。
现在让我们看看 didChangeDependencies 方法。
每个元素都有这个方法:
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
正如我们所见,此方法只是将一个元素标记为 脏 ,并且应该在下一帧重建此元素。 Rebuild 表示在对应的小部件元素上调用方法 build。
但是 "Whole sub-tree rebuilds when I rebuild InheritedWidget?" 呢?
在这里我们应该记住,Widget 是不可变的,如果您创建新的 widget,Flutter 将重建子树。我们该如何解决?
- 手动缓存小部件(手动)
- 使用 const 因为 value/class
的 const
来自docs:
[BuildContext.dependOnInheritedWidgetOfExactType] obtains the nearest widget
of the given type, which must be the type of a concrete
InheritedWidget subclass, and registers this build context with that
widget such that when that widget changes (or a new widget of that
type is introduced, or the widget goes away), this build context is
rebuilt so that it can obtain new values from that widget.
This is typically called implicitly from of() static methods, e.g.
Theme.of.
正如 OP 所指出的,InheritedWidget
实例不会改变......但它可以在小部件树中的相同位置被新实例替换。当发生这种情况时,可能需要重建已注册的小部件。 InheritedWidget.updateShouldNotify
方法进行此确定。 (参见:docs)
那么如何替换一个实例呢? InheritedWidget
实例可能包含在 StatefulWidget
中,后者可能会用新实例替换旧实例。
InheritedWidget 管理应用程序的集中数据并将其传递给 child,就像我们可以在此处存储购物车数量一样 here:
InheritedWidget 的正确使用方法是什么?到目前为止,我了解到它为您提供了将数据向下传播到 Widget 树的机会。在极端情况下,如果你把它作为 RootWidget,它可以从所有路径上的树中的所有 Widget 访问,这很好,因为我必须以某种方式让我的 ViewModel/Model 可以被我的 Widget 访问,而不必求助于全局变量或 Singletons。
但是 InheritedWidget 是不可变的,那么我该如何更新它呢?更重要的是,如何触发我的有状态小部件来重建它们的子树?
不幸的是,这里的文档非常不清楚,经过大量讨论后似乎没有人真正知道正确的使用方法。
我引用 Brian Egan 的话:
Yes, I see it as a way to propagate data down the tree. What I find confusing, from the API docs:
"Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state."
When I first read this, I thought:
I could stuff some data in the InheritedWidget and mutate it later. When that mutation happens, it will rebuild all the Widgets that reference my InheritedWidget What I found:
In order to mutate the State of an InheritedWidget, you need to wrap it in a StatefulWidget You then actually mutate the state of the StatefulWidget and pass this data down to the InheritedWidget, which hands the data down to all of it's children. However, in that case, it seems to rebuild the entire tree underneath the StatefulWidget, not just the Widgets that reference the InheritedWidget. Is that correct? Or will it somehow know how to skip the Widgets that reference the InheritedWidget if updateShouldNotify returns false?
问题出在你的引用上,不正确。
如您所说,InheritedWidget 与其他小部件一样是不可变的。因此他们不更新。它们是重新创建的。
问题是:InheritedWidget 只是一个简单的小部件,除了保存数据外什么都不做。它没有任何更新逻辑或任何东西。
但是,与任何其他小部件一样,它与 Element
相关联。
你猜怎么着?这个东西是可变的,flutter 会尽可能重用它!
更正后的报价为:
InheritedWidget, when referenced in this way, will cause the consumer to rebuild when InheritedWidget associated to an InheritedElement changes.
There's a great talk about how widgets/elements/renderbox are pluged together。 但简而言之,它们是这样的(左边是你的典型小部件,中间是 'elements',右边是 'render boxes'):
事情是:当你实例化一个新的小部件时; flutter 会将其与旧版本进行比较。重用它的“Element”,它指向一个 RenderBox。并且改变 RenderBox 属性。
好的,但这如何回答我的问题?
当实例化一个InheritedWidget,然后调用context.inheritedWidgetOfExactType
(或MyClass.of
,基本相同);这意味着它会监听与您的 InheritedWidget
关联的 Element
。每当那个 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 可能具有与以前完全相同的 child。确切地说,我指的是同一个实例。 具有与之前相同实例的小部件不会重建。
并且在大多数情况下(在您的应用程序的根目录下有一个继承的小部件),继承的小部件是常量。所以没有不必要的重建。
TL;DR
不要在 updateShouldNotify 方法中使用大量计算并使用 const 而不是 new创建小部件时
首先,我们应该了解什么是Widget、Element和Render对象。
- Render 对象是实际呈现在屏幕上的对象。它们是 可变的 ,包含绘画和布局逻辑。渲染树与 Web 中的文档对象模型 (DOM) 非常相似,您可以将渲染对象视为此树中的 DOM 节点
- Widget - 是对应该渲染的内容的描述。它们 不可变 并且便宜。因此,如果 Widget 回答了问题 "What?"(声明式方法),那么 Render 对象回答了问题 "How?"(命令式方法)。来自网络的类比是 "Virtual DOM".
- Element/BuildContext - 是 Widget 和 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 和 returns 一个布尔值:true 或 false。
与任何小部件一样,InheritedWidget 具有相应的 Element 对象。它是 InheritedElement。每次我们构建一个新的小部件时,InheritedElement 都会在小部件上调用 updateShouldNotify(在祖先上调用 setState)。当 updateShouldNotify returns 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;
}
- 我们将祖先添加为当前元素的依赖项 (_dependencies.add(ancestor))
- 我们将当前元素添加到祖先的依赖项中 (ancestor.updateDependencies(this, aspect))
- 我们 return 祖先的小部件是 inheritFromWidgetOfExactType (return ancestor.widget) 的结果
现在我们知道 InheritedElement 从哪里获取它的依赖项了。
现在让我们看看 didChangeDependencies 方法。 每个元素都有这个方法:
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
正如我们所见,此方法只是将一个元素标记为 脏 ,并且应该在下一帧重建此元素。 Rebuild 表示在对应的小部件元素上调用方法 build。
但是 "Whole sub-tree rebuilds when I rebuild InheritedWidget?" 呢? 在这里我们应该记住,Widget 是不可变的,如果您创建新的 widget,Flutter 将重建子树。我们该如何解决?
- 手动缓存小部件(手动)
- 使用 const 因为 value/class 的 const
来自docs:
[BuildContext.dependOnInheritedWidgetOfExactType] obtains the nearest widget of the given type, which must be the type of a concrete InheritedWidget subclass, and registers this build context with that widget such that when that widget changes (or a new widget of that type is introduced, or the widget goes away), this build context is rebuilt so that it can obtain new values from that widget.
This is typically called implicitly from of() static methods, e.g. Theme.of.
正如 OP 所指出的,InheritedWidget
实例不会改变......但它可以在小部件树中的相同位置被新实例替换。当发生这种情况时,可能需要重建已注册的小部件。 InheritedWidget.updateShouldNotify
方法进行此确定。 (参见:docs)
那么如何替换一个实例呢? InheritedWidget
实例可能包含在 StatefulWidget
中,后者可能会用新实例替换旧实例。
InheritedWidget 管理应用程序的集中数据并将其传递给 child,就像我们可以在此处存储购物车数量一样 here: