Как отслеживать изменения состояния компонента в MVVM?

«Как отслеживать изменения состояния компонента в MVVM?» — вопрос из категории State Management, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В архитектуре MVVM (Model-View-ViewModel) для Flutter я использую комбинацию ChangeNotifier/ValueNotifier с Provider или Riverpod. Вот полная реализация:

1. Базовый ViewModel с ChangeNotifier:

import 'package:flutter/foundation.dart';

class UserViewModel extends ChangeNotifier {
  String _name = '';
  String _email = '';
  bool _isLoading = false;

  String get name => _name;
  String get email => _email;
  bool get isLoading => _isLoading;

  void updateName(String newName) {
    if (_name != newName) {
      _name = newName;
      notifyListeners(); // Уведомляем слушателей об изменении
    }
  }

  void updateEmail(String newEmail) {
    if (_email != newEmail) {
      _email = newEmail;
      notifyListeners();
    }
  }

  Future<void> loadUserData() async {
    _isLoading = true;
    notifyListeners();

    try {
      // Имитация загрузки данных
      await Future.delayed(Duration(seconds: 2));

      _name = 'Алексей Петров';
      _email = 'alexey@example.com';
      _isLoading = false;
      notifyListeners();
    } catch (e) {
      _isLoading = false;
      notifyListeners();
      rethrow;
    }
  }

  void reset() {
    _name = '';
    _email = '';
    _isLoading = false;
    notifyListeners();
  }
}

2. Отслеживание изменений с Provider:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class UserProfileScreen extends StatelessWidget {
  const UserProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => UserViewModel(),
      child: Scaffold(
        appBar: AppBar(title: Text('Профиль пользователя')),
        body: Consumer<UserViewModel>(
          builder: (context, viewModel, child) {
            if (viewModel.isLoading) {
              return Center(child: CircularProgressIndicator());
            }

            return Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Имя: ${viewModel.name}', 
                    style: TextStyle(fontSize: 18)),
                  SizedBox(height: 8),
                  Text('Email: ${viewModel.email}',
                    style: TextStyle(fontSize: 18)),
                  SizedBox(height: 24),
                  ElevatedButton(
                    onPressed: () => viewModel.loadUserData(),
                    child: Text('Загрузить данные'),
                  ),
                  SizedBox(height: 16),
                  TextField(
                    decoration: InputDecoration(
                      labelText: 'Изменить имя',
                      border: OutlineInputBorder(),
                    ),
                    onChanged: (value) => viewModel.updateName(value),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

3. ValueNotifier для простых случаев:

class CounterViewModel {
  final ValueNotifier<int> counter = ValueNotifier(0);
  final ValueNotifier<bool> isEven = ValueNotifier(true);

  void increment() {
    counter.value++;
    isEven.value = counter.value % 2 == 0;
  }

  void dispose() {
    counter.dispose();
    isEven.dispose();
  }
}

// В виджете:
ValueListenableBuilder<int>(
  valueListenable: viewModel.counter,
  builder: (context, value, child) {
    return Text('Счётчик: $value');
  },
);

ValueListenableBuilder<bool>(
  valueListenable: viewModel.isEven,
  builder: (context, value, child) {
    return Text(value ? 'Чётное' : 'Нечётное');
  },
);

4. MVVM с Riverpod (рекомендуется для сложных приложений):

// providers/user_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final userViewModelProvider = StateNotifierProvider<UserViewModel, UserState>(
  (ref) => UserViewModel(),
);

class UserState {
  final String name;
  final String email;
  final bool isLoading;

  const UserState({
    this.name = '',
    this.email = '',
    this.isLoading = false,
  });

  UserState copyWith({
    String? name,
    String? email,
    bool? isLoading,
  }) {
    return UserState(
      name: name ?? this.name,
      email: email ?? this.email,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

class UserViewModel extends StateNotifier<UserState> {
  UserViewModel() : super(const UserState());

  Future<void> loadUserData() async {
    state = state.copyWith(isLoading: true);

    await Future.delayed(Duration(seconds: 2));

    state = state.copyWith(
      name: 'Алексей Петров',
      email: 'alexey@example.com',
      isLoading: false,
    );
  }

  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updateEmail(String email) {
    state = state.copyWith(email: email);
  }
}

// В виджете:
Consumer(
  builder: (context, ref, child) {
    final state = ref.watch(userViewModelProvider);

    return Column(
      children: [
        Text('Имя: ${state.name}'),
        Text('Email: ${state.email}'),
        if (state.isLoading) CircularProgressIndicator(),
        ElevatedButton(
          onPressed: () => ref.read(userViewModelProvider.notifier).loadUserData(),
          child: Text('Загрузить'),
        ),
      ],
    );
  },
);

5. Selector для оптимизации перестроений:

// Без Selector - перестраивается при любом изменении ViewModel
Consumer<UserViewModel>(
  builder: (context, viewModel, child) {
    return Text(viewModel.name);
  },
);

// С Selector - перестраивается только при изменении имени
Selector<UserViewModel, String>(
  selector: (context, viewModel) => viewModel.name,
  builder: (context, name, child) {
    return Text(name);
  },
);

// С Riverpod - автоматическая оптимизация
final userNameProvider = Provider<String>((ref) {
  return ref.watch(userViewModelProvider.select((state) => state.name));
});

6. Интеграция с сервисами и репозиториями:

class AuthViewModel extends ChangeNotifier {
  final AuthService _authService;

  AuthViewModel(this._authService);

  User? _currentUser;
  User? get currentUser => _currentUser;

  Future<void> login(String email, String password) async {
    try {
      _currentUser = await _authService.login(email, password);
      notifyListeners();
    } catch (e) {
      // Обработка ошибок
      rethrow;
    }
  }
}

// Инициализация с зависимостями:
MultiProvider(
  providers: [
    Provider<AuthService>(create: (_) => FirebaseAuthService()),
    ChangeNotifierProvider(
      create: (context) => AuthViewModel(
        context.read<AuthService>(),
      ),
    ),
  ],
  child: MyApp(),
);

Ключевые преимущества MVVM во Flutter:

  1. Разделение ответственности — View только отображает, ViewModel управляет состоянием
  2. Тестируемость — ViewModel можно тестировать без Flutter
  3. Реактивность — автоматическое обновление UI при изменении данных
  4. Жизненный цикл — правильное управление ресурсами через dispose
  5. Гибкость — можно комбинировать с любым state management решением

Best Practices:

  • Используйте ValueNotifier для простых значений
  • Используйте ChangeNotifier для сложных объектов с несколькими полями
  • Для production приложений рассмотрите Riverpod с StateNotifier
  • Всегда вызывайте notifyListeners() только при реальном изменении данных
  • Используйте Selectors для оптимизации производительности
  • Не храните BuildContext в ViewModel