Throughout my years of building apps with Flutter, one of the things I learned is how to manage state in an app. I wanted to write an article that takes a comprehensive look at state management and discusses some of the approaches Flutter offers.
What is a state
Technically the state of an app is all the objects it uses to display its UI and manage resource; it’s everything that exist in memory when the app is running. However, when talking about designing an app, state is the data you need in order to rebuild the UI of the app at any moment in time. State management is the process of organizing the app to access this data between screens and across the app. In Flutter there are two kinds of states: ephemeral state and app state (aka shared state).
Ephemeral state
Ephemeral state refers to a state that can be contained in a single widget and is rarely accessed by other parts of the widget tree. To derive an example from the previous blog post, an ephemeral state would be the selectedIndex for the BottomNavigationBar widget. Another example would be the progress of a loading animation.
In these cases, we don’t need to use any state management technique; We can just contain a field in a StatefulWidget to access the data and use setState() to update the UI.
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State createState() => _HomeScreenState();
}
class _HomeScreenState extends State {
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Screen'),
),
body: const SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('This is the home screen'),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
BottomNavigationBarItem(icon: Icon(Icons.chat), label: 'Chat')
],
currentIndex: selectedIndex, // New
onTap: (int index) {
setState(() {
selectedIndex = index; // New
});
},
),
);
}
}
App state
Application state refers to data that is shared across many parts of an app and/or kept between user sessions. Examples of app state include the dark/light mode setting of an app, login information of a user, and list of saved posts in a social medial app like Instagram.
App state is usually when state management techniques are need. Which state management technique is used depends on the complexity and design of your app.
State Management Options
Flutter offers many state management approaches. We are going to briefly discuss some of the commonly used options.
InheritedWidget
In the example app we created from the navigation blog post we shared data between screens by passing them through the widget’s constructor. While this approach works when an app just has a few screens, it can cause a lot of boilerplate and is generally not good practice for bigger, more complex app. So a solution Flutter came up with is InheritedWidget.
InheritedWidget gives us an effective way to host data in a parent widget so child widgets can access said data without having to receiving them through their constructors. Let’s take a look at how it works.
import 'package:flutter/material.dart';
class ParentWidget extends InheritedWidget {
const ParentWidget({
super.key,
required this.data,
required super.child,
});
final String data;
static ParentWidget of(BuildContext context) {
// This method looks for the nearest ParentWidget widget ancestor.
final result = context.dependOnInheritedWidgetOfExactType<ParentWidget>();
assert(result != null, 'No ParentWidget found in context');
return result!;
}
@override
// This method should return true if the old widget's data is different
// from this widget's data. If true, any widgets that depend on this widget
// by calling `of()` will be re-built.
bool updateShouldNotify(ParentWidget oldWidget) => data != oldWidget.data;
}
In the code above the ParentWidget is an InheritedWidget with the data field. We created the static method of() which implements dependOnInheritedWidgetOfExactType().
As the code below shows, a call to the of() method creates a dependency of type ParentWidget. When the current data value is not equal to the old value the updateShouldNotify is triggered, which causes the widget making the call to rebuild with the new data.
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});
@override
Widget build(BuildContext context) {
var data = ParentWidget.of(context).data;
return Scaffold(
body: Center(
child: Text(data),
),
);
}
}
Listenables
Another option to share state in your app is to use listenables. Listenable is an abstract class that updates one or more listeners. Two classes that implement Listenable are ChangeNotifier and ValueNotifier.
when ChangeNotifier is extended, the class extending it can call the method notifyListeners() if it needs to notify listeners. See example below.
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
We can subscribe to CounterNotifier by creating an object and passing it to the ListenableBuilder widget.
Column(
children: [
ListenableBuilder(
listenable: counterNotifier,
builder: (context, child) {
return Text('counter: ${counterNotifier.count}');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.increment();
},
),
],
)
ValueNotifier is a simpler extension of ChangeNotifier, which stores a single value. We can implement a counter notifier using the ValueNotifier.
ValueNotifier<int> counterNotifier = ValueNotifier(0);
Since ValueNotifier extends ChangeNotifier, we can use ListenableBuilder to subscribe to the counterNotifier. However ValueNotifier has its own listenable builder ValueListenableBuilder, which provides the value.
Column(
children: [
ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, child, value) {
return Text('counter: $value');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.value++;
},
),
],
)
Provider
The Provider package is another commonly used state management technique for app state. It is basically a wrapper around InheritedWidget. It helps to greatly reduce boilerplate, increase scalability for listenables, simplifies allocation/disposal of resources.
It is a great option for simple apps, and is easy to understand for for people who are new to Flutter and don’t have reason to use more complex approach approach.
There are three widgets you need to understand when using the Provider package: ChangeNotifier (which we have talked about in the listenables section above), ChangeNotifierProvider, and Consumer.
ChangeNotifier
ChangeNotifier can be used with Provider to encapsulate an app state. So you would create a class that extends ChangeNotifier, and instead of ListenableBuilder, you can use Provider to create a dependency and subscribe to the class.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});
@override
Widget build(BuildContext context) {
var counter = Provider.of<CounterNotifier>(context);
return Scaffold(
body: Center(
child: Text(counter.value),
),
);
}
}
ChangeNotifierProvider
Coming from the Provider package, ChangeNotifierProvider is a widget that provides an instance a ChangeNotifier to its children. The best way to use this is to put this widget right above widgets that need it in the widget tree.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterNotifier(),
child: const ChildWidget(),
),
);
}
create is a property that creates a new instance of the ChangeNotifier and rebuilds it when necessary.
Consumer
After putting ChangeNotifierProvider above the part of the widget tree that need it, we can access the app state from the ChangeNotifier through Consumer, another widget that comes with the Provider package
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<CounterNotifier>(
builder: (context, counter, child) {
return Scaffold(
body: Center(
child: Text(counter.count),
),
);
},
);
}
}
When using Consumer we have to specify the type of ChangeNotifier we want to access, which in the example above is CounterNotifier, or else the Provider package won’t know what you want to access.
The builder property is a function that gets triggered every time notifyListeners() is called in our ChangeNotifier function. It comes with an instance of BuildContext, an instance of the ChangeNotifier, and a child widget. The child widget is the widget subtree under the Consumer widget.
Riverpod
For most applications today managing asynchronous data is an integral part of the design. While Provider is a great option to begin exploring state management, it comes with some limitations. A solution to this is Riverpod
An anagram of the word Provider, Riverpod is a Flutter package that was built on top of the Provider package to address some of Provider’s limitations. It’s a robust state management library that provides a lightweight and flexible way to handle state and dependency injection.
Riverpod takes care of most of the app’s logic, performs network-requests with built-in error handling and caching, and automatically re-fetches data when necessary.
Another advantage that Riverpod has over Provider is that it doesn’t rely on BuildContext as much. You can define providers outside of the widget tree. This allows for separation of business logic from the rest of the app and gives access to the created providers anywhere in the app.
There are a lot of aspects of Riverpod we could explore in this article, but for the sake of brevity, you can checkout riverpod’s website for details on how to use it in your app.
Bloc
Business Logic Component, or Bloc, is a state management package and pattern in Flutter that separates the business logic of an app from it’s User Interface. It’s usually the recommended and preferred approach for big, complex production apps since it makes the code more unambiguous and easy to understand, scalable, and testable.
However using Bloc might create more boilerplate code, and it might take a second to learn how implement it.
We will learn how to implement Bloc in a different article. But you can also check
Conclusion
Wow this was a long article! Thank you for making it to the end
We discussed what state is, the two types of states, what state management is, and different state management solutions.
Feel free to leave any comment or question you have in the comment section, or reach out to me in the contact section.