Когда приложение растёт, его код может превратиться в огромную кучу, где всё перемешано: запросы к серверу, логика фильтрации, отображение кнопок. Android Clean Architecture — это способ организовать код так, чтобы он оставался понятным, тестируемым и готовым к изменениям. В этой статье я объясню, из каких слоёв состоит чистая архитектура, как они взаимодействуют и почему это спасает ваши нервы через полгода разработки.
Проблема: почему код превращается в спагетти
Представьте, что вы строите дом. Если сначала кладёте кирпичи, потом прямо на них тянете провода, а сверху заливаете бетон — в итоге получится монолит, где ничего нельзя поменять, не разрушив соседнее. Так и с приложениями. Типичный «экран-монолит» — это Activity, которая сама делает запросы, разбирает JSON, считает скидки и красит кнопки. Через пару месяцев добавить новую фичу становится страшно, потому что любое изменение ломает что-то в другом месте.
Clean Architecture решает эту проблему, разделяя код на слои с чёткими границами и правилами: кто кого знает и куда передавать данные. Идею придумал Роберт Мартин, а Android-сообщество адаптировало её под свои реалии.
Три главных слоя в Android Clean Architecture
В классическом варианте для Android выделяют три слоя: Presentation (он же UI), Domain (бизнес-логика) и Data (работа с источниками данных). Запомнить просто: самое внешнее — то, что видит пользователь, самое внутреннее — то, что считает и решает, а нижний ярус достаёт данные отовсюду.
| Слой | Что содержит | Примеры классов |
|---|---|---|
| Data | Реализацию доступа к данным: сеть, база, SharedPreferences | ApiService, RoomDao, RepositoryImpl |
| Domain | Бизнес-логику, модели-сущности, use case (сценарии) | GetUserUseCase, User (модель) |
| Presentation | UI: Activity, Fragment, Compose-экраны, ViewModel | MainViewModel, MainScreen (Compose) |
Правило зависимостей: кто кого знает
Самое важное правило чистой архитектуры — зависимости направлены внутрь. Внешний слой знает о внутренних, но не наоборот. Presentation может дергать Domain, Domain может вызывать интерфейсы, объявленные в нём же, а Data реализует интерфейсы из Domain и поставляет реальные данные. Но Data ничего не знает о Presentation, и Domain ничего не знает о базе данных или Retrofit.
Такой подход позволяет менять базу данных с Room на SQLDelight, не трогая UI. Или переписать экран с XML на Compose, оставив логику нетронутой.
Слой Data: откуда берутся данные
Этот слой отвечает за получение данных отовсюду: сервер, локальная база, файлы, кэш. Он содержит конкретные реализации интерфейсов-контрактов, описанных в Domain. Например:
// в Domain (просто описание)
interface UserRepository {
suspend fun getUser(id: Int): User
}
// в Data (реализация)
class UserRepositoryImpl(
private val api: ApiService,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: Int): User {
val cached = dao.getUser(id)
if (cached != null) return cached
val remote = api.getUser(id)
dao.insertUser(remote)
return remote
}
}
Обратите внимание: интерфейс UserRepository определён в Domain, а реализация — в Data. Это даёт свободу подменять источник данных без переписывания слоёв выше.
Слой Domain: чистая бизнес-логика
Это сердце приложения. В Domain нет упоминаний Android, Context, View. Только чистый Kotlin. Здесь живут:
- Модели (Entities) — простые data-классы, например
User,Order. - Use Cases — сценарии, которые описывают действия пользователя. Каждый Use Case — отдельный класс, отвечающий за одно действие: получить список товаров, добавить в корзину, рассчитать скидку.
- Интерфейсы репозиториев — контракты, которые позже реализуются в Data.
Use Case выглядит как простая функция:
class GetProductDetailsUseCase(
private val repository: ProductRepository
) {
suspend operator fun invoke(productId: Int): Product {
return repository.getProductById(productId)
}
}
Зачем выносить каждое действие в отдельный класс? Это позволяет тестировать бизнес-логику отдельно, заменять реализацию на заглушки и легко понимать, что делает приложение.
Слой Presentation: что видит пользователь
Здесь находится всё, что касается отображения: Activity, Fragment, Composable-функции, ViewModel. Presentation зависит от Domain: он вызывает Use Cases и показывает результат. Но не наоборот. ViewModel обращается к Use Case, получает данные и передаёт их в UI.
class ProductViewModel(
private val getProductDetailsUseCase: GetProductDetailsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<ProductUiState>(ProductUiState.Loading)
val uiState = _uiState.asStateFlow()
fun loadProduct(id: Int) {
viewModelScope.launch {
_uiState.value = try {
val product = getProductDetailsUseCase(id)
ProductUiState.Success(product)
} catch (e: Exception) {
ProductUiState.Error
}
}
}
}
Если завтра вы решите убрать экран и заменить его на уведомление, ViewModel останется той же, а изменится только Compose-функция. Это и есть сила разделения.
Как Clean Architecture работает с MVVM
MVVM — отличный компаньон для чистой архитектуры. ViewModel живёт в Presentation, но не содержит бизнес-логики — она делегирует работу Use Cases из Domain. Модель в MVVM представлена не просто данными, а результатами Use Cases. View (UI) подписывается на состояние ViewModel и отображает его. Схема взаимодействия проста:
- UI собирается из Composables.
- ViewModel вызывает UseCase из Domain.
- UseCase обращается к Repository (интерфейс в Domain).
- Реализация Repository в Data идёт в сеть или базу.
Таким образом, каждый слой отвечает за свою часть, а изменения в одном не ломают другие.
Практический пример: структура пакетов
Реальный проект, организованный по Clean Architecture, выглядит примерно так:
com.example.app
├── data
│ ├── local
│ │ ├── UserDao.kt
│ │ └── AppDatabase.kt
│ ├── remote
│ │ ├── ApiService.kt
│ │ └── dto (объекты для сети)
│ └── repository
│ └── UserRepositoryImpl.kt
├── domain
│ ├── model
│ │ └── User.kt
│ ├── repository
│ │ └── UserRepository.kt (интерфейс)
│ └── usecase
│ └── GetUserUseCase.kt
└── presentation
├── screen
│ ├── MainScreen.kt (Compose)
│ └── MainViewModel.kt
└── navigation
Каждый слой в своей папке, классы не перемешаны. Если нужно добавить новое действие, вы создаёте Use Case в domain, реализацию в data, и экран в presentation — всё прозрачно и предсказуемо.
Советы новичкам
Чистая архитектура может показаться сложной в начале, но эти рекомендации помогут не перегружать проект:
- Не усложняйте раньше времени. Если приложение состоит из трёх экранов, не надо плодить 50 Use Cases. Начните с простого MVVM, а когда почувствуете боль от перемешанного кода — вводите слои постепенно.
- Интерфейсы в Domain — ваш лучший друг. Благодаря им вы сможете тестировать Use Cases без реальной сети или базы.
- Используйте Hilt для внедрения зависимостей. Он упрощает связь между слоями и делает код чище.
- Не бойтесь дублирования. Иногда проще скопировать код, чем создавать кучу абстракций. Особенно на старте.
Итоги
Android Clean Architecture — это не строгий свод правил, а гибкий подход, который помогает писать приложения, готовые к изменениям. Вы разделяете код на Data, Domain и Presentation, направляете зависимости внутрь и получаете код, который легко тестировать и поддерживать. Начните с двух слоёв — UI и логики, а когда проект вырастет, вводите Domain. Через месяц вы заметите, что боязнь что-то сломать сменилась удовольствием от добавления новых фич.