DEV Community

loading...

State Management In Flutter

cyuket profile image CY UKET ・11 min read

Prerequisites

Tools required to follow the article.

  • Flutter SDK.
  • Android Studio or VS code.
  • Emulator or an Android device

Introduction

According to Wikipedia, Flutter is an open-source UI software development kit created by Google. It is used to develop applications for Android, iOS, Windows, Mac, Linux, Google Fuchsia and the web using Dart language .

In Flutter, everything is a widgets which have states, otherwise known as information rendered by the widgets. The state of your application is therefore important to consider, how widgets, content change base on actions and how data is passed on from one widget to another widget in the tree is very essential for the run-time performance of your application.

State is information that can be read synchronously when the widget is built and might change during the lifetime of the widget.

What We are Building

At the end of this article, we will show how set state and how to pass data from parent widget to the child widget.

Widget tree is a structure that represents how our widgets are organized.
Below is a widget tree that describes how the application we will be building.

figure 1 : Widget Tree of The Application

Setting up Project Tools

Create a new app with the flutter create app_namecommand and open the project on your preferred IDE.

From our figure 1 above we are going to create three stateless classes each representing our different levels in the tree.
Replace your main.dart with the following code:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Level1(),
    );
  }
}

class Level1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
      centerTitle: true,
        title: Text('Cy is 10'),
      ),
      body: Level2(),
    );
  }
}

class Level2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Level3(),
    );
  }
}

class Level3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('My Real Age is 10');
  }
}

The next line of code we see is

void main() => runApp(MyApp());

Dart programs has an entry point called main. When we run Flutter or dart file it first runs main function. In this case the main function is calling flutter specific function called runApp which takes any widget as an argument and created a layout which fills the screen in our case it takes MyApp as it argument.
MyApp is a stateless widget which returns MaterialApp widgetwhich contains three arguments title, theme and home. Home in our takes Level1 as it argument. Level1 returns a Scaffold widget which has an AppBar widget that contains a Text widget as its title. Also, the Scaffold widget has Level2 as its body. Level2returns aContainerwidget which hasLevel3as it child argument, finallyLevel3returns aTextwidget which prints out some strings to the screen.
If you noticed there is a widget that is missing from our code that is the button that will increment my age any time it's pressed. So you can guess which level we will be implementing that. Hope you guessed right, our button will be in our
Level2widget but it will be aColumnso it contains bothLevel3AndButton`.

So let’s update our Level2 class with the following code:

class Level2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(),
            FlatButton(
              color: Colors.blue,
              onPressed: () {},
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

State Management Techniques

There are several ways to manage the state of an application, especially in Flutter. None of these ways is termed “best”. For me, I would say choose a technique according to the complexity of your application.
These are different ways to manage state in a Flutter application which includes
SetState, Lifting State Up, Provider, Inherited Widget, Bloc, MVC, Scoped Model, MobX, Redux

For the lesson, we are going to look into the first three which is SetState, Lifting State Up and Provider.

SetState

Our widgets are either Stateless or Stateful widget.
Stateless Widget: As the name implies, it a widget which cannot change its properties throughout the run-time of the application. You can as well call a static widget. For Properties to Change It has to be rebuilt for changes to reflect on the widget. As you can see from our code all our widgets from MyApp to level3 are stateless widgets.
Stateful Widget: This can be said to be the opposite of a Stateless Widget. This allows you to change the state of widget even during the run-time of the application.
So for us to be able to increment our Age in our application when the button is clicked, one of our widgets will have to change to Stateful Widget and if you are quick enough you will notice it our Level2
But first, we need to make the age number in level3 dynamic. so let us update our Level3 first just above the @override add the following lines code:

final int age;
Level3(this.age);

And replace the Text widget with the code below:

Text(
  'My Real Age is $age',
  style: TextStyle(
    fontSize: 20,
    fontWeight: FontWeight.bold,
  ),
);

Our Level3 now looks like this:

class Level3 extends StatelessWidget {
  final int age;
  Level3(this.age);
  @override
  Widget build(BuildContext context) {
    return Text(
      'My Real Age is $age',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    );
  }
}

Let’s move over to Level2 and make first make it a Stateful widget.
Replace the Level2 with the following code:

class _Level2State extends State<Level2> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(age),
            FlatButton(
              color: Colors.blue,
              onPressed: () {},
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

We need to create an integer variable called age to hold an initial age value of 10, and we do that in the private class of the widget, the _Level2State
add the following code after the opening brace:

int age = 10;

You have to pass the variable age into the Level3 widget, update the how it’s been called:

Level3(age),

Now Let’s use setState to add to our age. If you noticed the flat button has an onPressed which requires a callback function that is triggered only when the button is pressed. So let's add our function to update the age.
update the onPressed function with the code below:

onPressed: () {
  setState(() {
    age++; 
  });
},

Your Level2 should look like this:

class _Level2State extends State<Level2> {
  int age = 10;
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(age),
            FlatButton(
              color: Colors.blue,
              onPressed: () {
                setState(() {
                  age++;
                });
              },
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

After this, if we run the application, when you press the button you notice the age increases each time the button is pressed. But we still have a problem and that is we are not able to update the age value on our AppBar widget. So to this, we have to lift the states.

Lifting State Up

This is a process of defining and setting all states at a high level in the tree so that all the children of the widget will be able to access.
Since our AppBar needs to access the age variable and change when it is pressed, we have to lift the state to the parent widget in our case is our Level1 widget.
So first we need to do the same as we did to our Level2 widget, change it stateful widget and declare our variable age.
Now we can add make the access the age in the AppBar, update the AppBar to:

appBar: AppBar(
  centerTitle: true,
  title: Text('Cy is $age'),
),

Level1 should look like:

class Level1 extends StatefulWidget {
  @override
  _Level1State createState() => _Level1State();
}

class _Level1State extends State<Level1> {
  int age = 10;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Cy is $age'),
      ),
      body: Level2(),
    );
  }
}

We also Have to anticipate the age variable in our Level2 and change it back to stateless widget since we will be handling the state in the parent widget
Replace the Level2 with the code below:

 class Level2 extends StatelessWidget {
  final int age;
  Level2({this.age});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(age),
            FlatButton(
              color: Colors.blue,
              onPressed: () {
                setState(() {
                  age++;
                });
              },
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

also, pass age into the Level2 from Level1, update the body with:

body: Level2(age: age,),

Now we observe that an error is indicated in the line that has the setState in the Level2 widget, that is telling us that we can’t set states in a stateless widget. So we go ahead and empty that function.
Now that both the AppBar and other widget bellow the tree can access the age variable let’s create a function that will update when the button is pressed.
Just after where you declared the variable age in Level1 let’s create the function:

void addAge() {
  setState(() {
    age++;
  });
}

Since our button is in Level2 we have to pass it down as props, so we need to anticipate this function in Level2 so update your Level2 constructor with the following:

final int age;
final Function onPressedFunction;
Level2({this.age, this.onPressedFunction});

now we can replace our onPressed in Level2 with the code bellow:

onPressed: onPressedFunction,

so let’s pass the function down to Level2 in Level1 :

body: Level2(
  age: age,
  onPressedFunction: addAge,
),

Our Level1 widget looks like :

class Level1 extends StatefulWidget {
  @override
  _Level1State createState() => _Level1State();
}

class _Level1State extends State<Level1> {
  int age = 10;

  void addAge() {
    setState(() {
      age++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Cy is $age'),
      ),
      body: Level2(
        age: age,
        onPressedFunction: addAge,
      ),
    );
  }
}

And Level2 looks like :

class Level2 extends StatelessWidget {
  final int age;
  final Function onPressedFunction;
  Level2({this.age, this.onPressedFunction});

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(age),
            FlatButton(
              color: Colors.blue,
              onPressed: onPressedFunction,
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

And Level3

class Level3 extends StatelessWidget {
  final int age;
  Level3(this.age);

  @override
  Widget build(BuildContext context) {
    return Text(
      'My Real Age is $age',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    );
  }
}

Restarting the application and pressing the button you can notice that both the text in the AppBar and the Text in Level3 changes simultaneously.
So basically we have been able to pass data from the top widget in the tree down to the last child in the tree and also update it with an action button which is in the middle level of the tree. You can as well take a bow cause our aim has been achieved right but I will introduce you to a better way of handling state with a package made available and recommended to us by the flutter team at Google

Provider

Provider is packaged is maintained by the flutter team and it is the recommended way of managing the state of an application. So let’s see how to use it in our application.

So if you are wondering why we are introducing Provider to our app, here is a simple reason, we want to access data anywhere in our widget tree without having to pass them from one widget to another.

First of all, we need to install the package to our application, click here to see the full documentation of the package. Let’s add it to our pubspec.yaml, after cupertino_icons lets, add the following code:

 provider: ^4.0.0 //4.0.0 is the current version as at when this article was written */

click on package get to download the packages.
Going back to our main.dart import provider so we can have access to the classes we will be needing, at the beginning of our file add this

import 'package:provider/provider.dart';

For us to spread our data and modify them we will need a class that extends a flutter class called ChangeNotifier, and defined our age and function to increment our age
At the bottom of our main.dart add the following code:

class AgeClass extends ChangeNotifier {
  int age = 10;
}

Let’s start using our provider package in our application. the package is always defined at the top of the widget tree. So let's go over to our MyApp and update what it is returning with the following:

return ChangeNotifierProvider<AgeClass>(
  create: (context) => AgeClass(),
  child: MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Level1(),
  ),
);

From the code we just added, we have a new widget from the provider package called ChangeNotifierProvider and the type of data we are expecting in return, in our case we are expecting the AgeClass which we add defined. Also it ChangeNotifierProvider requires a create function that takes the current context as an argument and returns the actual data, in our case it returning the AgeClass because that is where our data is stored.
Now our data is can be accessed with a Provider.of<*dataType expected*>(context)
so let’s go ahead and all our data where we need it, first the AppBar let’s update the title widget in Level1 with :

title: Text('Cy is ${Provider.of<AgeClass>(context).age}'),

then Text in Level3 with:

 Text(
  'My Real Age is ${Provider.of<AgeClass>(context).age}',
  style: TextStyle(
    fontSize: 20,
    fontWeight: FontWeight.bold,
  ),
);

Now if you run the app you will realize the function in the add button is not function let’s create that immediately. In the AgeClass, we will create a function to do this and notify every widget that is listening to data that is been modified and trigger a rebuild of only the widget with the notifyListeners() method.
so let’s add the following code to our AgeClass :

void addAge() {
  age++;
  notifyListeners();
}

we have created a function to increment our age let’s go ahead and append it to our FlatButton's onPress. Update the onPressed with the code below:

onPressed: Provider.of<AgeClass>(context).addAge,

so you can go ahead and change your Level1 back to a stateless widget
and also delete the variable age and the function addAge inside of it. Your Level1 should look like:

class Level1 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Cy is ${Provider.of<AgeClass>(context).age}'),
      ),
      body: Level2(),
    );
  }
}

In Level2, erase the Function onPressedFunction and the constructor for it to look as the code bellow:

class Level2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(),
            FlatButton(
              color: Colors.blue,
              onPressed: Provider.of<AgeClass>(context).addAge,
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

Also, erase the unused variable age in level three and its constructor. it should look like this:

class Level3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      'My Real Age is ${Provider.of<AgeClass>(context).age}',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    );
  }
}

And AgeClass

class AgeClass extends ChangeNotifier {
  int age = 10;

  void addAge() {
    age++;
    notifyListeners();
  }
}

so after following this tutorials your main.dart file should look like what I have bellow:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<AgeClass>(
      create: (context) => AgeClass(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Level1(),
      ),
    );
  }
}

class Level1 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Cy is ${Provider.of<AgeClass>(context).age}'),
      ),
      body: Level2(),
    );
  }
}

class Level2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Level3(),
            FlatButton(
              color: Colors.blue,
              onPressed: Provider.of<AgeClass>(context).addAge,
              child: Text('Add'),
            )
          ],
        ),
      ),
    );
  }
}

class Level3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      'My Real Age is ${Provider.of<AgeClass>(context).age}',
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
      ),
    );
  }
}

class AgeClass extends ChangeNotifier {
  int age = 10;

  void addAge() {
    age++;
    notifyListeners();
  }
}

Conclusion

During this article, we’ve learned about the different ways of state management, from using SetState and passing it down the three to increase the scope of the state by Lifting States up to a higher widget and using the provider package to handle states properly.

Happy Fluttering

Discussion (0)

pic
Editor guide