← Articles

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.

Getting started with Riverpod in Flutter — ANN Tech