Getting started with Riverpod in Flutter
By Ann Tech · 20 December 2024
Riverpod is a compile-safe, testable state management framework for Flutter. This guide covers the core concepts you need to build real features with Riverpod 2.x and code generation.
Setup
dependencies:
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.8
Wrap your app:
void main() {
runApp(const ProviderScope(child: MyApp()));
}
Provider types
// Simple computed value — like a function that can be watched
@riverpod
String greeting(GreetingRef ref) => 'Hello, Flutter!';
// Async data — for network or database reads
@riverpod
Future<List<Product>> products(ProductsRef ref) async {
return ref.read(productRepositoryProvider).getProducts();
}
// State that can change — class-based notifier
@riverpod
class CartNotifier extends _$CartNotifier {
@override
Cart build() => Cart.empty();
void addItem(Product product) {
state = state.copyWith(items: [...state.items, CartItem(product: product)]);
}
void removeItem(String productId) {
state = state.copyWith(
items: state.items.where((i) => i.product.id != productId).toList(),
);
}
Future<void> checkout() async {
final result = await ref.read(orderRepositoryProvider).createOrder(
OrderRequest(items: state.items),
);
state = Cart.empty();
ref.read(ordersNotifierProvider.notifier).add(result);
}
}
flutter pub run build_runner watch --delete-conflicting-outputs
Watching providers in widgets
class ProductsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch rebuilds the widget when value changes
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
data: (products) => ProductList(products: products),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
// For state widgets:
class CartScreen extends ConsumerStatefulWidget {
const CartScreen({super.key});
@override
ConsumerState<CartScreen> createState() => _CartScreenState();
}
class _CartScreenState extends ConsumerState<CartScreen> {
@override
Widget build(BuildContext context) {
final cart = ref.watch(cartNotifierProvider);
return CartView(cart: cart);
}
}
read vs watch
// watch: rebuild widget when value changes
final count = ref.watch(cartNotifierProvider.select((c) => c.items.length));
// read: get the current value once (in callbacks, not build)
ElevatedButton(
onPressed: () => ref.read(cartNotifierProvider.notifier).checkout(),
child: const Text('Checkout'),
)
// WRONG: read in build — won't rebuild when value changes
final products = ref.read(productsProvider); // Bug: stale data
select to narrow rebuilds
// Rebuilds on any cart change — items, total, coupon, everything
final cart = ref.watch(cartNotifierProvider);
// Rebuilds only when item count changes
final itemCount = ref.watch(
cartNotifierProvider.select((cart) => cart.items.length),
);
Always use .select() in widgets that only need one property from a large state object.
Provider families
Parameterize a provider with arguments:
@riverpod
Future<Product> productById(ProductByIdRef ref, String id) async {
return ref.read(productRepositoryProvider).getProduct(id);
}
// Usage:
final product = ref.watch(productByIdProvider('product-123'));
Listening for side effects
class CheckoutScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<Order?>>(checkoutNotifierProvider, (prev, next) {
next.whenOrNull(
data: (order) {
if (order != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Order ${order.id} placed!')),
);
context.go('/orders/${order.id}');
}
},
error: (e, _) => showErrorDialog(context, e.toString()),
);
});
return const CheckoutForm();
}
}
keepAlive for persistent state
@riverpod
class CartNotifier extends _$CartNotifier {
@override
Cart build() {
ref.keepAlive(); // Cart persists across navigation
return Cart.empty();
}
}
Without keepAlive, a provider is disposed when no widget is watching it — state resets on navigation away and back.
Testing
test('addItem increases cart count', () {
final container = ProviderContainer(
overrides: [
productRepositoryProvider.overrideWithValue(MockProductRepository()),
],
);
addTearDown(container.dispose);
final notifier = container.read(cartNotifierProvider.notifier);
expect(container.read(cartNotifierProvider).items, isEmpty);
notifier.addItem(testProduct);
expect(container.read(cartNotifierProvider).items.length, 1);
});
Common pitfalls
Calling ref.watch inside callbacks. ref.watch must only be called synchronously in build(). Using it in onTap or initState doesn't work. Use ref.read in callbacks.
Not disposing the ProviderContainer in tests. Each test should create its own ProviderContainer and call addTearDown(container.dispose). Sharing containers between tests causes state to leak between them.
Forgetting keepAlive for global state. Cart, auth state, and other app-global state must call ref.keepAlive() in build(). Without it, navigating away from all screens that watch the provider disposes and resets it.
Sign in to like, dislike, or report.