The roots of Flutter widget trees can go very deep ...
Very deep.
The component nature of Flutter widgets allows for very elegant, modular and flexible application designs. However, this can also result in a lot of boilerplate code for passing context. Check out what happens when we want to pass the accountId and scopeId from the page to the widget two levels below:
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) {
//
...
If left unchecked, this pattern can very easily creep across the entire codebase. We have personally parameterized over 30 widgets this way. Almost half of the working time, the widget received parameters only to pass them further, as in
MyWidget
the example above.
MyWidget's state is independent of parameters, and yet it rebuilds every time the parameters change!
Of course, there has to be a better way ...
Introducing InheritedWidget .
In a nutshell, it is a special kind of widget that defines the context at the root of a subtree. It can efficiently provide this context to every widget in that subtree. The access pattern should look familiar to a Flutter developer:
final myInheritedWidget = MyInheritedWidget.of(context);
This context is just a Dart class. Thus, it can contain whatever you want to stuff there. Many of the commonly used Flutter contexts, such as
Style
or MediaQuery
, are nothing more than InheritedWidgets that live on the level MaterialApp
.
If we supplement the above example using InheritedWidget, this is what we get:
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);
...
It is important to note:
- Constructors are now
const
what makes these widgets cacheable; which increases productivity. - When the parameters are updated, a new one is created
MyInheritedWidget
. However, unlike the first example, the subtree is not rebuilt. Instead, Flutter maintains an internal registry that keeps track of the widgets that have accessed thisInheritedWidget
, and rebuilds only those widgets that use this context. In this example, it isMyOtherWidget
. - If the tree is being rebuilt for reasons other than parameter changes, such as orientation changes, your code can still build a new one
InheritedWidget
. However, since the parameters remain the same, the widgets in the subtree will not be notified. This is the purpose of the functionupdateShouldNotify
implemented by yoursInheritedWidget
.
Finally, let's talk about good practices.
InheritedWidget should be small
Overloading them with a lot of context leads to the loss of the second and third advantages mentioned above, since Flutter cannot determine which part of the context is being updated and which part is being used by widgets. Instead:
class MyAppContext {
int teamId;
String teamName;
int studentId;
String studentName;
int classId;
...
}
Prefer to do:
class TeamContext {
int teamId;
String teamName;
}
class StudentContext {
int studentId;
String studentName;
}
class ClassContext {
int classId;
...
}
Use const to create your widgets
Without const, there is no selective rebuilding of the subtree. Flutter creates a new instance of every widget in the subtree and invokes it
build()
, wasting precious cycles, especially if your build methods are heavy enough.
Keep an eye on the scope of your InheritedWidget
-s
InheritedWidget
-y are placed at the root of the widget tree. This, in fact, determines their scope. In our team, we found that being able to declare context anywhere in the widget tree was overkill. We decided to restrict our contextual widgets to accept only Scaffold
(or its derivatives) as children. This way we ensure that the most granular context can be at the page level, and we get two scopes:
- Application-level widgets such as
MediaQuery
. They are available to any widget on any page in your application, since they are located at the root of your application's widget tree. - Page level widgets like
MyInheritedWidget
the example above.
You should choose one or the other depending on where the context applies.
Page level widgets cannot traverse a route border
It seems obvious. However, this has serious implications because most applications have more than one level of navigation. This is what your application might look like:
> School App [App Context]
> Student [Student Context]
> Grades
> Bio
> Teacher [Teacher Context]
> Courses
> Bio
This is what Flutter sees:
> School App [App Context]
> Student [Student Context]
> Student Grades
> Student Bio
> Teacher [Teacher Context]
> Teacher Courses
> Teacher Bio
From Flutter's perspective, there is no navigation hierarchy. Each page (or scaffold) is a tree of widgets associated with an application widget. Therefore, when you use
Navigator.push
these pages to display, they do not inherit the widget that carries the parent context. In the example above, you will need to explicitly pass the context Student
from the Student page to the Student Bio page.
While there are different ways to pass context, I suggest parameterizing routes in the old-fashioned way (for example, URL encoding if you're using named routes). It also ensures that pages can be built purely based on the route without having to use the context of their parent page.
Happy coding!
Be in time for the course!