State in Flutter
State in Flutter
Sanjib Sinha
* * * * *
This is a Leanpub book. Leanpub empowers authors and publishers with the
Lean Publishing process. Lean Publishing is the act of publishing an in-
progress ebook using lightweight tools and many iterations to get reader
feedback, pivot until you have the right book and build traction once you do.
* * * * *
I also strongly recommend to read the latest and updated articles on Flutter .
That is our first task. We need to go to The installation page of Flutter page,
from where we will download and install Flutter.
By the way, before delving into the book, I’d like to tell you that I write
regularly on Flutter and Dart programming language at ZeroDotOne. For
more Flutter related Articles and Resources
I know some of them still struggle with the concepts of State management in
Flutter, as there are too many options available.
In fact, when there are too many options are available, developers often find
it difficult to pick up the right one.
I hope this book will not only guide them to find the right choice, but at the
same time, it will help them understand how State object in Flutter works
underhood.
As I have just said, there are too many options. With reference to that, I have
added a note later. But let us see the options at one glance.
Let us explore that first. And after that, we will start our journey to
understand Flutter state management best practices.
By the way, if you are an absoluter beginner who want to start learning
Flutter along with Dart programming language, then please download my
previous book ** Beginning Flutter with Dart **
We must stick around one great choice, that is Riverpod state management
library, and will learn how to use it in detail.
However, before undertanding Riverpod I think it is mandatory to undertand
the Provider package and its limitations.
At the end of the book, if you have any question, feel free to contact me at: **
[email protected] **
It would be around 700 MB in size. While extracting the file it would take
around 1.30 GB place of your hard drive.
You may copy that extracted file to elsewhere, or you may keep it there
(figure 1.1).
We have kept the extracted flutter folder there and created a new
‘environment’ path for the user. Because we want to work through the
command prompt, in future, we have created this global environment path.
We can copy and paste the whole path there as the following:
1 “C:\Users\Downloads\flutter\bin”.
Now, we can open the command prompt and type ‘flutter doctor’ to see
whether we have any Flutter related IDE installed already. It will also check
whether we have any connected device or not.
We have not installed Android Studio or any other Flutter related IDE
beforehand. The command ‘flutter doctor’ has detected that (Figure 1.3).
The connected device is nothing but a virtual mobile device where we can
see and test our mobile application.
We need the Android Studio IDE first. It should be the best choice for one
reason. You cannot create a virtual device without the help of Android
Studio.
However, later I am going to use Visual Studio Code IDE. I have found VS
Code more flexible in writing code.
It is widely used.
In Flutter Doctor summary, we have found that Android Studio has not been
installed and there is no device available.
Next, we will also learn how to install Flutter in our macOS and Linux
machines. You can use any one of that operating system to learn Flutter and
Dart together.
Next we will issue the following command to extract Flutter, on our terminal:
1 //code 1.1
2 tar xf flutter_linux_1.17.2-stable.tar.xz
Now we can copy this extracted ‘flutter’ directory to a suitable place, where
we will build our first mobile application. In the ‘Documents’ directory, we
have created another directory named ‘development’. We will keep the
extracted ‘flutter’ directory there.
Just like Windows 10, we will now set the global path for ‘flutter’, so that
we can use ‘flutter’ command, anywhere in our machine, in the future.
We will do that using ‘vim’ or ‘nano’ text editor, that works on the terminal.
By the way, the commands are same for any macOS or Linux operating
system.
If you type the following command, the nano text editor will open up the
‘bashrc’ file.
1 //code 1.2
2 nano ~/.bashrc
We have to mention the full path as given above. We have kept our extracted
‘flutter/bin’ folder in the ‘/home/ss/Documents/development’ directory.
Our next step will be to download the Android Studio. Download the zipped
folder and extract it anywhere in the machine. We have kept it in our ‘/home/’
directory. Next, issue this command:
1 //code 1.4
2 ss@ss-desktop:~$ cd android-studio/bin/
3 ss@ss-desktop:~/android-studio/bin$ ./studio.sh
Once the Android Studio opens up, you can go to the ‘open folder’ option
and choose the flutter project we have created already. How we have created
it, we will come to that point in a minute.
Before that, we need to see the Android Studio and our newly created virtual
device.
Figure 1.4 – Android Studio and our first flutter project
Before opening the Android Studio, we have opened up our terminal, and
typed the following commands to reach to the newly installed ‘flutter’
directory.
1 //code 1.5
2 ss@ss-desktop:~$ cd Documents/development/flutter/
3 ss@ss-desktop:~/Documents/development/flutter$ flutter doctor
4 Doctor summary (to see all details, run flutter doctor -v):
5 [✓] Flutter (Channel stable, v1.17.2, on Linux, locale en_IN)
6
7 [✓] Android toolchain - develop for Android devices (Android SDK version
29.0.3)
8 [✓] Android Studio (version 3.5)
9 [✓] Android Studio (version 4.0)
10 [✓] IntelliJ IDEA Community Edition (version 2019.3)
11 [✓] VS Code (version 1.43.2)
12 [!] Connected device
13 ! No devices available
14
15 ! Doctor found issues in 1 category.
16 ss@ss-desktop:~/Documents/development/flutter$
As you have seen in the above output, ‘flutter doctor’ has found only one
issue. It has not found any connected device. Otherwise, we have already
installed Android Studio (version 4.0), which is the latest at the time of
writing this book. We have also installed IntelliJ IDEA Community Edition,
and we have also Visual Studio Code IDE.
We can use the virtual device from Android Studio, but we can use the Visual
Studio Code IDE or IntelliJ IDEA Community Edition IDE for writing our
code.
However, before that we need to create our first flutter project with the help
of flutter command as the following:
1 //code 1.6
2 flutter create my_first_flutter_app
When we want to create a new flutter project, we should always create like
the way I have just shown above. The naming convention is important here.
We can only use the underscore between the words. No hyphen or space is
allowed.
Now the time has come to go back to the Android Studio. We will pick up the
‘open folder’ option and choose to open the newly created flutter project. We
have named it as: ‘my_first_flutter_app’.
Figure 1.5 – Open the Android Virtual Device (AVD) manager from
tools menu
To open up the connected device, we need to open the Android Virtual
Device manager, or AVD manager in short.
Select any one of them and click the ‘green’ play button on the far right hand
side of any virtual device. It will automatically open up the ‘connected
device’ (Figure 1.6).
Figure 1.6 – We have the connected device on which we can test our
first mobile application
Now everything is ready. We can start building our first mobile application
from scratch using Flutter and Dart. Before closing down this section, we
should know a few good tips. Usually, the beginners encounter a few errors
when they try to run the command:
1 flutter doctor
It will ask you to accept the license. Accept it, and it will not give any error
anymore. Another problem often gives trouble to the new developers.
As a beginning Flutter developer, people often are stuck with this issue.
They cannot launch the virtual mobile device while working with Android
Studio.
We want that every code we write should reflect on the virtual device. It can
be done by going to the ‘AVD manager’ from tools. But sometimes an ugly
error pops up its head and tells that ‘/dev/kvm permission denied’.
In Ubuntu 18 or Mac OS, you can give user the permission by issuing this
command:
1 //code 1.7
2 sudo chmod 777 -R /dev/kvm
But it has a drawback. If someone else uses your machine, then the other user
also gets the permission.
It will solve the issue for ever. Now you can launch any virtual device you
want. You can launch the device with your Android Studio, and work with
any other IDE like IntelliJ or Visual Studio.
However, the concept of state in Flutter is not that easy and simple as it
appears in the above statement.
Why?
The stateful widget has an internal state that is absent in the stateless widget.
As we see in both cases the UI gets re-rendered when input data changes.
However, in stateful widget the UI also gets re-rendered when the internal
state or local data changes.
The following diagram will help you to understand the core concept of state
management in Flutter. However, I’ll explain it later.
Figure 2.1 – What is the main difference between Stateless and Stateful
Widget, and how external and internal state work in Flutter
Stateless widget is a widget that cannot re-run the build() method when its
properties change.
Well, if the above statement doesn’t make any sense, try to think this way.
When a stateless widget receives External data or Input data through its
Constructor it re-runs the build() method.
As a result, it re-renders the UI. In the above diagram we have shown this.
However it’s not true for stateless widget properties. In a stateless widget
we can change its property by pressing a RaisedButton.
Pressing the button will change the property, it will change from 0 to 1 and
from 1 to 2.
But, it cannot re-run the build() method and as a result it cannot re-render the
UI.
Moreover, for this reason, we cannot see any change on the screen. Still we
want to test this on an application.
As the user presses the button, it increases the number in the Debug Console,
like this:
Run the code in your IDE, you can clearly see the output. However it does
not reflect on the screen. It doesn’t re-run the build() method and re-renders
the UI.
Because we tried to change the state through internal state or local data.
Since that was the property of the widget class it didn’t work.
Inside the “ACenterClass” widget we will use a property and method. Using
Callback function to that method the RaisedButton named parameter
‘onPress’ will call that function.
Each time we press the button our debug console will show that the number
is increasing. However, since it cannot re-run the build() method, our app
cannot re-render the UI and we cannot see that increased number on the
screen
1 class ACenterClass extends StatelessWidget {
2 var pressRemoteCount = 0;
3 void pressRemote() {
4 pressRemoteCount = pressRemoteCount + 1;
5 print(pressRemoteCount);
6 }
7
8 @override
9 Widget build(BuildContext context) {
10 return Center(
11 child: Container(
12 alignment: Alignment.center,
13 width: 350.00,
14 height: 100.00,
15 decoration: BoxDecoration(
16 color: Colors.blue,
17 border: Border.all(
18 color: Colors.deepOrange,
19 width: 2.0,
20 style: BorderStyle.solid,
21 ),
22 borderRadius: BorderRadius.all(Radius.circular(40.0)),
23 boxShadow: [
24 BoxShadow(
25 color: Colors.black54,
26 blurRadius: 20.0,
27 spreadRadius: 20.0,
28 ),
29 ],
30 gradient: LinearGradient(
31 begin: Alignment.centerLeft,
32 end: Alignment.centerRight,
33 colors: [
34 Colors.red,
35 Colors.white,
36 ],
37 ),
38
39 ),
40 child: Column(
41 children: [
42 Text(
43 '$pressRemoteCount',
44 style: TextStyle(
45 fontSize: 30.0,
46 color: Colors.blue,
47 ),
48 ),
49 SizedBox(
50 height: 10.0,
51 ),
52 RaisedButton(
53 child: Text(
54 'Press Button',
55 style: TextStyle(
56 fontSize: 30.0,
57 color: Colors.blue,
58 ),
59 ),
60 onPressed: pressRemote,
61 ),
62 ],
63 ),
64 ),
65 );
66 }
67 }
Quite apart from the decoration, the real important piece of code is this:
1 var pressRemoteCount = 0;
2
3 void pressRemote() {
4
5 pressRemoteCount = pressRemoteCount + 1;
6
7 print(pressRemoteCount);
8
9 }
However, since pressing the button does not re-run the build() method, we
don’t see the value of the property ‘pressRemoteCount’ in the Text widget.
1 Text(
2
3 '$pressRemoteCount',
4
5 style: TextStyle( fontSize: 30.0, color: Colors.blue,
6
7 ),
Now, as we refactor the same code and change it to the stateful widget, it
works.
We can manage the internal state of the widget through class property. The
next image will show you how we can see the counter value on the screen.
Pressing the button will keep increasing the number.
Figure 2.2 – Going to change the internal state through Stateful Widget
class property
Not only that, as the internal data or property’s value changes it re-runs the
build() method and that in turn will re-render the UI.
And, as a result, we see that the counter value increases with the pressing of
the button. The next image will show you the same effect.
1 class ACenterClass extends StatefulWidget {
2 @override
3 _ACenterClassState createState() => _ACenterClassState();
4 }
5
6 class _ACenterClassState extends State<ACenterClass> {
7 var pressRemoteCount = 0;
8
9 void pressRemote() {
10 setState(() {
11 pressRemoteCount = pressRemoteCount + 1;
12 });
13 }
14
15 @override
16 Widget build(BuildContext context) {
17 return Center(
18 child: Container(
19 alignment: Alignment.center,
20 width: 350.00,
21 height: 100.00,
22 decoration: BoxDecoration(
23 color: Colors.blue,
24 border: Border.all(
25 color: Colors.deepOrange,
26 width: 2.0,
27 style: BorderStyle.solid,
28 ),
29 borderRadius: BorderRadius.all(Radius.circular(40.0)),
30 boxShadow: [
31 BoxShadow(
32 color: Colors.black54,
33 blurRadius: 20.0,
34 spreadRadius: 20.0,
35 ),
36 ],
37 gradient: LinearGradient(
38 begin: Alignment.centerLeft,
39 end: Alignment.centerRight,
40 colors: [
41 Colors.red,
42 Colors.white,
43 ],
44 ),
45
46 ),
47 child: Column(
48 children: [
49 Text(
50 '$pressRemoteCount',
51 style: TextStyle(
52 fontSize: 30.0,
53 color: Colors.blue,
54 ),
55 ),
56 SizedBox(
57 height: 10.0,
58 ),
59 RaisedButton(
60 child: Text(
61 'Press Button',
62 style: TextStyle(
63 fontSize: 30.0,
64 color: Colors.blue,
65 ),
66 ),
67 onPressed: pressRemote,
68 ),
69 ],
70 ),
71 ),
72 );
73 }
74 }
In the above code snippet, this part is important and makes the difference:
1 class ACenterClass extends StatefulWidget {
2 @override
3 _ACenterClassState createState() => _ACenterClassState();
4 }
5
6 class _ACenterClassState extends State<ACenterClass> {
7 var pressRemoteCount = 0;
8
9 void pressRemote() {
10 setState(() {
11 pressRemoteCount = pressRemoteCount + 1;
12 });
13 }
14
15 @override
16 Widget build(BuildContext context) {
17 return Center(
18
19 ....
Now, inside the Center widget we have our Text widget and RaisedButton
widget as before. Nonetheless, the re-rendered UI reflects the change. And
now it shows the increased value.
Figure 2.3 – The re-rendered UI reflects the change, and now it shows
the increased value
As the internal data changes, it re-runs the build() method and the UI gets re-
rendered in Flutter stateful widget
However, that could not re-run the build() method which was necessary to
re-render the UI so the user could see the change.
Therefore use setState to cause a rebuild of the widget and its descendants.
The build() will re-run anyway. And exactly that happens in the stateful
widget.
In the stateful widget the setState plays as a trigger that informs Flutter to re-
run the build() method so the descendant Text widget, which stores the
property value, will get rebuilt.
For more Flutter related Articles and Resources
3. What is callback in Flutter? How do you
pass a function in Flutter?
We are going to discuss two things in this Chapter 3. Flutter enthusiasts often
want to know what is callback in Flutter?
Along with it, comes another important question, how do you pass a function
in Flutter?
What is callback?
In computer programming we pass functions as argument to other code. We
give a name to this type of action – callback.
Why?
Because at some convenient time the ‘other code’ will call back or execute
the argument.
If we see the whole piece of code and see the image of our app, it will spill
the beans.
What I’m going to do? Well, let me tell you that. We’re going to build a
simple quiz app. The user views a question in text and below that question,
he’ll find three answer buttons.
In the main dart file we have the first piece of code, like this:
1 import 'package:flutter/material.dart';
2
3 void main() {
4 runApp(QuizApp());
5 }
6
7 class QuizApp extends StatelessWidget {
8 @override
9 Widget build(BuildContext context) {
10 return MaterialApp(
11 title: 'Quiz App',
12 debugShowCheckedModeBanner: false,
13 home: FirstPage(),
14 );
15 }
16 }
If we click any answer button that will take us to the next question.
However, flutter doesn’t like such congestion. We’ve written the whole
application in a single file. Whereas we could have separated them, and kept
them in separate directories.
As we have seen in the above code, there are two different parts. One is
question that reflects on the Text widget. And the other part is answers,
which reflect on button.
There is no change in our simple quiz app. Except that the RaisedButton
widgets get closer to each other, it looks like before.
Once you know how we use list in dart, or what is map in dart, it becomes
easy to understand the main topic of this article.
We’re going to discuss what is a map in Flutter? Not only that, we will also
discuss in detail how we can map a list in Flutter.
Based on that assumption we have created two custom widgets Answers and
Questions. Through Answers constructor, we pass two parameters – one is a
function, and the other is a string data type.
At the same time, we have also created another custom widget Questions that
has only one external data to be passed. And that is string.
There is always an index that starts from 0, and through that index we can get
the value. In a map, through the key, we get the value. As a result we use
different types of methods in map. Moreover, we can also include a map in
our list, and vice versa.
The Answers custom widget returns a RaisedButton widget that has a child
widget Text and another named parameter onPressed, which returns a void
function.
The great advantage of our above code is that inside the “_questions” list we
have accommodated the answers also.
The next challenge is we should be able to view the next question by clicking
the correct answer. Just like this:
Well, state is the information that a user can read synchronously with the
change in widget. During the lifetime of the widget, it can hold that data.
This is the most basic approach. Moreover, in this tutorial, we will see how
a widget can manage the state and renders the state to itself. Why do we need
state management?
To begin with, I must clarify one thing. State is an object. It is not a widget.
Although in Flutter, everything is widget.
Next, we’ll define the _stateChanged boolean which determines the box’s
current color.
Once the user taps the red box, it calls the setState() function to update the UI
and make the box color to green.
For a large scale application where we have to pass the state object to many
screens, or pages, this process is not good. In such cases we’ll use Provider
or Riverpod. Even we can use the BLOC architecture.
However, Provider is our best option. And we must stick to one good choice.
Still, to understand Flutter state management, you should know how it works
at the root level.
The State class, or object actually manages the internal state of the stateful
widget. In that sense, a stateful widget also depends on the State class, which
is not a widget.
In this tutorial, we will see how a stateful parent widget manages the state of
a stateless child widget.
If you’ve not read the previous article on how a widget manages its own
state,please read it, before we start.
So, we need to think about the callback first. Since the parent is importing the
state of the child widget, we don’t have to make the child widget stateful
anymore. It can be stateless.
The code is very straight forward. By default the state object should be
inactive. So we’ve made it true. It also manages the state of the child widget.
The mystery reveals itself at the child widget constructor, where two named
parameters point to a piece of data and a method that through its parameter
change the state of that data.
Before we start discusiing Inherited Widget, let us take a brief look at the
inside world of Widgets.
Yet, Invisible widgets also play crucial roles. Because they control the look
and structure of the body of the Flutter app.
1 Row(), Column(), ListView(), etc…
The Container() widget may belong to both categories. Why so? As we can
decorate the container giving it color, or changing its shape. We can also
make it invisible by only placing other visible widgets inside it.
When a widget changes its state, or we use a stateful widget, the widget
rebuilds its description.
Visible and invisible, both types of widgets help each other to describe how
the view of the app should look like. How do you use widgets in Flutter?
In the following example we will see a simple app that will return a text in
the center of the app body.
1 void main() {
2 runApp(
3 Center(
4 child: Text(
5 'Hello, world!',
6 style: TextStyle(
7 fontSize: 30.0,
8 fontWeight: FontWeight.bold,
9 ),
10 textDirection: TextDirection.ltr,
11 ),
12 ),
13 );
14 }
The simple code, displayed above, will render only a text “Hello World” on
the screen.
We have used only two widgets. Center() and Text(). The Center() widget
belongs to the type of the invisible widget. And the Text() widget belongs to
the type of the visible widget.
Since the root widget is Center(), the framework forces the root widget to
cover the whole screen. As a result, the Text() widget, which is the branch of
this small widget tree, ends up centered.
As of now, we are not going to maintain state. So our widgets will be sub-
classes of stateless widget.
However, we have used the default Scaffold widget that controls the whole
body of the app.
We could have written our own Scaffold, which will actually render a
material design in the same manner.
Let us check this new code snippet, where we have just written our own
piece of custom Scaffold widget.
1 import 'package:flutter/material.dart';
2
3 void main() {
4 runApp(OurApp());
5 }
6
7 class OurApp extends StatelessWidget {
8 @override
9 Widget build(BuildContext context) {
10 return MaterialApp(
11 title: 'Our App',
12 debugShowCheckedModeBanner: false,
13 home: OurScaffold(),
14 );
15 }
16 }
17
18 class OurScaffold extends StatelessWidget {
19 @override
20 Widget build(BuildContext context) {
21 // It creates a piece of material dsign
22 return Material(
23 // Column is a vertical, linear layout.
24 child: Column(
25 children: <Widget>[
26 Expanded(
27 child: Center(
28 child: Text(
29 'Hello, world!',
30 style: TextStyle(
31 fontSize: 30.0,
32 fontWeight: FontWeight.bold,
33 color: Colors.redAccent,
34 ),
35 ),
36 ),
37 ),
38 ],
39 ),
40 );
41 }
42 }
We have only changed the color of the text to show the difference.
1 color: Colors.redAccent,
By using the InheritedWidget when the lowest level widget, residing at the
bottom of the widget tree, tries to change its state, that process does not affect
other widgets above.
We don’t prefer that approach. For a small application we can use that
approach though.
The InheritedWidget tries to solve that riddle in its own way, although it has
some disadvantages too. We will come to that point in a minute.
While doing so the InheritedWidget rebuilds itself and at the same time it
rebuilds its consumer.
In doing so Flutter does not rebuild the other top-widgets through which the
context flows down to the bottom.
We need write a widget tree first. The tree starts with two InheritedWidget.
One passes the color through its context. And the other passes an integer
value through its context.
Let us see the main method through which we run our InheritedWidget app.
You get the full code snippets at my GitHub repo.
1 import 'package:flutter/material.dart';
2 import 'controller/inherited-widget/inherited_widget_on_top.dart';
3
4 main() => runApp(OurApp());
5
6 class OurApp extends StatelessWidget {
7 @override
8 Widget build(BuildContext context) {
9 return MaterialApp(
10 title: 'Our App',
11 debugShowCheckedModeBanner: false,
12 home: Scaffold(
13 body: InheritedWidgetOnTop(),
14 ),
15 );
16 }
17 }
Keeping the above philosophy in our mind, we need to at the base of the tree
keeps our two inherited widgets.
1 import 'package:flutter/material.dart';
2 import 'package:our_app/controller/inherited-widget/widgets-
lists/widgets_lists.dart\
3 ';
4
5 class InheritedWidgetOnTop extends StatefulWidget {
6 @override
7 _InheritedWidgetOnTopState createState() => _InheritedWidgetOnTopState();
8 }
9
10 class _InheritedWidgetOnTopState extends State<InheritedWidgetOnTop> {
11 @override
12 Widget build(BuildContext context) {
13 return ListView(
14 padding: const EdgeInsets.all(30.0),
15 children: [
16 EyeColor(
17 color: Colors.deepOrange,
18 child: Builder(builder: (BuildContext innerContext) {
19 return GrandParent();
20 })),
21 SizedBox(
22 height: 10.0,
23 ),
24 ChangingAge(
25 age: new ChangeAge(age: 25),
26 child: Builder(builder: (BuildContext textContext) {
27 return UncleClasses();
28 })),
29 ],
30 );
31 }
32 }
Through the “EyeColor”we pass a context that carries the color of eyes. The
descendant widgets, hopefully they are humans, will consume the context and
get their eye colors accordingly.
However, any descendant may not wish to consume that context, and choose
its own eye color.
The above code clearly indicates that the consumer of the inherited widget
“EyeColor” is the Grandparent widget. The Grandparent widget has
FatherClass widget as sub-tree.
Moreover, we can change the age of each uncle. We can change the age of the
child too by pressing buttons.
Nevertheless age changes when we press the button, the context maintains the
state so efficiently that the other ages do not get affected.
Inside our “lib” folder, the folder structure looks like the following:
1 controller/inherited-widget/widgets-lists/widgets_lists.dart
Yes, that’s a valid point. While building this simple app, we should
remember that in the widget tree there could be a lot of widgets that do not
consume the same context.
So there could be widgets that may not in the Scope. The value that context
carries on its shoulder will be null for those widgets that are not in the
Scope. Otherwise the context carries a default value.
It is as simple as that.
Taking a look at the above code will tell you that there is a static “of” method
that passes “context” as a parameter. And it returns the following line of
code:
1 return context.dependOnInheritedWidgetOfExactType<ChangingAge>();
It allows the class to create its own fallback logic. It is important for one
reason. There could be other widgets who are not consumers and they are not
in the scope.
Now the “of” method may return any type of data. In our app, the inherited
widget “EyeColor” returns Flutter Color class. The consumer widgets of
inherited widget EyeColor
The Grandparent is the main consumer, and it has a descendant FatherClass.
Let us close watch them first.
1 class GrandParent extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 final eyeColor = EyeColor.of(context).color;
5 return Column(
6 children: [
7 Text(
8 'I am the Grandparent, although I am a Ghost now! I had two sons.',
9 style: TextStyle(
10 fontWeight: FontWeight.bold,
11 fontSize: 25.0,
12 color: eyeColor,
13 ),
14 ),
15 SizedBox(
16 height: 10.0,
17 ),
18 FatherClass(),
19 ],
20 );
21 }
22 }
23
24 class FatherClass extends StatelessWidget {
25 @override
26 Widget build(BuildContext context) {
27 return Column(
28 children: [
29 Text(
30 'I am the Father. I have two brothers.',
31 style: TextStyle(
32 color: EyeColor.of(context).color,
33 fontSize: 30.0,
34 fontWeight: FontWeight.bold),
35 ),
36 ],
37 );
38 }
39 }
As you see on the above code, for Grandparent we have consumed the
context this way:
1 final eyeColor = EyeColor.of(context).color;
We have not finished our code snippets yet. We have another inherited widget
“ChangingAge”. Through the context an integer data, which is age of the
uncles and the child, flows down to the bottom.
Managing state becomes fairly simple with the inherited widgets. The main
consumer of the inherited widget “ChangingAge” is “UncleClasses”.
The main consumer has two more consumer widgets inside it.
FirstUncleClass and UncleClass.
The code of second uncle class is similar to that of the first uncle.
However,the design slightly differs. Another important thing is this widget
has a child. So we have declared it inside too.
The widget UnclesChildClass has the similar code structure, so we need not
repeat it.
Actually the same context, here age, flows down the family tree.
The context points to the exact position where any Widget stays at the Widget
tree. We’ll discuss this topic separatly in another Chapter.
Before you proceed you must add the dependency on provider to the
‘pubspec.yaml’ file.
Now you are set to use Provider to manage state in your app.
What is state? Well, state is something that exists on memory. When your app
is running, in most cases you will try to manage state. Provider helps you to
do that.
Because to build any type of complex app that handles multiple screens,
different variables, and user sessions, managing state is crucial, you should
plan it beforehand.
Why?
Flutter creates a very deep and nested widget tree. To manage state at the
lowest bottom, you cannot rebuild every top-widget. That is wasteful
memory consumption.
Provider in Flutter is the answer!
Enough talking. Let us try to do some code together. Our goal is to understand
the main concept of Flutter Provider.
Now we can provide this designed model to the desired widget. However
our desired widget positions itself at the lowest-bottom.
1 import
'package:basic_flutter_provider/controllers/a_very_deep_widget_tree.dart';
2
3 import 'package:flutter/material.dart';
4
5 class MyHomePage extends StatelessWidget {
6 @override
7 Widget build(BuildContext context) {
8 return Scaffold(
9 appBar: AppBar(
10 title: Text('Basic Provider Explained to Beginners'),
11 ),
12 body: Center(
13 child: AVeryDeepWidgetTree(),
14 // This trailing comma makes auto-formatting nicer for build methods.
15 ));
16 }
17 }
Let us see how it works. I will explain what is happening exactly inside. A
Very Deep and Nested Widget Tree
As I have said earlier, Flutter allows nested widget tree. In fact, you cannot
imagine a complex app without that.
Let us see the code first. Then I’ll explain what is happening.
1 import 'package:basic_flutter_provider/models/counting_the_number.dart';
2 import 'package:flutter/material.dart';
3 import 'package:provider/provider.dart';
4
5 class AVeryDeepWidgetTree extends StatelessWidget {
6 @override
7 Widget build(BuildContext context) {
8 // ‘Provider.of’, just like Consumer needs to know the type of the model.
9 //We need to specify the model ‘CountingTheNumber’.
10 final counter = Provider.of<CountingTheNumber>(context);
11 return Container(
12 padding: const EdgeInsets.all(20.0),
13 child: Column(
14 mainAxisAlignment: MainAxisAlignment.center,
15 children: <Widget>[
16 Text(
17 'This is a simple Text widget',
18 style: TextStyle(
19 color: Colors.black,
20 fontSize: 45.0,
21 fontWeight: FontWeight.bold,
22 ),
23 ),
24 //now we are going to build a very deep widget tree
25 Center(
26 child: Container(
27 child: Column(
28 mainAxisAlignment: MainAxisAlignment.center,
29 children: [
30 Text(
31 'This is another simple Text widget deep inside the tree.',
32 style: TextStyle(
33 fontSize: 35.0,
34 fontWeight: FontWeight.bold,
35 ),
36 ),
37 SizedBox(
38 height: 5.0,
39 ),
40 Text(
41 'You have pushed the button this many times:',
42 style: TextStyle(fontSize: 35.0),
43 ),
44 SizedBox(
45 height: 5.0,
46 ),
47 Text(
48 '${counter.number}',
49 style: TextStyle(fontSize: 25.0),
50 ),
51 SizedBox(
52 height: 5.0,
53 ),
54 FloatingActionButton(
55 onPressed: () {
56 counter.increaseNumber();
57 },
58 tooltip: 'Increment',
59 child: Icon(Icons.add),
60 ),
61 ],
62 ),
63 ),
64 ),
65 ],
66 ),
67 );
68 }
69 }
Remember our model class. We have only one variable that will reflect the
state change. So at the bottom-most widget we need to access it.
The Provider helps us to access the model data and method anywhere inside
that widget tree.
1 Text(
2 '${counter.number}',
3 style: TextStyle(fontSize: 25.0),
4 ),
5 SizedBox(
6 height: 5.0,
7 ),
8 FloatingActionButton(
9 onPressed: () {
10 counter.increaseNumber();
11 },
12 tooltip: 'Increment',
13 child: Icon(Icons.add),
14 ),
Clicking the button will change the state of the app by increasing the number.
The main drawback of the above code hides itself in this line of Code:
1 final CountingTheNumber counter = Provider.of<CountingTheNumber>(context);
We want to rebuild only the text part where the number will show itself. And
we want to rebuild the floating action button section.
Achieving that is easy. All we need to write a separate widget for those
parts. After that we will call it inside our view.
1 import 'package:flutter/material.dart';
2 import
'package:flutter_provider_explained_for_beginners/model/counting_the_number.d\
3 art';
4 import 'package:provider/provider.dart';
5
6 class ColumnClass extends StatelessWidget {
7 @override
8 Widget build(BuildContext context) {
9 // ‘Provider.of’, just like Consumer needs to know the type of the model.
10 // We need to specify the model ‘CountingTheNumber’.
11 //this time only this widget will be rebuilt
12 final CountingTheNumber counter = Provider.of<CountingTheNumber>(context);
13 return Column(
14 children: [
15 Text(
16 '${counter.number}',
17 style: TextStyle(fontSize: 25.0),
18 ),
19 SizedBox(height: 10.0),
20 FloatingActionButton(
21 onPressed: () {
22 counter.increaseNumber();
23 },
24 tooltip: 'Increment',
25 child: Icon(Icons.add),
26 )
27 ],
28 );
29 }
30 }
That’s all. Running the code will only change a very small segment when we
press the button and change the state.
We have seen what is Flutter state before. Although I’ve not written on state
management in flutter alone, but I have written on provider before.In that
article I’ve written on how to manage state in Flutter.
If you’ve already have read the previous article on Provider,of() method, and
understood the concept, you can use consumer quite easily. What is better?
Provider.of<T> or Consumer<T>?
Consumer allows us to build more granular widgets. At the same time, it also
solves most BuildContext misuse.
Rémi Rousselet also said that the choice is yours. So you can use any one of
them. Although you can use any one of them, without context your preference
is meaningless nevertheless.
Keeping context in my mind, I’ll always prefer using Consumer<T>, instead
of Provider.of()<T>.
Why? I am going to show you in a minute. What is the best state management
architecture in Flutter?
I opened the Flutter documentation page on 25th December, 2020 and found
this line:
** If you are new to Flutter and you don’t have a strong reason to choose
another approach (Redux, Rx, hooks, etc.), this is probably the approach you
should start with. The provider package is easy to understand and it doesn’t
use much code. It also uses concepts that are applicable in every other
approach. **
Although, you can use a bit low-level InheritedWidget concept, still that has
some limitations. I mentioned that in my previous chapter. You can pass data
and services to the descendant widgets by using InheritedWidget concept.
But Provider makes it more simple. In fact, provider actually works with
these low-level widgets.
We don’t want that. Flutter’s default State object rebuilds the whole widget
tree. While managing state in the deepest widget, we cannot allow this to
happen.
Suppose at the bottom of widget tree, inside any Text widget we want to
reflect our state change.
We should keep it at the topmost place of our widget tree. Why should we do
this?
We don’t want to fight with it, or force it to adopt something that goes against
its nature. On the contrary, we will take help from Flutter to do that heavy
lifting.
How we can access changed data? How we can change state through
Consumer widget?
We’re closing towards the lesser known facts to the unknown world of
provider package. As we have just seen the ChangeNotifierProvider
provides only one class here. The CountingTheNumber model class, which
we have placed in our models folder.
It is the best practice to put the Consumer widgets as deep as in the tree as
possible. That is what we have done.
The next code snippet gives you an idea how we use Consumer widget to get
the provided model class methods.
1 import 'package:flutter/material.dart';
2 import
'package:flutter_provider_explained_for_beginners/model/counting_the_number.d\
3 art';
4 import 'package:provider/provider.dart';
5
6 class ColumnClass extends StatelessWidget {
7 @override
8 Widget build(BuildContext context) {
9
10 /// we're using Consumer widget instead of Provider.of().
11 /// we've put our Consumer widget as deep as possible in the tree
12 return Column(
13 children: [
14 Container(
15 margin: const EdgeInsets.all(
16 5.0,
17 ),
18 child: Consumer<CountingTheNumber>(
19 builder: (context, message, child) {
20 return Column(
21 children: [
22 child,
23 Text(
24 '${message.message}',
25 style: TextStyle(fontSize: 25.0),
26 ),
27 ],
28 );
29 },
30
31 /// building a humongous widget tree
32 child: Row(
33 mainAxisAlignment: MainAxisAlignment.center,
34 children: [
35 Column(
36 children: [
37 Text(
38 'First Row',
39 style: TextStyle(
40 fontSize: 20.0,
41 color: Colors.blue,
42 ),
43 ),
44 SizedBox(
45 height: 10.0,
46 ),
47 Text(
48 'Second Row',
49 style: TextStyle(
50 fontSize: 20.0,
51 color: Colors.red,
52 ),
53 ),
54 ],
55 ),
56 const Divider(
57 color: Colors.black,
58 height: 20,
59 thickness: 5,
60 indent: 20,
61 endIndent: 0,
62 ),
63 Column(
64 children: [
65 Text(
66 'First Row',
67 style: TextStyle(
68 fontSize: 20.0,
69 color: Colors.red,
70 ),
71 ),
72 SizedBox(
73 height: 10.0,
74 ),
75 Text(
76 'Second Row',
77 style: TextStyle(
78 fontSize: 20.0,
79 color: Colors.blue,
80 ),
81 ),
82 ],
83 ),
84 ],
85 ),
86 ),
87 ),
88 SizedBox(height: 10.0),
89 Container(
90 margin: const EdgeInsets.all(
91 5.0,
92 ),
93 child: Consumer<CountingTheNumber>(
94 builder: (context, message, child) {
95 return Column(
96 children: [
97 FloatingActionButton(
98 onPressed: () {
99 message.testMessage();
100 },
101 tooltip: 'Increment',
102 child: Icon(Icons.ac_unit_rounded),
103 ),
104 child,
105 ],
106 );
107 },
108
109 /// building another humongous widget tree
110 child: Row(
111 mainAxisAlignment: MainAxisAlignment.center,
112 children: [
113 Column(
114 children: [
115 Text(
116 'First Row',
117 style: TextStyle(
118 fontSize: 20.0,
119 color: Colors.blue,
120 ),
121 ),
122 SizedBox(
123 height: 10.0,
124 ),
125 Text(
126 'Second Row',
127 style: TextStyle(
128 fontSize: 20.0,
129 color: Colors.red,
130 ),
131 ),
132 ],
133 ),
134 const Divider(
135 color: Colors.black,
136 height: 20,
137 thickness: 5,
138 indent: 20,
139 endIndent: 0,
140 ),
141 Column(
142 children: [
143 Text(
144 'First Row',
145 style: TextStyle(
146 fontSize: 20.0,
147 color: Colors.red,
148 ),
149 ),
150 SizedBox(
151 height: 10.0,
152 ),
153 Text(
154 'Second Row',
155 style: TextStyle(
156 fontSize: 20.0,
157 color: Colors.blue,
158 ),
159 ),
160 ],
161 ),
162 ],
163 ),
164 ),
165 ),
166 ],
167 );
168 }
169 }
Watch the bold sections. Reading them will explain how Consumer widgets
work. How do the Consumer widgets work?
The bold sections in the above code tells us one thing. In a Consumer widget
we must specify the type of the model that we want to access.
1 child: Consumer<CountingTheNumber>()
Here the model class is CountingTheNumber() that has two methods. When
we call one method that increases the number of the counter variable.
We have seen its usage in our previous article, where we have used
Provide.of()<T>.
The another method of the model class helps us to search and find one
character. By pressing the button we check whether the message starts with
that letter or not.
Clicking the button changes the state of the associated widget and gives us a
message.
When we click the blue icon button, it calls the method on our model class
that checks whether the name has started with letter ‘s’ or not. If it starts with
that letter it gives a welcome message.
Now everything happens without rebuilding the whole widget tree. Take a
good look at my GitHub repo : The Flutter Provider code repository for this
book
You will find how we have built a humongous widget tree above and below
of our Consumer widgets.
The very first step involves specifying the type of the model class. We have
already seen that line in the above code.
1 Consumer<CountingTheNumber>
What happens if we don’t specify the generics? The provider package cannot
help us any more. The provider is based on types. If you don’t mention the
type, it doesn’t know what to provide.
Each time we try to change the state by pressing the button, the
ChangeNotifier changes. In fact, we call the notifyListeners() in our model
class methods, just like below:
1 void testMessage() {
2 message.startsWith('S')
3 ? message = 'Hi Sanjib'
4 : message = 'First letter is not S';
5 notifyListeners();
6 }
The third argument is child. If you plan to add large sub-tree under the
control of your Consumer widget, then just go ahead. Use that child to build
more complex UI. However, when state changes, the child does not get
affected.
The large sub-tree you’ve just added under Consumer doesn’t change when
the model changes.
If you run the code, the first screen shows you a name: “Sanjib Sinha” at the
bottom most Widget.
Next, we’ve pressed the button and it changes the state of the bottom-most
Widget, and gives us a message, such as “Hi Sanjib”. While changing the
State of the bottom-most Widget, it does not change the top and bottom
widgets.
So, that’s it. Although I feel we should learn provider using more complex
examples.
The Flutter framework helps us to build beautiful design patterns. If you have
already known Flutter a little bit, then you must have learned one key
concept.
You cannot drag and drop interfaces. Instead you need to write the necessary
code.
However, since the coding part involves only Dart programming language, it
becomes easier for you. Of course, you should have a basic Dart
programming concept.
Now, in this context, we come to the second point. Any User Interface needs
user’s interaction. Your Flutter app should allow users to interact with itself.
No matter, how you design that app.
Think about the RaisedButton widget that uses a callback through onPressed.
First we should import material and call our main OurApp class using void
main runApp method.
1 import 'package:flutter/material.dart';
2
3 void main() {
4 runApp(OurApp());
5 }
Our next job involves creating a class named as OurApp that extends
StatelessWidget and it should have a build() method.
1 class OurApp extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 return MaterialApp(
5 title: 'Our App',
6 debugShowCheckedModeBanner: false,
7 home: Scaffold(
8 body: ACenterClass(),
9 ),
10 );
11 }
12 }
13
14 class ACenterClass extends StatelessWidget {
15 void pressRemote() {
16 print('Remote has been pressed.');
17 }
18
19 @override
20 Widget build(BuildContext context) {
21 return Center(
22 child: Container(
23 alignment: Alignment.center,
24 width: 350.00,
25 height: 350.00,
26 decoration: BoxDecoration(
27 color: Colors.blue,
28 border: Border.all(
29 color: Colors.deepOrange,
30 width: 2.0,
31 style: BorderStyle.solid,
32 ),
33 borderRadius: BorderRadius.all(Radius.circular(40.0)),
34 boxShadow: [
35 BoxShadow(
36 color: Colors.black54,
37 blurRadius: 20.0,
38 spreadRadius: 20.0,
39 ),
40 ],
41 gradient: LinearGradient(
42 begin: Alignment.centerLeft,
43 end: Alignment.centerRight,
44 colors: [
45 Colors.red,
46 Colors.white,
47 ],
48 ),
49 // to make shape active we need to comment out borderRadius property and vice
versa
50 //shape: BoxShape.circle,
51 ),
52 child: Column(
53 children: [
54 Text(
55 'Press',
56 style: TextStyle(
57 fontSize: 30.0,
58 color: Colors.blue,
59 ),
60 ),
61 SizedBox(
62 height: 10.0,
63 ),
64 RaisedButton(
65 child: Text(
66 'Press Button',
67 style: TextStyle(
68 fontSize: 30.0,
69 color: Colors.blue,
70 ),
71 ),
72 onPressed: pressRemote,
73 ),
74 ],
75 ),
76 ),
77 );
78 }
79 }
As we press the button, it debugs and prints the message in our terminal –
Remote has been pressed.
But, why?
Now a function can return nothing (void) or return either a built-in data type
or a custom data type.
State can include anything – the app’s assets, as we said, all the variables
that the Flutter framework keeps about the UI, user sessions that can be
shared in different parts of the app, etc.
Whenever we design an app, and start building it, we don’t have to manage
every state.
The simplest example is we press a button and the text changes on the screen.
Again we press the restore button, and the text disappears. We need to
provide the business logic so that it happens.
Consider a complex example, where a user adds an item to cart and that item
remains at that cart as long as user is logged in.
We may contain it in a single widget. That is why it is also called local state.
Suppose we want to show the current progress of a complex animation. Once
it is done, the UI is rebuilt, and we don’t want it anymore.
For that reason, we don’t have to need any specialized state management
techniques like ‘Provider’ for that.
The app is quite straight forward and simple. To start with we need to
organize our application in three directories - controller, model and view.
The main.dart file and the main method calls the runApp() method, in which
we pass our app - QuizApp().
1 import 'package:flutter/material.dart';
2 import 'package:quiz_app/view/quiz_app.dart';
3
4 void main() {
5 runApp(QuizApp());
6 }
As you have noticed, we need two more files. One stays in model directory.
It consists of a List of questions and different answers.
Basically, it is a List of Map that consists of two data types, String as key and
Object or List as value.
1 List<Map<String, Object>> questionList = [
2 {
3 'question': 'What is the synonym of Mendacity?',
4 'answers': ['truthfulness', 'daring', 'falsehood', 'enemy'],
5 },
6 {
7 'question': 'What is the synonym of Culpable?',
8 'answers': ['gay', 'guilty', 'falsehood', 'enemy'],
9 },
10 {
11 'question': 'What is the synonym of Rapacious?',
12 'answers': ['guilty', 'daring', 'falsehood', 'greedy'],
13 },
14 ];
Nothing fancy althogh, it gives us an idea of how we can organize our small
ephemeral state in a single widget.
There are many other techniques as well, but in this chapter we will only
learn Provider, because Google recommends it.
For ephemeral state management using setState() and a field inside the
StatefulWidget’s State class is enough, because, a single widget needs it, no
other part of the device can access its single private variable.
An app state or application state is not like that. We want to share the app
state across many parts of our app, not only that, we may want it to keep
between user sessions. In like manner, we can call it shared state.
To manage app state we can opt for several options. Nevertheless, Google
recommends Provider, we will have a brief look at other options as well.
Before that, let us see other approaches to manage state. Using setState() and
a field inside the StatefulWidget’s State class is another approach; yet that is
good and recommended for the ephemeral state. This lower-level approach
is made up when we create a new Flutter application.
Otherwise we might use MobX or GetX approach; the first one is a popular
library based conceptualization on observables and reactions, and the second
one is a simplified reactive state management solution.
There are plenty of open source resources available to learn any one of them,
thoroughly.
In this chapter, although, we will learn only Provider, the state management
recommended by Google, creator of Dart programming language and Flutter
framework.
At the time of writing this book, provider package is 4 and above. We will
always check the latest version.
The app state is something that we need to modify from many different
places, and to do that we have to pass around a lot of callbacks; for a
complex widget tree, it will be suicidal to replace several widgets again and
again.
To understand this mechanism we need to find out a solution that will not
disturb the widget tree as a whole, yet the app state will modify a few
widgets deep down the tree. Suppose we need to modify one widget that has
hundred widgets on top of it.
Without disturbing top hundred widgets, we can successfully handle the app
state using Provider.
Flutter has in-built mechanisms for widgets to provide data and services to
their distant descendants, it means not just the immediate children, but any
widgets below them.
Once our designed Model is provided to the desired widgets in our app
through the ChangeNotifierProvider declaration at the top, the Consumer
widgets that have subscribed to the notifications can use it.
1 return Consumer<FirstModel>(
2 builder: (context, value, child) {
3 return Text("The value : ${value.firstModelVariable}");
4 },
5 );
The first rule of using Consumer widget is we need to be specific about the
type of the model that we want to access. Suppose, we want ‘FirstModel’, so
we write Consumer<FirstModel>.
The ‘builder’ is called with three arguments, the first one is quite
familiar,‘context’; we get it in every build method. The second argument
‘value’ is the instance of the ChangeNotifier. Using that instance we can
define the app state, and along with it, we can also use the data in the model
according to our requirement.
The role of the third argument ‘child’ is quite interesting. Suppose we have a
large widget subtree under our Consumer that does not change when our
model changes.
Let us start with a very simple counter model. Through Provider, we will
change the counter number. We have two buttons – Increase and Decrease
(Figure 8.1). Imagine a number line, using these buttons, we can either move
towards the right side (positive), or towards the left side (negative).
Figure 11.1 – Simple Provider example
The next two images will show you how we have increased the value and
decreased the value tapping these two buttons respectively.
But before that we need to see the code and try to understand how we have
used the Provider package.
1 import 'package:flutter/widgets.dart';
2
3 /// using the mixin concept of dart that we have discussed
4 /// in our previous chapter
5 class CountingTheNumber with ChangeNotifier {
6 int value = 0;
7 void incrementTheValue() {
8 value++;
9 notifyListeners();
10 }
11
12 void decreaseValue() {
13 value--;
14 notifyListeners();
15 }
16 }
The above code snippets is quite simple. This is our model class through
which we want to manage the state of the counter in a ChangeNotifier.
Now, we can run the app and by tapping two buttons change the value.
Before that, let us have a close look at some parts of the above code.
1 final counter = Provider.of<CountingTheNumber>(context);
‘Provider.of’, just like Consumer needs to know the type of the model. We
need to specify the model ‘CountingTheNumber’. Now using the ‘counter’
we have accessed the model data.
1 Text(
2 '${counter.value}',
3 style: Theme.of(context).textTheme.headline4,
4 ),
5 …
6 RaisedButton(
7 onPressed: () => counter.incrementTheValue(),
8 child: Text(
9 'Increase',
10 style: TextStyle(
11 fontSize: 20.0,
12 ),
13 ),
14 ),
15 …
16 RaisedButton(
17 onPressed: () => counter.decreaseValue(),
18 child: Text(
19 'Decrease',
20 style: TextStyle(
21 fontSize: 20.0,
22 ),
23 ),
24 ),
After that, we can run the app once again, and it turns the counter value to 0.
Now, we can test the decrease button (Figure 8.3).
Figure 11.3 – Tapping the decrease button
The above code snippets give us an idea of how Provider package works.
Let us start with an image. We have extended our old code added a few more
generic models.
Now we can press the counter button, and besides, we will press a button to
change the text below. After that, we can also press the restore button to clear
that data and give an output of that.
Figure 11.4 – Provider example with many models
In the above image, it is evident that we have pressed the decrease button 4
times, however, the default text data - ‘Some Data’, has not been affected.
In the above code, we have used two Providers, inside the main() function.
1 runApp(MultiProvider(
2 providers: [
3 ChangeNotifierProvider(
4 create: (context) => CountingTheNumber(),
5 ),
6 ChangeNotifierProvider(
7 create: (context) => FirstModelProvider(),
8 ),
9 ],
10 child: MyApp(),
11 ));
Figure 11.5 – The ‘Press me’ button has been pressed and the value of
the model class has also been changed
If we click the ‘Reset’ button, the data has been cleared. The following figure
(Figure 11.5) shows that display of the screen.
Figure 11.6 – We have pressed the ‘Reset’ button, and the
corresponding value of model class is displayed
Now we are going to add another model class in the next code snippets. It
will add another button that will display the first name.
1 // main.dart
2
3 import 'models/providers/first_model_provider.dart';
4
5 import 'models/providers/counter_model_provider.dart';
6 import 'package:flutter/material.dart';
7 import 'package:provider/provider.dart';
8 import 'models/providers/second_model_provider.dart';
9 import 'views/my_app.dart';
10
11 void main() {
12 runApp(MultiProvider(
13 providers: [
14 ChangeNotifierProvider(
15 create: (context) => CountingTheNumber(),
16 ),
17 ChangeNotifierProvider(
18 create: (context) => FirstModelProvider(),
19 ),
20 ChangeNotifierProvider(
21 create: (context) => SecondModelProvider(),
22 ),
23 ],
24 child: MyApp(),
25 ));
26 }
27
28
29 // second_model_provider.dart
30
31 import 'package:flutter/widgets.dart';
32
33 class SecondModelProvider with ChangeNotifier {
34 String name = 'Some Name';
35 int age = 0;
36
37 void getFirstName() {
38 name = 'Json';
39 print(name);
40 notifyListeners();
41 }
42 }
43
44
45 // my_home_page.dart
46
47 import
'package:all_about_flutter_provider/models/providers/first_model_provider.dar\
48 t';
49 import
'package:all_about_flutter_provider/models/providers/second_model_provider.da\
50 rt';
51 import 'package:flutter/cupertino.dart';
52 import 'package:flutter/material.dart';
53 import 'package:provider/provider.dart';
54
55 import '../models/providers/counter_model_provider.dart';
56
57 class MyHomePage extends StatelessWidget {
58 /*
59 MyHomePage({Key key, this.title}) : super(key: key);
60
61 final String title;
62 */
63 final String title = 'Using Provider Examples';
64
65 @override
66 Widget build(BuildContext context) {
67 /// MyHomePage is rebuilt when counter changes
68 final counter = Provider.of<CountingTheNumber>(context);
69
70 return Scaffold(
71 appBar: AppBar(
72 title: Text(title),
73 ),
74 body: SafeArea(
75 child: ListView(
76 padding: const EdgeInsets.all(10.0),
77 children: <Widget>[
78 Text(
79 'You have pushed the button this many times:',
80 style: TextStyle(fontSize: 25.0),
81 textAlign: TextAlign.center,
82 ),
83
84 /// consumer or selector
85 Text(
86 '${counter.value}',
87 style: Theme.of(context).textTheme.headline4,
88 textAlign: TextAlign.center,
89 ),
90 SizedBox(
91 height: 10.0,
92 ),
93 Row(
94 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
95 children: <Widget>[
96 RaisedButton(
97 onPressed: () => counter.increaseValue(),
98 child: Text(
99 'Increase',
100 style: TextStyle(
101 fontSize: 20.0,
102 ),
103 ),
104 ),
105 SizedBox(
106 height: 10.0,
107 ),
108 RaisedButton(
109 onPressed: () => counter.decreaseValue(),
110 child: Text(
111 'Decrease',
112 style: TextStyle(
113 fontSize: 20.0,
114 ),
115 ),
116 ),
117 ],
118 ),
119 SizedBox(
120 height: 10.0,
121 ),
122 Column(
123 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
124 children: <Widget>[
125 Container(
126 padding: const EdgeInsets.all(10.0),
127 color: Colors.red,
128 child: Consumer<FirstModelProvider>(
129 builder: (context, firstModelProvider, child) =>
130 RaisedButton(
131 child: Text(
132 'Press me!',
133 style: TextStyle(fontSize: 20.0),
134 ),
135 onPressed: () {
136 firstModelProvider.supplyFirstData();
137 },
138 ),
139 ),
140 ),
141 Container(
142 padding: const EdgeInsets.all(10.0),
143 color: Colors.white30,
144 child: Consumer<FirstModelProvider>(
145 builder: (context, firstModelProvider, child) => Text(
146 firstModelProvider.someDate,
147 style: TextStyle(fontSize: 40.0),
148 ),
149 ),
150 ),
151 SizedBox(
152 height: 10.0,
153 ),
154 Container(
155 padding: const EdgeInsets.all(10.0),
156 color: Colors.red[200],
157 child: Consumer<FirstModelProvider>(
158 builder: (context, firstModelProvider, child) =>
159 RaisedButton(
160 child: Text(
161 'Reset',
162 style: TextStyle(fontSize: 20.0),
163 ),
164 onPressed: () {
165 firstModelProvider.clearData();
166 },
167 ),
168 ),
169 ),
170 SizedBox(
171 height: 10.0,
172 ),
173 Container(
174 padding: const EdgeInsets.all(10.0),
175 color: Colors.white30,
176 child: Consumer<SecondModelProvider>(
177 builder: (context, secondModel, child) => Text(
178 secondModel.name,
179 style: TextStyle(fontSize: 40.0),
180 ),
181 ),
182 ),
183 SizedBox(
184 height: 10.0,
185 ),
186 Container(
187 padding: const EdgeInsets.all(10.0),
188 color: Colors.red[200],
189 child: Consumer<SecondModelProvider>(
190 builder: (context, secondModel, child) => RaisedButton(
191 child: Text(
192 'Get First Name',
193 style: TextStyle(fontSize: 20.0),
194 ),
195 onPressed: () {
196 secondModel.getFirstName();
197 },
198 ),
199 ),
200 ),
201 ],
202 ),
203 ],
204 ),
205 ),
206
207 /// This trailing comma makes auto-formatting nicer for build methods.
208 );
209 }
210 }
This part of the code has handled the Consumer section. Therefore, let us
check that part first.
1 Container(
2 padding: const EdgeInsets.all(10.0),
3 color: Colors.white30,
4 child: Consumer<SecondModelProvider>(
5 builder: (context, secondModel, child) => Text(
6 secondModel.name,
7 style: TextStyle(fontSize: 40.0),
8 ),
9 ),
10 ),
11 SizedBox(
12 height: 10.0,
13 ),
14 Container(
15 padding: const EdgeInsets.all(10.0),
16 color: Colors.red[200],
17 child: Consumer<SecondModelProvider>(
18 builder: (context, secondModel, child) => RaisedButton(
19 child: Text(
20 'Get First Name',
21 style: TextStyle(fontSize: 20.0),
22 ),
23 onPressed: () {
24 secondModel.getFirstName();
25 },
26 ),
27 ),
28 ),
We are able to add another feature of state management through Provider. The
second model Provider is a simple class.
1 // second_model_provider.dart
2
3 import 'package:flutter/widgets.dart';
4
5 class SecondModelProvider with ChangeNotifier {
6 String name = 'Some Name';
7 int age = 0;
8
9 void getFirstName() {
10 name = 'Json';
11 print(name);
12 notifyListeners();
13 }
14 }
Next, if you proceed, you will find how Provider and Consumer work
together. First, we have pressed the decrease button for 3 times. Next, we
have pressed the ‘Press me’ button, and the ‘Data Changed’. After that,
finally, we have pressed the ‘Get First Name’ button, and the name appears
on the screen.
Each Consumer widget has persisted its state, one button-press does not
affect the other. The changed-data stays on the screen.
Before concluding this chapter, we will learn how we can separate business
logic, application logic and screen-view.
To do that, we will keep our models inside the ‘model’ folder and keep our
business logic there. We will keep our application logic inside the
‘controller’ folder, and finally we get the screen-view inside the ‘view’
folder.
This type of organization is necesary to make your code more readable and
reusable. Although I’ve not written comments to make my every intention
clear in the examples, I recommend readers to do that.
Take time, write your code along with detailed documentation, so that later
you can come back and understand your code, your every step.
Although it was not a full-blown, complete app, yet it could give you an idea
how we could organize our code.
In this chapter, we’ll learn that pattern in a detailed way. And this chapter is
a continuation of the last chapter.
And the second model class is the ‘MobileModel’ that has a list of selected
colors of which we will choose one for the background, and another for the
mobile. We will display the mobile color on the foreground, and the
background will be different.
Pressing the icon of the respective mobile will change the color of both –
foreground and background. At the same time a text will be displayed to
make us aware that foreground and background colors have been changed.
1 model/mobile_model.dart
2
3 import 'package:flutter/material.dart';
4 import 'package:flutter/widgets.dart';
5
6 class MobileModel with ChangeNotifier {
7 String backgroundColorOfFirst = 'Background';
8 String mobileColorOfFirst = 'Mobile';
9 String backgroundColorOfSecond = 'Background';
10 String mobileColorOfSecond = 'Mobile';
11 List<Color> selection = [
12 Colors.yellow,
13 Colors.blue,
14 Colors.orange,
15 Colors.pinkAccent,
16 Colors.green,
17 Colors.limeAccent,
18 ];
19
20 void changeColorToPurple() {
21 backgroundColorOfFirst = 'Background \n Purle';
22 mobileColorOfFirst = 'Mobile \n White.';
23 selection[0] = Colors.purple;
24 selection[4] = Colors.white;
25 notifyListeners();
26 }
27
28 void changeColorToRed() {
29 backgroundColorOfSecond = 'Background \n Black';
30 mobileColorOfSecond = 'Mobile \n Red.';
31 selection[1] = Colors.black;
32 selection[5] = Colors.red;
33 notifyListeners();
34 }
35
36 void restoreOldColorOfFirstMobile() {
37 backgroundColorOfFirst = 'Background \n Yellow';
38 mobileColorOfFirst = 'Mobile \n Green.';
39 selection[0] = Colors.yellow;
40 selection[4] = Colors.green;
41 notifyListeners();
42 }
43
44 void restoreOldColorOfSecondMobile() {
45 backgroundColorOfSecond = 'Background \n Blue';
46 mobileColorOfSecond = 'Mobile \n Limeaccent.';
47 selection[1] = Colors.blue;
48 selection[5] = Colors.limeAccent;
49 notifyListeners();
50 }
51 }
The model classes are the sources of date. That data should be displayed on
the screen-view. Not only that, that data must be changed on the tap of the
icon.
Therefore, we need some subscribers or Consumers who will get that data
and pass them to the view accordingly.
The role of Controller
Who will control that? The controllers. The controller will stay between
model and view; the controllers’ job is simple, it will play the role of the
communicator who will manage the communication between model and
view.
The data-source or model does not know where its data are going. The view
does not know where from the data are coming. The controller knows
everything. It controls every operation.
Even we have some controllers that will also decide what type of text style
we will follow.
If we go through the above code, we will see several Consumers that have
subscribed to those model classes. The role of these controllers are simple.
They will pass those data to the screen-view pages, which we will see in a
minute.
In the above code, there are one or two Consumers, not as much as the
mobile-specific controllers.
Before going to read the screen-view code, we will take a look at how our
flutter application looks like:
Figure 12.1 – The first look of the application that we are going to build
Now we can scroll down to the bottom and see what are waiting for us. At
the bottom part, we have two buttons, and below those buttons, we have two
mobile icons and respective text that tells us about the foreground and
background colors.
If we click the ‘Change Name’ button, it will display a text ‘Name Changed’.
Just below that text we have the ‘Clear name’ button. Pressing that button
will clear the text (Figure 8.9).
Figure 12.2 – The bottom part of our application
At the very bottom two mobile icons are visible. We can also see the name of
the foreground and background color in text. Tapping any icon will change
the foreground and background color, and at the same time, the description of
color displayed in text will also change.
Finally, let us see the screen-view page and the main method.
1 // view/second_home_app.dart
2
3
4 import 'package:first_flutter_app/controller/mobile_controller.dart';
5 import
'package:first_flutter_app/controller/second_home_page_controller.dart';
6 import 'package:first_flutter_app/model/first_model.dart';
7 import 'package:flutter/material.dart';
8 import 'package:provider/provider.dart';
9
10 class SecondHomeAppPage extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return MaterialApp(
14 debugShowCheckedModeBanner: false,
15 title: 'Second Provider Example',
16 home: Scaffold(
17 body: SafeArea(
18 child: ListView(
19 children: [
20 textStyleSacramento('Provider Examples'),
21 Container(
22 padding: const EdgeInsets.all(20.0),
23 child: Image.asset(
24 'images/sea1.jpg',
25 width: 300,
26 ),
27 ),
28 textStyleTrajanPro('We can add humongous widget tree below...'),
29 changeNameButton(),
30 Container(
31 padding: const EdgeInsets.all(30.0),
32 child: textStyleSacramento(
33 Provider.of<FirstModel>(context, listen: true).name),
34 ),
35 clearNameButton(),
36 SizedBox(
37 height: 10.0,
38 ),
39 Row(
40 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
41 children: [
42 changeColorButtonToPurple(),
43 VerticalLine(),
44 changeColorButtonToRed(),
45 ],
46 ),
47 SizedBox(
48 height: 10.0,
49 ),
50 Row(
51 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
52 children: [
53 restoreOldColorOfFirstMobile(),
54 VerticalLine(),
55 restoreOldColorOfSecondMobile(),
56 ],
57 ),
58 ],
59 ),
60 ),
61 ),
62 );
63 }
64 }
65
66 class VerticalLine extends StatelessWidget {
67 const VerticalLine({
68 Key key,
69 }) : super(key: key);
70
71 @override
72 Widget build(BuildContext context) {
73 return Center(
74 child: Container(
75 height: MediaQuery.of(context).size.height * 0.2,
76 width: 3,
77 color: Colors.black45,
78 ),
79 );
80 }
81 }
82
83 class HorizontalLine extends StatelessWidget {
84 const HorizontalLine({
85 Key key,
86 }) : super(key: key);
87
88 @override
89 Widget build(BuildContext context) {
90 return Center(
91 child: Container(
92 width: MediaQuery.of(context).size.width * 0.2,
93 height: 3,
94 color: Colors.black45,
95 ),
96 );
97 }
98 }
And the main method is as the following where we have used multi Provider
:
1 // main.dart
2
3 import 'package:first_flutter_app/model/first_model.dart';
4 import 'package:first_flutter_app/view/second_home_app.dart';
5 import 'package:flutter/material.dart';
6 import 'package:provider/provider.dart';
7
8 import 'model/mobile_model.dart';
9
10 void main() {
11 runApp(
12 MultiProvider(
13 providers: [
14 ChangeNotifierProvider(create: (context) => FirstModel()),
15 ChangeNotifierProvider(create: (context) => MobileModel()),
16 ],
17 child: SecondHomeAppPage(),
18 ),
19 );
20 }
Now, we can press the ‘Change Name’ button, and get the text. Let us do that,
and take a look at the lower bottom part.
Figure 12.3 – The lower bottom part of our application
Next, we will start operating at the lower bottom part. Remember, we have
already pressed the ‘Change Name’ button,and got the text displayed on the
top of the screen-view.
Now we are going to change the first mobile icon color, foreground and
background, both.
Let us see the image first, after that, we will discuss the code.
Figure 12.4 – The first mobile icon’s foreground and background color
have been changed and it has been reflected on the below text
We can clearly watch that the first mobile icon’s foreground has been
changed to white from green; at the same time the background color has been
changed from yellow to purple.
After that, we will take a look at the related mobile controller’s coding part.
1 Widget changeColorButtonToPurple() => Column(
2 children: [
3 Container(
4 padding: const EdgeInsets.all(10.0),
5 child: Consumer<MobileModel>(
6 builder: (context, value, child) => Container(
7 padding: const EdgeInsets.all(15.0),
8 child: FloatingActionButton(
9 backgroundColor: value.selection[0],
10 onPressed: () {
11 value.changeColorToPurple();
12 },
13 child: Icon(
14 Icons.mobile_screen_share,
15 color: value.selection[4],
16 ),
17 ),
18 ),
19 ),
20 ),
21 Divider(
22 thickness: 2.0,
23 ),
24 Consumer<MobileModel>(
25 builder: (context, value, _) => Text(
26 value.backgroundColorOfFirst,
27 style: TextStyle(
28 fontFamily: 'Trajan Pro',
29 fontSize: 20.0,
30 fontWeight: FontWeight.bold,
31 ),
32 ),
33 ),
34 Divider(
35 thickness: 2.0,
36 ),
37 Consumer<MobileModel>(
38 builder: (context, value, _) => Text(value.mobileColorOfFirst,
39 style: TextStyle(
40 fontFamily: 'Trajan Pro',
41 fontSize: 20.0,
42 fontWeight: FontWeight.bold,
43 )),
44 ),
45 ],
46 );
And, finally we can watch the screen-view page part, where we have called
this controller.
1 Row(
2 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
3 children: [
4 changeColorButtonToPurple(),
5 VerticalLine(),
6 changeColorButtonToRed(),
7 ],
8 ),
In the same row, we can call both controllers that will change the foreground
and background color.
Therefore, in the next image, we will see that the second mobile icon’s
foreground and background color have also been changed, because we have
tapped the second icon.
Figure 12.5 – The second mobile icon’s foreground and background
color have been changed
We can clearly see that the second mobile icon’s foreground changed to red,
and the background changed to black. The below text has also displayed the
name of the color respectively.
One thing is also evident, although two controllers belong to the same Row
widget, one change does not affect the other.
Our next step will be to restore the old data. First, we will click the restore
button below the first mobile icon. Secondly, we will click the second
restore button below the second mobile icon.
And finally, we will click the ‘Clear name’ button on the upper half of the
screen. It will first restore the old color of the first mobile icon, next, it will
change the second mobile icon; and finally it will clear the ‘name’ that was
stuck on the upper half of the screen.
Figure 12.6 – The final screen shot of our application
We have learned how we can use Provider package, and Consumer widget to
manage state efficiently. We have also learned how without rebuilding the
whole widget tree, we can change and persist state of our application.
In the next chapter, we will learn how to navigate from one screen to others
and come back with the help of Provider and keeping our State intact!
Although for a small app using stateful widget is okay. But for a full blown,
complicated app architecture always avoid stateful widget, and always try to
stick with the Provider package.
Why?
That will help you to avoid widget rebuilding. When you use ChangeNotifier
native Flutter class with Provider, value changes but widget is not rebuilt.
Hence the loading time increases. In any app development, managing time
complexity makes all the difference.
In the previous chapter we’ve seen how one can build a small quiz app in
Flutter. Since it was a small app, we had used stateful widget.
Figure 13.1 – A Quiz App using stateful widget
As a result, each time we click the button, the whole widget tree is rebuilt.
Remember, it’s a small app, so we don’t have an expensive parent widget
tree on the top of the widget where we return the changed value.
What I feel, using stateful widget is unnecessary. Moreover, for a small app
it’s okay. But what will happen when we will have an expensive parent
widget tree?
While rebuilding a single widget, it will actually rebuild the whole parent
widget tree.
Let us try to make the whole concept a little bit simpler than it seems. Take a
look at the image below.
We have rebuilt the previous quiz app using Provider package this time.
Figure 13.2 – The same Quiz App using stateless widget and provider
changenotifier
As you can see, there are several options from which you can choose the
correct synonym of the word Mendacity.
Clicking any button will take you to the next question and, besides it will
also display the correct answer.
The next image will show you how our app is working fine.
Figure 13.3 – The Quiz App using provider change notifier
In the above image, it’s clearly visible that the next screenshot shows us the
next question and at the same time,it also displays the synonym of the
previous word - Mendacity.
At the top appears the new question. In the Elevated button text four new
synonyms appear, and finally, the app displays the correct answer of the
previous question.
With reference to our previous query, whether we can avoid the unnecessary
widget rebuilding, the answer is, yes, we have achieved that in this quiz app.
Before diving deep into the code let me tell you one thing. You’ll get the
whole code snippet in my GitHub repository.
The first code repository with stateful widget
The second code repository with statless widget and provider package
We keep the providers above the app. Although it has a different reason
involving the test purpose, but still it has another advantage.
The NewQuizAppHome widget is the main user interface that we’ve already
seen in the above images.
Usually we notify the framework that the internal state of an object has
changed. To do that we need to change the internal state of a State object,
making the change in a function that we pass to setState(){}.
What is ChangeNotifier?
According to the Flutter documentation:
1 ChangeNotifier is a simple class included in the Flutter SDK which provides
change n\
2 otification to its listeners. In other words, if something is a ChangeNotifier,
you \
3 can subscribe to its changes.
One thing is clear. Our quiz app values, that means questions, answers, etc
can subscribe its changes.
1 import 'package:flutter/widgets.dart';
2
3 class QuestionAndAnswerModel extends ChangeNotifier {
4 List<Map<String, Object>> questions = [
5 {
6 'question': 'What is the synonym of Mendacity?',
7 'answers': ['truthfulness', 'daring', 'falsehood', 'enemy'],
8 },
9 {
10 'question': 'What is the synonym of Culpable?',
11 'answers': ['gay', 'guilty', 'falsehood', 'enemy'],
12 },
13 {
14 'question': 'What is the synonym of Rapacious?',
15 'answers': ['guilty', 'daring', 'falsehood', 'greedy'],
16 },
17 ];
18 int counter = 0;
19
20 String answerChecking = 'Click to check correct answer!';
21
22 void incrementCounter() {
23 counter++;
24 notifyListeners();
25
26 if (counter > 2) {
27 counter = 0;
28 }
29 checkAnswer();
30 }
31
32 void checkAnswer() {
33 if (counter == 0) {
34 answerChecking = 'Synonym of Rapacious was Greedy.';
35 } else if (counter == 1) {
36 answerChecking = 'Synonym of Mendacity was Falsehood.';
37 } else if (counter == 2) {
38 answerChecking = 'Synonym of Culpable was Guilty.';
39 } else {
40 answerChecking = 'Click to check correct answer!';
41 }
42 }
43 }
As Flutter encourages to break the app design into smaller segments, we keep
three controllers in separate directory.
I hope you have already got the answer. Yet it is always better we show the
code.
First the QuestionWidget, where we’re going to have the list of questions and
the index through which we can change the question by the press of button.
1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3 import '../model/question_and_answer_model.dart';
4
5 class QuestionWidget extends StatelessWidget {
6 const QuestionWidget({
7 Key key,
8 @required this.questions,
9 @required this.counter,
10 }) : super(key: key);
11
12 final List<Map<String, Object>> questions;
13 final int counter;
14
15 @override
16 Widget build(BuildContext context) {
17 return Text(
18 context
19 .watch<QuestionAndAnswerModel>()
20 .questions[context.watch<QuestionAndAnswerModel>().counter]
21 ['question'],
22 style: TextStyle(
23 fontSize: 25.0,
24 fontWeight: FontWeight.bold,
25 ),
26 );
27 }
28 }
The same way, we can watch and check the correct answer.
1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3 import '../model/question_and_answer_model.dart';
4
5 class CheckAnswerWidget extends StatelessWidget {
6 const CheckAnswerWidget({
7 Key key,
8 }) : super(key: key);
9
10 @override
11 Widget build(BuildContext context) {
12 return Text(
13 context.watch<QuestionAndAnswerModel>().answerChecking,
14 style: TextStyle(
15 fontSize: 20.0,
16 ),
17 );
18 }
19 }
However, the question is what is the most efficient way to use Provider
package?
Another question is how we can use Flutter ChangeNotifier class along with
Provider, so that it gives us the best result.
Will that really reduce widget re-building and enhance the efficiency of our
Flutter App?
In this chapter we will look into that matter. Not only that, we will also show
proof of it. To prove that Provider is more efficient than stateful widget, we
have used Android Studio Flutter Inspector and Flutter Performance,which
will track the widget rebuilding.
These tools will show you how many widgets are rebuilt when user presses
a button. With reference to the provider’s best usage patterns we will also
learn how to organize our code.
Once Flutter gets the provider, either it uses watch method to reflect the
changed property or read method to change the event or, in other words, call
the method on it.
Therefore how to use flutter provider doesn’t concern us. We want the proof
that provider works better and faster than stateful widget.
So let’s start with a simple stateful widget example where user presses a
button that increments the value. In this example we will see how a stateful
widget takes a toll on the whole widget tree.
If you search Internet, it says many things about stateful widget. People say
stateful widget is useful when the part of the user interface you are describing
can change dynamically.
We can do the same with the help of provider package, CgangeNotifier class
and a stateless widget.
Just like too much sunlight takes a heavy toll on your skin, a stateful widget
has a serious effect on the whole widget tree. Moreover, that bad effect starts
rebuilding widgets from the very top. That is from your home page, then it
rebuilds the scaffold widget, and all the child widgets under it.
Now under Scaffold widget, whatever widgets you have, all are rebuilt for a
press of button!
Now we have pressed the counter button six times. This press of button will
rebuild the Floating action button widget, and even the Icon widget. We
cannot even avoid if we use provider package.
Let me tell you about this image first. It demonstrates a simple stateful flutter
app on the left side. That indicates we have pressed the button six times.
On the right side of Android Studio, we have opened the Flutter Performance
window and ticked the “Track Widget rebuilds”, which shows us the proof.
Figure 14.3 – As you see the Scaffold has also been rebuilt 6 times for a
the press of a button
It clearly shows that MyHomePage has been rebuilt six time for a singular
button pressing. And that happens to Scaffold widget also.
However, when we will use provider package, this will never happen. We’ll
see the proof in a minute.
We can have more proof that will show you that in the similar way Scaffold
has been rebuilt six times too!
But we have enough proof. Now let’s concentrate on provider package. How
do we use provider efficiently so that we can avoid the whole widget tree
rebuilding?
First of all we will demonstrate how we can stop this bad effect on our
flutter app. If we use a stateful widget, a single button-press rebuilds the
whole widget tree and consumes a hell lot of memory.
We’ve just seen that effect.
Therefore, we must find the best solution. Our first target is simple. We
cannot stop the widget rebuilds totally. But we can try to make it sure that
less widgets are rebuilt in the process.
It means we can place one type of provider inside a one type of widget and
another type of widget inside another type of widget.
The golden rule is break your app in many small chunks. One model class
should have one task.
This time, our main app page is not rebuilt. Remember the top-most
MyHomePage widget in our previous stateful widget example.
Who will subscribe to these notifications? The First row widget will listen
to Number Model
According to our design patterns, the controller folder has three related
Widgets that will subscribe to these notifications.
Now we get the changed look of flutter app with the help of provider
package.
On the left side we can see that we have pressed the button 4 times, so we’ve
got 8. And on the right hand side of the screen, we can see how many widgets
get rebuilt.
To make it happen, the second row widget will listen to the name change
model.
1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3 import '../model/name_change_model.dart';
4
5
6 class SecondRowWidget extends StatelessWidget {
7 const SecondRowWidget({
8 Key key,
9 }) : super(key: key);
10
11 @override
12 Widget build(BuildContext context) {
13 return Row(
14 mainAxisSize: MainAxisSize.min,
15 children: [
16 Expanded(
17 child: Padding(
18 padding: EdgeInsets.all(20.0),
19 child: Column(
20 children: [
21 Text('${context.watch<NameChangeModel>().name}'),
22 /**
23 * ElevatedButton(
24 /// this is our another [NumberModel] listener
25 /// read() will fire the event the changes the number
26 /// by adding 2
27 ///
28 onPressed: () =>
29 context.read<NumberModel>().incrementNumberByTwo(),
30 child: Text('Increment'),
31 ),
32 */
33 FloatingActionButton(
34 onPressed: () => context.read<NameChangeModel>().changeName(),
35 tooltip: 'Increment',
36 child: Icon(Icons.add),
37 ), // Th
38 ],
39 ),
40 ),
41 ),
42 ],
43 );
44 }
45 }
The next image will demonstrate how the process of widget rebuilds have
been restricted in a great way.
This time, when we click the button to increment number, only First row
widget gets rebuilt. It is clearly visible in the image.
The third row widget subscribes to the name clear model event.
If you press the third button, the third row widget will listen to the event
declared in clear name model.
Let’s see the last image where we will see that every button-press restricts
the unnecessary widget rebuilds. Scaffold, AppBar widgets have never been
rebuilt, although it happened in the case of stateful widget.
On the right hand side, we can clearly see that Scaffold and AppBar widgets
have never been rebuilt whenever we’ve pressed the button. How many
times we have pressed, that really doesn’t matter. The widget rebuilds are
restricted in great amount.
Since we have one page or screen, we’ll identify the widget by name
FrenchTestFirstView. And the code is like the following.
1 import 'package:flutter/material.dart';
2 import '../controller/first_row_widget.dart';
3 import '../controller/second_row_widget.dart';
4 import '../controller/third_row_widget.dart';
5
6 class FrenchTestFirstView extends StatelessWidget {
7 const FrenchTestFirstView({Key key}) : super(key: key);
8
9 @override
10 Widget build(BuildContext context) {
11 return Center(
12 child: Column(
13 children: [
14 FirstRowWidget(),
15 SecondRowWidget(),
16 ThirdRowWidget(),
17 ],
18 ),
19 );
20 }
21 }
We’ve successfully split the long widget tree into smaller reusable widgets.
And our code is functioning perfectly reducing the widget rebuilds process.
We’ve already seen how to use the most popular flutter package, Provider,
for state management. Now we’re going to learn Riverpod to manage Flutter
state in a better way.
Yes, that’s true. We have shown earlier how Provider state management
package reduces widget-rebuilds.
So, the answer is Riverpod - the response to all the limitations of state
management packages for Dart and Flutter apps.
Moreover, the new state management package, Riverpod, created by the same
person Remi Rousselet, is also easy to maintain, test, and much less error-
prone.
From this list of options, we will always choose one according to our need.
That means, one option is always the best for one specific problem.
We’ll come to that point later. In this chapter. Because it needs a very
detailed introspection.
As an example, when we want to watch or fetch a data from our model class
we can use the simplest Provider from Riverpod package.
Now, we are going to learn how to use the simplest Provider from the
Riverpod package.
Just to make this chapter more interesting we will use our old friend, the
Provider state management package, also. In fact, you can always use two
packages side by side.
An ugly Provider Not Found pops up soemtimes. This error is no more there.
It’s a great freedom indeed.
We’ve not mentioned the version so that the packages can depend on the
latest one available.
Just like our previous Provider chapter, we have used a model class for this
Riverpod starter.
The floating action button below will change the string data below.
For the floating action event part we’ve used our old Provider package, not
Riverpod. Because with the help of Riverpod Provider we will only watch
the value.
Riverpod makes it really simple, because we can return the whole model
class like this:
1 final classTypeProviderModel = Provider<ProviderModel>((ref) {
2 return ProviderModel();
3 });
However, it does not mean that the provided object is now globally
accessible.
Of course, like any other global function we can call it from anywhere. But
we can also restrict the return value by scoping it locally.
It means the Riverpod package uses just one, yes, a single InheritedWidget.
And we should place it above the whole widget tree.
I’m not going to detail how it can store state of all Provider objects. But we
can use them anywhere.
We’ll show you a simpler method in a minute. That will drastically reduce
the boilerplate code.
However, the above method reduces the widget rebuilds more than any other
method. Here the Text widget is only rebuilt.
In the next code snippet we will see how we can pass “ScopedReader
watch” through build() method and drastically reduce the boilerplate code.
You’ll get the full code in this GitHub repository: The Riverpod all code
repository for this book
The first one will use “ScopedReader watch” as I’ve just mentioned.
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'elevated_button_widget.dart';
4 import '../model/any_type_provider_model.dart';
5
6 class ProviderExampleWidget extends ConsumerWidget {
7 const ProviderExampleWidget({Key key}) : super(key: key);
8
9 @override
10 Widget build(BuildContext context, ScopedReader watch) {
11 final littleMonk = watch(classTypeProviderModel);
12 return Column(
13 children: [
14 SizedBox(
15 height: 10.0,
16 ),
17 Padding(
18 padding: const EdgeInsets.all(8.0),
19 child: Text(
20 'Riverpod Provider example where we watch a String member variable'
21 ' that we have passed through a class method.',
22 style: TextStyle(
23 fontSize: 20.0,
24 fontWeight: FontWeight.bold,
25 ),
26 ),
27 ),
28 Center(
29 child: Padding(
30 padding: const EdgeInsets.all(8.0),
31 child: Text(
32 littleMonk.littleMonk,
33 style: TextStyle(fontSize: 30.0),
34 ),
35 ),
36 ),
37 SizedBox(
38 height: 10.0,
39 ),
40 Center(
41 child: Padding(
42 padding: const EdgeInsets.all(8.0),
43 child: Text(
44 littleMonk
45 .fetchName('Now we can pass any data to change above data,'
46 ' and watch it!'),
47 style: TextStyle(fontSize: 30.0),
48 ),
49 ),
50 ),
51 SizedBox(
52 height: 10.0,
53 ),
54 ElevatedButtonWidget(),
55 ],
56 );
57 }
58 }
Now we can watch the Riverpod Provider object in a simpler way than
before.
Now, we can easily access the model class member state like this:
1 Text(
2 littleMonk.littleMonk,
3 style: TextStyle(fontSize: 30.0),
4 ),
We can also change the state by passing a string data like this:
1 Text(
2 littleMonk
3 .fetchName('Now we can pass any data to change above data,'
4 ' and watch it!'),
5 style: TextStyle(fontSize: 30.0),
6 ),
However, we can also change the above provided object state by using our
old Provider package and mix it with the Riverpod.
To do that we have used another custom widget and keep it inside the
controller folder.
1 ElevatedButtonWidget(),
Let us see how we can now use the old Provider package to change the
provided object state by pressing a button.
Yes, that’s true. we have enough freedom to choose from any available
options. We prefer the ChangeNotifierProvider<T> widget and return the
Consumer<T> widgets just like before.
1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3 import '../model/any_type_provider_model.dart';
4
5 class ElevatedButtonWidget extends StatelessWidget {
6 const ElevatedButtonWidget({
7 Key key,
8 }) : super(key: key);
9
10 @override
11 Widget build(BuildContext context) {
12
13 return ChangeNotifierProvider<ProviderModel>(
14 // <--- ChangeNotifierProvider
15 create: (context) => ProviderModel(),
16 child: Column(
17 children: [
18 Container(
19 padding: const EdgeInsets.all(15),
20 color: Colors.blue[200],
21 child: Consumer<ProviderModel>(
22 // <--- Consumer
23 builder: (context, myModel, child) {
24 return FloatingActionButton(
25 onPressed: () => myModel.changeName(),
26 child: Icon(Icons.add),
27 tooltip: 'Change Name',
28 );
29 },
30 ),
31 ),
32 SizedBox(
33 height: 20.0,
34 ),
35 Container(
36 padding: const EdgeInsets.all(15),
37 color: Colors.redAccent,
38 child: Consumer<ProviderModel>(
39 // <--- Consumer
40 builder: (context, myModel, child) {
41 return Text(myModel.littleMonk);
42 },
43 ),
44 ),
45 ],
46 ),
47 );
48 }
49 }
Let’s take a look at the image below, so we can understand how the whole
structure reduces the widget-rebuilds.
Figure 15.2 – Mixing Flutter Riverpod and old Provider package
Riverpod Provider and the old Provider package reduce the widget rebuilds
As we have pressed the button the It changes the state of the provided object
globally. However, as you can see the locally scoped data state has not been
affected at all.
As we’ve tracked the widget rebuilds we can also see that on the right hand
side the statistics show how it benefits our app by reducing widget rebuilds.
When we’ve clicked the button, it only affects our custom
ElevatedButtonWidget() widget.
Before reading this chapter, you must check the Provider part of Riverpod
discussed in the last chapter.
In the last article we have mixed Riverpod and the old Provider package to
have some coding fun. We’ve also checked how we can reduce widget
rebuilds.
If you’re already familiar with the Provider state management package, you
must have used ChangeNotifierProvider already.
The new Riverpod state management package also uses the same
ChangeNotifierProvider, but with a certain flavor, making it more advanced.
So you can associate these two features of Flutter state management quite
easily.
We have seen use caes in our early examples. A model class that extends
ChangeNotifier, can call notifyListeners() any time when the state in that
class changed or updated.
We’ll see this difference in a minute, before that we must take a brief look at
why autodispose is important?
There are multiple reasons for using this autodispose method that
ChangeNotifierProvider in Riverpod comes with.
The main advantage is:
1 The first reason is, when using Firebase, to close the connection and avoid
unnecess\
2 ary cost is really important.
3
4 To reset the state when the user leaves a screen, go to another page and re-
enters i\
5 t.
Since it is a widget, it can have many descendants and we can directly sends
the updated data to the bottom-most widget, without rebuilding the widget
tree.
However, in Riverpod, the whole scenario takes a different route. Not only it
helps us to auto-dispose, but it also simplifies the whole experience.
Now, in future, when we’ll build an E-Commerce Flutter app we’ll apply the
same logic to add items to cart.
Since we have not used the typical model-view-controller pattern this time,
this ChangeNotifierProvider has a large descendants.
Still the right hand side panel shows us how it helps us reduce the widget
rebuilds.
Of course while building app, we would certainly avoid this process. We’ll
make our Flutter app structure more loosely coupled.
It’s our main method, this time we haven’t used the ProviderScope to make it
look simple.
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:riverpod_examples/change-notifier-
provider/change_notifier_provider.\
4 dart';
5 import 'package:riverpod_examples/state-notifier-
provider/state_notifier_provider.da\
6 rt';
7
8 import 'state-provider/view/state_provider_example.dart';
9
10 void main() {
11 runApp(ProviderScope(child: App()));
12 }
13
14 class App extends StatelessWidget {
15 // This widget is the root of your application.
16 @override
17 Widget build(BuildContext context) {
18 return MaterialApp(
19 title: 'Flutter Demo',
20 theme: ThemeData(
21 primarySwatch: Colors.blue,
22 home: HomeOfChangeNotifierProvider(),
23 );
24 }
25 }
Now the syntax becomes much simpler than before. We can easily combine
this object with others.
Like any other provider the ChangeNotifierProvider ensures one thing. The
widget that reflects the updated data only gets recomputed.
1 class HomeOfChangeNotifierProvider extends ConsumerWidget {
2 const HomeOfChangeNotifierProvider({Key key}) : super(key: key);
3
4 @override
5 Widget build(BuildContext context, ScopedReader watch) {
6 final nameChangeNotifierProvider = watch(nameChangeNotifier);
7
8 return Scaffold(
9 appBar: AppBar(
10 title: Text('Riverpod Examples'),
11 ),
12 body: Padding(
13 padding: const EdgeInsets.all(18.0),
14 child: Center(
15 child: ListView.builder(
16 itemBuilder: (context, index) {
17 return Text(
18 nameChangeNotifierProvider.listOfNames[index].toString(),
19 style: TextStyle(
20 fontSize: 30.0,
21 fontWeight: FontWeight.bold,
22 ),
23 );
24 },
25 itemCount: nameChangeNotifierProvider.listOfNames.length,
26 ),
27 ),
28 ),
29 floatingActionButton: FloatingActionButton(
30 onPressed: () => nameChangeNotifierProvider.addNames('Now you can '
31 'add as many names using ChangeNotifierProvider as you want!'),
32 child: Icon(Icons.add),
33 ),
34 );
35 }
36 }
Let’s see how state management becomes much more easier than before with
less boilerplate code.
1 Widget build(BuildContext context, ScopedReader watch) {
2 final nameChangeNotifierProvider = watch(nameChangeNotifier);
3
4 ...
In the next chapter we’ll dig deep into StateProvider in Riverpod, another
Provider variant of Riverpod.
For that you need to know what StateProvider means in AngularJs first. After
that, we will discuss this Provider variant in Flutter Riverpod. Moreover,
we’ll also try to build a Flutter app that will use StateProvider that provides
different types of objects.
Riverpod has many Provider variants. As a result, you can use any one of
them and get the same result. Certainly Provider is the most important
component in Riverpod to manage state and autodispose the state object, all
the same StateProvider has all characteristics we need.
The greatest advantage is of course, it reduces the widget rebuilds. But that
too depends on how you plan the architecture of the app.
The Flutter app, we’re going to build, will three different segments.
In the second segment, as you scroll down the app, you will find four
elevated buttons. The first button in the first row will change the name. The
second button in the first row will change the city.
Just below that row we have second row, where we have again a first button
that changes the name back to the previous one. And the second button in the
second row will change the name and replaces the city name with the
previous name.
They all are String objects, as in Dart everything is an object. The state
object takes the String value, and with the press of buttons, it will change the
state accordingly.
As we scroll down more, we’ll find the third segment where the state object
is a data model class. This class has two states. One is a String member
variable and another is an Integer member variable. We’ll change the state
using class constructor.
We’ll track every widget rebuild so it gives us a clear idea how our app is
working.
In Riverpod we can have two providers expose a state of the same “type”:
We couldn’t do that in package Provider.
To start with we’ll first see how our Riverpod StateProvider app looks like:
I have just described this app above. It looks similar to the description
having three separate segments. As we have used ListView widget as the top
layout widget, so we could have added more below to scroll down safely.
Let us start with the entry point where we have our main method. Then we
will discuss the code piece by piece.
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:riverpod_examples/change-notifier-
provider/change_notifier_provider.\
4 dart';
5 import 'package:riverpod_examples/state-notifier-
provider/state_notifier_provider.da\
6 rt';
7
8 import 'state-provider/view/state_provider_example.dart';
9
10 // import 'provider/controller/provider_example_widget.dart';
11
12 void main() {
13 runApp(ProviderScope(child: App()));
14 }
15
16 class App extends StatelessWidget {
17 // This widget is the root of your application.
18 @override
19 Widget build(BuildContext context) {
20 return MaterialApp(
21 title: 'Flutter Demo',
22 theme: ThemeData(
23 primarySwatch: Colors.blue,
24 ),
25 home: Home(),
26 );
27 }
28 }
29
30 class Home extends StatelessWidget {
31 const Home({Key key}) : super(key: key);
32
33 @override
34 Widget build(BuildContext context) {
35 return Scaffold(
36 appBar: AppBar(
37 title: Text('Riverpod Examples'),
38 ),
39
40 body: StateProviderExample(),
41 );
42 }
43 }
As you can see we have wrapped our Flutter app with ProviderScope().
Next important thing we have done is we’ve used two custom widgets to
structure our app.
Although the widget tree starts with StateProviderExample() widget it
returns a ListView widget, so we can build a UI that user can scroll.
Let us see the immediate descendant layout UI widget that is child to the
StateProviderExample() widget.
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import '../controller/change_name_and_city.dart';
4 import '../controller/clear_name_and_city.dart';
5 import '../controller/counter_widget.dart';
6 import '../controller/raise_and_reduce_age_of_little_monk.dart';
7 import '../controller/watch_name_and_city.dart';
8 import '../model/any_type_provider_model.dart';
9
10 class StateProviderExample extends ConsumerWidget {
11 const StateProviderExample({Key key}) : super(key: key);
12
13 @override
14 Widget build(BuildContext context, ScopedReader watch) {
15 final stateProviderIntegerObject = watch(stateProviderInteger).state;
16 final stateProviderNameObject = watch(stateProviderName).state;
17 final stateProviderCityObject = watch(stateProviderCity).state;
18 final stateProviderInstance = watch(stateProviderClass).state;
19
20 return Padding(
21 padding: EdgeInsets.all(
22 10.0,
23 ),
24 child: ListView(
25 children: [
26 Text(
27 'Providers come in many variants, but they all work the same way.'
28 ' Let us start with a simple counter then doing some more complex staff',
29 style: TextStyle(
30 fontSize: 20.0,
31 ),
32 ),
33 Padding(
34 padding: EdgeInsets.all(
35 8.0,
36 ),
37 child: CounterWidget(
38 stateProviderIntegerObject: stateProviderIntegerObject),
39 ),
40 Space(),
41 WatchNameAndCity(
42 stateProviderNameObject: stateProviderNameObject,
43 stateProviderCityObject: stateProviderCityObject),
44 ChangeNameAndCity(),
45 ClearNameAndCity(),
46 Space(),
47 Text(
48 '${stateProviderInstance.littleMonk}.'
49 ' I am now ${stateProviderInstance.ageOfLittleMonk} years old!',
50 style: TextStyle(
51 fontSize: 30.0,
52 ),
53 ),
54 RaiseAndReduceAgeOfLittleMonk(),
55 ],
56 ),
57 );
58 }
59 }
60
61 class Space extends StatelessWidget {
62 const Space({
63 Key key,
64 }) : super(key: key);
65
66 @override
67 Widget build(BuildContext context) {
68 return SizedBox(
69 height: 10.0,
70 );
71 }
72 }
As I was saying, the layout UI will showcase the controller widgets, and they
come one after another. As a result inside the ListView widget children we
place all controller widgets.
By the way, at the top, we have our first custom widget CounterWidget that
passes one StateProvider object. At any rate that should be an integer, and it
happens to be that.
However, before we check the controller widgets separately, let us check the
data model first.
The data model used in this flutter app, is not complex either. Now as
regards StateProvider objects, we have defined four type of StateProvider
objects that we have discussed earlier.
Mind you, in this data model, we have have two providers that expose a state
of the same “type”, that is String.
1 import 'package:flutter_riverpod/flutter_riverpod.dart';
2
3 final stateProviderInteger = StateProvider<int>((ref) {
4 return 100;
5 });
6
7 /// As opposed to when using package:provider,
8 /// in Riverpod we can have two providers expose a state of the same "type":
9 ///
10
11 final stateProviderName = StateProvider<String>((ref) {
12 return 'John';
13 });
14 final stateProviderCity = StateProvider<String>((ref) {
15 return 'Chicago';
16 });
17
18 class StateProviderModel {
19 String _littleMonk = '';
20 int _ageOfLittleMonk = 0;
21 String get littleMonk => _littleMonk;
22 int get ageOfLittleMonk => _ageOfLittleMonk;
23 StateProviderModel(this._littleMonk, this._ageOfLittleMonk);
24 }
25
26 final stateProviderClass = StateProvider<StateProviderModel>((ref) {
27 return new StateProviderModel('I am a Little Monk', 6);
28 });
We can clearly see that inside each method we have passed BuildConetxt
context so the data flows through that context and we can read the Provider
state.
The provider state object is either integer, or String, or even a data class.
However in each case, the distinction is clear enough.
Because our app logic is clear, it’s become easier for us to visualize what is
going to happen.
Figure 17.2 – Riverpod StateProvider Flutter app where we have
pressed several buttons; however the Scaffold widget has not been
rebuilt. You can track the widget rebuilds on the right hand side.
Now we can take a separate look at each controller widget where we have
used used those earlier defined state properties and methods.
Now it entirely depends on you to which provider to listen and how will you
do that. It also depends on the variant of Provider.There could be multiple
possible values you want to listen to.
Anyway, since we have StateProvider and our first data model is an integer
data object, first we have written it like this:
1 final stateProviderInteger = StateProvider<int>((ref) { return 100; });
Take a look at our custom controller widget CounterWidget that extends the
StatelessWidget and still can update the value.
1 import 'package:flutter/material.dart';
2 import 'useful_read_providers.dart';
3 import 'horizontal_space.dart';
4
5 class CounterWidget extends StatelessWidget {
6 const CounterWidget({
7 Key key,
8 @required this.stateProviderIntegerObject,
9 }) : super(key: key);
10
11 final int stateProviderIntegerObject;
12
13 @override
14 Widget build(BuildContext context) {
15 return Row(
16 children: [
17 Text(
18 '${stateProviderIntegerObject.toString()}',
19 style: TextStyle(
20 fontSize: 30.0,
21 ),
22 ),
23 HorizontalSpace(),
24 FloatingActionButton(
25 onPressed: () => increment(context),
26 tooltip: 'Increment',
27 child: Icon(Icons.add),
28 ),
29 HorizontalSpace(),
30 Container(
31 padding: EdgeInsets.all(
32 8.0,
33 ),
34 child: Text(
35 'Clicking the button \n will increase \n the number by 1',
36 style: TextStyle(
37 fontSize: 15.0,
38 fontWeight: FontWeight.bold,
39 ),
40 ),
41 ),
42 ],
43 );
44 }
45 }
The second one, that is the read() part is even simpler than the watch(). Is it
true? Look at the one line of code that we used in floating action button.
1 onPressed: () => increment(context),
Now whatever time you press the floating action button to increment the
counter value, it won’t affect the parent widgets of the tree. So in all cases,
the Scaffold() never gets rebuilt.
By and large other controller widgets follow the same rule. Because we want
multiple controller widgets to handle the changing the name and city, we have
placed them in separate widgets.
However, at the same time, we want to get back the old name and city. So we
need to have another controller widget for that.
1 import 'package:flutter/material.dart';
2 import 'horizontal_space.dart';
3 import 'useful_read_providers.dart';
4
5 class ClearNameAndCity extends StatelessWidget {
6 const ClearNameAndCity({
7 Key key,
8 }) : super(key: key);
9
10 @override
11 Widget build(BuildContext context) {
12 return Padding(
13 padding: EdgeInsets.all(
14 8.0,
15 ),
16 child: Row(
17 children: [
18 ElevatedButton(
19 onPressed: () => clearName(context),
20 child: Text(
21 'Change name',
22 style: TextStyle(
23 fontSize: 20.0,
24 ),
25 ),
26 ),
27 HorizontalSpace(),
28 ElevatedButton(
29 onPressed: () => clearCity(context),
30 child: Text(
31 'Change City',
32 style: TextStyle(
33 fontSize: 20.0,
34 ),
35 ),
36 ),
37 ],
38 ),
39 );
40 }
41 }
On top of that, at the same time we need to place the name and city side by
side through another custom widget. As a result, one button-press changes the
String data object.
Certainly for the absolute beginners this process of breaking down the whole
app architecture seems daunting, but it is necessary. Moreover, it makes our
code testable, produces less boilerplate code.
Now as regards the less boilerplate, the Riverpod state management makes a
huge difference.
Why so? Because now we can place our Provider watch() and read() in our
controller folder and call them as necessary.
In our data model we have defined the Provider state object as a class.
1 class StateProviderModel {
2 String _littleMonk = '';
3 int _ageOfLittleMonk = 0;
4 String get littleMonk => _littleMonk;
5 int get ageOfLittleMonk => _ageOfLittleMonk;
6 StateProviderModel(this._littleMonk, this._ageOfLittleMonk);
7 }
8
9 final stateProviderClass = StateProvider<StateProviderModel>((ref) {
10 return new StateProviderModel('I am a Little Monk', 6);
11 });
Consequently our task becomes easier now. And as a result, we can raise the
age of little monk and reverse it back with an updated text message.
Firstly, the context plays the key role throughout this app. Secondly we need
to pass that context through the method we’ve defined.
No doubt, not only it reduces the widget rebuilds, but at the same time, it also
makes less boilerplate.
It’s a good practice that you break your spaghetti code in separate modules.
However, sometimes it may backfire. Consequently it unnecessarily rebuilds
one single widget again and again. It not only consumes memory, but it also
slows down the app’s performance.
On top of that, it does not make your flutter app performant enough.
We need to track the widget rebuilds while building our app. That is the first
step. The second most important step is to rectify the fault.
For that reason we could have avoided the following extraction of a separate
widget:
1 import 'package:flutter/material.dart';
2
3 class HorizontalSpace extends StatelessWidget {
4 const HorizontalSpace({
5 Key key,
6 }) : super(key: key);
7
8 @override
9 Widget build(BuildContext context) {
10 return VerticalDivider(
11 thickness: 2.0,
12 color: Colors.red,
13 );
14 }
15 }
We’ll come to that point and discuss the core concepts in detail in the later
part of this chapter.
In such cases, the subscribers listen to the notification and update the state
accordingly.
However, in the first part of this chapter we keep things very simple so you
can have an idea about how StateNotifierProvider works with StateNotifier.
Either you can place this data model in a separate model folder or you can
use it with the main UI layout.
Whatever we do, when we run this simple app, it looks like this:
Figure 18.1 – A simple flutter app using StateNotifierProvider in
Riverpod
As we press the floating action button, the String text is getting added one
after another, just like the below image.
Now, we can track the widget rebuilds and watch the progress bar. We have
pressed the button seven times. As a result, the Text widget that displays the
provided String data object has been rebuilt 21 times.
Since we have placed everything under Scaffold() that widget was also
rebuilt 7 times.
Now we can run the app and add as many items as we want.
The advantage of Riverpod is it gives you enough freedom to choose from
many options. If you compare them it looks like a difficult task.
However, keep one thing in mind, you will always choose the best Provider
variant according to the nature of your app that you’re going to build.
However, they are not exactly the same. Although they depend on each other.
You may think of State Notifier as the Flutter independent state management
component.
While State Notifier entirely revolves around one single property state, State
Notifier Provider manages to provide that state.
State Notifier is just like mutable ChangeNotifier, which acts like Flutter
default ValueNotifier.
With their help any widget can listen to the state, however Provider has to to
provide that state.
Now StateNotifier can either operate with a single primitive data type, like
integer; or, it can handle a very complex user-defined data type.
If the bottom-most widget has an expensive widget tree on the top and still it
wants to listen to the state, two things can happen.
When we press a button to listen to the state in the bottom-most widget, the
whole widget tree gets rebuilt. And that will be worst scenario. It happens in
the case of stateful widget.
Of course, the best solution is something else. In that case, only the bottom-
most widget gets rebuilt and everything on the top remains like before.
We’re going to show it in a minute. Moreover, you can take a look at the
image above where I have tried to demonstrate the whole app structure.
On the left hand side, we can see the app. And, on the right hand side, we can
track the widget rebuilds.
Despite the fact that the app looks very simple, in reality the structure is not
that simple.
A simple data model class that extends StateNotifier looks like this:
1 class NameNotifier extends StateNotifier<String> {
2 NameNotifier() : super('StateNotifier');
3
4 void addNames(String names) {
5 state = names;
6 }
7
8 void updateNames(String names) {
9 state = names;
10 }
11 }
12
13 final nameNotifierProvider = StateNotifierProvider<NameNotifier>((ref) {
14 return NameNotifier();
15 });
In addition to the simplicity of the nature of the state here (it is String),
StateNotifierProvider also acts in a simple way. It returns the state as a
reference.
Now, any widget, anywhere in the widget tree, can listen to it.
As we have two data models in model folders, we can think about creating
the layout UI now. Next, we should place them in separate folders.
After that we will press every button to see how it affects the top widgets.
Besides, we’ll track the widget rebuilds of child widgets where we’re
listening to the state.
Figure 18.4 - Flutter app uses StateNotifierProvider and reduces widget
rebuilds
The right hand side Flutter Performance statistics shows us how only child
widgets has got rebuilt. On the other hand, this state changing process has not
touched the top widgets.
The main method calls a runApp() method and passes our App(), which
returns HomeCartNotifier().
Because we don’t want to rebuild the top widgets when the bottom-most
widgets listen to the state.
Although the An Expensive Widget makes a long widget tree and a child of
Scaffold widget, when we change the state, they will not be rebuilt.
However, we need to be aware of not using this advantage too much in one
single screen.
Instead we can use multiple screens to pass state.
Even though for this example we use one single screen, this is not the best
practice. Well, let us see An Expensive Widget tree.
1 class AnExpensiveWidget extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 return Padding(
5 padding: EdgeInsets.all(
6 10.0,
7 ),
8 child: ListView(
9 children: [
10 Text(
11 'We can place any expensive widget tree here!',
12 style: Theme.of(context).textTheme.headline5,
13 ),
14 SizedBox(
15 height: 10.0,
16 ),
17 Text(
18 'One example of State Notifier Provider'
19 ' can be placed here',
20 style: Theme.of(context).textTheme.headline6,
21 ),
22 SizedBox(
23 height: 10.0,
24 ),
25 class AnExpensiveWidget extends StatelessWidget {
26 @override
27 Widget build(BuildContext context) {
28 return Padding(
29 padding: EdgeInsets.all(
30 10.0,
31 ),
32 child: ListView(
33 children: [
34 Text(
35 'We can place any expensive widget tree here!',
36 style: Theme.of(context).textTheme.headline5,
37 ),
38 SizedBox(
39 height: 10.0,
40 ),
41 Text(
42 'One example of State Notifier Provider'
43 ' can be placed here',
44 style: Theme.of(context).textTheme.headline6,
45 ),
46 SizedBox(
47 height: 10.0,
48 ),
49 HomeStateNotifierProvider(),
50 // HomeStateNotifierProvider(),
51 Text(
52 'Another example of State Notifier Provider'
53 ' given below!',
54 style: Theme.of(context).textTheme.headline6,
55 ),
56 SizedBox(
57 height: 10.0,
58 ),
59 CartNotifierProvider(),
60 ],
61 ),
62 );
63 }
64 }
65 ,
66 Text(
67 'Another example of State Notifier Provider'
68 ' given below!',
69 style: Theme.of(context).textTheme.headline6,
70 ),
71 SizedBox(
72 height: 10.0,
73 ),
74 CartNotifierProvider(),
75 ],
76 ),
77 );
78 }
79 }
As we progress we will see how these two child widgets only get rebuilt.
And, as a result, that widget rebuilds will not affect the higher widgets.
As you can see, we’ve used two floating action buttons. One for adding
names. And the other for updating the names.
But there is only one change to make our code readable. Hence we use
another custom widget that controls the operation.
1 class CartNotifierProvider extends ConsumerWidget {
2 const CartNotifierProvider({Key key}) : super(key: key);
3
4 @override
5 Widget build(BuildContext context, ScopedReader watch) {
6 final cartStateNotifierProvider = watch(itemNotifier.state);
7 return BodyWidget(cartStateNotifierProvider: cartStateNotifierProvider);
8 }
9 }
When the return part of the above code is important you can guess we should
keep that custom Body Widget inside the controller folder.
However, that enhances the readability of our code. And to maintain that
readability we should document by adding comments at the appropriate
position.
While we have done so many things to reduce widget rebuilds, has that
worked really? Well, we have proof in our hand.
Since we have tracked the widget rebuilds at each step, we have solid
evidence.
Look here at the below image, where it clearly shows the number of rebuilds
of the Scaffold widget.
Remember, the same is true for the topmost widget App. Besides, it is also
true for every descendant widgets that do not listen to the state.
Figure 18.5 - The topmost parent widget and its descendant widgets are
not rebuilt as state changes
In this chapter, we’re going to see how we can successfully migrate from old
Riverpod version to the newest version.
As regards to this tutorial, you may get the full code in this GitHub repository
mentioned in the last chapter. And besides, if you want to dig deep into the
flutter state management, you may read all updated Flutter articles in my
website. For more Flutter related Articles and Resources
By the way, the above mentioned flutter app using Riverpod was built using
Riverpod ^0.13.0. and that is not working anymore.
All right, now we are ready to migrate and it looks like the following
screenshot.
Next, we need to migrate from old Riverpod to the new, by issuing the
following command.
1 riverpod migrate
Subsequently, we can click any button to change the provider value or you
may say the state of the app.
Figure 19.4 – Riverpod flutter app
working after migration
With reference to the above error, let us know a few important facts about
hash code.
A hash code is a single integer which represents the state of the object. Not
only that, it also affects operator == comparisons.
Now, each Object has unique identity, and the single integer hash code
implements that. Now equality of two objects depends on default operator ==
implementation. Two objects are equal if and only if they are identical.
While using Riverpod state management, we must keep one thing in mind.
Why?
Unequal objects may have the same hash code, but if it happens too often,
clashes occur and the efficiency reduces affecting the data structures, such as
HashSet or HashMap.
According to the creator of both packages, Remi Rousselet, and other flutter
developers as well, Provider has had some limitations. Riverpod has
overcome those limitations.
Both the state management packages have similarity and also has many
differences.
Provider can create, observe and dispose state without rebuilding widgets. It
makes objects visible in Flutter’s devtool. Testing and composing is not
difficult. Since the data flow is unidirectional, the app is scalable.
These are all advantages that has made provider package so popular.
From now on, all the providers we’ll create, the ProviderScope will store
the state of them.
The new concept in the latest Riverpod is the ref object of type WidgetRef.
What is a WidgetRef?
In the latest Riverpod, we can watch or read provider’s value by using this
ref object.
When we use Consumer or ConsumerState, the WidgetRef object is available
as an argument, and it can interact with any provider.
Because all Riverpod providers are global so that we can access them
easily. As a result, we can handle state management logic outside the widget
tree.
We need to remember that Provider does not let us change the value. To do
that we need to create a StateProvider. It will be a global value.
1 final referenceValue = StateProvider((ref) => 0);
Now, we can watch and read the provider’s value quite easily.
1 import 'package:flutter/material.dart';
2
3 import 'package:flutter_riverpod/flutter_riverpod.dart';
4
5 class ProviderAppSample extends StatelessWidget {
6 const ProviderAppSample({Key? key}) : super(key: key);
7
8 @override
9 Widget build(BuildContext context) {
10 return const MaterialApp(
11 home: ProviderHome(),
12 );
13 }
14 }
15
16 final referenceValue = StateProvider((ref) => 0);
17
18 class ProviderHome extends ConsumerWidget {
19 const ProviderHome({Key? key}) : super(key: key);
20
21 @override
22 Widget build(BuildContext context, WidgetRef ref) {
23 final counterWatch = ref.watch(referenceValue);
24 final counterRead = ref.read(referenceValue);
25 return Scaffold(
26 body: Center(
27 child: Column(
28 children: [
29 Container(
30 margin: const EdgeInsets.all(20),
31 padding: const EdgeInsets.all(20),
32 child: Text(
33 '${counterWatch.state}',
34 style: const TextStyle(
35 fontSize: 100,
36 fontFamily: 'Allison',
37 fontWeight: FontWeight.bold,
38 color: Colors.red,
39 ),
40 ),
41 ),
42 const SizedBox(
43 height: 20,
44 ),
45 ElevatedButton(
46 onPressed: () => counterRead.state++,
47 child: const Text(
48 'Press to Increment',
49 style: TextStyle(
50 fontSize: 30,
51 color: Colors.white,
52 ),
53 ),
54 ),
55 ],
56 ),
57 ),
58 );
59 }
60 }
And after that, we can either watch the change, and implement the change.
1 child: Text(
2 '${counterWatch.state}',
3 ...
4 ElevatedButton(
5 onPressed: () => counterRead.state++,
6 ...
Therefore you may read the updated flutter articles there. For more Flutter
related Articles and Resources
The above counter displays a number 0. From where that number has come,
we’ll see in our code in a minute.
Now, as we press the floating action button below, the state of the app
changes and we get the next number 1.
Broadly speaking, it’s a very simple app and usually comes by default when
we create a flutter app. However, that’s a stateful widget that manages state
of the app automatically.
To tell the truth, we don’t have any stateful widget in place. But we’re
managing state with the help of Riverpod package.
Then we have a simple counter data model class like the following one.
1 import 'package:flutter_riverpod/flutter_riverpod.dart';
2
3 class Countering extends StateNotifier<int> {
4 Countering() : super(0);
5
6 void increment() => state++;
7 }
The number 0 in the above screenshot comes from the initial state that we’ve
mentioned in the constructor.
Now, we can use the following code to get the state of the app.
1 final counter = ref.watch(counterProvider);
Every Flutter Application is different, but you can solve most state
management riddles with any single Provider variant of Riverpod package.
I have used more than one code repositories for this book.