In Riverpod state management, which provider should I use and when? What are Provider, FutureProvider, NotifierProvider, AsyncNotifierProvider, and StreamProvider? When should I use Riverpod annotations (@riverpod)? What are Mutations in Riverpod 3.0?

#riverpod#state-management#provider#asyncnotifier#notifier#mutations#dependency-injection

Answer

Overview

Riverpod is Flutter's most powerful state management and dependency injection framework. Modern Riverpod (v3.0+) solves two critical problems in Flutter development:

text
1. Dependency Injection (DI)
2. State Management

Riverpod 3.0 introduces major improvements:

  • Mutations for side effects (POST, PUT, DELETE)
  • Unified provider APIs
  • Better async lifecycle handling
  • Improved code generation
  • Retry mechanisms

This guide covers production-level architecture with real-world examples.


Quick Decision Guide

text
Need dependency injection?
  → Use Provider with @riverpod

Need read-only async data (GET)?
  → Use FutureProvider or AsyncNotifierProvider

Need mutable state (local)?
  → Use NotifierProvider

Need async mutable state (API)?
  → Use AsyncNotifierProvider

Need side effects (POST/PUT/DELETE)?
  → Use Mutation (Riverpod 3.0+)

Need realtime streams?
  → Use StreamProvider or StreamNotifierProvider

Core Concept: Providers

Riverpod revolves around Providers.

A provider is a function or class that:

  • Exposes data/state
  • Caches values automatically
  • Rebuilds UI when state changes
  • Enables dependency injection

Providers can represent:

text
Services (ApiClient, AuthService)
Repositories (UserRepository)
Configurations (AppConfig)
Async data (User profile, Posts)
Business logic (LoginNotifier)

1. Dependency Injection in Riverpod

Key Concept

Providers are Riverpod's Dependency Injection system.

Instead of manually creating instances:

dart
// ❌ Without DI - Manual instantiation
class UsersService {
  final repo = UserRepository();
}

With Riverpod:

dart
// ✅ With Riverpod DI - Injected

UserRepository userRepository(UserRepositoryRef ref) {
  return UserRepository();
}

// Usage
final repo = ref.read(userRepositoryProvider);

Benefits:

  • Centralized dependency management
  • Easy testing (swap implementations)
  • Automatic lifecycle management
  • Dependencies injected automatically

2. Types of Providers - Complete Guide

Provider Comparison Table

Provider TypePurposeMutableAsyncUse Case
ProviderDependency Injection❌ No⚠️ Can beInject services, configs
FutureProviderAsync read-only data❌ No✅ YesAPI GET, database queries
StreamProviderRealtime streams❌ No✅ YesFirebase, WebSocket
NotifierProviderMutable sync state✅ Yes❌ NoCounter, local UI state
AsyncNotifierProviderMutable async state✅ Yes✅ YesUser profile, posts list
StreamNotifierProviderMutable stream state✅ Yes✅ YesChat messages
StateProviderSimple mutable state✅ Yes❌ NoTheme, filters (deprecated)

3. Provider - Dependency Injection

Purpose

Inject dependencies across the app.

When to Use

text
✅ API clients
✅ Repositories
✅ Services
✅ Configurations
✅ Utility classes

Example

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

part 'providers.g.dart';

// Inject API client

Dio dio(DioRef ref) {
  return Dio(BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
  ));
}

// Inject repository (depends on Dio)

UserRepository userRepository(UserRepositoryRef ref) {
  final dio = ref.watch(dioProvider);
  return UserRepository(dio);
}

Key Point: Provider is for dependency injection, not state management.


4. FutureProvider - Async Read-Only Data

Purpose

Fetch data once (GET requests).

When to Use

text
✅ Fetch user profile (one-time)
✅ Load config from API
✅ Database queries
❌ Refreshable lists (use AsyncNotifier instead)

Example

dart

Future<User> userProfile(UserProfileRef ref, int userId) async {
  final repo = ref.watch(userRepositoryProvider);
  return repo.getUserById(userId);
}

// UI Usage
class UserProfileScreen extends ConsumerWidget {
  final int userId;

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

    return userAsync.when(
      data: (user) => Text('Hello, ${user.name}'),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

Limitation: Cannot refresh/reload manually. For that, use AsyncNotifier.


5. StreamProvider - Realtime Streams

Purpose

Listen to continuous data streams.

When to Use

text
✅ Firebase Firestore streams
✅ WebSocket connections
✅ Real-time notifications
✅ Live location updates

Example

dart

Stream<List<Message>> messagesStream(MessagesStreamRef ref, String chatId) {
  final firestore = ref.watch(firestoreProvider);
  return firestore
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .orderBy('timestamp')
      .snapshots()
      .map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
}

// UI Usage
class ChatScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final messagesAsync = ref.watch(messagesStreamProvider('chat123'));

    return messagesAsync.when(
      data: (messages) => ListView.builder(
        itemCount: messages.length,
        itemBuilder: (context, index) => MessageTile(messages[index]),
      ),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

6. NotifierProvider - Mutable Synchronous State

Purpose

Manage mutable state without async operations.

When to Use

text
✅ Counter
✅ Form state (before submission)
✅ UI toggles (theme, dark mode)
✅ Filters, sorting
❌ API calls (use AsyncNotifier)

Example

dart

class Counter extends _$Counter {
  
  int build() {
    return 0; // Initial state
  }

  void increment() {
    state++; // Mutable
  }

  void decrement() {
    state--;
  }

  void reset() {
    state = 0;
  }
}

// UI Usage
class CounterScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

7. AsyncNotifierProvider - Most Common in Production

Purpose

Manage mutable async state (API calls, caching).

When to Use

text
✅ Fetch + cache user list
✅ Pagination
✅ Refresh/reload data
✅ CRUD operations (via mutations)
✅ Network state management

Example

dart

class Users extends _$Users {
  
  Future<List<User>> build() async {
    // Initial fetch
    final repo = ref.read(userRepositoryProvider);
    return repo.fetchUsers();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    final repo = ref.read(userRepositoryProvider);
    state = await AsyncValue.guard(() => repo.fetchUsers());
  }

  Future<void> addUser(User user) async {
    // Optimistic update
    final currentUsers = state.value ?? [];
    state = AsyncData([...currentUsers, user]);

    try {
      await ref.read(userRepositoryProvider).createUser(user);
    } catch (e) {
      // Rollback on error
      state = AsyncData(currentUsers);
      rethrow;
    }
  }
}

// UI Usage
class UsersScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    return RefreshIndicator(
      onRefresh: () => ref.read(usersProvider.notifier).refresh(),
      child: usersAsync.when(
        data: (users) => ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) => UserTile(users[index]),
        ),
        loading: () => Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(child: Text('Error: $err')),
      ),
    );
  }
}

This is the most common pattern in production apps.


8. Riverpod Mutations (Riverpod 3.0+)

What Are Mutations?

Mutations handle side effects like:

text
POST requests
PUT updates
DELETE operations
Form submissions
Login/logout
File uploads

Why Mutations?

Problem without Mutations:

dart
// ❌ Pollutes state with UI concerns
class Users extends _$Users {
  bool isCreating = false; // UI state mixed with data
  String? createError;

  Future<void> createUser(User user) async {
    isCreating = true;
    createError = null;
    // ... logic
  }
}

With Mutations:

dart
// ✅ Clean separation
final createUserMutation = Mutation<User>();

// State stays clean (just data)

Mutation States

StateMeaning
text
MutationIdle
No operation running
text
MutationPending
Loading (request in progress)
text
MutationSuccess
Request succeeded
text
MutationError
Request failed

Production Mutation Example

dart
import 'package:riverpod_mutation/riverpod_mutation.dart';

// Define mutation
final createUserMutation = Mutation<User>();

// Use in UI
class CreateUserButton extends ConsumerWidget {
  final String name;

  
  Widget build(BuildContext context, WidgetRef ref) {
    final mutation = ref.watch(createUserMutation);

    return ElevatedButton(
      onPressed: mutation is MutationPending
          ? null
          : () async {
              createUserMutation.run(ref, (tsx) async {
                // Get repository
                final repo = tsx.get(userRepositoryProvider);

                // Execute mutation
                final user = await repo.createUser(name);

                // Invalidate users list to refresh
                ref.invalidate(usersProvider);

                return user;
              });
            },
      child: mutation is MutationPending
          ? CircularProgressIndicator()
          : Text('Create User'),
    );
  }
}

Mutations Are Optional

You can still use AsyncNotifier for write operations:

dart

class Users extends _$Users {
  
  Future<List<User>> build() async => fetchUsers();

  // Write operation without mutation
  Future<void> createUser(String name) async {
    final repo = ref.read(userRepositoryProvider);
    await repo.createUser(name);
    ref.invalidate(usersProvider); // Refresh
  }
}

When to use Mutations:

  • ✅ Need explicit loading/error states in UI
  • ✅ Complex form submissions
  • ✅ Retry logic needed

When to skip Mutations:

  • ✅ Simple CRUD with AsyncNotifier is enough
  • ✅ Don't need separate mutation state

9. When to Use Riverpod Annotations (@riverpod)

Always Use @riverpod for New Code

Modern Riverpod uses code generation with

text
@riverpod
annotation.

Setup:

yaml
# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.0
  riverpod_annotation: ^2.3.0

dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^2.3.0

Run generator:

bash
flutter pub run build_runner watch

Benefits:

  • Less boilerplate
  • Type-safe
  • Auto-generated providers
  • Better refactoring support

Without @riverpod (manual):

dart
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepository();
});

With @riverpod (generated):

dart

UserRepository userRepository(UserRepositoryRef ref) {
  return UserRepository();
}
// Provider auto-generated!

10. Production-Level Architecture

Recommended Stack

text
Riverpod (annotation + code generation)
   +
MVVM Architecture
   +
Feature-First Folder Structure
   +
Repository Pattern
   +
Dio for networking

Project Structure

text
lib/
├── core/
│   ├── network/
│   │   └── dio_provider.dart
│   └── router/
│       └── app_router.dart
├── features/
│   ├── users/
│   │   ├── data/
│   │   │   ├── models/
│   │   │   │   └── user_model.dart
│   │   │   ├── repositories/
│   │   │   │   └── user_repository.dart
│   │   │   └── data_sources/
│   │   │       └── user_remote_data_source.dart
│   │   ├── domain/
│   │   │   └── entities/
│   │   │       └── user.dart
│   │   ├── presentation/
│   │   │   ├── providers/
│   │   │   │   └── users_provider.dart
│   │   │   ├── screens/
│   │   │   │   └── users_screen.dart
│   │   │   └── widgets/
│   │   │       └── user_tile.dart
│   │   └── users_feature.dart
│   └── posts/
│       └── ... (same structure)
└── main.dart

11. Complete Production Example

API: JSONPlaceholder (https://jsonplaceholder.typicode.com)

We'll build a Users feature with:

  • Fetch users (GET)
  • Create user (POST with Mutation)
  • MVVM + Feature-first structure
  • Riverpod annotation

Step 1: Core - Dio Provider

dart
// core/network/dio_provider.dart
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'dio_provider.g.dart';


Dio dio(DioRef ref) {
  return Dio(
    BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com',
      connectTimeout: Duration(seconds: 5),
      receiveTimeout: Duration(seconds: 3),
      headers: {
        'Content-Type': 'application/json',
      },
    ),
  )..interceptors.add(LogInterceptor(
      requestBody: true,
      responseBody: true,
    ));
}

Step 2: Domain - Entity

dart
// features/users/domain/entities/user.dart
class User {
  final int id;
  final String name;
  final String email;
  final String phone;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
  });
}

Step 3: Data - Model

dart
// features/users/data/models/user_model.dart
import '../../domain/entities/user.dart';

class UserModel extends User {
  UserModel({
    required super.id,
    required super.name,
    required super.email,
    required super.phone,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] ?? 0,
      name: json['name'] ?? '',
      email: json['email'] ?? '',
      phone: json['phone'] ?? '',
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'phone': phone,
    };
  }
}

Step 4: Data - Remote Data Source

dart
// features/users/data/data_sources/user_remote_data_source.dart
import 'package:dio/dio.dart';
import '../models/user_model.dart';

class UserRemoteDataSource {
  final Dio dio;

  UserRemoteDataSource(this.dio);

  Future<List<UserModel>> fetchUsers() async {
    final response = await dio.get('/users');
    final List<dynamic> data = response.data;
    return data.map((json) => UserModel.fromJson(json)).toList();
  }

  Future<UserModel> getUserById(int id) async {
    final response = await dio.get('/users/$id');
    return UserModel.fromJson(response.data);
  }

  Future<UserModel> createUser(UserModel user) async {
    final response = await dio.post('/users', data: user.toJson());
    return UserModel.fromJson(response.data);
  }

  Future<UserModel> updateUser(int id, UserModel user) async {
    final response = await dio.put('/users/$id', data: user.toJson());
    return UserModel.fromJson(response.data);
  }

  Future<void> deleteUser(int id) async {
    await dio.delete('/users/$id');
  }
}

Step 5: Data - Repository

dart
// features/users/data/repositories/user_repository.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/network/dio_provider.dart';
import '../../domain/entities/user.dart';
import '../data_sources/user_remote_data_source.dart';
import '../models/user_model.dart';

part 'user_repository.g.dart';


UserRepository userRepository(UserRepositoryRef ref) {
  final dio = ref.watch(dioProvider);
  final dataSource = UserRemoteDataSource(dio);
  return UserRepository(dataSource);
}

class UserRepository {
  final UserRemoteDataSource _dataSource;

  UserRepository(this._dataSource);

  Future<List<User>> fetchUsers() async {
    return await _dataSource.fetchUsers();
  }

  Future<User> getUserById(int id) async {
    return await _dataSource.getUserById(id);
  }

  Future<User> createUser({
    required String name,
    required String email,
    required String phone,
  }) async {
    final userModel = UserModel(
      id: 0, // Server will assign
      name: name,
      email: email,
      phone: phone,
    );
    return await _dataSource.createUser(userModel);
  }

  Future<User> updateUser(int id, User user) async {
    final userModel = UserModel(
      id: user.id,
      name: user.name,
      email: user.email,
      phone: user.phone,
    );
    return await _dataSource.updateUser(id, userModel);
  }

  Future<void> deleteUser(int id) async {
    await _dataSource.deleteUser(id);
  }
}

Step 6: Presentation - Providers (ViewModel)

dart
// features/users/presentation/providers/users_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../data/repositories/user_repository.dart';
import '../../domain/entities/user.dart';

part 'users_provider.g.dart';

// Main users list provider (ViewModel)

class Users extends _$Users {
  
  Future<List<User>> build() async {
    // Initial fetch
    return _fetchUsers();
  }

  Future<List<User>> _fetchUsers() async {
    final repo = ref.read(userRepositoryProvider);
    return await repo.fetchUsers();
  }

  // Refresh users
  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchUsers());
  }

  // Delete user
  Future<void> deleteUser(int id) async {
    // Optimistic delete
    final currentUsers = state.value ?? [];
    final updatedUsers = currentUsers.where((u) => u.id != id).toList();
    state = AsyncData(updatedUsers);

    try {
      await ref.read(userRepositoryProvider).deleteUser(id);
    } catch (e) {
      // Rollback on error
      state = AsyncData(currentUsers);
      rethrow;
    }
  }
}

// Single user provider

Future<User> userDetail(UserDetailRef ref, int userId) async {
  final repo = ref.read(userRepositoryProvider);
  return await repo.getUserById(userId);
}

Step 7: Presentation - Mutation (Optional)

dart
// features/users/presentation/providers/create_user_mutation.dart
import 'package:riverpod_mutation/riverpod_mutation.dart';
import '../../domain/entities/user.dart';

// Create user mutation
final createUserMutation = Mutation<User>();

// Update user mutation
final updateUserMutation = Mutation<User>();

Step 8: Presentation - Screen

dart
// features/users/presentation/screens/users_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/users_provider.dart';
import '../widgets/user_tile.dart';
import 'create_user_screen.dart';

class UsersScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Users'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () => ref.read(usersProvider.notifier).refresh(),
          ),
        ],
      ),
      body: usersAsync.when(
        data: (users) {
          if (users.isEmpty) {
            return Center(child: Text('No users found'));
          }

          return RefreshIndicator(
            onRefresh: () => ref.read(usersProvider.notifier).refresh(),
            child: ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                final user = users[index];
                return UserTile(
                  user: user,
                  onDelete: () async {
                    final confirmed = await showDialog<bool>(
                      context: context,
                      builder: (context) => AlertDialog(
                        title: Text('Delete User'),
                        content: Text('Delete ${user.name}?'),
                        actions: [
                          TextButton(
                            onPressed: () => Navigator.pop(context, false),
                            child: Text('Cancel'),
                          ),
                          TextButton(
                            onPressed: () => Navigator.pop(context, true),
                            child: Text('Delete'),
                          ),
                        ],
                      ),
                    );

                    if (confirmed == true) {
                      await ref.read(usersProvider.notifier).deleteUser(user.id);
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('${user.name} deleted')),
                      );
                    }
                  },
                );
              },
            ),
          );
        },
        loading: () => Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: $error'),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () => ref.read(usersProvider.notifier).refresh(),
                child: Text('Retry'),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => CreateUserScreen()),
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

Step 9: Presentation - Widget

dart
// features/users/presentation/widgets/user_tile.dart
import 'package:flutter/material.dart';
import '../../domain/entities/user.dart';

class UserTile extends StatelessWidget {
  final User user;
  final VoidCallback? onDelete;

  const UserTile({
    required this.user,
    this.onDelete,
  });

  
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(
        child: Text(user.name[0].toUpperCase()),
      ),
      title: Text(user.name),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(user.email),
          Text(user.phone, style: TextStyle(fontSize: 12)),
        ],
      ),
      trailing: IconButton(
        icon: Icon(Icons.delete, color: Colors.red),
        onPressed: onDelete,
      ),
    );
  }
}

Step 10: Create User Screen (with Mutation)

dart
// features/users/presentation/screens/create_user_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/create_user_mutation.dart';
import '../providers/users_provider.dart';
import '../../data/repositories/user_repository.dart';

class CreateUserScreen extends ConsumerStatefulWidget {
  
  _CreateUserScreenState createState() => _CreateUserScreenState();
}

class _CreateUserScreenState extends ConsumerState<CreateUserScreen> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();

  
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final mutation = ref.watch(createUserMutation);

    return Scaffold(
      appBar: AppBar(title: Text('Create User')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _nameController,
              decoration: InputDecoration(labelText: 'Name'),
            ),
            SizedBox(height: 16),
            TextField(
              controller: _emailController,
              decoration: InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
            ),
            SizedBox(height: 16),
            TextField(
              controller: _phoneController,
              decoration: InputDecoration(labelText: 'Phone'),
              keyboardType: TextInputType.phone,
            ),
            SizedBox(height: 32),
            ElevatedButton(
              onPressed: mutation is MutationPending
                  ? null
                  : () async {
                      final name = _nameController.text.trim();
                      final email = _emailController.text.trim();
                      final phone = _phoneController.text.trim();

                      if (name.isEmpty || email.isEmpty || phone.isEmpty) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('All fields required')),
                        );
                        return;
                      }

                      createUserMutation.run(ref, (tsx) async {
                        final repo = tsx.get(userRepositoryProvider);
                        final user = await repo.createUser(
                          name: name,
                          email: email,
                          phone: phone,
                        );

                        // Refresh users list
                        ref.invalidate(usersProvider);

                        return user;
                      });

                      // Navigate back on success
                      if (mutation is MutationSuccess) {
                        Navigator.pop(context);
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('User created successfully')),
                        );
                      }
                    },
              child: mutation is MutationPending
                  ? SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : Text('Create User'),
            ),
            if (mutation is MutationError)
              Padding(
                padding: EdgeInsets.only(top: 16),
                child: Text(
                  'Error: ${mutation.error}',
                  style: TextStyle(color: Colors.red),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Step 11: Main App

dart
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'features/users/presentation/screens/users_screen.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Production Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: UsersScreen(),
    );
  }
}

12. Complete Feature Structure

text
lib/
├── core/
│   └── network/
│       ├── dio_provider.dart
│       └── dio_provider.g.dart        # Generated
├── features/
│   └── users/
│       ├── data/
│       │   ├── data_sources/
│       │   │   └── user_remote_data_source.dart
│       │   ├── models/
│       │   │   └── user_model.dart
│       │   └── repositories/
│       │       ├── user_repository.dart
│       │       └── user_repository.g.dart  # Generated
│       ├── domain/
│       │   └── entities/
│       │       └── user.dart
│       └── presentation/
│           ├── providers/
│           │   ├── users_provider.dart
│           │   ├── users_provider.g.dart   # Generated
│           │   └── create_user_mutation.dart
│           ├── screens/
│           │   ├── users_screen.dart
│           │   └── create_user_screen.dart
│           └── widgets/
│               └── user_tile.dart
└── main.dart

13. Best Practices Summary

1. Always Use Code Generation

dart
Use 
Avoid manual provider declaration

2. Dependency Injection

dart
Inject via Provider
Don't create instances manually

3. State Management

dart
AsyncNotifier for mutable async state
FutureProvider for read-only async
NotifierProvider for sync mutable state
Don't use StateProvider (deprecated)

4. Mutations

dart
Use for explicit side effects (POST/PUT/DELETE)
Clean separation of concerns
⚠️ Optional - AsyncNotifier can handle writes too

5. Architecture

dart
Feature-first structure
✅ MVVM (ViewModel = Provider)
Repository pattern
Dependency injection

14. Migration from Old Riverpod

ChangeNotifier → AsyncNotifier

dart
// ❌ Old - ChangeNotifier
class UserNotifier extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;

  Future<void> fetchUser() async {
    _isLoading = true;
    notifyListeners();
    _user = await api.getUser();
    _isLoading = false;
    notifyListeners();
  }
}

// ✅ New - AsyncNotifier

class UserNotifier extends _$UserNotifier {
  
  Future<User?> build() async {
    return await ref.read(apiProvider).getUser();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => ref.read(apiProvider).getUser());
  }
}

StateNotifier → NotifierProvider

dart
// ❌ Old - StateNotifier
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// ✅ New - NotifierProvider

class Counter extends _$Counter {
  
  int build() => 0;
  void increment() => state++;
}

15. Summary Table

FeatureProviderFutureProviderNotifierProviderAsyncNotifierProviderMutation
PurposeDIAsync readSync stateAsync stateSide effects
Mutable
Async⚠️
Refresh
Use @riverpod⚠️
Production Use✅ Always⚠️ Limited✅ Often✅ Most Common✅ Optional

16. Key Takeaways

Provider is for Dependency Injection (inject services, repos)

AsyncNotifier is the most common provider in production (mutable async state)

FutureProvider is for read-only async data (cannot refresh)

Mutations are optional but useful for explicit side effects

Always use @riverpod annotation for code generation

Feature-first + MVVM + Repository is the recommended architecture


Resources