Learn Riverpod through real-world examples. Each example includes full source code, explanation, and best practices.
The classic Flutter counter app built with Riverpod state management.
// providers/counter.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
// main.dart
class CounterApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final counter = ref.read(counterProvider.notifier);
return Scaffold(
appBar: AppBar(title: Text('Counter: $count')),
body: Center(
child: Column(
children: [
Text('$count', style: Theme.of(context).textTheme.headlineMedium),
Row(
children: [
ElevatedButton(
onPressed: counter.decrement,
child: Text('-'),
),
ElevatedButton(
onPressed: counter.reset,
child: Text('Reset'),
),
ElevatedButton(
onPressed: counter.increment,
child: Text('+'),
),
],
),
],
),
),
);
}
}Fetching data from REST APIs with error handling and loading states.
// models/user.dart
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
// providers/users.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
@riverpod
Future<List<User>> userList(UserListRef ref) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
@riverpod
Future<User> user(UserRef ref, int id) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/$id'),
);
if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('User not found');
}
}
// widgets/user_list.dart
class UserListWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userListProvider);
return usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UserDetailPage(userId: user.id),
),
),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text('Error: $error'),
),
);
}
}Complex state management with multiple related providers and computed values.
// models/product.dart
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}
class CartItem {
final Product product;
final int quantity;
CartItem({required this.product, required this.quantity});
}
// providers/cart.dart
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() => [];
void addProduct(Product product) {
final existingIndex = state.indexWhere(
(item) => item.product.id == product.id,
);
if (existingIndex >= 0) {
// Increase quantity
final updatedItem = CartItem(
product: product,
quantity: state[existingIndex].quantity + 1,
);
state = [
...state.sublist(0, existingIndex),
updatedItem,
...state.sublist(existingIndex + 1),
];
} else {
// Add new item
state = [...state, CartItem(product: product, quantity: 1)];
}
}
void removeProduct(String productId) {
state = state.where((item) => item.product.id != productId).toList();
}
void updateQuantity(String productId, int quantity) {
if (quantity <= 0) {
removeProduct(productId);
return;
}
final index = state.indexWhere((item) => item.product.id == productId);
if (index >= 0) {
final updatedItem = CartItem(
product: state[index].product,
quantity: quantity,
);
state = [
...state.sublist(0, index),
updatedItem,
...state.sublist(index + 1),
];
}
}
void clear() => state = [];
}
// Computed providers
@riverpod
double cartTotal(CartTotalRef ref) {
final cartItems = ref.watch(cartProvider);
return cartItems.fold(
0.0,
(sum, item) => sum + (item.product.price * item.quantity),
);
}
@riverpod
int cartItemCount(CartItemCountRef ref) {
final cartItems = ref.watch(cartProvider);
return cartItems.fold(
0,
(sum, item) => sum + item.quantity,
);
}Handle form state and validation with Riverpod providers.
// providers/login_form.dart
class LoginFormState {
final String email;
final String password;
final String? emailError;
final String? passwordError;
final bool isLoading;
LoginFormState({
this.email = '',
this.password = '',
this.emailError,
this.passwordError,
this.isLoading = false,
});
LoginFormState copyWith({
String? email,
String? password,
String? emailError,
String? passwordError,
bool? isLoading,
}) {
return LoginFormState(
email: email ?? this.email,
password: password ?? this.password,
emailError: emailError ?? this.emailError,
passwordError: passwordError ?? this.passwordError,
isLoading: isLoading ?? this.isLoading,
);
}
bool get isValid =>
email.isNotEmpty &&
password.isNotEmpty &&
emailError == null &&
passwordError == null;
}
@riverpod
class LoginForm extends _$LoginForm {
@override
LoginFormState build() => LoginFormState();
void updateEmail(String email) {
state = state.copyWith(
email: email,
emailError: _validateEmail(email),
);
}
void updatePassword(String password) {
state = state.copyWith(
password: password,
passwordError: _validatePassword(password),
);
}
String? _validateEmail(String email) {
if (email.isEmpty) return 'Email is required';
if (!email.contains('@')) return 'Invalid email format';
return null;
}
String? _validatePassword(String password) {
if (password.isEmpty) return 'Password is required';
if (password.length < 6) return 'Password must be at least 6 characters';
return null;
}
Future<void> submit() async {
if (!state.isValid) return;
state = state.copyWith(isLoading: true);
try {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
// Handle success
} catch (e) {
// Handle error
} finally {
state = state.copyWith(isLoading: false);
}
}
}Looking for more examples? Check out our GitHub repository for complete sample applications including authentication, database integration, and complex UI patterns.