Whenever you are creating an application, let's say a counter application, you want to ensure that the data remains consistent even when the app's configuration changes. What does "configuration" mean? Configuration refers to events such as navigating to another screen or tilting your phone, causing the widgets to rebuild themselves. In such cases, we want to ensure that the data persists and doesn’t get erased.
How do we achieve this? There are two options:
Local State Management Solutions:
This involves managing state for each particular screen. Using the analogy of Uncle Bob, we have an Uncle whose job is to keep the data intact even though the configuration changes. You can have a separate Uncle for each screen, each responsible for managing its own data.Global State Management Solutions:
Instead of separate Uncles, we have one main Uncle who manages the state for the entire app. This is called global state management. Some examples of state management solutions for global state management include Provider, BLoC, Riverpod, and context.
For local state management, we typically use stateful widgets, which manage state within a specific scope. For global state management, we use solutions like inherited widgets. Understanding inherited widgets at their core helps you learn how state management solutions work internally.
In addition, we have tools like Provider
and ChangeNotifierProvider
, which build upon inherited widgets to provide better state management capabilities. These tools allow you to maintain and update state efficiently, as you’ve seen in the previous explanation.
Here's a simple example of a StatefulWidget in Flutter:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterWidget(),
);
}
}
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
In this example, CounterWidget
is a StatefulWidget that maintains a counter. The _CounterWidgetState
class holds the state, which includes the _counter
variable. The setState
method is used to update the counter and rebuild the UI whenever the button is pressed.you might wonder why does the setState takes fucntion as an parameter setState(() { } ) . Assume you have a you have a cook he makes you a pasta dish and gives it to you , you see that it has lesser salt so you say 1. add more salt 2. add more cheese . the cook then takes your intructions and executes it adds salt and more cheese and brings back the dish and gives it to you .
similarly we need to tell the setState a set of instructions similarly to how we said to the cook and in programming a set of intructions are also called fucntion . so we pass a fucntion to the setState it executes the function and re-builds the ui similary to how the cook then brings the dish back the only catch here is unlike the cook the setState build the dish again as widgets in flutter immutable so they cant be chnage we need to build them again using the updated the data .
Inherited Widget
here assume you have an uncle that remmbers things for you and reminds you about them whever you ask him . The uncle lets call him bob also has a special magic in him . Lets say you and your borother has a piggy bank and whever someone give money to him he adds (updates) it in your piggy bank .
Uncle Bob can only remmber one person so instead of remmbering you and your bother uncle bob can remmber your dad → so he will update all your dad’s children.
Now lets see it in example :
Creating uncle bob :
class UncleBob extends InheritedWidget {
var child1_money = 0;
var child2_money = 0 ;
UncleBob({super.key, required this.child1_money , required this.child2_money , required super.child});
@override
bool updateShouldNotify(UncleBob oldWidget) => oldWidget.child2_money != child2_money || oldWidget.child1_money != child1_money ;
static Uncle takes(BuildContext context) {
var result = context.dependOnInheritedWidgetOfExactType<Uncle>();
return result!;
}
}
Now lets see in detail how would you create uncle bob :
Uncle bob is a type of uncle ( InheritedWidget ) so we do this by extending the uncleBob to InheritedWidget.
class UncleBob extends InheritedWidget
Now we have to say what things uncle bob will remmber and whoes child will uncle bob remind / update it to . We do this by passing the values and the child Widget in the constructor
var child1_money = 0;
var child2_money = 0 ;
UncleBob({super.key, required this.child1_money , required this.child2_money , required super.child});
Next we need to explain the uncle bob when should he notify us about the update in our money . Should he tell when child1 gets money or child2 gets money or both .
Here we take uncleBob as a prameter and check if money uncle bobs had has chnaged then the money uncle bob has right now ! if yes then notify us
@override
bool updateShouldNotify(UncleBob oldWidget) => oldWidget.child2_money != child2_money
|| oldWidget.child1_money != child1_money ;
But heres the catch what if you had a cousion and he also wanted to how much money you guys are making and maybe take some of it . You cant allow it as anyone can ask uncle bob . We should say to uncle bob that only childrens who are depended on you (Uncle bob [Inherited Widget] ) should get to know it . Here unlce bob takes your information ( who is your father ) which is called “context” .
see in the code using your context uncle bob can see if you depend on him by using context.dependOnInheritedWidgetOfExactType<Uncle>();
if you are depending on him then you can asccess the data or else not .
why “static” ?
remember some childrens will not depend on any or a specifc uncle so making the the takes ( or “of” which is usually used ) ensures that even if some child tries to calls the uncle that it doesn’t depends on it doesnt throws any error .
static Uncle takes(BuildContext context) {
var result = context.dependOnInheritedWidgetOfExactType<Uncle>();
return result!;
}
Accessing Uncle bob
as we discussed above anyone can give uncle bob their information ( context ) and access the data .
Uncle takes
the context
and you can access the data if you dont depend on him then it will return null .
Note : in practice the .takes(context) is replaced with .of(context)
var child1= Uncle.takes(context).child1_money ;
var child2 = Uncle.takes(context).child2_money ;
ChangeNotifier Provider
The reason we use ChangeNotifierProvider
is due to inherited widgets. Inherited widgets store data and notify their children who are listening to them when the data changes, rebuilding the widgets that use that data. This is the core role of an inherited widget.
We use ChangeNotifierProvider
because if we consider Uncle Bob as a person in the inherited widget analogy, Uncle Bob's job is only to store the particular data and notify when the data has changed. However, if I want Uncle Bob to perform a specific task and inform us when the task has been completed, that's not what inherited widgets are made for. This is why we use Provider.
To notify, we use ChangeNotifier
. We can create a new "Uncle" that extends ChangeNotifier
. This Uncle's job is simply to notify us whenever something happens. We can instruct this Uncle to notify at a particular event using notifyListeners
. Here's the line of code where Uncle Bob extends ChangeNotifier
:
class UncleBob extends ChangeNotifier {
String taskStatus = "Not completed";
void completeTask() {
taskStatus = "Completed";
notifyListeners(); // Notify listeners about the change
}
}
The main Uncle, which is Uncle Bob, is represented by ChangeNotifierProvider
. Similar to how inherited widgets work, we pass the ChangeNotifier
instance inside the create
method. Uncle Bob will handle the changes and notify us. The descendants can then use context.read
, context.watch
, or context.select
to interact with the data.
Here are examples of watching, reading, and selecting data:
1. Watching the data
When you watch the data, the widget rebuilds automatically when the data changes. You use context.watch
to listen to changes.
Widget build(BuildContext context) {
// Watch the taskStatus data
String status = context.watch<UncleBob>().taskStatus;
return Text('Task status: $status');
}
2. Reading the data
Reading the data does not rebuild the widget. It simply allows you to access the current value, but it will not trigger a rebuild. You use context.read
to get the data.
Widget build(BuildContext context) {
// Read the taskStatus data without rebuilding the widget
String status = context.read<UncleBob>().taskStatus;
return ElevatedButton(
onPressed: () {
context.read<UncleBob>().completeTask(); // Trigger a task completion
},
child: Text('Complete Task'),
);
}
3. Selecting specific data
Selecting data allows you to rebuild only when a specific part of the data changes. You use context.select
to select a specific value from the ChangeNotifier
.
Widget build(BuildContext context) {
// Select a specific piece of data to rebuild only when it changes
String status = context.select((UncleBob uncle) => uncle.taskStatus);
return Text('Task status: $status');
}
Watching the data: When watching a particular data, Uncle Bob informs us whenever the data changes. This automatically rebuilds the widgets that depend on the data.
Reading the data: Reading data will not rebuild any widgets. It is used to call a function or modify a particular data without triggering a rebuild.
Selecting data: Using
select
ensures that widgets are rebuilt only for a specific piece of data when it changes. If the selected data doesn't change, the widget will not rebuild.
Thank you for taking the time to read through this article 🙏. I hope it helped clarify the concepts of local and global state management in Flutter, as well as how tools like ChangeNotifierProvider
and InheritedWidget
can be leveraged for efficient state handling ⚡. Understanding these fundamentals will not only improve your app’s performance 🚀 but also make state management easier to handle as your projects grow 💡. Happy coding! 👨💻👩💻