Stateful Widgets
Learning Objectives
- You know of the concept of state in Flutter.
- You know how to create stateful widgets.
- You know how to use the GetX library for managing state.
Application state
Application state refers to the variables and their values in an application. Depending on implementation, application state can influence the behavior of an application, such as changing what is shown to the user. 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 not from the programmer, and some of them can also be adjusted by the user through e.g. interaction with the application.
As an example of hidden variables, Flutter has buttons that react to the button press by showing an animation, where the animation is controlled by application state. The example below demonstrates this using the ElevatedButton widget. When you run the program, you see a button with the text “Current count is: 0”. Clicking the button shows a brief visual cue that indicates that the button has been pressed.
Buttons have also functionality that is invoked when pressed. As an example, above, pressing the button calls the function increment
that increases the value of the variable count
by one. This change is not reflected in the text shown to the user, as the widget is stateless. In other words, even though the value of count
changes, the Text widget is not recreated or updated to reflect the change.
Flutter has both stateless and stateful widgets. The primary purpose of stateless widgets is to create widgets that do not change over time. In contrast, stateful widgets are used to create widgets that can change over time. The state of a stateful widget is stored in a separate object that is created when the widget is created. The state object is mutable, meaning that the values of the variables in the object can be changed. Given explicit instructions, when the state object changes, the widget is rebuilt, and the changes are reflected in the user interface.
Here, we briefly look into Flutter’s “vanilla” functionality for building widgets with state. Then, we look into using GetX for state management.
Stateful widget
Stateful widgets are created 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 of 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 CountWidget
that extends StatefulWidget
. The class has a method State<CountWidget> 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
.
When trying out the example, you’ll notice that the text still does not change when the button is pressed. This is because the widget is not aware of the change in the state.
Notifying Flutter about state changes is done using a method setState
that is inherited from the class State
. The method setState
takes a function as a parameter — typically, the changes to the state are defined within the function.
In the following, we have changed the method _increment
to use the setState
to notify about the change in the value of _count
. Now, when the button is pressed, the value of _count
is increased by one, and the change is shown to the user.
In practice, 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 why the text changes when the button is pressed.
The following example outlines a similar application, but now the text is centered on the screen and the button is replaced with an icon that is placed to the lower right corner of the application using Scaffold’s floatingActionButton
property. The icon is a heart, and the text shows the number of likes. When the heart is pressed, the number of likes is increased by one.
When applications grow in size, maintaining the state with stateful widgets can become cumbersome, as the state is spread across multiple widgets. For this, Flutter has a range of state management libraries that help in managing the state in a more structured way. Next, we’ll briefly look into the basics of handling state with GetX, which we also looked into in the part on navigation.
State and GetX
GetX provides reactive variables, which are variables that can notify about changes in their values. When using GetX, reactive variables are declared by adding .obs
to the variable declaration.
Observing changes in state is done using the Obx widget. The Obx
widget takes a function that returns a widget as an argument. When using the Obx
widget, GetX calls the function whenever the value of a reactive variable changes, which leads to the recreation of the widget returned by the function, and updating the shown widget in the user interface.
Contrary to “vanilla” Flutter, where stateful widgets require creating a class extending StatefulWidget
and a class extending State
, GetX handles the state changes internally and can be used with classes that extend the StatelessWidget
class.
The following outlines a rewrite of the above application, where the state is managed with GetX. The variable _count
is declared as a reactive variable by adding .obs
to the variable declaration. The Obx
widget is used to observe the variable; whenever the value of _count
changes, the text widget is recreated.
Note that, above, the variable _count
is not declared as an integer but as var
. After declaring an integer variable using .obs
, a new variable is created — in this case, the type of the variable is RxInt, which is a part of GetX. The var
is used as Flutter can infer the type of the variable from the context, but RxInt
would also work.
In other words, the variable _count
is not an integer but an object that has the properties of an integer. The object has a method ++
that increases the value of the object by one, and a method --
that decreases the value of the object by one. However, if the value needs to be set, it is done by assigning a new value to the object’s property value
, e.g. _count.value = 42
.