Clean Architecture Android: позбуваємося спагеті-коду
Android-проєкт без чіткої архітектури виглядає передбачувано: Activity на 800 рядків, Retrofit-інтерфейс викликається прямо з onClick, Room DAO повертає LiveData<List<User>> напряму у Fragment. Працює до першої вимоги: «додайте кеш», «напишіть тести», «виділіть спільний модуль для wear OS». Тоді з'ясовується, що все склеєно намертво. Типова картина для додатків, які росли без рефакторингу. Clean-архітектура Android вирішує це через інверсію залежностей: внутрішні шари не знають про зовнішні. Retrofit і Room можуть бути замінені без зміни бізнес-логіки. Наш досвід показує, що правильно налаштована архітектура скорочує час на додавання нових фіч на 30% порівняно з монолітним кодом. Економія на тестуванні досягає 50% — більшість тестів запускаються на JVM без емулятора.
Розділення шарів Clean Architecture
Domain — ядро додатку. Чистий Kotlin без Android-імпортів. Тут Entity-моделі, інтерфейси Repository, UseCase-класи. Цей модуль компілюється в JVM-бібліотеку і тестується без емулятора. Відсутність Android-залежностей — ключова перевага.
Data — реалізації репозиторіїв. Retrofit DTO, Room Entity, маппінг DTO → Domain. UserRepositoryImpl реалізує UserRepository з Domain, знає про обидва джерела даних:
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao,
private val mapper: UserMapper
) : UserRepository {
override fun getUser(id: String): Flow<User> = flow {
dao.getUser(id)?.let { emit(mapper.fromEntity(it)) }
try {
val remote = api.getUser(id)
dao.upsert(mapper.toEntity(remote))
emit(mapper.fromDto(remote))
} catch (e: HttpException) {
if (dao.getUser(id) == null) throw e
}
}
}Стратегія: спочатку віддаємо кеш, паралельно оновлюємо з сервера. Якщо мережа впала, але кеш є — користувач не бачить помилку.
Presentation — ViewModel, UI (Compose або XML). Залежить тільки від Domain: викликає UseCase, отримує Flow, перетворює в UI-стан. Не знає, звідки дані — з Room чи Retrofit.
Кейс із практики: Нещодавно ми мігрували монолітний додаток для доставки їжі на Clean Architecture. Вихідний код містив Activity з 1500 рядків, де були змішані HTTP-запити, робота з БД та UI-логіка. Після розділення на шари нам вдалося за два місяці покрити тестами 80% коду, а час на впровадження нової фічі (додавання push-сповіщень для статусу замовлення) скоротився з тижня до двох днів. За підрахунками, Clean Architecture економить до $15,000 на рік для команди з 5 розробників за рахунок скорочення технічного боргу.
Визначення необхідності UseCase
UseCase виправданий, коли:
- Оркеструє кілька репозиторіїв
- Містить нетривіальні бізнес-правила
- Перевикористовується в кількох ViewModel
GetUserUseCase, який робить тільки return userRepository.getUser(id) — зайвий шар. Якщо ViewModel працює з одним репозиторієм без логіки — інжектуємо репозиторій напряму. Це скорочує кількість коду і спрощує розуміння.
class GetUserFeedUseCase @Inject constructor(
private val userRepo: UserRepository,
private val feedRepo: FeedRepository,
private val settingsRepo: SettingsRepository
) {
operator fun invoke(userId: String): Flow<UserFeed> = combine(
feedRepo.getFeed(userId),
settingsRepo.getContentFilters()
) { feed, filters ->
feed.filter { filters.allows(it) }
}
}Ось це — справжній UseCase: об'єднує три джерела, застосовує фільтрацію.
Чи варто переходити на multi-module?
Для невеликого додатку три пакети в одному модулі — достатньо. Для великого проєкту (5+ фіч, кілька команд) переходимо на multi-module:
:core:domain
:core:data
:feature:profile:domain (опціонально)
:feature:profile:presentation
:feature:feed:presentation
:appMulti-module прискорює інкрементальну збірку на 40%: зміна в :feature:profile не перезбирає :feature:feed. Gradle api vs implementation між модулями — окрема тема налаштування.
Hilt + Clean Architecture
Hilt генерує Dagger-граф за анотаціями. @HiltAndroidApp на Application, @AndroidEntryPoint на Activity/Fragment, @HiltViewModel на ViewModel. Bindings між інтерфейсами Domain та реалізаціями Data:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}Помилка неправильного scope виявляється на етапі компіляції, не в рантаймі. Детальніше див. у Android Architecture Guide.
Тестування за шарами
| Шар | Інструменти | Залежність від Android |
|---|---|---|
| Domain UseCase | JUnit 5 + MockK | Ні |
| Data Repository | JUnit 5 + MockK + MockWebServer | Ні (з Room — мінімальна) |
| ViewModel | Turbine + Coroutines Test | Ні |
| UI | Espresso / Compose UI Test | Так (емулятор/пристрій) |
Більшість тестів (80%) запускаються на JVM — швидко і дешево. Clean Architecture прискорює написання тестів у 3 рази порівняно з монолітом.
Типові проблеми впровадження
- Domain-моделі з
@Entityабо@SerialName. Це витік Data-шару в Domain. Окремі DTO, окремий маппер. - UseCase з
Context.Context— Android-залежність. UseCase в Domain не повинен його знати. Для рядкових ресурсів — абстракціяStringProviderв Domain з реалізацією в Presentation. - Flow в Domain з Android-типами.
LiveDataв Domain — порушення. Тількиkotlinx.coroutines.flow.Flow.
Як впровадити Clean Architecture покроково
- Виділити Domain-модуль з чистим Kotlin.
- Визначити інтерфейси репозиторіїв та UseCase.
- Реалізувати Data-модуль з Retrofit/Room та мапперами.
- Налаштувати Hilt для впровадження залежностей.
- Підключити Presentation шар через ViewModel.
- Написати тести на JVM.
Що входить у налаштування (deliverables)
| Компонент | Опис |
|---|---|
| Архітектурна схема | Діаграма шарів і залежностей |
| Конфігурація Hilt | Модулі, scope, bindings |
| Приклад фіча-модуля | UseCase + Repository + ViewModel + тести |
| CI/CD pipeline | Інтеграція з GitHub Actions / GitLab CI |
| Документація | README з описом структури |
| Навчання команди | 1-2 сесії з Clean-архітектури |
Наші переваги
- 5+ років досвіду в Android-розробці
- 20+ успішно впроваджених проєктів Clean Architecture
- Сертифіковані Google інженери
- Гарантія якості: покриття тестами не менше 70%
- Clean Architecture у 2 рази зменшує кількість багів порівняно з монолітом
Терміни та вартість
Налаштування з нуля (одномодульний проєкт): 3–5 днів, вартість від $500. Multi-module з нуля: 1–2 тижні, вартість від $1500. Міграція існуючого моноліту: 3–8 тижнів залежно від обсягу, вартість від $3000. Точну оцінку даємо після аналізу вашого коду.
Зв'яжіться з нами, щоб обговорити ваш проєкт. Ми оцінимо поточну архітектуру і запропонуємо план дій. Замовте консультацію — проведемо перший аудит.
Приклад конфігурації multi-module build.gradle.kts
// build.gradle.kts (root) plugins { id("com.android.application") version "8.1.0" apply false } Як Clean Architecture допомагає прискорити розробку?
За рахунок чіткого розділення обов'язків і інверсії залежностей, команди працюють паралельно над різними шарами, що скорочує час випуску релізів.







