DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Riverpod 2.0 Complete Guide — Notifier & AsyncNotifier for Modern State Management

Flutter Riverpod 2.0 Complete Guide — Notifier & AsyncNotifier for Modern State Management

Riverpod 2.0 introduced Notifier and AsyncNotifier as modern replacements for StateNotifierProvider. Combined with riverpod_generator, they drastically cut boilerplate.

Riverpod 2.0 API Overview

Old API New API Use Case
StateProvider @riverpod + Ref Simple values
StateNotifierProvider @riverpod class X extends Notifier Sync state
FutureProvider @riverpod Future<X> x(Ref ref) Async data
StateNotifierProvider (async) @riverpod class X extends AsyncNotifier Async state

Setup

dependencies:
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  riverpod_generator: ^2.4.0
  build_runner: ^2.4.9
Enter fullscreen mode Exit fullscreen mode

Notifier — Synchronous State

// lib/providers/counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}
Enter fullscreen mode Exit fullscreen mode
flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(children: [
      Text('$count'),
      ElevatedButton(
        onPressed: ref.read(counterProvider.notifier).increment,
        child: const Text('+1'),
      ),
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

AsyncNotifier — Async State with Optimistic Updates

// lib/providers/notes_provider.dart
@riverpod
class Notes extends _$Notes {
  @override
  Future<List<Note>> build() async => _fetchNotes();

  Future<List<Note>> _fetchNotes() async {
    final userId = supabase.auth.currentUser!.id;
    final data = await supabase.from('notes').select()
      .eq('user_id', userId).order('created_at', ascending: false);
    return data.map(Note.fromJson).toList();
  }

  Future<void> addNote(String content) async {
    final previous = state.valueOrNull ?? [];
    // Optimistic update
    state = AsyncData([Note(id: 'temp', content: content, createdAt: DateTime.now()), ...previous]);
    try {
      await supabase.from('notes').insert({
        'user_id': supabase.auth.currentUser!.id,
        'content': content,
      });
      state = AsyncData(await _fetchNotes());
    } catch (e) {
      state = AsyncData(previous); // Rollback
      rethrow;
    }
  }

  Future<void> deleteNote(String id) async {
    final previous = state.valueOrNull ?? [];
    state = AsyncData(previous.where((n) => n.id != id).toList());
    try {
      await supabase.from('notes').delete().eq('id', id);
    } catch (e) {
      state = AsyncData(previous);
      rethrow;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling AsyncValue

class NotesScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(notesProvider).when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
        Text('Error: $e'),
        ElevatedButton(
          onPressed: () => ref.invalidate(notesProvider),
          child: const Text('Retry'),
        ),
      ])),
      data: (notes) => notes.isEmpty
        ? const Center(child: Text('No notes yet'))
        : ListView.builder(
            itemCount: notes.length,
            itemBuilder: (_, i) => ListTile(
              title: Text(notes[i].content),
              trailing: IconButton(
                icon: const Icon(Icons.delete),
                onPressed: () => ref.read(notesProvider.notifier).deleteNote(notes[i].id),
              ),
            ),
          ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Provider Dependencies

@riverpod
class UserProfile extends _$UserProfile {
  @override
  Future<Profile?> build() async {
    final session = ref.watch(authStateProvider).value?.session;
    if (session == null) return null;
    final data = await supabase.from('profiles').select()
      .eq('id', session.user.id).single();
    return Profile.fromJson(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

test('Counter increments correctly', () {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  expect(container.read(counterProvider), 0);
  container.read(counterProvider.notifier).increment();
  expect(container.read(counterProvider), 1);
});
Enter fullscreen mode Exit fullscreen mode

Summary

Riverpod 2.0's Notifier / AsyncNotifier + code generation gives you type-safe, low-boilerplate state management. The optimistic update pattern makes Supabase CRUD feel instant.


Building an AI Life Management app with Flutter × Supabase at 自分株式会社. Sharing indie dev insights every week.

Top comments (0)