Flutter is a popular framework for building mobile applications. It provides a rich set of widgets and tools for creating beautiful and functional apps. Flutter was developed by Google and released in 2017. It is used by so many popular apps like Google Pay, Google Ads, Supercell and so many more. Managing state in Flutter apps can be tricky, and this is where Riverpod comes in.
What is Riverpod?
It is a state management solution for Flutter that makes handling app state straightforward and type-safe. Think of it as a smart container that holds and manages your app's data.
Why Should You Use It?
Riverpod solves several common problems that Flutter developers face:
Dependency Management: It helps you organize and access your app's data from anywhere without passing it through multiple widget layers.
Type Safety: Unlike some other state management solutions, Riverpod is completely type-safe. This means you'll catch errors at compile-time rather than runtime.
Testing: Riverpod makes testing your app's state and business logic straightforward.
Caching: Built-in caching mechanisms for network requests and expensive computations.
Error Handling: Elegant ways to handle and display errors in your app.
Let's dive deeper and see how to install Riverpod in your Flutter project and we'll also explore some of the features of Riverpod.
Getting Started with Riverpod
Before starting, you can try out this solution online in your browser without having to install anything. Test it on DartPad or on Zapp.
Installation
Let's start by adding the necessary packages to your Flutter project. Open your pubspec.yaml
file and add the following dependencies:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
dev_dependencies:
build_runner: ^2.4.8
riverpod_generator: ^2.3.9
After adding these dependencies, run the following command in your terminal:
flutter pub get
This will install the necessary packages and add them to your project.
Setting up Your App
To use Riverpod, you need to wrap your app with a ProviderScope
widget. This widget initializes Riverpod for your entire app:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire app
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Demo',
home: HomePage(),
);
}
}
Creating Your First Provider
Let's create a simple counter example to understand how Riverpod works. We'll start with a basic provider:
// Define a provider that stores a simple integer value
final counterProvider = StateProvider<int>((ref) => 0);
Now, let's create a widget that uses this provider:
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the counter value
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: Text('RiverPod Counter Example')),
body: Center(
child: Text(
'Count: $count',
style: Theme.of(context).textTheme.headlineMedium,
)
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}
Understanding the Basic Concepts
Let's break down what we just did:
Provider: This is where you define your data. In our example,
counterProvider
holds an integer value.ConsumerWidget: This is a special widget that can listen to providers. It's similar to
StatelessWidget
but with an extraWidgetRef
parameter.ref.watch(): This method subscribes to a provider and rebuilds the widget whenever the value changes.
ref.read(): Used to read the provider's value once or modify it without listening to changes.
Different Types of Providers
The framework offers several types of providers for different use cases:
- Provider: For values that never change
final nameProvider = Provider((ref) => 'John Doe');
- StateProvider: For simple state that can change
final counterProvider = StateProvider((ref) => 0);
- NotifierProvider: For complex state management:
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(
() => CounterNotifier(),
);
Common use cases where Riverpod can be useful
API data fetching
One of the most common use cases is handling API calls and managing their states. Here's an example that demonstrates how to fetch user data from an API and handle loading, error, and success states elegantly:
// A provider that fetches user data
@riverpod
Future<User> fetchUser(FetchUserRef ref, {required String userId}) async {
final response = await http.get('api.example.com/users/$userId');
return User.fromJson(response.data);
}
// Using the data in a widget
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(fetchUserProvider(userId: '123'));
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (user) => Text('Welcome ${user.name}!'),
);
}
}
In this example, we've created an AsyncNotifierProvider
that handles the API call. The when
method provides a clean way to handle all possible states of the async operation. This pattern is particularly useful because:
- It automatically handles loading states
- It provides built-in error handling
- The UI automatically updates when the data changes
- It's type-safe and null-safe
- The data can be easily cached and reused across the app
Form Management
Another powerful use case for Riverpod is managing form state. Here's an example of how to handle a login form with validation:
@riverpod
class LoginFormNotifier extends Notifier<LoginFormState> {
@override
LoginFormState build() => LoginFormState.initial();
void updateEmail(String email) {
state = state.copyWith(
email: email,
emailError: email.contains('@') ? null : 'Invalid email',
);
}
void updatePassword(String password) {
state = state.copyWith(
password: password,
passwordError: password.length >= 6 ? null : 'Password too short',
);
}
Future<void> submit() async {
state = state.copyWith(isLoading: true);
try {
await ref.read(authServiceProvider).login(state.email, state.password);
state = state.copyWith(isSuccess: true);
} catch (e) {
state = state.copyWith(error: e.toString());
} finally {
state = state.copyWith(isLoading: false);
}
}
}
// Using the form in a widget
class LoginForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final formState = ref.watch(loginFormNotifierProvider);
return Form(
child: Column(
children: [
TextField(
onChanged: (value) => ref.read(loginFormNotifierProvider.notifier).updateEmail(value),
decoration: InputDecoration(
errorText: formState.emailError,
),
),
TextField(
onChanged: (value) => ref.read(loginFormNotifierProvider.notifier).updatePassword(value),
decoration: InputDecoration(
errorText: formState.passwordError,
),
),
ElevatedButton(
onPressed: formState.isValid
? () => ref.read(loginFormNotifierProvider.notifier).submit()
: null,
child: formState.isLoading
? CircularProgressIndicator()
: Text('Login'),
),
],
),
);
}
}
The form management example above showcases how Riverpod elegantly handles complex form state and validation. By using a dedicated LoginFormNotifier
, we keep all form-related logic and state neatly organized in one place, making it easy to maintain and test.
The form provides real-time validation as users type, instantly showing feedback for invalid email formats or short passwords. During form submission, the UI automatically updates to show loading states and disable the submit button, preventing duplicate submissions. Both field-level validation errors and submission errors are handled smoothly, ensuring users always know what's wrong and how to fix it.
What's particularly nice about this approach is how clean the UI code remains. The widget itself focuses purely on layout and presentation, while all the business logic lives in the notifier. This pattern works well for any form complexity - from simple login forms to multi-step wizards with complex validation rules.
Conclusion
This state management solution brings type safety, dependency management, and elegant error handling to Flutter applications. Its flexibility makes it suitable for various use cases, from simple counter apps to complex forms and API integrations.
This is just the beginning of what you can do. For more information and advanced usage, check out the official Riverpod documentation.
Happy coding! ๐
Top comments (1)
Avoid legacy riverpod tools. In brief, avoid legacy ChangeNotifier, StateNotifier (and their providers) and StateProvider. Use only Provider, FutureProvider, StreamProvider, and Notifier, AsyncNotifier, StreamNotifier (and their providers).