← Назад к вопросам

Что такое MVVM?

2.3 Middle🔥 161 комментариев
#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

MVCMVPMVVM
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 приложений, потому что:

  1. Хорошо разделяет UI от бизнес-логики
  2. Легко тестировать
  3. Масштабируется хорошо
  4. Использует силы Dart (ChangeNotifier, Provider и т.д.)

Это modern стандарт в мобильной разработке.