Что такое паттерн MVVM?

Ответ

MVVM (Model-View-ViewModel) — это архитектурный паттерн, который разделяет ответственность в приложении на три слоя для улучшения тестируемости и поддержки кода.

  • Model: Представляет бизнес-логику и данные приложения (например, классы домена, репозитории, источники данных).
  • View: Отвечает за отображение UI и обработку пользовательского ввода. Во Flutter это Widget (чаще StatelessWidget).
  • ViewModel: Выступает посредником между Model и View. Он содержит логику представления, преобразует данные из Model в форму, удобную для отображения в View, и часто является ChangeNotifier или используется с Stream/StateNotifier для реактивного обновления UI.

Преимущества во Flutter:

  • Чёткое разделение: Widget остаётся «глупым» и только рисует то, что ему передали.
  • Упрощённое тестирование: ViewModel можно тестировать отдельно от Flutter-виджетов.
  • Хорошо сочетается с пакетами для управления состоянием, такими как provider, riverpod, bloc (который можно рассматривать как вариацию ViewModel).

Пример реализации с ChangeNotifier (паттерн Provider):

// Model
class User {
  final String name;
  final int age;
  User({required this.name, required this.age});
}

// ViewModel
class UserViewModel extends ChangeNotifier {
  User _currentUser = User(name: 'Alice', age: 30);

  String get userName => _currentUser.name;
  String get userAge => '${_currentUser.age} years';

  void updateUserAge(int newAge) {
    _currentUser = User(name: _currentUser.name, age: newAge);
    notifyListeners(); // Сообщает View об изменении
  }
}

// View
class UserView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Используем Consumer для подписки на изменения ViewModel
    return Consumer<UserViewModel>(
      builder: (context, viewModel, child) {
        return Column(
          children: [
            Text('Name: ${viewModel.userName}'),
            Text('Age: ${viewModel.userAge}'),
            ElevatedButton(
              onPressed: () => viewModel.updateUserAge(31),
              child: Text('Have Birthday'),
            ),
          ],
        );
      },
    );
  }
}

Ответ 18+ 🔞

Давай разберём этот MVVM, чтобы ты не сидел, как мартышлюшка, и не пытался понять, где тут модель, а где виджет. Это же, ёпта, не хитрая жопа, а вполне логичная штука.

Представь, что ты делаешь приложение. Без MVVM у тебя всё свалено в одну кучу: и данные из интернета тянешь, и кнопки красишь, и логику проверок пишешь — всё в одном файле на тысячу строк. Потом баг найти — это пиздец, волнение ебать, терпения ноль ебать. А с MVVM ты просто делишь всё по полочкам.

Model (Модель) — это твои голые данные и правила, как с ними работать. Например, класс User с полями name и age. Или репозиторий, который ходит в сеть и приносит этих юзеров. Это как склад: тут лежат ящики с данными, но сам склад не знает, как их показывать на витрине.

View (Вид) — это твоя витрина, интерфейс. Во Flutter это виджеты (StatelessWidget или StatefulWidget). Его задача — тупо нарисовать то, что ему сказали, и передать нажатия кнопок куда следует. Он не должен сам решать, какого цвета сделать текст, если юзеру меньше 18. Он просто спрашивает: «Эй, ViewModel, что рисовать?» И рисует. Если он начинает сам думать — это уже распиздяй.

ViewModel (Модель представления) — вот тут вся магия. Это твой главный по тачанке, посредник. Он берёт сырые данные из Model (например, объект User), перемалывает их во что-то удобное для View (скажем, делает из числа возраста строку «30 лет»). В нём же живёт вся логика экрана: что делать при нажатии кнопки, как реагировать на ошибку. А главное — он умеет кричать View: «Эй, данные изменились, перерисуйся!» Обычно он для этого расширяет ChangeNotifier или использует стримы.

В чём профит, чувак?

  1. Тестируемость. Ты можешь взять свой ViewModel и протестировать его логику отдельно от Flutter, без всяких эмуляторов. Это овердохуища удобно.
  2. Чистота. Твой Widget становится тупым и безответственным (в хорошем смысле). Он только рисует. Всю грязную работу делает ViewModel.
  3. Гибкость. Паттерн отлично дружит с популярными библиотеками вроде provider или riverpod. Bloc — это, по сути, тот же ViewModel, только на стримах и с более строгими правилами.

Смотри, как это выглядит в коде на примере с ChangeNotifier (пакет provider):

// Model - просто данные, как есть
class User {
  final String name;
  final int age;
  User({required this.name, required this.age});
}

// ViewModel - тут мозги и состояние экрана
class UserViewModel extends ChangeNotifier {
  User _currentUser = User(name: 'Alice', age: 30);

  // Геттеры готовят данные для View
  String get userName => _currentUser.name;
  String get userAge => '${_currentUser.age} years';

  // Метод с бизнес-логикой
  void updateUserAge(int newAge) {
    _currentUser = User(name: _currentUser.name, age: newAge);
    notifyListeners(); // Кричит View: "Йоу, обновись, старина!"
  }
}

// View - тупая, но красивая витрина
class UserView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Consumer слушает крики ViewModel
    return Consumer<UserViewModel>(
      builder: (context, viewModel, child) {
        return Column(
          children: [
            Text('Name: ${viewModel.userName}'), // Просто рисуем
            Text('Age: ${viewModel.userAge}'),
            ElevatedButton(
              onPressed: () => viewModel.updateUserAge(31), // Делегируем логику
              child: Text('Have Birthday'),
            ),
          ],
        );
      },
    );
  }
}

Вот и вся философия. View не лезет в Model, Model не знает о View. А ViewModel всех мирит и командует. Получается аккуратно, и когда через полгода заказчик скажет «а давайте вместо возраста показывать зодиак», ты не будешь сам от себя охуевать, а просто пойдёшь в один файл — в ViewModel — и поменяешь один геттер. Красота, да и только.