Flutter widget trees can be deep…
The composable nature of Flutter widgets lends itself to very elegant, modular and flexible app design. However, it can also lead to a lot of boilerplate code for passing context around. Watch what happens when we want to pass accountId and scopeId from the page to a widget two levels below:
If not kept in check, this pattern can spread very easily throughout your code base. We had over 30 widgets being parameterized this way. Nearly half the time, the widget was receiving parameters just to pass it along, just like the
MyWidget example given above.
MyWidget’s state is independent of the parameters and yet it was getting rebuilt every time the parameters were changing!
Surely, there must be a better way…
In a nutshell, this is a special kind of widget that defines a context at the root of a sub-tree. It can efficiently deliver this context to every widget in that sub-tree. The access pattern would look familiar to Flutter developers:
final myInheritedWidget = MyInheritedWidget.of(context);
This context is just a Dart class. As such, it can contain anything you care to put in there. Many of the frequently used Flutter contexts such as
MediaQuery are nothing but inherited widgets living at the
If we augment the example above using inherited widgets, here’s what we get:
It is important to note:
- The constructors are now
constmaking these widgets cacheable; thus increasing performance.
- When the parameters get updated, a new
MyInheritedWidgetis built. However, unlike the first example, the subtree is not rebuilt. Instead, Flutter keeps an internal registry that keeps track of widgets that have accessed this inherited widget and only rebuilds widgets that use this context. In this example, that is
- If the tree gets rebuilt due to a reason not related to the parameters such as orientation change, your code can still build a new inherited widget. However, since the parameters are the same, widgets in the sub-tree would not be notified. This is purpose of the
updateShouldNotifyfunction implemented by your inherited widget.
Finally, let’s talk about good practices:
Keep inherited widgets small
Overloading them with a lot of context ends up costing you the 2nd and 3rd advantage mentioned above since Flutter cannot detect which part of the context is updated and which part the widgets are using. Instead of:
Use const to build your widgets
Without const, selective rebuilding of the sub-tree does not happen. Flutter creates a new instance of each widget in the sub-tree and calls build() wasting precious cycles especially if your build methods are heavy.
Properly scope your inherited widgets
Inherited widgets are placed at the root of a widget tree. That, in effect, becomes their scope. In our team, we found that the power to declare context anywhere in the widget tree is a bit too much. So, we restrict our context widgets to accept only
Scaffold (or its derivatives) as a child. That way, we ensure the most granular context can be at the page level ending up with two types of scope:
- App-scoped widgets such as
MediaQuery. These are accessible by any widget on any page in your app since they sit at the root of your app widget tree.
- Page-scoped widgets such as the
MyInheritedWidgetin the example above.
You should choose one or the other depending on where the context is applicable.
Page-scoped widgets cannot cross route boundary
This one seems like a no-brainer. However, it has profound implications because most apps have more than one levels of navigation. This is what your app could look like:
This is what Flutter sees:
From Flutter’s perspective, a navigation hierarchy does not exist. Each page (or scaffold) is a widget tree tied to the application widget. Therefore, when you use
Navigator.push to display these pages, they do not inherit the widget carrying parent’s context. In the example above, you will need to pass the
Student context from Student page to Student Bio page explicitly.
Although there are different ways to pass context, I suggest parameterizing your routes the old-fashioned way (such as URL style encoding if you use named routes). This also ensures the pages can be constructed solely from the route without needing the context from their parent page.