Налаштування Dynamic Feature Modules для Android-програми
Програма доставки їжі має вбудований модуль AR-перегляду блюд — важить 23 MB додаткових ресурсів. Цю функцію відкривають 8% користувачів. Решта 92% завантажують 23 MB, які їм ніколи не потребуються. Dynamic Feature Modules вирішує саме це: AR-модуль завантажується лише коли користувач натиснув «подивитися в AR».
Архітектура проекту з DFM
Проект перебудовується на multi-module структуру. app стає base модулем — містить лише критичний для старту функціонал. Тяжкі або рідко використовувані функції виносяться до окремих dynamic feature модулів.
Структура build.gradle для DFM:
// dynamic feature module build.gradle
plugins {
id("com.android.dynamic-feature")
}
android {
defaultConfig { minSdk = 21 }
}
dependencies {
implementation(project(":app")) // залежність від base module
}
У app/build.gradle:
android {
dynamicFeatures += setOf(":feature_ar", ":feature_premium")
}
Три режими встановлення
Install-time (dist:install-time) — модуль встановлюється разом з програмою. Різниця від звичайного модуля в тому, що він може поставлятися як окремий APK та бере участь у слайсингу.
On-demand (dist:on-demand) — завантажується за запитом через Play Feature Delivery API:
val manager = SplitInstallManagerFactory.create(context)
val request = SplitInstallRequest.newBuilder()
.addModule("feature_ar")
.build()
manager.startInstall(request)
.addOnSuccessListener { sessionId ->
// модуль встановлюється, sessionId для відслідковування
}
.addOnFailureListener { exception ->
// SplitInstallException — обробляємо коди помилок
}
Conditional (dist:conditions) — автоматична встановлення при відповідності умовам: версія Android, наявність OpenGL ES, країна користувача.
Проблеми при внедренні
Навігація. З базового модуля не можна імпортувати класи з DFM напрямки (циклічна залежність). Навігація будується через Intent з explicit class name як строкою або через Navigation Component з include-dynamic. @Navigator з DynamicNavHostFragment — правильний спосіб для Jetpack Navigation:
<navigation>
<include-dynamic
android:id="@+id/ar_graph"
android:name="com.example.feature_ar"
app:moduleName="feature_ar"
app:graphResId="@navigation/ar_navigation" />
</navigation>
SplitCompat. Для доступу до ресурсів та класів встановленого DFM потрібно включити SplitCompat у Application:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
SplitCompat.install(this)
}
Без цього ClassNotFoundException при спробі використовувати клас з щойно встановленого модуля — поширена помилка.
Тестування. DFM не працюють на звичайній APK-сборці — лише при встановленні через Play Store або через bundletool. Для локальної розробки використовуємо bundletool install-apks або internal testing track у Play Console. Написати тест без врахування цього обмеження — втратити день.
Стан сесії. SplitInstallSessionState проходить через кілька стану: PENDING → DOWNLOADING → INSTALLING → INSTALLED. При розмірі модуля більше 10 MB Google потребує показати користувачу діалог підтвердження (SplitInstallException з кодом REQUIRES_USER_CONFIRMATION). Обробляти обов'язково, інакше встановлення мовчки перериває.
Випадок: навігація через DFM без Jetpack Navigation
У проекті на базі користувацького Router пришлось реалізувати lazy-loading модулів без Jetpack Navigation. Рішення: інтерфейс FeatureProvider у base-модулі, реалізація у DFM через ServiceLocator. DFM реєструє свій FeatureProvider при завантаженні через reflection (єдиний випадок де це виправдано — саме для DFM bootstrapping). Base-модуль запитує FeatureProvider через SplitInstallManager.installedModules.
Що перевіряємо при налаштуванні
- Граф залежностей: DFM залежить від base, ніколи навпаки
- SplitCompat ініціалізований у всіх Application/Activity
- Всі DFM-сценарії покриті обробкою
REQUIRES_USER_CONFIRMATION - Bundletool-тест на реальних пристроях перед публікацією
- Deferred install для некритичних модулів — не блокуємо користувача
Часові рамки
Налаштування одного DFM-модуля з навігацією — 3–5 днів. Переведення існуючої монолітної програми на multi-module з кількома DFM — 2–4 тижні залежно від сцепленості коду.







