Introduction
State management in Flutter is a way to manage the data that changes over time and affects the UI of the application. It’s all about managing the state of your app in a predictable way.
There are many ways to manage state in Flutter, and the best choice depends on the needs of the project and the team’s familiarity with the tools.
Provider: It is a mixture between dependency injection (DI) and state management, built with widgets for widgets. It’s simple to understand and it doesn’t use much boilerplate code and is recommended by the Flutter team.
Riverpod: This is a newer state management solution that aims to overcome some limitations of Provider. It’s more flexible and safe than Provider, but it has a steeper learning curve.
Bloc / Cubit: These are advanced state management libraries that use streams. They separate business logic from UI and make it easy to test. Bloc stands for Business Logic Component.Cubit is a version of Bloc.
GetX: This is a micro-framework that combines state management, dependency injection and route management. It’s simple to use and doesn’t require much boilerplate code.
A StatefulWidget in Flutter is a widget that can change over time. It is used for state management. The state for a StatefulWidget is created, used, and then destroyed when the widget is removed from the widget tree.
Pros
Simplicity: It’s a straightforward way to manage state inside your application.
Built-in: It’s built into Flutter SDK, So you don’t need to add any extra external dependencies to your project.
Ideal for local state: It’s great for widgets where the state is local, doesn’t need to be shared with other parts of the app, and doesn’t change very often
Things You Need
Not scalable: For complex apps It’s not ideal, as the state is tied to the widget lifecycle, so if the widget is removed from the tree, the state is lost.
Difficult to test: Because the state is tied to the widget, it can be difficult to test states.
Example
Counter_with_stateful_screen.dart
import 'package:flutter/material.dart';
class CounterWithStatefulScreen extends StatefulWidget {
const CounterWithStatefulScreen({
super.key,
});
@override
CounterWithStatefulScreenState createState() {
return CounterWithStatefulScreenState();
}
}
class CounterWithStatefulScreenState extends State {
int counter = 0;
void _incrementCounter() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter with Stateful Widget')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Counter:',
),
Text(
'$counter',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _incrementCounter,
child: const Text('Increment'),
),
],
),
),
);
}
}
In this code, The counter variable is initialized to 0.
Whenever the ElevatedButton is pressed, it calls the _incrementCounter function, which increments the value of the counter and triggers a rebuild of the widget.
The _incrementCounter function is used to increment the value of the counte and uses the setState function to notify the Flutter framework that the state has changed and the widget needs to be rebuilt and the new value of counter is displayed in the Text widget.
GetX
This state management solution for Flutter combines state management, dependency injection, and route management in a quick and practical way, giving you a micro framework with many solutions for various tasks.
Pros
Simplicity: It is easy to learn and it doesn’t require much boilerplate code and very easy for beginners to understand.
Powerful features: GetX includes not only a state management solution, but also has dependency injection and route management capabilities.
Reactive: GetX is reactive, which means it automatically updates the UI when the state changes.
Cons
Not idiomatic Flutter: GetX uses a different approach than the one recommended by the Flutter team. If you’re used to using Provider or Riverpod, GetX might feel a bit different.
Less community support: While GetX is growing in popularity, it doesn’t have as much community support as older libraries like Provider or Bloc.
Example
counter_controller.dart
import 'package:get/get.dart';
class CounterController extends GetxController {
var counter = 0.obs; // "obs" makes it observable
void increment() {
counter++;
}
}
counter_with_getX_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:state_management_examples/examples/controller/counter_controller.dart';
class CounterWithGetXScreen extends StatelessWidget {
const CounterWithGetXScreen({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Counter App with GetX'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Display the counter value using Obx widget
Obx(
() => Text(
'Counter: ${Get.find().counter.value}',
style: const TextStyle(fontSize: 24),
),
),
const SizedBox(height: 16),
// Button to increment the counter
ElevatedButton(
onPressed: () {
Get.find().increment();
},
child: const Text('Increment'),
),
],
),
),
),
// Initialize the GetX service for the CounterController
initialBinding: BindingsBuilder(() {
Get.lazyPut(() => CounterController());
}),
);
}
}
In this code, counter is a variable and It’s declared in the CounterController class, which is a GetxController. The GetxController class is provided by the GetX library for state management.
The counter variable is initialized to 0 and made observable by appending .obs.
The increment method is used to increment the value of the counter. Unlike in StatefulWidget, you don’t need to call setState because counter is an observable:
When the increment() function is called, it increments the value of the counter. Any widgets that are observing counters will automatically rebuild to reflect the new value.
So, every time the increment method is called, the counter is incremented and thee observing widgets are rebuilt to reflect the new value on screen.
Provider
Provider uses the concept of InheritedWidget to efficiently propagate data across the widget tree.
Pros
Simplicity: Provider is easy to understand and use. It doesn’t require much boilerplate code.
Efficiency: Provider is built on top of InheritedWidget, which is an efficient way of propagating data in the widget tree.
Scoped Access: Provider allows you to scope the access to the data to a certain part of the widget tree, which can help to manage complexity in large applications.
Cons
Not Reactive: Unlike some other state management solutions, Provider is not inherently reactive. You need to use it in combination with other classes like ChangeNotifier, ValueNotifier, or Stream to make it reactive.
Boilerplate: While Provider itself is simple, using it with ChangeNotifier can lead to some boilerplate code.
Example
counter_model.dart
import 'package:flutter/material.dart';
class CounterModel extends ChangeNotifier {
int _counter = 0;
int get value => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
counter_with_provider_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_management_examples/examples/models/counter_model.dart';
class CounterWithProviderScreen extends StatelessWidget {
const CounterWithProviderScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Consumer widget listens to changes in the CounterModel and rebuilds when necessary
Consumer(
builder: (context, counter, child) {
return Text(
'Counter: ${counter.value}',
style: const TextStyle(fontSize: 24),
);
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Increment the counter value using the CounterModel
Provider.of(context, listen: false).increment();
},
child: const Text('Increment'),
),
],
),
),
);
}
}
In the provided code, the CounterWithProviderScreen widget uses the Provider package to manage the state of a counter. The state is managed by the CounterModel class.
The ElevatedButton widget, when pressed, calls the increment method of the CounterModel to increment the counter. This is done using Provider.of<CounterModel>(context, listen: false).increment():
Here, Provider.of<CounterModel>(context, listen: false) is used to access the CounterModel instance from the widget tree. The listen: false argument is used because this part of the code doesn’t need to react to changes in the CounterModel.
In the CounterModel class, the increment method increments the _counter variable and then calls notifyListeners:
The notifyListeners method is used to notify all the listeners (typically UI widgets) that are observing this model, that a change has occurred. This will cause those widgets to rebuild and update based on the new state.
So, every time the increment method is called, _counter is incremented and all observing widgets are notified to update their state
BloC-Cubit
Cubit is a simpler and easier version of the BLoC (Business Logic Component) Pattern. It’s a state management solution that uses a reactive state model. It’s part of the bloc library.
Pros
Simplicity: Cubit is simpler than BLoC as it doesn’t require Streams and Sinks. You just have to deal with simple functions and state objects.
Testability: Cubit is highly testable. You can easily test the state changes in response to function calls.
Separation of Concerns: Cubit encourages separation of business logic from UI, which leads to more maintainable and scalable code.
Reactivity: Cubit is reactive so,When a state changes, the UI automatically updates.
Cons
Learning Curve: If you’re not familiar with reactive programming or state management concepts, there might be a learning curve.
Overkill for Simple Apps: For very simple apps, using Cubit might be overkill. It’s more suited for medium to large applications.
Requires Boilerplate: While less than BLoC, Cubit still requires some boilerplate code.
Example
Counter_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
counter_with_blocCubit_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:state_management_examples/examples/cubit/counter_cubit.dart';
class CounterWithBlocCubitScreen extends StatelessWidget {
const CounterWithBlocCubitScreen({super.key});
@override
Widget build(BuildContext context) {
// Get the instance of CounterCubit using BlocProvider
final CounterCubit counterCubit = BlocProvider.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Counter App with Bloc/Cubit'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Display the current counter value using BlocBuilder
BlocBuilder(
builder: (context, state) {
return Text(
'Counter: $state',
style: const TextStyle(fontSize: 24),
);
},
),
const SizedBox(height: 16),
// Button to increment the counter
ElevatedButton(
onPressed: () {
counterCubit.increment();
},
child: const Text('Increment'),
),
],
),
),
);
}
}
This code defines a CounterWithBlocCubitScreen widget that uses the CounterCubit for state management.
The CounterCubit instance is obtained using BlocProvider.of<CounterCubit>(context). This instance is used to interact with the CounterCubit.
The ElevatedButton widget, when pressed, calls the increment method of the CounterCubit to increment the counter. This is done using counterCubit.increment():
In the CounterCubit, the increment method would increment the counter value and emit the new state. The BlocBuilder widget listens for these state changes and rebuilds the Text widget to display the new counter value:
So, every time the increment method is called, the counter value is incremented and the UI is updated to reflect the new state.
RiverPod
In riverpod you define providers which manage your state. Providers can be accessed from anywhere in your code, not just from the widget tree. You can observe the state of a provider and rebuild your UI when that state changes. It was created by the same person who made Provider.
Pros
Flexibility: Riverpod is not limited by the widget tree, which means you can access your providers from anywhere in your code.
Strong Typing: Riverpod is more type safe than Provider. It’s harder to make mistakes because the compiler can catch more errors.
Immutability: Riverpod providers are immutable, which can help prevent bugs.
Testability: Riverpod is designed to be easy to test.
Cons
Learning Curve: Riverpod has a steeper learning curve than Provider, especially if you’re not already familiar with Provider.
Less Community Support: Riverpod is newer and less widely used than Provider, so there’s less community support and fewer resources available.
Example
counter_with_riverpod_screen.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:state_management_examples/examples/notifier/notifier_provider.dart';
class CounterWithRiverPodScreen extends StatelessWidget {
const CounterWithRiverPodScreen({super.key});
@override
Widget build(BuildContext context) {
WidgetRef? handle; // Variable to hold the reference to the widget's context
return Scaffold(
appBar: AppBar(
title: const Text('Counter App with Riverpod'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
HookConsumer(
builder: (context, ref, _) {
handle = ref; // Assign the reference to the handle variable
final count = ref
.watch(counterProvider); // Read the counterProvider state
return Text(
'$count',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
handle!
.read(counterProvider.notifier)
.increment(); // Call the increment method of the counterProvider notifier
},
child: const Text('Increment'),
),
],
),
),
);
}
}
notifier_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_management_examples/examples/notifier/counter_notifier.dart';
final counterProvider = StateNotifierProvider((_) => Counter());
counter_notifier.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
// Defining a class named Counter that extends StateNotifier
class Counter extends StateNotifier {
// Constructor for the Counter class that initializes the state with 0
Counter() : super(0);
// Method to increment the state value by 1
void increment() => state++;
}
This code defines a CounterWithRiverPodScreen widget that uses the counterProvider for state management. The counterProvider is a StateNotifierProvider that provides an instance of Counter, which is a StateNotifier<int>.The Counter class manages an integer state which represents a counter. The initial state of the counter is set to 0 in the constructor.
The increment method is used to increment the counter. It does this by incrementing the state:
In the CounterWithRiverPodScreen widget, the HookConsumer widget is used to observe the counterProvider:
The HookConsumer widget rebuilds whenever the state of the counterProvider changes. It displays the current counter value in a Text widget.
The ElevatedButton widget, when pressed, calls the increment method of the Counter to increment the counter. This is done using handle.read(counterProvider.notifier)
.increment():
So, every time the increment method is called, the counter value is incremented and the UI is updated to reflect the new state.
Conclusion
Today you learned about different types of state management in the flutter ecosystem and created simple counter apps in each state management.
Choosing the best state management depends on the type of project you are working on or plan to work on, as it really depends on the scope of the project and approach that you need.
Now, practice and implement what you learned from this basic tutorial and let us know what great things you have done.
If this article has helped in any way and you have learned something new today, Kindly consider sharing the article so others can also benefit and learn how to build Flutter applications.
Subscribe to our website for more interesting articles on AI and other interesting fields of IT.
You can find the code for this here:
Repository Link:
- Repo link: github