effidevFlutter · Cloudflare edge · Cloud cost optimization
English

Flutter Riverpod 3.0 Architecture Guide: Notifier, AsyncNotifier, and AutoDispose Patterns

Flutter Riverpod 3.0 Architecture Guide

Flutter Riverpod 3.0 Architecture Guide: Notifier, AsyncNotifier, and AutoDispose Patterns

Riverpod, Flutter’s leading state management solution, has arrived with a major version 3.0 architectural overhaul. Legacy patterns like StateNotifier, ChangeNotifier, and manually created providers are now completely superseded by class-based Notifier / AsyncNotifier and standardized code generation via riverpod_generator.

For context, the @riverpod code generation approach has enabled autoDispose by default (releasing memory when screen routes are unmounted) since Riverpod 2.0 already. What 3.0 actually changes is unifying the previously fragmented provider type interfaces — AutoDispose* / Family* and friends — into a single Provider/Notifier API, simplifying the surface considerably.

In this article, we cover the core breaking changes in Riverpod 3.0 and practical production patterns for using Notifier, AsyncNotifier, and family parameters.


1. Comparing Legacy Riverpod vs. Riverpod 3.0 Standards

Feature Legacy Riverpod (1.x / 2.x) Riverpod 3.0 Standard
Type Interface Split across Provider / AutoDisposeProvider / FamilyProvider, etc. Unified into Provider/Notifier (default autoDispose itself has been in place since 2.0 for @riverpod code generation)
State Controllers StateNotifierProvider / FutureProvider Notifier / AsyncNotifier with @riverpod annotation
Family Arguments Manual .family wrappers Native method parameters in generated build methods
Async State Handling Manual AsyncValue.when() handling Enhanced AsyncValue helpers & automatic state guarding

2. Practical Notifier and AsyncNotifier Implementation

1) Synchronous State: Notifier Implementation

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_provider.g.dart';

// @riverpod annotation enables autoDispose by default
@riverpod
class CounterNotifier extends _$CounterNotifier {
  @override
  int build() {
    // Initial state setup (Re-evaluated on screen remounts)
    return 0;
  }

  void increment() {
    state = state + 1;
  }

  void decrement() {
    state = state - 1;
  }
}

2) Asynchronous State: AsyncNotifier & Family Implementation

import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/user.dart';

part 'user_provider.g.dart';

// Pass family parameters directly as arguments to the build method
@riverpod
class UserDetailNotifier extends _$UserDetailNotifier {
  @override
  Future<User> build(String userId) async {
    // Async data fetching
    return await ref.read(userRepositoryProvider).fetchUser(userId);
  }

  Future<void> updateUserName(String newName) async {
    // Set loading state
    state = const AsyncLoading();

    // Guarded state update
    state = await AsyncValue.guard(() async {
      final updatedUser = await ref
          .read(userRepositoryProvider)
          .updateName(arg, newName); // 'arg' accesses the family parameter (userId)
      return updatedUser;
    });
  }
}

3. Consuming AsyncValue & Handling AutoDispose in Widgets

In the UI layer, subscribe to state updates using ref.watch and branch rendering using AsyncValue.when.

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

class UserProfileScreen extends ConsumerWidget {
  final String userId;
  const UserProfileScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Subscribe to provider with family parameter
    final userAsync = ref.watch(userDetailNotifierProvider(userId));

    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: userAsync.when(
        data: (user) => Column(
          children: [
            Text('Name: ${user.name}'),
            ElevatedButton(
              onPressed: () {
                ref
                    .read(userDetailNotifierProvider(userId).notifier)
                    .updateUserName('New Name');
              },
              child: const Text('Update Name'),
            ),
          ],
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(child: Text('Error: $err')),
      ),
    );
  }
}

4. Production AutoDispose Considerations

  1. Global Auth & Settings State: For application-wide singletons (e.g., authentication tokens or global theme preferences), explicitly set @Riverpod(keepAlive: true).
  2. Dynamic ref.keepAlive() Links: To temporarily preserve state while an async transaction completes after screen unmount, call final link = ref.keepAlive(); and release it later via link.close().
  3. Disposal Lifecycle: AutoDispose doesn’t release memory the instant the last listener disappears — it waits one frame’s worth of grace period, and only disposes the state if it’s still unused after that frame. Plan caching strategies (e.g. for temporarily saved form input) accordingly.

Summary and Conclusion

Riverpod 3.0 delivers a modern state management paradigm with minimal boilerplate and default memory safety.

[!NOTE]

  1. Migrate away from legacy StateNotifier to class-based Notifier and AsyncNotifier.
  2. Standardize on riverpod_generator to automate family parameter handling and provider definitions.
  3. Understand default autoDispose rules and explicitly annotate global providers with @Riverpod(keepAlive: true) to eliminate leaks.

Adopting Riverpod 3.0 elevated type safety and maintainability across Flutter codebases.