Flutter小部件树的根可以非常深...
非常深。
Flutter小部件的组件性质允许进行非常优雅,模块化和灵活的应用程序设计。但是,这也可能导致大量用于传递上下文的样板代码。看看当我们要将accountId和scopeId从页面传递到以下两个级别的小部件时会发生什么:
class MyPage extends StatelessWidget {
final int accountId;
final int scopeId;
MyPage(this.accountId, this.scopeId);
Widget build(BuildContext context) {
return new MyWidget(accountId, scopeId);
}
}
class MyWidget extends StatelessWidget {
final int accountId;
final int scopeId;
MyWidget(this.accountId, this.scopeId);
Widget build(BuildContext context) {
// -
new MyOtherWidget(accountId, scopeId);
...
}
}
class MyOtherWidget extends StatelessWidget {
final int accountId;
final int scopeId;
MyOtherWidget(this.accountId, this.scopeId);
Widget build(BuildContext context) {
//
...
如果不加以检查,此模式很容易在整个代码库中蔓延。通过这种方式,我们已经亲自设置了30多个小部件的参数。
MyWidget
如上例所示,小部件将近一半的工作时间仅接收参数以进一步传递它们。
MyWidget的状态与参数无关,但是每次参数更改时它都会重新构建!
当然,必须有更好的方法... InheritedWidget
简介。 简而言之,它是一种特殊的小部件,它在子树的根处定义上下文。它可以有效地将该上下文提供给该子树中的每个小部件。对于Flutter开发人员来说,访问模式应该看起来很熟悉:
final myInheritedWidget = MyInheritedWidget.of(context);
此上下文只是Dart类。因此,它可以包含您想要填充的任何内容。许多常用的Flutter上下文(例如
Style
or MediaQuery
)只不过是位于关卡上的InheritedWidgets MaterialApp
。
如果使用InheritedWidget作为上述示例的补充,那么我们将得到:
class MyInheritedWidget extends InheritedWidget {
final int accountId;
final int scopeId;
MyInheritedWidget(accountId, scopeId, child): super(child);
@override
bool updateShouldNotify(MyInheritedWidget old) =>
accountId != old.accountId || scopeId != old.scopeId;
}
class MyPage extends StatelessWidget {
final int accountId;
final int scopeId;
MyPage(this.accountId, this.scopeId);
Widget build(BuildContext context) {
return new MyInheritedWidget(
accountId,
scopeId,
const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget();
Widget build(BuildContext context) {
// -
const MyOtherWidget();
...
}
}
class MyOtherWidget extends StatelessWidget {
const MyOtherWidget();
Widget build(BuildContext context) {
final myInheritedWidget = MyInheritedWidget.of(context);
print(myInheritedWidget.scopeId);
print(myInheritedWidget.accountId);
...
重要的是要注意:
- 现在
const
,构造函数使这些窗口小部件可缓存。这提高了生产率。 - 更新参数后,将创建一个新参数
MyInheritedWidget
。但是,与第一个示例不同,不会重建子树。相反,Flutter维护一个内部注册表,该注册表跟踪已访问this的小部件InheritedWidget
,并仅重建使用此上下文的那些小部件。在此示例中,为MyOtherWidget
。 - 如果由于参数更改之外的其他原因(例如方向更改)而重建树,则您的代码仍可以构建新的树
InheritedWidget
。但是,由于参数保持不变,因此不会通知子树中的窗口小部件。这是updateShouldNotify
您实现的功能的目的InheritedWidget
。
最后,让我们谈谈良好做法。
InheritedWidget应该很小
由于Flutter无法确定上下文的哪一部分正在更新以及窗口小部件正在使用哪一部分,因此将它们与很多上下文一起重载会导致失去上述第二和第三项优点。代替:
class MyAppContext {
int teamId;
String teamName;
int studentId;
String studentName;
int classId;
...
}
喜欢做:
class TeamContext {
int teamId;
String teamName;
}
class StudentContext {
int studentId;
String studentName;
}
class ClassContext {
int classId;
...
}
使用const创建小部件
没有const,就不会有选择地重建子树。Flutter为子树中的每个小部件创建一个新实例并调用它
build()
,浪费了宝贵的周期,尤其是在您的构建方法足够繁琐的情况下。
密切注意InheritedWidget
-s的范围
InheritedWidget
-y放置在小部件树的根目录。实际上,这决定了它们的范围。在我们的团队中,我们发现能够在小部件树中的任何地方声明上下文是过大的。我们决定限制上下文小部件仅接受Scaffold
(或其派生)子级。这样,我们确保最精细的上下文可以在页面级别,并且得到两个作用域:
- 应用程序级小部件,例如
MediaQuery
。它们适用于任何应用程序中的任何页面上小部件,因为它们位于应用程序的窗口小部件树的根。 - 页面级别的小部件
MyInheritedWidget
,如上面的示例。
您应该根据上下文的位置选择一个或另一个。
页面级小部件不能穿越路线边界
似乎很明显。但是,这具有严重的含义,因为大多数应用程序具有多个级别的导航。这是您的应用程序的外观:
> School App [App Context]
> Student [Student Context]
> Grades
> Bio
> Teacher [Teacher Context]
> Courses
> Bio
这是Flutter看到的:
> School App [App Context]
> Student [Student Context]
> Student Grades
> Student Bio
> Teacher [Teacher Context]
> Teacher Courses
> Teacher Bio
从Flutter的角度来看,没有导航层次结构。每个页面(或支架)都是与应用程序小部件关联的小部件树。因此,当您使用
Navigator.push
这些页面进行显示时,它们不会继承带有父上下文的窗口小部件。在上面的示例中,您需要将上下文Student
从“学生”页面显式传递到“学生简历”页面。
虽然有不同的方式来传递上下文,但我建议以老式的方式对路由进行参数化(例如,如果使用命名路由,则使用URL编码)。它还确保了可以完全基于路由来构建页面,而不必使用其父页面的上下文。
祝您编码愉快!
及时上课!