In this article, we will provide a general overview of state management and then delve into some of the most interesting state management solutions, that includes Provider, Riverpod, and BLoC
State management is a complex topic.
As you explore Flutter, there comes a time when you need to share application state between screens throughout your app. There are numerous approaches you can consider.
Points to consider before choosing an approach.
By using provider instead of manually writing InheritedWidget, you get:
You can execute the following command to install Provider:
flutter pub add provider
When using Provider, you need to comprehend three concepts: ChangeNotifier, ChangeNotifierProvider, and Consumer.
ChangeNotifier is a straightforward class provided by the Flutter SDK that offers change notifications to its listeners. In simpler terms, if something is a ChangeNotifier, you can subscribe to receive notifications about its changes.
In Provider, ChangeNotifier is a means to encapsulate your application state. For uncomplicated apps, a single ChangeNotifier may suffice. However, in more complex applications, you may have multiple models, resulting in several ChangeNotifiers.
The only code specific to ChangeNotifier is the invocation of notifyListeners(). Employ this method whenever the model undergoes changes that could impact your app’s UI. All other elements in CartModel pertain to the model itself and its associated business logic.
Here’s an example of our DataProvider:
class Counter with ChangeNotifier, DiagnosticableTreeMixin {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
/// Makes `Counter` readable inside the devtools by listing all of its properties
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('count', count));
}
}
ChangeNotifierProvider is a widget that furnishes its descendants with an instance of a ChangeNotifier. This widget is part of the provider package.
Placing ChangeNotifierProvider is straightforward; it should be positioned above the widgets requiring access to it.
In the given example, we have a ProviderPage containing a ChangeNotifierProvider
void main() {
runApp(
/// Providers are above [MyApp] instead of inside it, so that tests
/// can use [MyApp] while mocking the providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
A recommended practice is to position your Consumer widgets as deep in the widget tree as feasible. This approach avoids the need to rebuild substantial segments of the UI due to minor changes in specific details.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('You have pushed the button this many times:'),
/// Extracted as a separate widget for performance optimization.
/// As a separate widget, it will rebuild independently from [MyHomePage].
///
/// This is totally optional (and rarely needed).
/// Similarly, we could also use [Consumer] or [Selector].
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),
/// Calls `context.read` instead of `context.watch` so that it does not rebuild
/// when [Counter] changes.
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class Count extends StatelessWidget {
const Count({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
/// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headlineMedium,
);
}
}
Riverpod works in a similar fashion to Provider. It offers compile safety and testing without depending on the Flutter SDK.
You can execute the following command to install Riverpod:
flutter pub add flutter_riverpod
Providers are the most important part of a Riverpod application. A provider is an object that encapsulates a piece of state and allows listening to that state.
final counterProvider = StateNotifierProvider((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter(): super(0);
void increment() => state++;
}
We need to encapsulate the entire application within a “ProviderScope” widget. This widget serves as the container for our providers’ state.
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire project
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
class Home extends ConsumerWidget {
const Home({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('${ref.watch(counterProvider)}'),
),
floatingActionButton: FloatingActionButton(
// The read method is a utility to read a provider without listening to it
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
There are three primary usages for “ref”:
Using the bloc library allows us to separate our application into three layers:
You can execute the following command to install BLoC:
flutter pub add flutter_bloc
In summary, BLoC architecture offers a structured, efficient, and scalable way to manage your app’s business logic and state, contributing to a more maintainable and robust Flutter application.
Let’s explore how to utilise BlocProvider for supplying a CounterBloc to a CounterPage, and how to respond to state changes using BlocBuilder.
counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}
}
main.dart
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
counter_page.dart
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()),
),
),
],
),
);
}
}
At this point we have successfully separated our presentational layer from our business logic layer. Notice that the CounterPage widget knows nothing about what happens when a user taps the buttons. The widget simply tells the CounterBloc that the user has pressed either the increment or decrement button.
I hope this explanation / blog guides you in correct usage and implementation of State management in your Flutter App.
Hey, having any query? Our experts are just one click away to respond you.
Contact Us