When working on frontend applications, a common use case is "Updating the screen's UI dynamically". setState
is one of the ways to accomplish this in flutter.
Overview
- Tech Terms
- What is a State Object in flutter
- Example code
- When to use setState
- Going one step ahead
- Summary
- Final Note
Tech Terms
These are some common terms used in flutter. The concepts apply to other frameworks also, although each framework has its own technical term for them.
Widget: Any UI component on the screen is called a Widget in flutter. A Widget can have its own Widgets, like a tree structure.
StatefulWidget: A Widget that can change dynamically. Generally used when we want to modify something on the screen's UI.
What is a State Object in flutter?
setState
is called inside a State class. Let's understand this in detail.
State is simply the information of a StatefulWidget. Every StatefulWidget has a State Object. This State Object keeps a track of the variables and functions that we define inside a StatefulWidget.
State Object is actually managed by corresponding Element Object of the Widget, but for this blog, we will only focus on the Widget part. If you don't know what Element Object is, I'll encourage you to read about it. It's not required to know for this blog though.
class MyWidget extends StatefulWidget { // immutable Widget
@override
_MyWidgetState createState() => _MyWidgetState();
// creating State Object of MyWidget
}
class _MyWidgetState extends State<MyWidget> { // State Object
@override
Widget build(BuildContext context) {
return Container();
}
}
Since StatefulWidget itself is immutable (cannot be modified), we use State Object to modify the UI.
We tell this State Object to update our screen's UI using a function called setState()
.
Function Definition
void setState(VoidCallback fn) {
...
}
This setState()
takes a function as it's parameter.VoidCallback
is just a fancy way of saying: void Function()
typedef VoidCallback = void Function();
Example
We'll create a simple counter app (I know it's very common but bear with me).
- This will have a variable
counter
initialized with0
. - We will display this
counter
on the screen inside theText
Widget. - Next, we'll have a button that increases this
counter
's value by1
. - The UI should be updated with the new value of
counter
.
Let's see the steps written above one by one.
When the Widget is created, the value of the counter is 0
int counter = 0;
Displaying value of counter on screen
Widget build(BuildContext context){
return
...
Text(`counter value: $counter`)
...
}
When we click on a button, we increment the value of the counter
onTap: () {
/// increments counter's value by 1
counter++;
},
Update the UI
onTap: () {
// passing an anonymous function to setState
// that increments counter's value by 1
// and update the UI
setState(() {
counter++;
});
},
#Fun Fact :
We can update our variables first and then call the setState()
function, since setState
just informs the underlying framework that
"update this Widget's UI in next frame" (
marks it dirty
).
The underlying framework will use the last value that is defined before calling the setState
function.
This is the same as above
onTap: () {
counter++;
setState(() {});
},
Example Code
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget { // widget class
const MyWidget({Key? key}) : super(key: key);
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> { // state class
int counter = 0; // initializing counter
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// displaing the counter
Text(
'counter value: $counter',
textAlign: TextAlign.center,
),
TextButton(
onPressed: () {
//incrementing counter
setState(() {
counter++;
});
},
child: const Text('Tap here to increment counter'),
)
],
),
);
}
}
When to use setState() ?
When we want to change the UI of the screen.
We don't need to call setState
every time we change a variable. We call setState
only when we want the change in a variable to reflect on the UI of the screen.
For instance, say you have a form containing a text field and a button to submit it.
import 'package:flutter/material.dart';
class MyForm extends StatefulWidget {
const MyForm({Key? key}) : super(key: key);
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
@override
Widget build(BuildContext context) {
return Column(
children: [
// text field
Form(
child: TextFormField(),
),
// submit button
OutlinedButton(
onPressed: () {},
child: const Text('Submit'),
),
],
);
}
}
User types in the text field and clicks on submit button. Then we display that text field's text below the submit button.
import 'package:flutter/material.dart';
class MyForm extends StatefulWidget {
const MyForm({Key? key}) : super(key: key);
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
// we'll use this to save the text user types
String userText = "";
// this will keep track of submit button's tapped action/event
// and display the userText below submit button
bool hasSubmitted = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
Form(
child: TextFormField(
// triggered when we save the form
onSaved: (value) {
// store the text-field's value into
// the userText variable
},
),
),
OutlinedButton(
// this is triggered whenever we click on button
onPressed: () {
// validate and save the form
// display the text below the button
},
child: const Text('Submit'),
),
// this will display the userText only
// if user has clicked on submit button
if (hasSubmitted) Text(userText)
],
);
}
}
Steps:
- User types in the text-field
- User clicks submit button
onPressed
function of submit button is triggered- Inside
onPressed
function:- Validate and save the form
- This will trigger the
onSaved
function in theTextFormField
- Inside
onSaved
function:- Store the text field's value in the
userText
variable
- Store the text field's value in the
- Update
hasSubmitted
variable withtrue
Implementation:
import 'package:flutter/material.dart';
class MyForm extends StatefulWidget {
const MyForm({Key? key}) : super(key: key);
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
String userText = "";
bool hasSubmitted = false;
// for getting access to form
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Column(
children: [
Form(
// attaching key to form
key: _formKey,
child: TextFormField(
onSaved: (value) {
/// updating userText variable
if (value != null) userText = value;
},
),
),
OutlinedButton(
onPressed: () {
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
// saving form
_formKey.currentState!.save();
// updating hasSubmitted
hasSubmitted = true;
},
child: const Text('Submit'),
),
if (hasSubmitted) Text(userText)
],
);
}
}
There's one small step left.
When you run this program, you'll notice that nothing happens when we click on submit button.
Here setState
comes to the rescue!
Now the question is where should we call it?
There are two places in the program where we are updating variables.
- inside
onSaved
function - insided
onPressed
function
So either one of these or both places should be the answer.
Let's ask one question to ourselves.
"On which variable update, do I want to update the UI of the screen?"
Is it userText
inside onSaved
function
orhasSubmitted
inside onPressed
function?
You got it right!
It's inside the onPressed
function after the hasSubmitted
variable has been updated.
...
onPressed: () {
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
// saving form
_formKey.currentState!.save();
// updating hasSubmitted
hasSubmitted = true;
setState(() {});
},
...
Again, this is same as below:
onPressed: () {
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
// saving form
_formKey.currentState!.save();
setState(() {
// updating hasSubmitted
hasSubmitted = true;
});
},
Why use setState
here?
In our logic, we used hasSubmitted
variable as a condition to show the userText
. So only after updating hasSubmitted
value, does the UI show our desired result.
Going one step ahead :
- What happens when you use the
setState
inside theonSaved
function only?
...
onSaved: (value) {
/// updating userText variable
if (value != null) userText = value;
setState(() {});
},
),
),
OutlinedButton(
onPressed: () {
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
// saving form
_formKey.currentState!.save();
// updating hasSubmitted
hasSubmitted = true;
},
...
It works here also. Surprise!!
But why? This goes against everything we've read so far in this blog.
So here's what happens.
When we call setState
, the Widget inside we called it is marked as dirty
.
Now whenever the framework actually rebuilds the UI of the screen, it will take into account all the latest values of the respective variables and paint the pixels on the screen.
This happens 60 times per second usually, which is the frame per second (fps) rate of flutter. That means approximately every 16ms (1000/60 ms).
If there is any other change until the next frame renders, those changes will also be reflected on the screen's UI.
The change in hasSubmitted
variable falls under such case.
How do we verify it?
Let's add print statements and see exactly when does the UI actually rebuild.
import 'package:flutter/material.dart';
class MyForm extends StatefulWidget {
const MyForm({Key? key}) : super(key: key);
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
String userText = "";
bool hasSubmitted = false;
// for getting access to form
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
print('Widget build called');
return Column(
children: [
Form(
// attaching key to form
key: _formKey,
child: TextFormField(
onSaved: (value) {
print('inside save');
if (value != null) userText = value;
print('hasSubmitted value before setState: $hasSubmitted');
setState(() {});
print('hasSubmitted value after setState: $hasSubmitted');
},
),
),
OutlinedButton(
onPressed: () async {
print('button clicked: ->');
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
print('before calling save');
// saving form
_formKey.currentState!.save();
print('after calling save');
print('hasSubmitted value after calling save: $hasSubmitted');
// updating hasSubmitted
hasSubmitted = true;
print(
'hasSubmitted value after updating hasSubmitted: $hasSubmitted');
},
child: const Text('Submit'),
),
if (hasSubmitted) Text(userText)
],
);
}
}
With these print statements, we can see the order in which the flutter framework updates the UI.
Let's write something in the text field and click on submit button.
Inside console:
Widget build called
button clicked: ->
before calling save
inside save
hasSubmitted value before setState: false
hasSubmitted value after setState: false
after calling save
hasSubmitted value after calling save: false
hasSubmitted value after updating hasSubmitted: true
Widget build called
Clearly, the Widget is built after the hasSubmitted
is set to true
.
So the major question is: "Why can't we use setState
inside onSaved
function instead of inside onPressed
function, it seems to work fine."
Because this method doesn't work for all the use cases. And it is also logically wrong.
When you'll come back to refactor the code (like for adding a new feature), things might not work as expected. Debugging the code will also be difficult since the problem is in the old code.
Let's see an example of such a use case.
Okay, we're almost done. This is the most important part. We've been building up to this point since the starting of this blog.
Let's go back to the steps of this example and add one more thing to it.
- After saving the form, say we want to make an API call to send the data that the user typed to a server. For simplicity, let's say we're going to do some computation with the
userText
data which will take a minimum of 20ms (this can be any duration of your choice).
This is a practical example. Usually, we do communicate with our application's backend server.
Does our desired result still happen?
onPressed: () async {
print('button clicked: ->');
// validating form
if (!_formKey.currentState!.validate()) {
return;
}
print('before calling save');
// saving form
_formKey.currentState!.save();
print('after calling save');
print('hasSubmitted value after calling save: $hasSubmitted');
/// some computation that takes 20ms
await Future.delayed(const Duration(milliseconds: 20), () {});
// updating hasSubmitted after 20ms
hasSubmitted = true;
print(
'hasSubmitted value after updating hasSubmitted: $hasSubmitted');
},
See inside onPressed
function. We're awating
a Future
and then updating the hasSubmitted
value.
If you don't know what await, async and Future means, then just think that we're basically saying to program, "Hey flutter framework, we're gonna do some task that'll probably take some time. Please do it in the next iteration." I'll encourage you to read about asynchronous programming. This concept is not exclusive to dart.
#Note: We can also use a Timer to get the same effect, but here we'll use Future.
Inside console:
Widget build called
button clicked: ->
before calling save
inside save
hasSubmitted value before setState: false
hasSubmitted value after setState: false
after calling save
hasSubmitted value after calling save: false
Widget build called
hasSubmitted value after updating hasSubmitted: true
Now, we don't see the UI updates on the screen. And according to our print statements order, hasSubmitted
variable is updated after the Widget has been rebuilt.
Reason?
Warning: This includes some advanced topics. I'll try to explain it as simply as possible.
There is a queue of microtasks. One by one all the tasks are performed by dart. When we use await
, we tell dart to first complete the current task (waiting for 20 ms), then move on to the next one in the queue.
So although some tasks (like rendering of the screen and waiting for 20 ms) are being performed concurrently, the tasks below the await
keyword (updating hasSubmitted
variable) will not be performed till the current task is completed.
So, when the framework actually rendered the dirty
Widget(MyForm), hasSubmitted
variable's value was not updated. Hence we don't see our typed text below the submit button.
Something to search about:
There is also an event queue
. When we don't wait for the Future to complete but want to continue on to the next microtask, the task in Future is added to event queue
.
Want to experiment more?
Try changing the duration to 0 seconds.
The UI still doesn't update as desired.
If you wanna dig deep into why after encountering the
await
keyword, does the code below it doesn't run synchronously even if the duration is 0 seconds, then read about the event loop in dart. There are other resources also that you can easily find on the internet. This concept is not exclusive to dart.
With this new use case (using a Future), our desired output is not achieved. Below is the short summary.
/// Approach 1: this is good (recommended)
setState((){
hasSubmitted = true;
});
...
/// Approach 2: this is also good
hasSubmitted = true;
setState((){});
...
/// Approach 3: this is not good
setState((){});
hasSubmitted = true;
- What happens when we use
setState
inside both the functions?
This case is dangerous.
Since our desired result is achieved, we overlook the one extra call to setState
inside the onSaved
function.
I hope now you got an idea of when and when not to use setState
.
Since this was a very simple example with only two variables to think of, it's easy to implement our logic into code.
When the program becomes bigger and complicated, keeping track of updating the variables and UI becomes cumbersome.
Then we use a mix of StatefulWidget's setState
and/or some other state management solution.
I'll encourage you to read flutter official docs and build apps to get a grip on this.
Summary
setState
is a way to dynamically change the UI.- We call it inside the State Object class of the StatefulWidget.
- Calling
setState
marks the corresponding Widgetdirty
. When flutter builds the next frame (approx. every 16ms), it renders the Widget according to the latest values of the State Object. - Where we call
setState
matters a lot. - There are other state management solutions also.
Final Note
Thank you for reading this article. If you enjoyed it, consider sharing it with other people.
If you find any mistakes, please let me know.
Feel free to share your opinions below.