DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Riverpod Deep Dive: Provider, StateNotifier, and AsyncNotifier

Flutter Riverpod Deep Dive: Provider, StateNotifier, and AsyncNotifier

Pick the right Provider type and your state management code becomes almost trivial.

Provider Selection Guide

What are you managing?
  ├── Read-only value / DI             → Provider
  ├── Mutable synchronous state        → StateNotifierProvider / NotifierProvider
  └── Async data (API / DB)            → FutureProvider / AsyncNotifierProvider
Enter fullscreen mode Exit fullscreen mode

Provider: Read-Only Values and Dependency Injection

final supabaseProvider = Provider<SupabaseClient>((ref) {
  return Supabase.instance.client;
});

// Derived value from another provider
final userNameProvider = Provider<String>((ref) {
  final user = ref.watch(currentUserProvider);
  return user?.name ?? 'Guest';
});

// In a widget
final userName = ref.watch(userNameProvider);
Enter fullscreen mode Exit fullscreen mode

StateNotifierProvider: Mutable Synchronous State

// Immutable state class
class CartState {
  const CartState({required this.items, this.isLoading = false});
  final List<CartItem> items;
  final bool isLoading;

  CartState copyWith({List<CartItem>? items, bool? isLoading}) => CartState(
    items: items ?? this.items,
    isLoading: isLoading ?? this.isLoading,
  );
}

class CartNotifier extends StateNotifier<CartState> {
  CartNotifier() : super(const CartState(items: []));

  void addItem(CartItem item) =>
    state = state.copyWith(items: [...state.items, item]);

  void removeItem(String id) =>
    state = state.copyWith(
      items: state.items.where((i) => i.id != id).toList(),
    );

  void clear() => state = const CartState(items: []);
}

final cartProvider = StateNotifierProvider<CartNotifier, CartState>(
  (ref) => CartNotifier(),
);

// In widget
final cart = ref.watch(cartProvider);
ref.read(cartProvider.notifier).addItem(item);
Enter fullscreen mode Exit fullscreen mode

AsyncNotifierProvider: Async State (Riverpod 2.x Recommended)

class TasksNotifier extends AsyncNotifier<List<Task>> {
  @override
  Future<List<Task>> build() => _fetchTasks();

  Future<List<Task>> _fetchTasks() async {
    final data = await ref.read(supabaseProvider)
      .from('tasks')
      .select()
      .order('created_at', ascending: false);
    return data.map(Task.fromJson).toList();
  }

  Future<void> addTask(String title) async {
    final previous = state;
    state = const AsyncLoading();
    try {
      await ref.read(supabaseProvider)
        .from('tasks')
        .insert({'title': title});
      state = AsyncData(await _fetchTasks());
    } catch (_) {
      state = previous; // rollback on error
      rethrow;
    }
  }
}

final tasksProvider = AsyncNotifierProvider<TasksNotifier, List<Task>>(
  TasksNotifier.new,
);

// In widget
Widget build(BuildContext context, WidgetRef ref) {
  return ref.watch(tasksProvider).when(
    data: (tasks) => TaskList(tasks: tasks),
    loading: () => const CircularProgressIndicator(),
    error: (e, _) => ErrorWidget(e.toString()),
  );
}
Enter fullscreen mode Exit fullscreen mode

family: Parameterized Providers

final taskDetailProvider = FutureProvider.family<Task, String>(
  (ref, taskId) async {
    final data = await ref.read(supabaseProvider)
      .from('tasks')
      .select()
      .eq('id', taskId)
      .single();
    return Task.fromJson(data);
  },
);

// Usage
final task = ref.watch(taskDetailProvider(taskId));
Enter fullscreen mode Exit fullscreen mode

Summary

Provider               → read-only, DI
StateNotifierProvider  → sync state (cart, filters, selections)
AsyncNotifierProvider  → async state (API/DB) ← Riverpod 2.x default
FutureProvider         → one-shot async (config loading etc.)
family                 → parameterized (per-ID data)
Enter fullscreen mode Exit fullscreen mode

In Riverpod 2.x, AsyncNotifier is the standard for async. Keep StateNotifier for synchronous-only state.

Top comments (0)