Navigating StatefulWidgets Part 1
Greg Perry
Posted on March 8, 2024
A look at Flutter’s StatefulWidget, its State and its Lifecycle.
If you’re going to use Flutter, you’re going to use StatefulWidget’s. A lot of them. Called one after the other. It’s inevitable. As you learn Flutter, your apps will get more complicated with more StatefulWidgets. While a Stateless widget is to never change once created, a StatefulWidget’s State object can change internally in response to user interactions and system events.
In this article, I’ll review with you the ‘event’ functions that are typically fired in both the StatefulWidget object and, in particular, the State object while your Flutter app is running. We’ll review which ones will fire and which ones won’t depending on the circumstance — particularly when the StatefulWidget is called with or without the const keyword.
Learn By Example
I’ll use the ‘starter app’ generated for you every time you create a ‘New Flutter Project…’. Highlighted below is the one internal change caused by user interaction and applied to mutable data. By design, such changes are to occur in the State object leaving the StatefulWidget and the remaining State object’s code untouched.
Make It Final
Another look at this simple example app below highlights what is indicated to the compiler and to the runtime engine to be final, constant, and unchanging. Note the keywords, const and final, dotted here and there in the code. Explicitly placing these keywords in your code will only improve runtime performance. The const keyword, for example, will make variables and objects unchanging after they’re first compiled and before the code is even run. They’re defined at compile-time and not created at runtime! Very important. Once defined, they’ll provide a ‘value’ at runtime. They’re not created at runtime. Remember this!
As you see above, the whole class, MyHomePage, is now constant. Unchanging. It was given a ‘constant constructor’ by placing the const keyword before the name of its constructor. This means all instance fields in that class must be declared final. Further, to take full advantage of this, that class must then be called with the const keyword. See below. I'll show you why shortly.
A Constant Practice
If your app produces objects that will never change, you should make these objects compile-time constants — assigned with the const keyword. Variables that will contain an unchanging value even before you run your app should also be assigned the const keyword. Variables assigned a value only once at runtime should be declared with the keyword, final.
In time, as you work with Flutter, you’ll easily spot such instances in your code. However, you could let your Linter pick them out for you. As you know, in most IDE’s, if you forget a semicolon to terminate a line, you’ll likely see a little red line indicating the issue. Missing const and final keywords, depending on the analysis settings, will be highlighted as well.
Note, that although a final object cannot be modified, its fields can be changed. However, in comparison, a const object and its fields cannot be changed. Once defined, they’re now forever immutable.
The Life of a StatefulWidget
Let’s get back to this simple example app and its StatefulWidget. We’ll now examine the ‘events’ that occur in a StatefulWidget and its accompanying State object. You can follow along with the gist file, counter_app_with_prints.dart. There are several print() methods now introduced to the code. They are placed in locations to demonstrate the ‘lifecycle’ of the StatefulWidget and its State object. See below.
Sacrifice Performance For Clarity
Note, that having now a print() method in the MyHomePage’s constructor makes it no longer eligible to be a constant object. The keyword, const, in the class and where it was called had to be removed. See below. Something to remember, a constant constructor can’t have a constructor body.
Follow The Arrows
Now with those print() methods, we can readily see what happens when this simple counter app first starts up. As expected, looking at the console screen below, the two classes that make up the StatefulWidget and State object are created, and then the State object’s build() function is called to display the resulting screen.
An Unique Start
Unique to StatefulWidgets, if and when first created, their corresponding State classes will have two functions called one after the other: initState() and didChangeDependencies(). The initState() method will only be called when a State object is first created. However, once created, the State object’s didChangeDependencies() will be called again and again, but only if that StatefulWidget has become a ‘dependent’ to an InheritedWidget using either of these two functions:
dependOnInheritedWidgetOfExactType()
dependOnInheritedElement()
When an InheritedWidget is called again in Flutter, all its ‘dependent’ widgets will have their corresponding build() functions called again as well. A very powerful circumstance that I‘ll demonstrate in part two of this article. For now, remember the one-time call for the initState() and didChangeDependencies() methods. Traditionally, it’s in the initState() method where you ‘initialize’ any necessary resources to be used by that State object at that particular time. The didChangeDependencies() method tells you the State object is going to be built again because it is dependent of an InheritedWidget.
A Button Pressed
Press the FloatingActionButton once, and you get what follows on your IDE’s console screen below. Calling the State object’s setState() method will cause the State object’s build() function to be called again. As you see, the integer variable, _counter, was incremented by the time that build() function was executed again, displaying now the number, one.
Every time you press the button, you’re going to see the State object’s build() function get called again. Below, is a screenshot of the console screen when the button was pressed six times.
It Gets Complicated
So far, this has been simple. It’s a really simple app. However, your apps are not going to be this simple. They’re going to be complicated. Let’s see if I can introduce such complexity while still using the concept of a simple counter app.
Two Screens; Four Counters
So I’ll now provide you with another version of that counter app. A copy is available to you in the gist, three_counter_app_with_prints. In this version, there are four counters across two separate screens. Things have changed in ways you would see in more complex apps. First and foremost, compared to the original counter app, you’re going to see StatefulWidgets starting up other StatefulWidgets.
In truth, since we’re using the Flutter framework’s MaterialApp widget, the graphic was modified to highlight the MaterialApp Widget and two other StatefulWidgets that already call one another. So even the lone counter app has more StatefulWidgets than you know. Note, that the Navigator widget manages all the screens that make up a Flutter app.
Below is a video of this more complicated counter app. The State object, HomePageState, now has its own counter displayed in the AppBar widget. There’s a StatefulWidget called, FirstPage, displaying the original counter. There’s a separate counter contained in a separate StatefulWidget called, FirstCounter, and so now two FloatingActionButtons are on the one screen.
Finally, you have the ‘Second Page’ StatefulWidget displaying its counter on a separate screen altogether — brought up using the Navigator’s push() function. This more complex app is also peppered with print commands so to highlight the underlying sequence of events that commonly occur.
Looking at the console screen below, you can see the sequence in which the StatefulWidgets and their State objects are created. You can see what functions are called and in what order.
Highlighted in the second screenshot above, is the separate StatefulWidget named, FirstCounter, called in the FirstPage’s State object’s build() function. Below is a graphic depicting the part of that Widget tree we’re now examining. Run the gist in your own IDE to follow along.
Self Contained
In the video below, the ‘First Counter’ StatefulWidget is being changed through user interaction. Its FloatingActionButton is being tapped three times and the console screen conveys what happens as a result. With every press of the button, its State object’s setState() method is called which, in turn, results in the State object’s build() function being called soon after.
A New State; A New Widget?
However, something is not quite right in this example app when the original counter is then pressed three times. Granted, the State object, FirstPageState, will have its build() function called with every press, but note the extra lines highlighted below. The function, didUpdateWidget(), tells you that the StatefulWidget, FirstCounter, is being called again and again with every press as well?!
Further, its State object, FirstCounterState, although not being re-created, is having its build() function being called again and again as well?! Now, if that’s not by design, that’s not very efficient! Only the FirstPageState’s build() function needs to be called again and again in this instance.
However, it continues with the HomePage State object updating its counter! Below we now see the FirstPage StatefulWidget and the FirstCounter StatefulWidget both being called again and again when, frankly, they don’t need to be. Let’s see why this is happening.
Constant Vigilance
Looking at the screenshots below, you can see the problem. Both classes use constant contructors and yet both are not being called with the const keyword. The Linter highlights the issue with a curly line declaring the const keyword would improve performance. Extraneous code will not run if the const keyword is in use. This will be demonstrated next.
Keep It Constant
In the first screenshot below, I’ve commented out the original return statement that calls the FirstPage StatefulWidget without a const keyword allowing for the next line to run. It’s the very same copy of the original StatefulWidget, however, it’s being called with the const keyword. In the second screenshot below, this copy, in turn, calls its ‘FirstCounter’ equivalent also with the const keyword.
I know, I know, it’s a bit of overkill making two more complete copies of these StatefulWidgets only to be called with the const keyword, but it’s to make things a little easier for you to understand. With your gist copy, you can then simply comment and uncomment the different return statements and come to appreciate the use of the const keyword.
With that one slight modification (adding the const keyword), the video below now produces a very different set of lines on the console screen. Note the names, _FirstCounterWithConstState and _FirstPageWithConstState. They are the only objects being manipulated by having their buttons pressed and so there are no extra StatefulWidgets or build() functions being called. Very efficient. Remember the const keyword!
A New Key; A New State
Let’s continue this little exercise and allow the next return statement that follows to now run. As you see in the first screenshot below, a unique Key value is now being passed every time the _FirstPage StatefulWidget is called. In the previous two return statements, the Key value was unchanging — it was, in fact, the null value. In both cases, there were no parameters passed.
However, a change in the Key value passed to a StatefulWidget results in an interesting consequence that you should be aware of. It’ll affect both the FirstPage StatefulWidget and the FirstCounter StatefulWidget even with the use of the const keyword (see the second screenshot below).
In the video below, look what happens when the ‘Title Counter’ button is pressed. Highlighted below you now see two new function calls in the console screen: deactivate() and dispose()
You see, in Flutter, when the HomePageState’s build() function is called again, a unique Key value is passed to the FirstPage StatefulWidget. Doing that, will dispose of the original State objects and create new ones! Both the FirstPageState and the FirstCounterState are re-created! You can see that happening below.
As a result, as you see in the video above, the moment the ‘Title Counter’ button is tapped, the two counters on the screen change to zero. Those two counters are coming from two completely new State objects. See below. This is also something to remember.
Also, note the dispose() method calls above. They were not called right after their corresponding deactivate() calls. That’s because, unlike the deactivate() method, the dispose() method is not explicitly called by the Flutter framework but is delegated to the underlying engine as part of the ongoing ‘garbage collection’ process.
The deactivate() method is a more reliable place to release ‘time constraint’ resources as it’s consistently and predictably called IF the State object is being disposed of. That’s because it’s left to the underlying engine’s own discretion to call dispose(), and you won’t ever know when that will be.
A Big IF
Note the big IF is in the sentence above, ‘IF the State objec is being disposed of’. However, the deactivate() method is also called when the State object is not being disposed of, it’s just being moved.
Your app may simply move Widgets around within the Column widget for example. It could happen, and so you should be prepared for this. In Flutter, your app is ‘perceived’ as one big tree, with each Widget connected to the previous in a giant Linked List frankly. Changing the position of a Widget within a Column widget, the Flutter engine, for efficiency, won’t re-create the Widget’s Element counterpart (more on that later), but simply reorder the Widgets in the tree.
You see, the deactivate() method is also called when a Widget is removed (marked inactive and disconnected from the previous Widget) from the tree, and the activate() method is called when re-inserted into the tree. So keep that in mind, if you do release resources in the deactivate() method, you may want to retain them again in the activate() method.
To Be Continued
The second part of this article, Navigating StatefulWidgets Part 2, continues with the remaining return statements. There, I’ll demonstrate to you the InheritedWidget’s special powers.
Of course, we also have the ‘Second Page’ StatefulWidget displaying its counter on a separate screen altogether. That will bring up a whole other ‘can of worms’ because of the completely separate branch produced by Flutter’s Navigator class. Look at the Widget tree graphic below for a hint.
Cheers.
Posted on March 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.