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

Как распределяется ответственность между элементами?

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

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

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

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

Распределение ответственности в архитектуре приложения

Этот вопрос о том, как правильно организовать код так, чтобы каждый компонент отвечал за одно и не вторгался в зону ответственности других. Это основной принцип чистой архитектуры — 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 для управления зависимостями.

Заключение

Правильное распределение ответственности:

  1. UI — только отображает и реагирует на действия
  2. ViewModel — управляет состоянием экрана
  3. Use Cases — реализуют бизнес правила
  4. Repositories — абстрагируют источники данных
  5. Data Sources — работают с API и БД

Это делает код тестируемым, масштабируемым и легким для поддержки.