Flutter How MVVM Architecture will looks like ?
#flutter#architecture#mvvm
Answer
Overview
MVVM (Model-View-ViewModel) is an architectural pattern that separates business logic from UI. In Flutter, it's commonly implemented using state management solutions like Provider, Riverpod, or BLoC.
MVVM Components
| Component | Responsibility | Flutter Implementation |
|---|---|---|
| Model | Data layer (entities, API calls) | Dart classes, repositories |
| View | UI layer (widgets) | StatelessWidget, StatefulWidget |
| ViewModel | Business logic, state management | ChangeNotifier, BLoC, Cubit |
MVVM Architecture Diagram
text┌─────────────┐ │ View │ ← Widgets (UI) │ (Widgets) │ └──────┬──────┘ │ │ Observes/Binds ↓ ┌─────────────┐ │ ViewModel │ ← Business Logic + State │ (Provider) │ └──────┬──────┘ │ │ Calls ↓ ┌─────────────┐ │ Model │ ← Data Layer (API, DB) │(Repository) │ └─────────────┘
Folder Structure
textlib/ ├── models/ # Data models │ ├── user.dart │ └── product.dart ├── views/ # UI (Widgets) │ ├── home_page.dart │ ├── user_list_page.dart │ └── product_detail_page.dart ├── viewmodels/ # Business logic │ ├── user_viewmodel.dart │ └── product_viewmodel.dart ├── services/ # API calls, repositories │ ├── api_service.dart │ ├── user_repository.dart │ └── database_service.dart └── main.dart
Complete MVVM Example
1. Model (Data Layer)
dart// lib/models/user.dart class User { final int id; final String name; final String email; User({required this.id, required this.name, required this.email}); factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], ); } Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'email': email, }; } }
2. Repository (Data Source)
dart// lib/services/user_repository.dart import 'package:http/http.dart' as http; import 'dart:convert'; import '../models/user.dart'; class UserRepository { final String baseUrl = 'https://jsonplaceholder.typicode.com Future<List<User>> fetchUsers() async { final response = await http.get(Uri.parse('$baseUrl/users')); if (response.statusCode == 200) { final List<dynamic> data = jsonDecode(response.body); return data.map((json) => User.fromJson(json)).toList(); } else { throw Exception('Failed to load users'); } } Future<User> fetchUserById(int id) async { final response = await http.get(Uri.parse('$baseUrl/users/$id')); if (response.statusCode == 200) { return User.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to load user'); } } Future<User> createUser(User user) async { final response = await http.post( Uri.parse('$baseUrl/users'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(user.toJson()), ); if (response.statusCode == 201) { return User.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to create user'); } } }
3. ViewModel (Business Logic)
dart// lib/viewmodels/user_viewmodel.dart import 'package:flutter/foundation.dart'; import '../models/user.dart'; import '../services/user_repository.dart'; class UserViewModel extends ChangeNotifier { final UserRepository _repository = UserRepository(); // State List<User> _users = []; bool _isLoading = false; String? _errorMessage; // Getters List<User> get users => _users; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; bool get hasError => _errorMessage != null; // Actions Future<void> fetchUsers() async { _isLoading = true; _errorMessage = null; notifyListeners(); try { _users = await _repository.fetchUsers(); } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; notifyListeners(); } } Future<void> addUser(User user) async { _isLoading = true; notifyListeners(); try { final newUser = await _repository.createUser(user); _users.add(newUser); } catch (e) { _errorMessage = e.toString(); } finally { _isLoading = false; notifyListeners(); } } void clearError() { _errorMessage = null; notifyListeners(); } }
4. View (UI Layer)
dart// lib/views/user_list_page.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../viewmodels/user_viewmodel.dart'; class UserListPage extends StatefulWidget { _UserListPageState createState() => _UserListPageState(); } class _UserListPageState extends State<UserListPage> { void initState() { super.initState(); // Fetch users on page load WidgetsBinding.instance.addPostFrameCallback((_) { context.read<UserViewModel>().fetchUsers(); }); } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Users (MVVM)')), body: Consumer<UserViewModel>( builder: (context, viewModel, child) { // Loading state if (viewModel.isLoading) { return Center(child: CircularProgressIndicator()); } // Error state if (viewModel.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Error: ${viewModel.errorMessage}'), ElevatedButton( onPressed: () => viewModel.fetchUsers(), child: Text('Retry'), ), ], ), ); } // Success state if (viewModel.users.isEmpty) { return Center(child: Text('No users found')); } return ListView.builder( itemCount: viewModel.users.length, itemBuilder: (context, index) { final user = viewModel.users[index]; return ListTile( leading: CircleAvatar(child: Text(user.name[0])), title: Text(user.name), subtitle: Text(user.email), ); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: () { // Add user context.read<UserViewModel>().addUser( User(id: 0, name: 'New User', email: 'new@example.com'), ); }, child: Icon(Icons.add), ), ); } }
5. Main (Setup)
dart// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'viewmodels/user_viewmodel.dart'; import 'views/user_list_page.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => UserViewModel()), ], child: MaterialApp( title: 'MVVM Example', home: UserListPage(), ), ); } }
MVVM with Different State Management
Using BLoC
dart// ViewModel (BLoC) class UserBloc extends Bloc<UserEvent, UserState> { final UserRepository repository; UserBloc(this.repository) : super(UserInitial()) { on<FetchUsers>(_onFetchUsers); } Future<void> _onFetchUsers(FetchUsers event, Emitter<UserState> emit) async { emit(UserLoading()); try { final users = await repository.fetchUsers(); emit(UserLoaded(users)); } catch (e) { emit(UserError(e.toString())); } } } // Events abstract class UserEvent {} class FetchUsers extends UserEvent {} // States abstract class UserState {} class UserInitial extends UserState {} class UserLoading extends UserState {} class UserLoaded extends UserState { final List<User> users; UserLoaded(this.users); } class UserError extends UserState { final String message; UserError(this.message); } // View BlocBuilder<UserBloc, UserState>( builder: (context, state) { if (state is UserLoading) { return CircularProgressIndicator(); } else if (state is UserLoaded) { return ListView.builder( itemCount: state.users.length, itemBuilder: (context, index) => ListTile(title: Text(state.users[index].name)), ); } else if (state is UserError) { return Text('Error: ${state.message}'); } return Text('No data'); }, )
MVVM Principles
1. Separation of Concerns
dart// ✅ Good - Separated class UserViewModel extends ChangeNotifier { // Business logic only } class UserListPage extends StatelessWidget { // UI only } // ❌ Bad - Mixed class UserListPage extends StatefulWidget { // UI + Business logic + API calls (tightly coupled) }
2. Testability
dart// Test ViewModel independently test('fetchUsers loads users', () async { final mockRepo = MockUserRepository(); final viewModel = UserViewModel(mockRepo); when(mockRepo.fetchUsers()).thenAnswer((_) async => [User(id: 1, name: 'Test', email: 'test@test.com')]); await viewModel.fetchUsers(); expect(viewModel.users.length, 1); expect(viewModel.users[0].name, 'Test'); });
Best Practices
| Practice | Recommendation |
|---|---|
| ViewModel | No BuildContext, no Flutter imports |
| View | Only UI code, no business logic |
| Model | Plain Dart classes, no Flutter dependencies |
| Repository | Handle API calls, database access |
| State | Managed in ViewModel via ChangeNotifier/BLoC |
| Testing | Test ViewModel independently |
MVVM vs MVC vs MVP
| Pattern | View Logic | ViewModel/Presenter |
|---|---|---|
| MVVM | Observes ViewModel | No direct View reference |
| MVC | Controller updates View | Controller knows View |
| MVP | Passive (no logic) | Presenter updates View directly |
Key Takeaways
Model: Data classes + repositories (API, DB)
View: Widgets (UI only, no business logic)
ViewModel: Business logic + state (ChangeNotifier, BLoC)
Benefits: Testable, maintainable, scalable