50.1 InheritedWidget
First, we will introduce the InheritedWidget
component and then focus on the relationship between the didChangeDependencies
callback in the State
class and the InheritedWidget
.
1. Introduction
InheritedWidget
is a crucial functional component in Flutter that provides a way to share data down the widget tree. For instance, if we share data through an InheritedWidget
at the root of the application, we can access that shared data in any descendant widget! This feature is especially convenient in scenarios where data needs to be shared across the entire widget tree. For example, the Flutter SDK uses InheritedWidget
to share application themes and locale (current language) information.
InheritedWidget
is similar to the context feature in React. Compared to passing data down step-by-step, it allows components to pass data across multiple levels. The data flow direction of InheritedWidget
is top-down, which is the opposite of the notification direction (to be introduced in the next chapter).
Now, let’s look at the InheritedWidget
version of the previous “Counter” example. Note that this example is primarily to demonstrate the functionality of InheritedWidget
and is not the recommended implementation for a counter.
First, we extend InheritedWidget
to store the current counter click count in the data
property of ShareDataWidget
:
class ShareDataWidget extends InheritedWidget { ShareDataWidget({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); final int data; // Data to be shared in the subtree, storing click count // Convenience method for widgets in the subtree to access shared data static ShareDataWidget? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>(); } // This callback decides whether to notify the subtree to rebuild when data changes @override bool updateShouldNotify(ShareDataWidget old) { return old.data != data; } }
Next, we implement a child component _TestWidget
, which references the data in ShareDataWidget
within its build
method. Additionally, it logs information in the didChangeDependencies()
callback:
class _TestWidget extends StatefulWidget { @override __TestWidgetState createState() => __TestWidgetState(); } class __TestWidgetState extends State<_TestWidget> { @override Widget build(BuildContext context) { // Use shared data from InheritedWidget return Text(ShareDataWidget.of(context)!.data.toString()); } @override // Detailed explanation to follow. void didChangeDependencies() { super.didChangeDependencies(); // Called when the InheritedWidget in the parent or ancestor changes (when updateShouldNotify returns true). // This callback won't be called if build does not depend on InheritedWidget. print("Dependencies change"); } }
2. didChangeDependencies
When we previously introduced StatefulWidget
, we mentioned that the State
object has a didChangeDependencies
callback, which is called by the Flutter framework when "dependencies" change. This "dependency" refers to whether a child widget is using data from an InheritedWidget
in the parent widget! If it is used, the child widget is considered dependent; if not, it is considered independent. This mechanism allows child components to update themselves when their dependent InheritedWidget
changes! For instance, when themes, locales, etc., change, the didChangeDependencies
method of dependent child widgets will be called.
Finally, we create a button that increments the value of ShareDataWidget
each time it is pressed:
class InheritedWidgetTestRoute extends StatefulWidget { @override _InheritedWidgetTestRouteState createState() => _InheritedWidgetTestRouteState(); } class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> { int count = 0; @override Widget build(BuildContext context) { return Center( child: ShareDataWidget( // Use ShareDataWidget data: count, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: const EdgeInsets.only(bottom: 20.0), child: _TestWidget(), // Child widget depends on ShareDataWidget ), ElevatedButton( child: Text("Increment"), // Each click increments count and rebuilds; ShareDataWidget's data will be updated onPressed: () => setState(() => ++count), ) ], ), ), ); } }
After running, the interface is shown in Figure :
Each time the button is clicked, the counter increments, and the console prints the following log:
I/flutter ( 8513): Dependencies change
It is evident that when dependencies change, the didChangeDependencies()
method is called. However, readers should note that if the build
method of _TestWidget
does not use the data from ShareDataWidget
, its didChangeDependencies()
will not be called, as it does not depend on ShareDataWidget
. For example, if we modify the code of __TestWidgetState
as shown below, didChangeDependencies()
will not be called:
class __TestWidgetState extends State<_TestWidget> { @override Widget build(BuildContext context) { // Use shared data from InheritedWidget // return Text(ShareDataWidget.of(context)!.data.toString()); return Text("text"); } @override void didChangeDependencies() { super.didChangeDependencies(); // The build method does not depend on InheritedWidget, so this callback will not be called. print("Dependencies change"); } }
In the code above, we commented out the part of the build()
method that depends on ShareDataWidget
and returned a fixed Text
. As a result, when the Increment button is clicked, although the data
of ShareDataWidget
changes, since __TestWidgetState
does not depend on ShareDataWidget
, the didChangeDependencies
method of __TestWidgetState
will not be called. This mechanism is quite understandable because it is reasonable and performance-friendly to only update the widgets that use the data when the data changes.
Question: How does the Flutter framework know whether a child widget depends on the parent InheritedWidget
?
What Should be Done in didChangeDependencies()
?
Generally, child widgets rarely override this method, as the Flutter framework also calls the build()
method to rebuild the component tree when dependencies change. However, if you need to perform some expensive operations, such as network requests, after dependencies change, the best approach is to execute them in this method to avoid executing these expensive operations every time build()
is called.
50.2 In-Depth Understanding of InheritedWidget
Now, let’s consider what to do if, in the above example, we only want to reference the data from ShareDataWidget
in __TestWidgetState
, but do not want the didChangeDependencies()
method of __TestWidgetState
to be called when ShareDataWidget
changes. The answer is straightforward: we just need to modify the implementation of ShareDataWidget.of()
:
// Convenience method for widgets in the subtree to access shared data static ShareDataWidget of(BuildContext context) { // return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>(); return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget; }
The only change is the way to obtain the ShareDataWidget
object, replacing dependOnInheritedWidgetOfExactType()
with context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget
. What is the difference between these two methods? Let’s look at the source code of these two methods (the implementation code is in the Element
class; we will introduce the relationship between Context
and Element
later):
@override InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() { final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; return ancestor; } @override InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; // Additional code if (ancestor != null) { return dependOnInheritedElement(ancestor, aspect: aspect) as T; } _hadUnsatisfiedDependencies = true; return null; }
We can see that dependOnInheritedWidgetOfExactType()
calls the dependOnInheritedElement
method, which has the following source code:
@override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) { assert(ancestor != null); _dependencies ??= HashSet<InheritedElement>(); _dependencies.add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; }
In the dependOnInheritedElement
method, the main function is to register the dependency! With this, we can clearly understand the difference: calling dependOnInheritedWidgetOfExactType()
registers a dependency, while getElementForInheritedWidgetOfExactType()
does not. Therefore, when dependOnInheritedWidgetOfExactType()
is called, the relationship between InheritedWidget
and its dependent descendant components is registered. Afterward, when InheritedWidget
changes, it will update the dependent descendant components, which means calling their didChangeDependencies()
and build()
methods. In contrast, when getElementForInheritedWidgetOfExactType()
is called, there is no registered dependency, so when InheritedWidget
changes, it will not update the corresponding descendant widgets.
Note that if we change the implementation of the ShareDataWidget.of()
method in the above example to call getElementForInheritedWidgetOfExactType()
, after running the example and clicking the "Increment" button, we will find that the didChangeDependencies()
method of __TestWidgetState
will indeed not be called. However, its build()
will still be called! The reason for this is that clicking the "Increment" button calls the setState()
method of _InheritedWidgetTestRouteState
, which will rebuild the entire page. Since the __TestWidget
in this example does not have any caching, it will also be rebuilt, hence its build()
method will be called.
Now, this raises a question: we actually only want to update the components in the subtree that depend on ShareDataWidget
, but now every time we call the setState()
method of _InheritedWidgetTestRouteState
, all child nodes will be rebuilt, which is unnecessary. Is there a way to avoid this? The answer is caching! A simple approach is to encapsulate a StatefulWidget
to cache the subtree of child widgets. In the next section, we will demonstrate how to cache and use InheritedWidget
to achieve global state sharing in Flutter by implementing a Provider
widget.