What is setState() in flutter and when to use it?

What is setState() in flutter and when to use it?

·

13 min read

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

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 with 0.
  • We will display this counter on the screen inside the Text Widget.
  • Next, we'll have a button that increases this counter's value by 1.
  • 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:

  1. User types in the text-field
  2. User clicks submit button
  3. onPressed function of submit button is triggered
  4. Inside onPressed function:
    1. Validate and save the form
    2. This will trigger the onSaved function in the TextFormField
    3. Inside onSaved function:
      1. Store the text field's value in the userText variable
    4. Update hasSubmitted variable with true

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
or
hasSubmitted 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 the onSaved 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 Widget dirty. 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.

Did you find this article valuable?

Support Nikki Goel by becoming a sponsor. Any amount is appreciated!