Реалізація модульної архітектури мобільного додатку
Monolith із 50+ екранами в одному модулі — історія про те, як команда з 8 розробників чекає 4 хвилини повної збірки на кожну зміну, та як фіча однієї команди ламає тести іншої тому що обидва імпортують один і той же синглтон. Модульна архітектура вирішує саме ці проблеми. Не «масштабованість у майбутньому» — а повільна збірка та merge-конфлікти прямо зараз.
Як ділимо на модулі
Стандартний підхід — розділення за функціями зі спільним шаром:
:app — точка входу, DI-граф, навігація
:core:network — HTTP клієнт, interceptors
:core:storage — БД, SharedPreferences/DataStore
:core:ui — спільні компоненти, тема
:core:auth — токени, AuthInterceptor
:feature:profile — профіль користувача
:feature:catalog — каталог товарів
:feature:checkout — оформлення замовлення
:feature:orders — історія замовлень
Кожен :feature:* модуль залежить тільки від :core:* і нічого не знає про інші функції. Навігація між функціями — через абстракцію: NavigationManager у :core, яку кожна функція використовує без прямого імпорту іншого feature-модуля. Android: Navigation Component з NavGraph на рівні :app, deep links зареєстровані в маніфестах модулів. iOS: аналогічно — Coordinator pattern або Router з protocol-based навігацією.
Android. Кожен :feature:* — окремий Gradle-модуль (:feature:catalog → catalog/build.gradle.kts). Gradle Configuration Cache + Build Cache (~/.gradle/caches) радикально скорочує час пересборки: змінюємо тільки :feature:checkout — пересобирається тільки він. --configuration-cache у Gradle 8.x стабільний. Kapt → KSP: міграція annotation processor'ів з Kapt на KSP (Kotlin Symbol Processing) дає +30–50% до швидкості генерації коду.
iOS. Модулі реалізуються через Swift Packages (локальні) або як Framework targets у Xcode workspace. Swift Package Manager підтримує локальні пакети через path: у Package.swift. Workspace з кількома проектами (Feature1.xcodeproj, Feature2.xcodeproj) — застарілий підхід, новий — монорепо з локальними SPM пакетами + один основний .xcodeproj. Tuist або XcodeGen автогенерують .xcodeproj з конфігураційних файлів, що виключає merge-конфлікти у .pbxproj.
DI у модульній архітектурі
DI-граф не повинен бути монолітним. Android: Hilt з @InstallIn(SingletonComponent::class) для core-залежностей та @InstallIn(ViewModelComponent::class) для feature-специфічних. Кожен feature-модуль оголошує свої @Module-класи, Hilt собирає граф під час компіляції. Це означає: якщо :feature:catalog не підключено у :app — його DI-модуль не компілюється та не впливає на збірку.
На iOS — ручний DI через Resolver/Swinject, або чистий factory-паттерн без DI-фреймворку. У модульному додатку DI-фреймворк на Swift менш критичний: залежності передаються через ініціалізатори, а Composition Root у :app собирає все разом.
Dynamic Delivery та опціональні модулі
Android дозволяє доставляти feature-модулі за запитом через Play Feature Delivery. Користувачі завантажують базовий додаток, важкі функції (AR примірка, offline карти) — при першому використанні. SplitInstallManager управляє завантаженням, SplitCompatActivity активує. iOS не має аналога — App Clips заповнюють іншу нішу.
Приклад. Ритейл-додаток: 6 команд, 80+ екранів, Android + iOS. До модуляризації: повна збірка Android — 7 хвилин, iOS — 11 хвилин. Після розподілу на 14 модулів з налаштованим Build Cache: інкрементальна пересборка при змінах одного feature-модуля — 45–90 секунд. Merge-конфлікти у спільному коді знизилися приблизно втричі. Паралельна розробка функцій без блокувань між командами.
Час реалізації
Модуляризація існуючого додатку — трудоємний рефакторинг:
| Розмір проекту | Орієнтовні строки |
|---|---|
| 20–40 екранів, 1–2 команди | 4–8 тижнів |
| 50–100 екранів, 3–5 команд | 2–4 місяці |
| 100+ екранів, складні залежності | 4–8 місяців |
Ціна розраховується індивідуально після аудиту архітектури та аналізу залежностей існуючого коду.
Типові помилки при самостійній модуляризації
Циклічні залежності. :feature:profile імпортує :feature:orders, :feature:orders імпортує :feature:profile. Gradle не соберуватиме — циклічна залежність. Рішення: винести спільні моделі в :core:domain або :shared:models, обидві функції залежать тільки від нього.
Занадто дрібні модулі. :core:extensions з 5 extension-функціями — overhead без користі. Раціональний мінімум: модуль виправданий, якщо його можна змінювати незалежно та він не залежить від модулів, які змінюються частіше.
Конфлікти Resource ID. На Android при об'єднанні ресурсів кількох модулів виникають колізії імен. Конвенція: префікс модуля для всіх ресурсів (catalog_item_card_background, не просто item_card_background). R class isolation через android.nonTransitiveRClass=true у gradle.properties — обов'язково для великих проектів.







