Tous les articles
FlutterDartMobile

Flutter + Riverpod 3: Code Generation, AsyncNotifier, and Offline-First Architecture

//
·8 min de lecture

Riverpod 3 replaces manual provider declarations with @riverpod code generation, ships AsyncNotifier for async state machines, and provides first-class patterns for offline-first mobile apps with local caching.

Riverpod 3 is the current stable state-management library for Flutter, and its code-generation story has matured significantly. The @riverpod annotation eliminates boilerplate, while AsyncNotifier and Notifier provide typed state machines that scale from simple counters to complex offline-first architectures.

>Setup

yaml
# pubspec.yaml
dependencies:
  flutter_riverpod: ^3.0.0
  riverpod_annotation: ^3.0.0

dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^3.0.0
  riverpod_lint: ^3.0.0
Dart
// main.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

>@riverpod code generation

Dart
// providers/user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_provider.g.dart';  // generated

// Simple async provider — fetches once
@riverpod
Future<User> user(Ref ref, {required String id}) async {
  final repo = ref.watch(userRepositoryProvider);
  return repo.getById(id);
}

// Family variant — same, auto-inferred from parameters
// Generated: userProvider(id: '42')

// Run code generation
// dart run build_runner watch --delete-conflicting-outputs

>AsyncNotifier — async state machine

Dart
// providers/posts_notifier.dart
part 'posts_notifier.g.dart';

@riverpod
class PostsNotifier extends _$PostsNotifier {
  @override
  Future<List<Post>> build() async {
    // build() is called once — returns the initial state
    return _loadPosts();
  }

  Future<List<Post>> _loadPosts() async {
    final repo = ref.read(postRepositoryProvider);
    return repo.fetchAll();
  }

  Future<void> create(String title) async {
    // Optimistic update
    state = AsyncData([
      ...state.requireValue,
      Post.optimistic(title: title),
    ]);

    try {
      final repo = ref.read(postRepositoryProvider);
      final post = await repo.create(title: title);
      state = AsyncData([
        ...state.requireValue.where((p) => !p.isOptimistic),
        post,
      ]);
    } catch (e) {
      // Revert — reload from source of truth
      state = AsyncLoading();
      state = await AsyncValue.guard(_loadPosts);
    }
  }
}

// UI — ConsumerWidget
class PostsList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postsAsync = ref.watch(postsNotifierProvider);
    return postsAsync.when(
      data:    (posts) => ListView(children: posts.map(PostTile.new).toList()),
      loading: () => const CircularProgressIndicator(),
      error:   (err, _) => Text('Error: $err'),
    );
  }
}

>keepAlive — prevent provider disposal

Dart
@Riverpod(keepAlive: true)
UserRepository userRepository(Ref ref) {
  // This provider stays alive even if no widget is listening.
  // Useful for repositories, HTTP clients, database connections.
  return UserRepository(ref.watch(httpClientProvider));
}

// Manual keepAlive with cancel link
@riverpod
Future<SearchResults> search(Ref ref, String query) async {
  final link = ref.keepAlive();      // prevent disposal while in flight
  final results = await searchApi(query);
  // link.close() — call to allow disposal again (e.g. on page leave)
  return results;
}

>Offline-first pattern

Dart
@riverpod
class OfflineFirstPosts extends _$OfflineFirstPosts {
  @override
  Future<List<Post>> build() async {
    // 1. Return cached data immediately
    final cache = ref.read(localCacheProvider);
    final cached = await cache.getPosts();
    if (cached.isNotEmpty) {
      state = AsyncData(cached);  // show stale data
    }

    // 2. Fetch fresh data in background
    try {
      final fresh = await ref.read(postRepositoryProvider).fetchAll();
      await cache.savePosts(fresh);
      return fresh;
    } catch (_) {
      // Network error — return cached if available
      if (cached.isNotEmpty) return cached;
      rethrow;
    }
  }
}