Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
MVVM (Model-View-ViewModel) архитектура
Что такое MVVM
MVVM — это архитектурный паттерн, который разделяет приложение на три слоя:
- Model — бизнес-логика и данные
- View — UI, то что видит пользователь
- ViewModel — промежуточный слой, который объединяет Model и View
Это паттерн часто используется в мобильной разработке и веб-приложениях для разделения ответственности и облегчения тестирования.
Сравнение архитектур
MVC (старый подход):
User Input → Controller → Model ↔ View ↔ User
MVP:
User Input → Presenter ↔ Model
↓
View (тупая, только отрисовка)
MVVM (современный подход):
User Input → View ↔ ViewModel ↔ Model
(binaing) (state)
Слои MVVM
1. Model (Модель данных)
class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
// factory, copyWith, toJson и т.д.
}
class UserRepository {
Future<User> fetchUser(String id) async {
final response = await _apiClient.get('/users/$id');
return User.fromJson(response);
}
Future<void> updateUser(User user) async {
await _apiClient.put('/users/${user.id}', user.toJson());
}
}
Model знает о:
- Структуре данных
- Как получить/сохранить данные (API, БД, локальное хранилище)
- Валидация данных
Мodel не знает о:
- UI
- ViewModel
- Как отрисовывать данные
2. ViewModel (Логика представления)
class UserViewModel with ChangeNotifier {
final UserRepository _repository;
// Состояние
User? _user;
bool _isLoading = false;
String? _error;
// Геттеры
User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
UserViewModel({required UserRepository repository})
: _repository = repository;
// Бизнес-логика
Future<void> loadUser(String id) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_user = await _repository.fetchUser(id);
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> updateUser(String newName) async {
if (_user == null) return;
_isLoading = true;
notifyListeners();
try {
final updatedUser = _user!.copyWith(name: newName);
await _repository.updateUser(updatedUser);
_user = updatedUser;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
ViewModel знает о:
- Бизнес-логика приложения
- Состояние UI (загрузка, ошибки, данные)
- Как преобразовать данные для UI
ViewModel не знает о:
- Как отрисовывать Widget'ы
- BuildContext
- MaterialApp или CupertinoApp
3. View (UI слой)
class UserScreen extends StatelessWidget {
final String userId;
const UserScreen({required this.userId});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserViewModel>(
create: (_) => UserViewModel(
repository: context.read<UserRepository>(),
)..loadUser(userId),
child: Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: Consumer<UserViewModel>(
builder: (context, viewModel, _) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (viewModel.error != null) {
return Center(child: Text('Error: ${viewModel.error}'));
}
final user = viewModel.user;
if (user == null) {
return Center(child: Text('No user found'));
}
return Column(
children: [
Text(user.name, style: Theme.of(context).textTheme.headlineSmall),
Text(user.email),
SizedBox(height: 16),
TextField(
onSubmitted: (newName) => viewModel.updateUser(newName),
decoration: InputDecoration(hintText: 'New name'),
),
],
);
},
),
),
);
}
}
View знает о:
- Как отрисовывать Widget'ы
- Как реагировать на пользовательский ввод
- BuildContext и навигация
View не знает о:
- Откуда берутся данные
- Как выполняется бизнес-логика
Преимущества MVVM
1. Тестируемость
test('UserViewModel should load user', () async {
final mockRepository = MockUserRepository();
when(mockRepository.fetchUser('123'))
.thenAnswer((_) async => User(id: '123', name: 'John', email: 'john@example.com'));
final viewModel = UserViewModel(repository: mockRepository);
await viewModel.loadUser('123');
expect(viewModel.user?.name, 'John');
expect(viewModel.isLoading, false);
verify(mockRepository.fetchUser('123')).called(1);
});
Мы тестируем бизнес-логику отдельно от UI!
2. Разделение ответственности
- Дизайнер работает с View
- Бекенд-разработчик работает с Model (API)
- Разработчик мобила работает с ViewModel (связывает их)
3. Переиспользуемость Один ViewModel может использоваться разными View'ями (например, список юзеров и детали юзера).
4. Легко масштабировать Когда приложение растёт, архитектура остаётся понятной.
MVVM в разных фреймворках
Flutter + Provider
ChangeNotifierProvider<UserViewModel>(
create: (_) => UserViewModel(...),
child: UserScreen(),
)
Flutter + Riverpod
final userProvider = FutureProvider((ref) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser('123');
});
Flutter + Bloc
BlocProvider<UserBloc>(
create: (context) => UserBloc(userRepository: context.read()),
child: UserScreen(),
)
MVVM vs MVC vs MVP
| MVC | MVP | MVVM | |
|---|---|---|---|
| Two-way binding | Да | Нет | Да (重要) |
| Тестируемость | Слабая | Хорошая | Отличная |
| Сложность | Низкая | Средняя | Средняя |
| Для Flutter | Не рекомендуется | Можно | Рекомендуется |
| Bind Model to View | Напрямую | Через Presenter | Через ViewModel |
Real-world пример: Чат
// Model
class Message {
final String id;
final String text;
final String senderId;
final DateTime timestamp;
}
class ChatRepository {
Stream<List<Message>> getMessages(String chatId) { ... }
Future<void> sendMessage(String chatId, String text) { ... }
}
// ViewModel
class ChatViewModel with ChangeNotifier {
final ChatRepository _repository;
final String _chatId;
List<Message> _messages = [];
String _newMessage = '';
ChatViewModel({required ChatRepository repository, required String chatId})
: _repository = repository, _chatId = chatId {
_init();
}
void _init() {
_repository.getMessages(_chatId).listen((messages) {
_messages = messages;
notifyListeners();
});
}
void updateNewMessage(String text) {
_newMessage = text;
notifyListeners();
}
Future<void> sendMessage() async {
if (_newMessage.isEmpty) return;
await _repository.sendMessage(_chatId, _newMessage);
_newMessage = '';
notifyListeners();
}
}
// View
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ChatViewModel>(
create: (_) => ChatViewModel(
repository: context.read<ChatRepository>(),
chatId: 'chat_123',
),
child: Consumer<ChatViewModel>(
builder: (context, viewModel, _) => Column(
children: [
Expanded(
child: ListView.builder(
itemCount: viewModel.messages.length,
itemBuilder: (context, index) {
final message = viewModel.messages[index];
return MessageBubble(message: message);
},
),
),
TextField(
onChanged: viewModel.updateNewMessage,
onSubmitted: (_) => viewModel.sendMessage(),
),
],
),
),
);
}
}
Выводы
MVVM — это отличный паттерн для Flutter приложений, потому что:
- Хорошо разделяет UI от бизнес-логики
- Легко тестировать
- Масштабируется хорошо
- Использует силы Dart (ChangeNotifier, Provider и т.д.)
Это modern стандарт в мобильной разработке.