Android Clean Architecture: простое руководство по слоям

Когда приложение растёт, его код может превратиться в огромную кучу, где всё перемешано: запросы к серверу, логика фильтрации, отображение кнопок. 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. Через месяц вы заметите, что боязнь что-то сломать сменилась удовольствием от добавления новых фич.