Flutter Riverpod Tutorial Part 1: Provider and StateProvider, StateNotifierProvider, ChangeNotifierProvider

Search for a command to run...

No comments yet. Be the first to comment.
In this series, I'll cover every concept of Riverpod in a comprehensive manner, so that any user can understand total ins and outs of any Riverpod concept.
Part-1 Link : https://harishkunchala.com/flutter-riverpod-tutorial-part-1-provider-and-stateprovider-statenotifierprovider-changenotifierprovider FutureProvider FutureProvider allows us to work with Futures in our Flutter Application It automatical...
While I generally write about Flutter. This problem has been bugging me for a while and there seem to be limited resources to find a solution. So for anyone who is looking for a solution. Here you go: Type edge:flags into the address bar and open it...
In this tutorial, we’ll go through covering interceptor, which is a very crucial feature of Dio. You can find the entire source code for the project here: dio_tasker Understanding Interceptors: What is an interceptor? An interceptor in Dio is a powe...

In this part, we’ll enhance our Task Manager app by parsing JSON responses and handling different types of errors. We’ll use Dio’s built-in error handling mechanisms. You can find the source code of the entire app here: dio_tasker Parsing JSON Respon...

While building apps in Flutter is extremely fluid and wonderfully intuitive. We obviously know that we can’t build the next TikTok or Twitter by only building offline apps. Which is why we absolutely need internet capabilities in our Flutter apps. Th...

Understanding when to use setState in Flutter is crucial for managing our app’s state effectively. Here’s a detailed guide: When to use setState ? Updating the UI: Use setState when we need to update the UI in response to changes in the internal sta...

Before we start using Riverpod. Let's set it up in our Flutter Project
Add the dependency:
flutter pub add flutter_riverpod
Wrap our application with ProviderScope: This is necessary for Riverpod to work. It should wrap your top-level app widget.
So this our main.dart
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const HomeScreen(),
);
}
}
You can find the starter code here: https://github.com/khkred/flutter_stack/tree/starter_code
Providers are the fundamental building blocks of Riverpod. They are objects that encapsulate state or values and allow for dependency injection. Providers are declaratively defined and can be consumed anywhere in the widget tree.
There are different types of Providers so we'll go one.
This is the simplest form of Provider
It exposes a single value that does not change over time
It's used mainly for exposing a constant or an object that contains business logic
ProviderNow as we know we use Provider to all a single value that doesn't change over time. So let's create a String for now that provides greeting and then call it with a Provider.
import 'package:flutter_riverpod/flutter_riverpod.dart';
const String greeting = 'Hello, Riverpod!';
final greetingProvider = Provider<String>((ref) => greeting);
Now I can call the Provider in our HomeScreen like this:
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home Screen'),
),
body: Center(
child: Text(greeting),
),
);
}
}
Perfect we can run the app and see the output

You can find the source code for our Provider here: https://github.com/khkred/flutter_stack/tree/provider
StateProvider holds a mutable state that can be read and modified
It's ideal for simple states like toggles, counters, etc.
StateProviderLet's create a simple counter app using StateProvider:
In our basic_providers.dart let's add a counterProvider
final counterProvider = StateProvider<int>((ref) => 0);
Now let's use the counterProvider in our home_screen.dart
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
//Basic Provider
final greeting = ref.watch(greetingProvider);
//State Provider
final int count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home Screen'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(greeting),
const SizedBox(height: 20),
Text(
'Count: $count',
style: const TextStyle(fontSize: 24),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
)
);
}
}

In the above example:
We defined a StateProvider that initializes the counter to 0.
HomeScreen is a ConsumerWidget which uses ref.watch to listen to the provider.
When the button is pressed, ref.read is used to increment the counter.
You can find the source code for them here: https://github.com/khkred/flutter_stack/tree/state_provider
The StateNotifierProvider allows you to separate your state from your business logic
Which can help make your code more testable and reusable.
It uses the StateNotifier class to handle logic and emit changes in state.
Step 1: Setting Up StateNotifier
We are going to create a small to-do list . So that we add and remove the values and see them in our to_do_screen
So here's our TodoNotifier :
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TodoNotifier extends StateNotifier<List<String>> {
//We are initializing the state with an empty list
TodoNotifier() : super([]);
//This method will add a new todo to the list
void addTodo(String todo) {
state = [...state, todo];
}
//This method will remove a todo from the list
void removeTodo(String todo) {
state = state.where((item) => item != todo).toList();
}
}
final todoCounterProvider = StateProvider<int>((ref) => 0);
final todoProvider =
StateNotifierProvider<TodoNotifier, List<String>>((ref) => TodoNotifier());
class TodoListScreen extends ConsumerWidget {
const TodoListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
List<String> todos = ref.watch(todoProvider);
final todoCounter = ref.watch(todoCounterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Todo List'),
),
body: ListView(
children: todos
.map((todo) => ListTile(
title: Text(todo),
))
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(todoCounterProvider.notifier).state++;
ref.read(todoProvider.notifier).addTodo('Todo $todoCounter');
},
child: const Icon(Icons.add),
),
);
}
}
As we can see once we added todo even if we go back to another screen. The value still stays, thanks to state management of Riverpod

Source Code: https://github.com/khkred/flutter_stack/tree/state_notifier_provider
The ChangeNotifierProvider is useful when your state management involves multiple pieces of state that need to be updated in response to actions. It works well with Flutter's ChangeNotifier.
Step 1: Create a ChangeNotifier
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
final counterChangeNotifierProvider = ChangeNotifierProvider((ref) => CounterNotifier());
Step 2: Using ChangeNotifierProvider in your UI
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_stack/providers/counter_notifier.dart';
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final counterNotifier = ref.watch(counterChangeNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter'),),
body: Center(
child: Text('Count: ${counterNotifier.count}', style: const TextStyle(fontSize: 25),)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counterNotifier.increment();
},
child: const Icon(Icons.add),
),
);
}
}
In this example:
CounterNotifier manages the counter state and uses notifyListeners() to inform listeners about state changes.
The UI reacts to these changes by rebuilding whenever notifyListeners() is called.

You can download the source code here: https://github.com/khkred/flutter_stack/tree/change_notifier
StateNotifierProvider and ChangeNotifierProviderChangeNotifierProvider and StateNotifierProvider in Riverpod serve similar purposes in managing state, but they have key differences in how they manage and update that state. Each is suited to different scenarios depending on the complexity of the state and the specific needs of your application. Here’s a breakdown of the differences:
Flutter Integration: ChangeNotifier is a part of the Flutter framework itself, making it a familiar option for those who have used Flutter’s Provider package before transitioning to Riverpod.
State Management: It allows for mutable state management within the ChangeNotifier class. You directly mutate the state of the object and call notifyListeners() to inform all the listeners about changes.
Use Case: Best suited for more granular control over the state when multiple properties within a model can change independently, and you want to notify widgets to rebuild whenever any property changes.
Performance: Every time notifyListeners() is called, all the widgets that listen to the provider will rebuild. This can lead to performance issues if not managed carefully, especially with large or complex widgets.
Example:
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Notifies all the listening widgets to rebuild
}
}
Separation of Concerns: StateNotifier comes from the state_notifier package and is not part of core Flutter, offering a cleaner separation of state management from UI.
Immutable State Management: Typically, StateNotifier works with immutable states. You replace the entire state instead of mutating it. This pattern is beneficial for predictability and debugging.
Use Case: Ideal for scenarios where the entire state object is replaced rather than mutating individual fields within the state. It’s particularly effective in more complex state management situations where immutability is a priority.
Performance: Since it encourages immutability, the state changes are more predictable and easier to track. This can lead to better performance optimizations, as widgets react only to relevant state changes.
Example:
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state = state + 1; // Replaces the state with a new value
}
}
Complexity: If your state is complex but does not require the widgets to update for partial changes, or you prefer immutability, StateNotifierProvider is likely more suitable.
Familiarity and Fine Control: If you need fine-grained control over what changes within the state or are more familiar with traditional Flutter state management, ChangeNotifierProvider might be the better choice.
Each of these providers has its strengths and is designed to work best under different circumstances. The choice between them often comes down to personal preference and specific project requirements concerning state management practices.