Flutter, Google’s UI toolkit, has gained massive popularity for building beautiful and performant applications across multiple platforms. As Flutter apps grow in complexity, managing state effectively becomes critical. While there are several state management solutions available, Riverpod stands out as a compile-safe, predictable, and testable alternative to Provider. In this blog post, we’ll dive into Riverpod, exploring its features, benefits, and implementation with comprehensive code examples.
What is Riverpod?
Riverpod is a reactive caching and data-binding framework and is a complete rewrite of Provider. It is designed to be safer, more flexible, and easier to use. Key features of Riverpod include:
- Compile-Safe: Catches errors during compilation rather than at runtime.
- Testable: Simplifies testing with overrides and test scopes.
- Predictable: Follows a strict unidirectional data flow, enhancing predictability.
- Composable: Allows easy composition and combination of state providers.
- Multi-Platform: Supports multiple platforms, including web, desktop, and mobile.
Why Choose Riverpod?
Riverpod addresses several common issues found in other state management solutions, providing benefits such as:
- Safety: Eliminates context-related issues, improving reliability.
- Clarity: Uses providers as a single source of truth, reducing complexity.
- Extensibility: Simplifies integration with existing and new widgets, promoting reusability.
Getting Started with Riverpod
Step 1: Add Riverpod Dependencies
Include Riverpod in your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
riverpod_generator: ^2.3.0
build_runner: ^2.4.6
Run flutter pub get to install the dependencies.
Step 2: Wrap Your App with ProviderScope
Ensure that your application is wrapped with ProviderScope, which makes providers available throughout your app:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// Wrapping the entire app with "ProviderScope"
// enables widgets to read providers.
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Riverpod Example')),
body: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Hello Riverpod'),
);
}
}
Basic Provider Example
Step 1: Define a Provider
Create a simple provider to hold a value:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A simple provider that returns a String value.
final helloWorldProvider = Provider((_) => 'Hello world');
Step 2: Consume the Provider in a Widget
Use Consumer or ConsumerWidget to read and display the provider’s value:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Define the provider
final helloWorldProvider = Provider((_) => 'Hello world');
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final helloWorldValue = ref.watch(helloWorldProvider);
return Center(
child: Text(helloWorldValue),
);
}
}
In this example:
helloWorldProvideris defined usingProvider.MyHomePageextendsConsumerWidgetto easily access providers.ref.watch(helloWorldProvider)retrieves the value ofhelloWorldProvider.
StateProvider Example
Step 1: Define a StateProvider
Create a StateProvider to manage simple, mutable state:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A StateProvider that holds an integer and can be modified.
final counterProvider = StateProvider((ref) => 0);
Step 2: Update the State in a Widget
Use ref.read(counterProvider.notifier).state++ to update the state when a button is pressed:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Define the provider
final counterProvider = StateProvider((ref) => 0);
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final counterValue = ref.watch(counterProvider);
return Scaffold(
body: Center(
child: Text('Counter value: $counterValue'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Increment the counter state using .notifier
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
),
);
}
}
In this example:
counterProvideris defined usingStateProviderwith an initial value of 0.- Clicking the
FloatingActionButtonincrements the state usingref.read(counterProvider.notifier).state++.
FutureProvider Example
Step 1: Define a FutureProvider
Use FutureProvider to handle asynchronous data fetching:
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A FutureProvider that fetches data asynchronously.
final dataProvider = FutureProvider((ref) async {
// Simulate fetching data from an API
await Future.delayed(const Duration(seconds: 2));
return 'Fetched data';
});
Step 2: Consume the FutureProvider in a Widget
Handle the state of the future with AsyncValue:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Define the provider
final dataProvider = FutureProvider((ref) async {
// Simulate fetching data from an API
await Future.delayed(const Duration(seconds: 2));
return 'Fetched data';
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return Scaffold(
body: Center(
child: data.when(
data: (value) => Text('Data: $value'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
),
);
}
}
In this example:
dataProviderusesFutureProviderto simulate fetching data from an API.- The
.whenmethod handles different states:data,loading, anderror.
StreamProvider Example
Step 1: Define a StreamProvider
Use StreamProvider to listen to streams and update the UI:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
// A StreamProvider that emits values every second.
final clockProvider = StreamProvider((ref) {
return Stream.periodic(const Duration(seconds: 1), (count) => DateTime.now());
});
Step 2: Consume the StreamProvider in a Widget
Display the current time using AsyncValue:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; // Import intl package for date formatting
// Define the provider
final clockProvider = StreamProvider((ref) {
return Stream.periodic(const Duration(seconds: 1), (count) => DateTime.now());
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final time = ref.watch(clockProvider);
return Scaffold(
body: Center(
child: time.when(
data: (dateTime) => Text('Current time: ${DateFormat('HH:mm:ss').format(dateTime)}'), // Format the date
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
),
);
}
}
In this example:
clockProvideremits the current time every second using aStream.- The UI updates dynamically with the new time using the
.whenmethod to handle different states.
Provider with Parameters (Family)
Family allows you to create providers that depend on external parameters. It’s beneficial when dealing with lists, where each item needs its own unique state.
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A provider family that returns a personalized greeting.
final greetingProvider = Provider.family((ref, name) {
return 'Hello, $name!';
});
Then, you can use this provider like this:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Define the provider
final greetingProvider = Provider.family((ref, name) {
return 'Hello, $name!';
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider('Riverpod User'));
return Center(
child: Text(greeting),
);
}
}
Here:
greetingProviderusesProvider.familyto take a name as a parameter.- The widget then consumes this provider, passing ‘Riverpod User’ as the name.
Using Riverpod with StateNotifierProvider
Step 1: Create a StateNotifier
Implement a StateNotifier to manage more complex state logic:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart'; // Import foundation for required keyword
// A simple data class for our state
@immutable
class CounterState {
const CounterState({required this.count});
final int count;
// Methods to help update the state
CounterState copyWith({int? count}) {
return CounterState(
count: count ?? this.count,
);
}
}
// The StateNotifier managing the counter state
class CounterNotifier extends StateNotifier {
CounterNotifier() : super(const CounterState(count: 0)); // Initial state
// Method to increment the counter
void increment() {
state = state.copyWith(count: state.count + 1);
}
}
Step 2: Define a StateNotifierProvider
Associate the StateNotifier with a StateNotifierProvider:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart'; // Import foundation for required keyword
// A simple data class for our state
@immutable
class CounterState {
const CounterState({required this.count});
final int count;
// Methods to help update the state
CounterState copyWith({int? count}) {
return CounterState(
count: count ?? this.count,
);
}
}
// The StateNotifier managing the counter state
class CounterNotifier extends StateNotifier {
CounterNotifier() : super(const CounterState(count: 0)); // Initial state
// Method to increment the counter
void increment() {
state = state.copyWith(count: state.count + 1);
}
}
// Define a StateNotifierProvider that creates and exposes the CounterNotifier
final counterNotifierProvider = StateNotifierProvider((ref) {
return CounterNotifier();
});
Step 3: Consume the StateNotifierProvider in a Widget
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart'; // Import foundation for required keyword
// A simple data class for our state
@immutable
class CounterState {
const CounterState({required this.count});
final int count;
// Methods to help update the state
CounterState copyWith({int? count}) {
return CounterState(
count: count ?? this.count,
);
}
}
// The StateNotifier managing the counter state
class CounterNotifier extends StateNotifier {
CounterNotifier() : super(const CounterState(count: 0)); // Initial state
// Method to increment the counter
void increment() {
state = state.copyWith(count: state.count + 1);
}
}
// Define a StateNotifierProvider that creates and exposes the CounterNotifier
final counterNotifierProvider = StateNotifierProvider((ref) {
return CounterNotifier();
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final counterState = ref.watch(counterNotifierProvider);
return Scaffold(
body: Center(
child: Text('Counter value: ${counterState.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Access and call the increment method from the CounterNotifier
ref.read(counterNotifierProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
In this setup:
CounterStaterepresents the state using an immutable data class.CounterNotifiermanages the state and provides methods to update it.- The widget rebuilds every time the state changes and calls the
incrementmethod.
Conclusion
Riverpod provides a compile-safe, testable, and composable solution for managing state in Flutter applications. By embracing Riverpod, you can avoid common issues found in traditional state management approaches and build robust, predictable applications. The comprehensive examples covered—from basic providers to complex StateNotifier integrations—offer a solid foundation for leveraging Riverpod effectively in your Flutter projects.