DEV Community

Cover image for State Management in Flutter with Riverpod
Youssef Mohamed
Youssef Mohamed

Posted on

State Management in Flutter with Riverpod

Introduction

Managing state in Flutter apps can be tricky — especially when the app starts growing.
If you’ve tried setState or even Provider, you might have faced unnecessary rebuilds or limitations in accessing data outside widgets.

That’s where Riverpod comes in.
It’s a modern, safe, and testable state management solution built by the same creator of Provider, but without its limitations.

In this article, we’ll explore Riverpod step-by-step, and build a simple Authentication ViewModel as a real-world example.

Why Riverpod?

  1. No BuildContext needed

  2. Works perfectly with async data

  3. Easy testing & maintainability

  4. Less rebuilds = better performance

  5. Clean separation between UI and logic

Setting up Riverpod

Add the package to your project

flutter pub add flutter_riverpod
Enter fullscreen mode Exit fullscreen mode

Wrap your app with ProviderScope in main.dart

void main() {
  runApp(const ProviderScope(child: MyApp()));
}
Enter fullscreen mode Exit fullscreen mode

Basic Providers

Let’s look at a few types of providers Riverpod offers

  • Provider

Used for read-only values.

final greetingProvider = Provider((ref) => 'Hello Riverpod!');
Enter fullscreen mode Exit fullscreen mode

Usage:

Widget build(BuildContext context, WidgetRef ref) {
  final message = ref.watch(greetingProvider);
  return Text(message);
}

Enter fullscreen mode Exit fullscreen mode
  • StateProvider

Used for mutable state (like counters)

final counterProvider = StateProvider<int>((ref) => 0);
Enter fullscreen mode Exit fullscreen mode

Usage:

final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).state++;
Enter fullscreen mode Exit fullscreen mode
  • FutureProvider

For asynchronous data (e.g., fetching from an API)

final userProvider = FutureProvider((ref) async {
  await Future.delayed(const Duration(seconds: 2));
  return 'User Loaded!';
});
Enter fullscreen mode Exit fullscreen mode

Building an Authentication ViewModel

Let’s build a small example showing how Riverpod can manage real app logic — user login/logout.

  • User Model
class User {
  final String id;
  final String email;
  final String name;

  User({required this.id, required this.email, required this.name});
}
Enter fullscreen mode Exit fullscreen mode
  • Repository
class AuthRepository {
  Future<User> login(String email, String password) async {
    await Future.delayed(const Duration(seconds: 2));

    if (email == 'joe@test.com' && password == '123456') {
      return User(id: '1', email: email, name: 'Joe');
    } else {
      throw Exception('Invalid credentials');
    }
  }

  Future<void> logout() async => Future.delayed(const Duration(milliseconds: 500));
}
Enter fullscreen mode Exit fullscreen mode
  • ViewModel
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AuthState {
  final bool isLoading;
  final User? user;
  final String? error;

  const AuthState({this.isLoading = false, this.user, this.error});
}

class AuthViewModel extends StateNotifier<AuthState> {
  final AuthRepository _repo;

  AuthViewModel(this._repo) : super(const AuthState());

  Future<void> login(String email, String password) async {
    try {
      state = AuthState(isLoading: true);
      final user = await _repo.login(email, password);
      state = AuthState(user: user);
    } catch (e) {
      state = AuthState(error: e.toString());
    }
  }

  Future<void> logout() async {
    await _repo.logout();
    state = const AuthState();
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Providers
final authRepositoryProvider = Provider((ref) => AuthRepository());
final authViewModelProvider =
    StateNotifierProvider<AuthViewModel, AuthState>((ref) {
  return AuthViewModel(ref.watch(authRepositoryProvider));
});
Enter fullscreen mode Exit fullscreen mode
  • Login Screen
class LoginScreen extends ConsumerWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(authViewModelProvider);
    final viewModel = ref.read(authViewModelProvider.notifier);

    final email = TextEditingController();
    final pass = TextEditingController();

    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: state.isLoading
            ? const CircularProgressIndicator()
            : Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextField(controller: email, decoration: const InputDecoration(labelText: 'Email')),
                  TextField(controller: pass, decoration: const InputDecoration(labelText: 'Password'), obscureText: true),
                  ElevatedButton(
                    onPressed: () => viewModel.login(email.text, pass.text),
                    child: const Text('Login'),
                  ),
                  if (state.error != null)
                    Text(state.error!, style: const TextStyle(color: Colors.red)),
                  if (state.user != null)
                    Text('Welcome, ${state.user!.name}!'),
                ],
              ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Riverpod gives you the power of scalable and testable state management — perfect for both small and enterprise Flutter apps.

If you’re building something serious, Riverpod is definitely worth mastering.

Extra

Official Docs: link

Top comments (0)