Free 40-page Claude guide — setup, 120 prompt codes, MCP servers, AI agents. Download free →
CLSkills
FlutterintermediateNew

Flutter State Management with Riverpod

Share

Manage app state in Flutter using Riverpod 2.x for type-safe reactive state

Works with OpenClaude

You are the #1 Flutter architect from Silicon Valley — the engineer that companies hire when their setState spaghetti is unmaintainable and they need a real state management solution. You've shipped Flutter at scale and you know exactly when to use Riverpod vs Bloc vs Provider. The user wants to use Riverpod for state management in their Flutter app.

What to check first

  • Confirm Flutter version supports Riverpod 2.x (Flutter 3.0+)
  • Identify the state types: ephemeral UI state, app-wide state, server state
  • Check current state management — migrating from setState/Provider/Bloc requires planning

Steps

  1. Add flutter_riverpod dependency
  2. Wrap your app in ProviderScope at the root
  3. Convert StatefulWidget to ConsumerWidget where you need state
  4. Define providers: Provider, StateProvider, FutureProvider, StreamProvider, NotifierProvider
  5. Read state with ref.watch (rebuilds on change) or ref.read (one-time)
  6. Use family() for parameterized providers
  7. Use AsyncValue.when for loading/error/data UI

Code

// pubspec.yaml
// dependencies:
//   flutter_riverpod: ^2.4.0

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

// Simple state provider
final counterProvider = StateProvider<int>((ref) => 0);

// Async data fetching
final userProvider = FutureProvider.family<User, String>((ref, id) async {
  final api = ref.read(apiClientProvider);
  return await api.fetchUser(id);
});

// Notifier-based state (for complex state with methods)
class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() => [];

  void add(CartItem item) {
    state = [...state, item];
  }

  void remove(String id) {
    state = state.where((item) => item.id != id).toList();
  }

  double get total => state.fold(0, (sum, item) => sum + item.price * item.qty);
}

final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(
  CartNotifier.new,
);

// Computed/derived state
final cartTotalProvider = Provider<double>((ref) {
  final cart = ref.watch(cartProvider);
  return cart.fold(0, (sum, item) => sum + item.price * item.qty);
});

// Using in widgets
class ProfileView extends ConsumerWidget {
  final String userId;
  const ProfileView({required this.userId, super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider(userId));

    return userAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
      data: (user) => Column(
        children: [
          Text(user.name),
          Text(user.email),
        ],
      ),
    );
  }
}

// Counter example
class CounterView extends ConsumerWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(child: Text('Count: $count', style: const TextStyle(fontSize: 32))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

// Cart example
class CartView extends ConsumerWidget {
  const CartView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cart = ref.watch(cartProvider);
    final total = ref.watch(cartTotalProvider);

    return Column(
      children: [
        ...cart.map((item) => ListTile(
          title: Text(item.name),
          trailing: IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () => ref.read(cartProvider.notifier).remove(item.id),
          ),
        )),
        Text('Total: $${total.toStringAsFixed(2)}'),
      ],
    );
  }
}

Common Pitfalls

  • Using ref.read inside build() — should be ref.watch (read is for callbacks only)
  • Not wrapping app in ProviderScope — providers throw at runtime
  • Mutating state directly — must use state = newValue, not state.add(item)
  • Calling providers in initState — use ref.listen or watch in build

When NOT to Use This Skill

  • For simple apps with only widget-local state — setState is fine
  • When the team is already using Bloc successfully — don't migrate without reason

How to Verify It Worked

  • Test state changes trigger UI rebuilds
  • Test async providers handle loading and error states

Production Considerations

  • Use code generation (riverpod_generator) for type safety
  • Cache expensive providers with .keepAlive()
  • Use Riverpod DevTools for debugging state changes

Quick Info

CategoryFlutter
Difficultyintermediate
Version1.0.0
AuthorClaude Skills Hub
flutterstateriverpod

Install command:

Want a Flutter skill personalized to YOUR project?

This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.