Ответ
В архитектуре 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:
- Разделение ответственности — View только отображает, ViewModel управляет состоянием
- Тестируемость — ViewModel можно тестировать без Flutter
- Реактивность — автоматическое обновление UI при изменении данных
- Жизненный цикл — правильное управление ресурсами через
dispose - Гибкость — можно комбинировать с любым state management решением
Best Practices:
- Используйте
ValueNotifierдля простых значений - Используйте
ChangeNotifierдля сложных объектов с несколькими полями - Для production приложений рассмотрите Riverpod с
StateNotifier - Всегда вызывайте
notifyListeners()только при реальном изменении данных - Используйте
Selectorsдля оптимизации производительности - Не храните BuildContext в ViewModel