State management
Learning objectives
- You know what state is and why state management is needed.
- You know how to create widgets that update on state change.
So far, our applications have included buttons with event-driven functionality for navigating between screens. We've also practiced working with variables and used string interpolation. Combining buttons with string interpolation, we could create an application that shows the value of a variable in a button.
The above program shows a button with the text "Current count is: ", followed by the value of the variable count
. When we press the button, a function that prints the value of the variable count
is executed.
We can easily change the function associated to the button to increment the value of count
and to print the incremented value to the console.
When we try out the application, we notice that while the value of the variable count
that is being printed to the console indeed increases, the text shown in the button does not reflect this change. This happens because the button is created when the program is run, and the changes to the value of count
are in no way linked to the button.
Application state
Application state refers to the variables in the application and the idea that the variable values can and do influence the application. Depending on the used framework, some of the variables can be hidden from the user and the programmer, some can be hidden from the user but available for the programmer, and some of them can also be adjusted by the user.
As an example, in the navigation functionality, there can be a briefly visible animation that is shown when we move from one screen to another. This animation is managed using variables that are shown to the programmer or the user (unless we as programmers decide to adjust the animation).
So far, when building our own widgets, we have extended the StatelessWidget
. Let's look into StatefulWidget next.
Creating widgets with state
In Flutter, creating widgets with state is done using StatefulWidget and State. They work in tandem -- a class that extends State
outlines the logic of the widget and defines how it is built, while a class that extends StatefulWidget
defines non-changing variables for the widget (if any) and provides a createState
method used to create the widget out of the class extending State
.
The following example outlines the interplay of these two classes. We define a class called CounterWidget
that extends StatefulWidget
. The class has a method State<CounterWidget> createState()
that is used to create an instance of the class _CountState
, which in turn extends State
. We use type parameters in the class definition for _CountState
to explicitly outline that the state is related to the CounterWidget
.
In the _CountState
class, we have an integer _count
that is set to 0. The _CountState
class also defines a function Widget build(BuildContext context)
, which is used to create the widget. The implementation is similar to what we have seen when working with stateless widgets.
When you try out the above example, you see a program that shows the value 0
.
Why the underscore _?
Unlike languages like Java, Dart does not feature explicit keywords for defining methods (or classes) public and private. The underscore is used for this purpose -- prefixing a variable name or a class name with the underscore indicates that they are private, and hence cannot be accessed from outside.
Changing widget state
Changing the widget state is realized using a method setState
that is inherited from the class State
. When we call the method setState
, we indicate that the state of the widget has changed. This leads to the build
method being called, and consequently to the widget being recreted and shown again.
The method setState
takes a function as a parameter, where the function defines the change in state.
In the following example, we have created a method _increment
to the _CountState
class. The method _increment
calls setState
, passing it a function that increments the value of the variable _count
by one. In addition, we have changed the build
method so that instead of showing the value of _count
as text, we show the value of _count
as button text. Pressing the button leads to calling the _increment
method.
Now, when you try out the application, you'll observe that the value shown in the button changes whenever the button is pressed.
Built components and state change
Calling the setState
method leads to a notification of a change in state, which in turn leads to calling the method build
. The newly created widget that is returned from the method build
replaces the old widget. If the widget includes other widgets, they are also recreated.
This is demonstated in the application below. The application uses a stateless widget called MyText
that simply shows a Text widget and an ElevatedButton widget with a count. Pressing the button calls the method _increment
, which in turn calls the setState function and changes the the count.
When you try out the application, you notice that whenever the button is pressed, the text MyText built
appears in the console. This holds even though the MyText component does not contain anything that would need to be recreated.
Note that, however, only the widget(s) defined in the build
method of the class whose state changes are recreated. Other widgets remain the same.
On the role of const
When we use the keyword const
to create (and define) widgets, Flutter can create a cache of the widgets and reuse them. To demonstrate this, the following example contains the above program adjusted so that the MyText
widget has a const
constructor and the widget is created using the const
keyword.
When you try out the application, you'll notice that the MyText widget is no longer recreated on every state change.
Properties in StatefulWidget
A stateful widget can contain properties, declared using final
, which are then available in the state related to that widget. Properties from the StatefulWidget can be accessed in the state through the widget
property, inherited from state.
This is demonstrated in the following application. The class MahnaMahnaLyrics
extends StatefulWidget
and contains a property lyrics
that is a list (of lyrics). Class _MahnaMahnaLyricsState
has an index variable _i
that is used to keep track of the index of the word in the lyrics that is presently shown. Whenever a user presses the button, the lyrics move one word further.
State and StatefulWidget
Here, we briefly practiced building widgets that have state and that are recreated whenever the state changes. This is needed to change what is shown to the user when the application state changes.
We used a combination of StatefulWidget
and State
, which are "vanilla" features of Flutter. A sample of a program using them is shown below. The program uses the named function Transform.rotate from Transform to rotate a button -- the rotation is determined based on the state, which is changed whenever the button is pressed.
Let's consider a situation where we would have a hierarchy of widgets, where a child widget would wish to influence the state of a parent widget. One solution would be to use a function as a property of the child widget, which the parent widget would provide as an argument. This function would then be called by the child widget upon an action where the parent state would need to be changed. This is demonstrated in the example below.
In the above case, we would have to pass a function to the child widget as an argument. If the widget that would be responsible for changing the state (or would need to use the state) would be further along the tree, the function would have to be passed through all the widgets in the tree, even through those that do not need it.
This approach is not ideal (and not recommended). We wish to avoid passing data or functionality through widgets that do not need them.
Let's next look into Riverpod
, which is a library that can be used to manage state in Flutter applications.