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

ComponentResponsibilityFlutter Implementation
ModelData layer (entities, API calls)Dart classes, repositories
ViewUI layer (widgets)StatelessWidget, StatefulWidget
ViewModelBusiness logic, state managementChangeNotifier, 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

text
lib/
├── 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

PracticeRecommendation
ViewModelNo BuildContext, no Flutter imports
ViewOnly UI code, no business logic
ModelPlain Dart classes, no Flutter dependencies
RepositoryHandle API calls, database access
StateManaged in ViewModel via ChangeNotifier/BLoC
TestingTest ViewModel independently

MVVM vs MVC vs MVP

PatternView LogicViewModel/Presenter
MVVMObserves ViewModelNo direct View reference
MVCController updates ViewController knows View
MVPPassive (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


Resources