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.
Setting up Project Tools
Create a new app with the flutter create app_name
command 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 a
Containerwidget which has
Level3as it child argument, finally
Level3returns a
Textwidget which prints out some strings to the screen.
Level2
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 ourwidget but it will be a
Columnso it contains both
Level3And
Button`.
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
Top comments (0)