56 MVVM in Riverpod
Q: I’m wondering if MVVM architecture can be used with Riverpod. If so, please give a simple example with explanation and diagrams.
Excellent question! Yes, MVVM (Model-View-ViewModel) architecture works beautifully with Riverpod. In fact, Riverpod’s design naturally supports MVVM patterns. Let me show you how to implement a simple user profile app using MVVM with Riverpod.
56.1 MVVM + Riverpod Example: User Profile App
56.1.1 Model Layer (Data & Business Logic)
// models/user.dart
class User {
final String id;
final String name;
final String email;
final int age;
{
User(this.id,
required this.name,
required this.email,
required this.age,
required });
{
User copyWith(String? id,
String? name,
String? email,
int? age,
}) {
return User(
: id ?? this.id,
id: name ?? this.name,
name: email ?? this.email,
email: age ?? this.age,
age
);}
}
// services/user_repository.dart
class UserRepository {
// Simulates API calls
Future<User> fetchUser(String id) async {
await Future.delayed(Duration(seconds: 1)); // Simulate network delay
return User(
: id,
id: 'John Doe',
name: 'john.doe@example.com',
email: 30,
age
);}
Future<void> updateUser(User user) async {
await Future.delayed(Duration(milliseconds: 500));
// Simulate API update
'User updated: ${user.name}');
print(}
}
56.1.2 ViewModel Layer (Riverpod Providers)
// viewmodels/user_viewmodel.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Repository provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepository();
});
// ViewModel state
class UserViewState {
final User? user;
final bool isLoading;
final String? error;
{
UserViewState(this.user,
this.isLoading = false,
this.error,
});
{
UserViewState copyWith(? user,
Userbool? isLoading,
String? error,
}) {
return UserViewState(
: user ?? this.user,
user: isLoading ?? this.isLoading,
isLoading: error ?? this.error,
error
);}
}
// ViewModel (StateNotifier)
class UserViewModel extends StateNotifier<UserViewState> {
final UserRepository _repository;
this._repository) : super(UserViewState());
UserViewModel(
// ViewModel methods (business logic for UI)
Future<void> loadUser(String userId) async {
= state.copyWith(isLoading: true, error: null);
state
try {
final user = await _repository.fetchUser(userId);
= state.copyWith(user: user, isLoading: false);
state } catch (e) {
= state.copyWith(error: e.toString(), isLoading: false);
state }
}
Future<void> updateUserName(String newName) async {
if (state.user == null) return;
final updatedUser = state.user!.copyWith(name: newName);
// Optimistic update
= state.copyWith(user: updatedUser);
state
try {
await _repository.updateUser(updatedUser);
} catch (e) {
// Revert on error
= state.copyWith(error: e.toString());
state }
}
void clearError() {
= state.copyWith(error: null);
state }
}
// ViewModel provider
final userViewModelProvider = StateNotifierProvider<UserViewModel, UserViewState>((ref) {
final repository = ref.watch(userRepositoryProvider);
return UserViewModel(repository);
});
56.1.3 View Layer (Flutter Widgets)
// views/user_profile_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class UserProfilePage extends ConsumerWidget {
@override
, WidgetRef ref) {
Widget build(BuildContext contextfinal viewState = ref.watch(userViewModelProvider);
final viewModel = ref.read(userViewModelProvider.notifier);
return Scaffold(
: AppBar(title: Text('User Profile')),
appBar: Padding(
body: EdgeInsets.all(16),
padding: Column(
child: CrossAxisAlignment.stretch,
crossAxisAlignment: [
children// Load user button
ElevatedButton(: () => viewModel.loadUser('123'),
onPressed: Text('Load User'),
child,
): 20),
SizedBox(height
// Content based on state
if (viewState.isLoading)
,
LoadingView()else if (viewState.error != null)
ErrorView(: viewState.error!,
error: () => viewModel.clearError(),
onRetry,
)else if (viewState.user != null)
: viewState.user!),
UserDetailsView(userelse
,
EmptyView(),
],
),
)
);}
}
class LoadingView extends StatelessWidget {
@override
{
Widget build(BuildContext context) return Center(
: Column(
child: [
children,
CircularProgressIndicator(): 16),
SizedBox(height'Loading user...'),
Text(,
],
)
);}
}
class ErrorView extends StatelessWidget {
final String error;
final VoidCallback onRetry;
{required this.error, required this.onRetry});
ErrorView(
@override
{
Widget build(BuildContext context) return Center(
: Column(
child: [
children.error, color: Colors.red, size: 48),
Icon(Icons: 16),
SizedBox(height'Error: $error'),
Text(: 16),
SizedBox(height
ElevatedButton(: onRetry,
onPressed: Text('Retry'),
child,
),
],
)
);}
}
class UserDetailsView extends ConsumerWidget {
final User user;
{required this.user});
UserDetailsView(
@override
, WidgetRef ref) {
Widget build(BuildContext contextfinal viewModel = ref.read(userViewModelProvider.notifier);
return Card(
: Padding(
child: EdgeInsets.all(16),
padding: Column(
child: CrossAxisAlignment.start,
crossAxisAlignment: [
children'Name: ${user.name}', style: TextStyle(fontSize: 18)),
Text(: 8),
SizedBox(height'Email: ${user.email}'),
Text(: 8),
SizedBox(height'Age: ${user.age}'),
Text(: 16),
SizedBox(height
ElevatedButton(: () => _showEditNameDialog(context, viewModel),
onPressed: Text('Edit Name'),
child,
),
],
),
)
);}
void _showEditNameDialog(BuildContext context, UserViewModel viewModel) {
final controller = TextEditingController(text: user.name);
showDialog(: context,
context: (context) => AlertDialog(
builder: Text('Edit Name'),
title: TextField(
content: controller,
controller: InputDecoration(labelText: 'Name'),
decoration,
): [
actions
TextButton(: () => Navigator.pop(context),
onPressed: Text('Cancel'),
child,
)
TextButton(: () {
onPressed.updateUserName(controller.text);
viewModel.pop(context);
Navigator},
: Text('Save'),
child,
),
],
)
);}
}
class EmptyView extends StatelessWidget {
@override
{
Widget build(BuildContext context) return Center(
: Text('No user data. Press "Load User" to fetch data.'),
child
);}
}
56.2 MVVM Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ MVVM + Riverpod │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ MODEL │ │ VIEWMODEL │ │ VIEW │ │
│ │ │ │ │ │ │ │
│ │ • User │◄───┤ • UserViewModel │◄───┤ • UserProfilePage│ │
│ │ • UserRepository│ │ • UserViewState │ │ • LoadingView │ │
│ │ │ │ │ │ • ErrorView │ │
│ │ Data & Business │ │ UI Logic & │ │ UI Components │ │
│ │ Logic │ │ State Mgmt │ │ & User Input │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
56.3 Riverpod Provider Relationship Diagram
Provider Dependency Graph:
┌─────────────────────────────────┐
│ userRepositoryProvider │
│ ┌─────────────────────────┐ │
│ │ UserRepository │ │
│ │ • fetchUser() │ │
│ │ • updateUser() │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
│
│ Injected via ref.watch()
▼
┌─────────────────────────────────┐
│ userViewModelProvider │
│ ┌─────────────────────────┐ │
│ │ UserViewModel │ │
│ │ • loadUser() │ │
│ │ • updateUserName() │ │
│ │ • clearError() │ │
│ │ │ │
│ │ State: UserViewState │ │
│ │ • user: User? │ │
│ │ • isLoading: bool │ │
│ │ • error: String? │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
│
│ ref.watch() / ref.read()
▼
┌─────────────────────────────────┐
│ View Layer │
│ • UserProfilePage │
│ • LoadingView │
│ • ErrorView │
│ • UserDetailsView │
└─────────────────────────────────┘
56.4 Data Flow Diagram
User Interaction Flow:
1. User taps "Load User"
│
▼
2. View calls: ref.read(userViewModelProvider.notifier).loadUser('123')
│
▼
3. UserViewModel.loadUser() called
│
├─ Sets state: isLoading = true
├─ Calls: _repository.fetchUser('123')
│ │
│ ▼
│ UserRepository.fetchUser() (Model layer)
│ │
│ ▼
│ Returns User data
│
└─ Sets state: user = userData, isLoading = false
│
▼
4. Riverpod notifies all watchers
│
▼
5. UserProfilePage rebuilds
│
▼
6. Shows UserDetailsView with user data
State Management Flow:
┌─────────────────────────────────────────────────────────────┐
│ UserViewState │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Initial: { user: null, isLoading: false, error: null }│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Loading: { user: null, isLoading: true, error: null }│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Success: { user: User(), isLoading: false, error: null }││ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Error: { user: null, isLoading: false, error: "..." }│ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
56.5 Key Benefits of MVVM + Riverpod
56.5.1 1. Clear Separation of Concerns
- Model: Pure business logic and data (testable without Flutter)
- ViewModel: UI state management and coordination (testable with mocks)
- View: Pure UI components (focused on presentation)
56.5.2 2. Testability
// Easy to unit test ViewModel
'should load user successfully', () async {
test(final mockRepo = MockUserRepository();
final viewModel = UserViewModel(mockRepo);
when(mockRepo.fetchUser('123')).thenAnswer((_) async => testUser);
await viewModel.loadUser('123');
.state.user, equals(testUser));
expect(viewModel.state.isLoading, false);
expect(viewModel});
56.5.3 3. Reactive UI
Views automatically update when ViewModel state changes, similar to your experience with reactive programming in R.
56.5.4 4. Dependency Injection
Riverpod handles dependency injection cleanly:
final userViewModelProvider = StateNotifierProvider<UserViewModel, UserViewState>((ref) {
final repository = ref.watch(userRepositoryProvider); // Auto-injected
return UserViewModel(repository);
});
This MVVM + Riverpod pattern scales very well for complex Flutter applications and provides excellent separation of concerns, making your code more maintainable and testable.