Как распределяется ответственность между элементами?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Распределение ответственности в архитектуре приложения
Этот вопрос о том, как правильно организовать код так, чтобы каждый компонент отвечал за одно и не вторгался в зону ответственности других. Это основной принцип чистой архитектуры — Single Responsibility Principle (SRP).
Архитектурные слои Flutter приложения
Правильное распределение ответственности требует разделения приложения на слои:
┌─────────────────────────────────┐
│ UI Layer (Presentation) │ ← Виджеты, экраны, UI логика
├─────────────────────────────────┤
│ State Management Layer │ ← Provider, BLoC, Riverpod
├─────────────────────────────────┤
│ Business Logic Layer (Use Cases) │ ← Бизнес логика
├─────────────────────────────────┤
│ Repository Pattern │ ← Абстракция над данными
├─────────────────────────────────┤
│ Data Sources Layer │ ← API, БД, SharedPreferences
└─────────────────────────────────┘
1. UI Layer — Отвечает за отображение
Этот слой отвечает ТОЛЬКО за:
- Отображение данных
- Обработка user input
- Навигация
ЧТО НЕ ДОЛЖНО БЫТЬ здесь:
- ❌ API запросы
- ❌ Database операции
- ❌ Сложная бизнес логика
- ❌ Валидация правил (должна быть в business layer)
class UserProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Consumer<UserViewModel>(
builder: (context, viewModel, _) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView(
children: [
Text(viewModel.user.name),
Text(viewModel.user.email),
ElevatedButton(
onPressed: () => viewModel.logout(), // Вызывает use case
child: Text('Logout'),
),
],
);
},
),
);
}
}
2. State Management / ViewModel Layer
Отвечает за:
- Управление состоянием экрана
- Вызов use cases
- Трансформация данных для UI
- Error handling на уровне экрана
class UserViewModel extends ChangeNotifier {
final _getUserUseCase = GetUserUseCase(); // Инъекция зависимости
late User _user;
bool _isLoading = false;
String? _error;
User get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> loadUser(String userId) async {
_isLoading = true;
notifyListeners();
try {
// Вызовом use case, не прямым API
_user = await _getUserUseCase(userId);
_error = null;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> logout() async {
await _logoutUseCase(); // Еще один use case
notifyListeners();
}
}
3. Business Logic Layer — Бизнес правила
Этот слой содержит use cases — функции, которые реализуют бизнес правила приложения.
Отвечает за:
- Валидация данных
- Бизнес правила
- Координация с несколькими репозиториями
- Трансформация данных для следующего слоя
class AuthenticateUserUseCase {
final UserRepository _userRepository;
final TokenRepository _tokenRepository;
final AnalyticsService _analytics;
AuthenticateUserUseCase({
required UserRepository userRepository,
required TokenRepository tokenRepository,
required AnalyticsService analytics,
})
: _userRepository = userRepository,
_tokenRepository = tokenRepository,
_analytics = analytics;
Future<User> call(String email, String password) async {
// Валидация (бизнес правило)
if (!_isValidEmail(email)) {
throw InvalidEmailException();
}
if (password.length < 8) {
throw WeakPasswordException();
}
// Получение данных через репозитории
final user = await _userRepository.authenticate(email, password);
// Сохранение токена
await _tokenRepository.saveToken(user.token);
// Analytics логирование
_analytics.logEvent('user_authenticated', {'user_id': user.id});
return user;
}
bool _isValidEmail(String email) {
// Валидация email
return RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
}
}
4. Repository Layer — Абстракция над данными
Отвечает за:
- Абстракция источников данных (API, DB, cache)
- Выбор правильного источника
- Кэширование
- Комбинирование данных из разных источников
abstract class UserRepository {
Future<User> getUserById(String id);
Future<void> saveUser(User user);
Future<void> deleteUser(String id);
}
class UserRepositoryImpl implements UserRepository {
final RemoteUserDataSource _remoteDataSource;
final LocalUserDataSource _localDataSource;
UserRepositoryImpl({
required RemoteUserDataSource remoteDataSource,
required LocalUserDataSource localDataSource,
})
: _remoteDataSource = remoteDataSource,
_localDataSource = localDataSource;
@override
Future<User> getUserById(String id) async {
try {
// Сначала пытаемся получить с API
final user = await _remoteDataSource.getUserById(id);
// Кэшируем локально
await _localDataSource.saveUser(user);
return user;
} catch (e) {
// Если API недоступен, берём с локального кэша
return await _localDataSource.getUserById(id);
}
}
@override
Future<void> saveUser(User user) async {
// Сохраняем локально и синхронизируем с сервером
await _localDataSource.saveUser(user);
await _remoteDataSource.saveUser(user);
}
}
5. Data Source Layer — Источники данных
Отвечает за:
- API запросы (HTTP)
- Database операции (SQLite, Hive)
- SharedPreferences
- File System
abstract class RemoteUserDataSource {
Future<User> getUserById(String id);
}
class RemoteUserDataSourceImpl implements RemoteUserDataSource {
final http.Client _httpClient;
RemoteUserDataSourceImpl({required http.Client httpClient})
: _httpClient = httpClient;
@override
Future<User> getUserById(String id) async {
final response = await _httpClient.get(
Uri.parse('https://api.example.com/users/$id'),
);
if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load user');
}
}
}
abstract class LocalUserDataSource {
Future<User> getUserById(String id);
Future<void> saveUser(User user);
}
class LocalUserDataSourceImpl implements LocalUserDataSource {
final Box<UserModel> _userBox; // Hive box
LocalUserDataSourceImpl({required this.Box<UserModel> userBox})
: _userBox = userBox;
@override
Future<User> getUserById(String id) async {
final model = _userBox.get(id);
if (model == null) throw Exception('User not found');
return model.toEntity();
}
@override
Future<void> saveUser(User user) async {
await _userBox.put(user.id, UserModel.fromEntity(user));
}
}
Практический пример: Полный flow
// 1. User взаимодействует с UI
ElevatedButton(
onPressed: () => viewModel.fetchUser('123'),
child: Text('Load User'),
)
// 2. ViewModel вызывает use case
class UserViewModel {
final GetUserUseCase _getUser;
Future<void> fetchUser(String id) async {
final user = await _getUser(id); // Вызов use case
_user = user;
notifyListeners();
}
}
// 3. Use case вызывает repository
class GetUserUseCase {
Future<User> call(String id) async {
return await _repository.getUserById(id);
}
}
// 4. Repository выбирает источник данных
class UserRepositoryImpl {
Future<User> getUserById(String id) async {
try {
return await _remote.getUserById(id); // API
} catch (e) {
return await _local.getUserById(id); // Cache
}
}
}
// 5. Data source делает HTTP запрос
class RemoteUserDataSource {
Future<User> getUserById(String id) async {
final response = await http.get('...');
return User.fromJson(response);
}
}
Зависимости между слоями
UI Layer
↓ (зависит от)
State Management
↓
Business Logic (Use Cases)
↓
Repository Pattern
↓
Data Sources
Правило: Зависимость должна быть односторонней, от верхнего к нижнему.
✅ UI может зависить от ViewModel ✅ ViewModel может зависить от Use Cases ❌ Use Case НЕ должен знать о UI ❌ Data Source НЕ должен знать о Repository
Dependency Injection
Для правильного распределения ответственности нужна инъекция зависимостей:
void main() {
// Создаём graph зависимостей
final userRepository = UserRepositoryImpl(
remoteDataSource: RemoteUserDataSourceImpl(httpClient: http.Client()),
localDataSource: LocalUserDataSourceImpl(userBox: Hive.box('users')),
);
final getUserUseCase = GetUserUseCase(userRepository);
final userViewModel = UserViewModel(getUserUseCase);
runApp(MyApp(viewModel: userViewModel));
}
Оптимально использовать пакет типа GetIt или Riverpod для управления зависимостями.
Заключение
Правильное распределение ответственности:
- UI — только отображает и реагирует на действия
- ViewModel — управляет состоянием экрана
- Use Cases — реализуют бизнес правила
- Repositories — абстрагируют источники данных
- Data Sources — работают с API и БД
Это делает код тестируемым, масштабируемым и легким для поддержки.