DEV Community

Cover image for Navigating StatefulWidgets Part 2
Greg Perry
Greg Perry

Posted on • Updated on

Navigating StatefulWidgets Part 2

A continued look at Flutter’s StatefulWidget, its State and its Lifecycle.

I’ll continue where I left off in my previous article, Navigating StatefulWidgets Part 1, working with the gist, three_counter_app_with_prints and commenting out the list of return statements to demonstrate how both the StatefulWidget and its State object behave under certain circumstances in your typical Flutter app. With numerous print() methods in and among the code, we’ll get some good insight as to what event functions fire in the Flutter framework (from user or system interaction) and in what order.

Let’s begin.

Your Inheritance

The next two return statements involve an Inherited Widget. State objects will fire a particular event function called, didChangeDependencies, because an InheritedWidget is now involved, and I’ll walk through the whole sequence of Flutter framework function calls that finally result in the build() functions of isolated and dispersed Stateless & StatefulWidgets being called again, and thus, rebuilding them seemingly by magic.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Let’s look at the first screenshot above. The InheritedWidet is taking in the StatefulWidget, _FirstPageInherited. Yes, it’s another copy of the original StatefulWidget, FirstPage, and reproduced again merely for demonstration purposes. It has a FirstCounter StatefulWidget equivalent named, _FirstCounterInherited, and it has been modified to better demonstrate an InheritedWidget’s effect on StatefulWidgets.

Note, that the keyword, const, is used to make the _FirstPageInherited class a constant defined at compile-time. Back in part one of this article, we saw the necessity of the const keyword in calling such StatefulWidgets, so I won’t hash over that again. We know it’s important that the const keyword be there. Comment it out on your own time to remember why.

Depend On It

So, you now know the ‘Inherited’ version of FirstCounterState does things a little differently, and yet, judging from the video and console screen below, it appears to be behaving as in part one of this article. The FirstCounter button is tapped three times, and we get a count up to three:

Yes, but that console screen above is from the first article. It’s not using the _FirstCounterInheritedState class. It’s using the original FirstCounterState class. The second screenshot below uses the _FirstCounterInheritedState class, and there’s a difference. Tap on the screenshots below to zoom in for a closer look.

Non-inherited version vs. Inherited version

So, a button press causes the ‘HomePage’ State object to call its setState() method. Hence its build() function is called soon after. Next, the _FirstCounterInheritedState’s build() function is called updating the count, but not before calling its didChangeDependencies() method.
Do you know why?
It’s because the StatefulWidget, _FirstCounterInherited, is a ‘dependent’ to some InheritedWidget (actually its Element is) that was called again when the Home Page was rebuilt again. Do you follow?
The first screenshot below is of the _FirstCounterInheritedState class. When the button is pressed, indeed, it’s the Home Page State object that calls its setState() method: homePageState.setState((){});
The second screenshot is of that InheritedWidget. Granted, not much to it, but it gets the job done.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

In the first screenshot below you readily see the InheritedWidget being called again in the HomePageState object. In the second screenshot, you see in the FirstCounterInheritedState object how its StatefulElement object, _context, becomes dependent on that InheritedWidget. Still follow?

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

No Inheritance? No Change

As a quick aside, I’ll comment out that ‘dependency’ line in the _FirstCounterInheritedState class, and we can see what happens. The FirstCounter continues to increment its counter, but its build() function is not called again — the interface is not updated. Simple as that. Remember, it’s being called with the const keyword (see the second screenshot below) and so now there’s no means for it to be rebuilt, and so the number displayed remains at zero. That dependency would overcome this.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Inherit The State

Let’s do a walkthrough of the Flutter framework a bit to show you how this is all made possible. I’ve returned the ‘dependency line’ back and will quickly show you what happens when this function is called.

context.dependOnInheritedWidgetOfExactType<_InheritedWidget>();

The second screenshot below is of that function itself deep in the Flutter framework. You see, Flutter makes note of all the InheritedWidgets that may exist on a particular branch of the Widget tree and records them (actually records their InheritedElement counterpart) in the Map variable, _inheritedElements.

three_counter_app.dart

framework.dart

That Map variable is simply looked up by the key for an InheritedWidget of type, T. In this case, T is of type, _InheritedWidget. If found, that InheritedWidget is returned by the function, but not before its InheritedElement counterpart records the dependency.

_FirstCounterInheritedState’s StatefulElement object records the InheritedWidget in its Set variable, _dependencies, and then the _InheritedWidget’s own InheritedElement records the StatefulElement object itself into its Map variable, _dependents. See the screenshots below.

Set<InheritedElement>? _dependencies;
Map<Element, Object?> _dependents;

framework.dart

framework.dart

Now, when the InheritedWidget is called again, all the widgets referenced within that Map object, dependents,will be ‘rebuilt’. I’ll show you.

In the first screenshot below, when the InheritedWidget is called again, its InheritedElement object calls its updated() method. There, its InheritedWidget calls its updateShouldNotify() function. You’re able to override that function and dictate if the process continues. If that function returns true, the process continues, and eventually the InheritedElement’s notifyClients() method is called to iterate through that Map object, _dependents. See the second screenshot below.

framework.dart

framework.dart

The screenshots below depict the next few functions called.
Each Element object, dependent, will be called in turn which leads to calling its markNeedsBuild() method. See the second screenshot below. It’s the very same function called by a State object’s setState() method. Note, after all this, the corresponding State object’s didChangeDependencies() method is called indicating the State object is going to be rebuilt because of an InheritedWidget.
And there you have it.

framework.dart

framework.dart

framework.dart

framework.dart

Constantly No Inheritance

An InheritedWidget usually takes in a non-constant parameter and so are not eligible to have a constant constructor and be called with the const keyword. That’s a good thing since adding the keyword in this case (see below) makes this InheritedWidget simply worthless. It’s compiled and provides a Widget at runtime — nothing more. When the HomePage State object’s build() function is called again and again counters are incremented, but the interface is immutable so only zeros will ever be displayed. You shouldn’t find yourself calling an InheritedWidget with the const keyword very often. It’s true power will not be much good to you.

three_counter_app_with_prints.dart

The Next Page

We should turn now to the ‘Second Page’ StatefulWidget displaying its counter on a separate screen altogether. It’s brought up using the Navigator class’s push() function. More complex apps will no doubt have the Navigator class handling a great number of separate screens represented on the Widget tree. Each is handled as a separate branch.

This is by no means an exaggeration, that SecondPage StatefulWidget, because it's working with the Navigator class, is situated on a ‘separate’ branch on the Widget tree. See below. As such it has no direct access via the Widget tree to the original three StatefulWidgets ( HomePage, FirstPage and FirstCounter). None.

That’s a problem.

As you by now realized, this app has been turning to the Widget tree to retrieve, for example, the Home Page’s State object to readily increment its title bar counter from the FirstPage StatefulWidget.
See below.
However, there’s no such luck when attempting to access that counter from the Second Page’s State object. Both function calls in the second screenshot below will return null. The StatefulWidgets, HomePage, and SecondPage simply don’t reside in the same branch. So how then do you access the Home Page’s State object from the Second Page?

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Offhand, you could explicitly pass them as parameters to the SecondPage StatefulWidget in some fashion. Of course, you’ve other options. That’s why you see several third-party State Management solutions out there (i.e. Bloc, RiverPod, MobX, etc.). Of course, it’s not their sole reason, but these packages do provide a means to transverse this disconnect between branches allowing specific data to be shared by multiple screens. I’ll do the same here but using a very minimalist approach — so as not to distract you from the topic at hand.

State Control

I just whipped up a ‘State Controller’ class that simply takes in one State object so that it can then be manipulated by an otherwise out-of-reach State object. You'll see what I mean next.

The first screenshot is of the class called, StateController. The second screenshot is in the HomePageState class where its very reference is collected by this State Controller, and the third screenshot is where, by using this State Controller, the HomPageState class object is then utilized in the State class, SecondPageState. A simple approach.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

As you see in the screenshots below, this Controller class allows for all the functions and features of the State object, HomePageState, to be accessed — particularly its instance field, counter. It does this simply by implementing Flutter’s State class and the mixin, CounterState:

class StateController implements State, CounterState {

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

So, as you see in the video, the SecondPage StatefulWidget can then easily manipulate the HomePage’s counter like the FirstPage StatefulWidget does. For that matter, any Widget could then do the same from any screen.

Take Me Home

Finally, let’s step back a bit and take a closer look at the Home Page and its Titlebar counter. It’s a piece of mutable data situated in the Home Page’s State object created at the start of this app. And yet, it is updated and changed by other State objects further along the Widget tree and in other screens altogether. In more complex apps, this is going to be a thing.

In the video below, the button, Title Counter, is tapped three times and the title bar is updated accordingly. Let’s see how that’s done.

Now remember, there are currently two arrangements being showcased here. One takes advantage of an InheritedWidget, the other approach does not. Each arrangement comes about depending on which return statement is used. You can see them both in the two screenshots below.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

I reluctantly created yet another copy of the SecondPage StatefulWidget as there are, because of the InheritedWidget, subtle differences between these two arrangements. With this extra copy, you can readily them. The next few screenshots are paired up to highlight these differences.

In the first screenshot below, without the InheritedWidget involved, the SeconPageState object merely rebuilds itself with its setState() method. In the other approach, taking advantage of an InheritedWidget, the HomePageState object’s setState() method is explicitly called (so to call its InheritedWidget again) with the line, con.setState((){});
See the second screenshot below.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Now, look below at the next pair of screenshots as a consequence of what’s above. Without the InheritedWidget, the Titlebar count has to be explicitly updated with a setState() method call after the Navigator.push() function. Otherwise, the count will not be updated once you’ve left the SecondPage and returned to the FirstPage. See the first screenshot below.

In the second screenshot below, the InheritedWidget implementation already updates the FirstPage StatefulWidget with every press, so the setState() is not necessary. Again, those third-party State Management solutions would have their own varied approach to all this.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

See for yourself below. In either case, commenting out the appropriate line will result in returning from the Second Page having incremented the Titlebar count, and yet its change is not updated. It’s been incremented, to be sure. However, the interface back from the Second Page has not yet been rebuilt.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Less Is More

In the first screenshot below, (with no InheritedWidget) since the First Page’s State object is explicitly called to rebuild using its setState() method, the Titlebar counter is simply presented in the build() function and will display any changes. A traditional approach.
However, in the other approach, I’ve taken advantage of an InheritedWidget, to demonstrate again and again you should not rebuild the whole screen over again (particularly really complex ones with lots of widgets) but just rebuild, in this case, the FirstCounter’s count and the Titlebar’s count when a button is pressed.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

And so, in the second screenshot above, you see a new StatefulWidget called, HomeTitle. Granted, not much to it as you see below, but it gets the job done. Notice it’s called with the const keyword? What does that tell you?

three_counter_app_with_prints.dart

When either incrementing the FirstCounter StatefulWidget’s count or the HomePage StatefulWidget’s Titlebar count, the InheritedWidget is indirectly called again. That’s because the HomePageState object’s setState() method is called. See below.
Both counters are rebuilt when either of the two buttons are pressed, but only those widgets highlighted below— not the rest of the interface! In my opinion, this is the best reason to use InheritedWidgets. The rest of the interface (with possibly its many many widgets) are left untouched. That’s efficiency and better runtime performance.

three_counter_app_with_prints.dart

three_counter_app_with_prints.dart

Climb The Tree

As an aside, in many instances, this app has been turning to the Widget tree to retrieve the HomePage’s State object, HomePageState. See the first screenshot below. Let’s take a quick peek at the Flutter framework’s inner workings (second screenshot below) to see what’s happening when you use the function, findAncestorStateOfType().

three_counter_app_with_prints.dart

framework.dart

The term, Widget tree, is a bit misleading when going this deep into the Flutter framework. At this scope, it’s an Element tree. Each StatefulWidget has a StatefulElement counterpart, each InheritedWidget has an InheritedElement counterpart, each StatelessWidget has a StatelessElement, etc.

Each Element object has a reference to its parent Element. You can see that reference in the function above. The parent element is stored in the field called, _parent. In Flutter, this results in some really long Linked Lists. Each such list represents one branch of this so-called tree.

The findAncestorStateOfType() function simply goes back up through the parent Elements looking for one of type, StatefulElement. See below. Each StatefulElement has a reference to its State object with the instance field, state. If one of type, T, is found, that field is cast to that specified type, T, and ‘Bob’s your uncle’!

framework.dart

Fortunately, such a ‘brute force’ kind of algorithm is not too expensive on CPU cycles in this case. After all, HomePage is the ‘grandparent’ of the FirstPage StatefulWidget in the Element tree. See below. It’s a convenient way to access your app’s State objects. However, the Flutter team does suggest the findAncestorStateOfType() function be used judiciously:

/// Calling this method is relatively expensive (O(N) in the depth of the
/// tree). Only call this method if the distance from this widget to the
/// desired ancestor is known to be small and bounded.

Yet another reason those third-party State Management solutions are so popular. They also allow you quick and reliable access only the State objects you’re concerned with.

Just Scratching The Surface

The second screenshot above depicts once again the Navigator class responsible for presenting the first screen, HomePage, in this humble little counter app. And yet, this humble little app has been sitting on top of an already complicated Flutter framework with many StatefulWidgets starting up other StatefulWidgets.

Looking at the second screenshot, you might ask yourself, ‘How many widgets are between that Navigator class and the HomePage StatefulWidget and linked together in the Element tree?’

Using my IDE, I counted about 50 Elements of different sorts between those two widgets. See below. About 20 are StatefulWidgets, 11 are InheritedWidgets and 4 are StatelessWidgets — and those are only the ones I readily recognize as such Widgets!

Now, imagine the true number of Widgets involved in the whole app?! That humble little app:

Cheers.

→ Other Stories by Greg Perry

Top comments (0)