Setting Up Dynamic Feature Modules for Android Application
Food delivery application has built-in AR-view module for dishes — weighs 23 MB additional resources. This function is opened by 8% of users. Other 92% download 23 MB they never need. Dynamic Feature Modules solves exactly this: AR-module downloads only when user taps "view in AR".
Project Architecture with DFM
Project restructures to multi-module structure. app becomes base module — contains only critical startup functionality. Heavy or rarely used functions extracted to separate dynamic feature modules.
Structure build.gradle for DFM:
// dynamic feature module build.gradle
plugins {
id("com.android.dynamic-feature")
}
android {
defaultConfig { minSdk = 21 }
}
dependencies {
implementation(project(":app")) // dependency on base module
}
In app/build.gradle:
android {
dynamicFeatures += setOf(":feature_ar", ":feature_premium")
}
Three Installation Modes
Install-time (dist:install-time) — module installed together with application. Difference from regular module is that it can be shipped as separate APK and participates in slicing.
On-demand (dist:on-demand) — loads on request through Play Feature Delivery API:
val manager = SplitInstallManagerFactory.create(context)
val request = SplitInstallRequest.newBuilder()
.addModule("feature_ar")
.build()
manager.startInstall(request)
.addOnSuccessListener { sessionId ->
// module installs, sessionId for tracking
}
.addOnFailureListener { exception ->
// SplitInstallException — handle error codes
}
Conditional (dist:conditions) — automatically installs when conditions met: Android version, OpenGL ES presence, user country.
Implementation Issues
Navigation. Cannot import classes from DFM directly from base module (circular dependency). Navigation built through Intent with explicit class name as string or through Navigation Component with include-dynamic. @Navigator with DynamicNavHostFragment — correct way for 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. To access resources and classes from installed DFM need to enable SplitCompat in Application:
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
SplitCompat.install(this)
}
Without this ClassNotFoundException when trying to use class from just-installed module — common error.
Testing. DFM don't work on regular APK build — only when installed through Play Store or through bundletool. For local development use bundletool install-apks or internal testing track in Play Console. Write test without accounting for this limitation — lose a day.
Session state. SplitInstallSessionState passes through several states: PENDING → DOWNLOADING → INSTALLING → INSTALLED. For module size > 10 MB Google requires showing user confirmation dialog (SplitInstallException with code REQUIRES_USER_CONFIRMATION). Must handle, otherwise installation silently interrupted.
Case: Navigation Through DFM Without Jetpack Navigation
In project based on custom Router had to implement lazy-loading modules without Jetpack Navigation. Solution: FeatureProvider interface in base-module, implementation in DFM through ServiceLocator. DFM registers its FeatureProvider on load through reflection (only case where justified — exactly for DFM bootstrapping). Base-module requests FeatureProvider through SplitInstallManager.installedModules.
What We Check When Setting Up
- Dependency graph: DFM depends on base, never reverse
- SplitCompat initialized in all Application/Activity
- All DFM scenarios covered with
REQUIRES_USER_CONFIRMATIONhandling - Bundletool test on real devices before publishing
- Deferred install for non-critical modules — don't block user
Timeframe
Setting up one DFM module with navigation — 3–5 days. Converting existing monolithic application to multi-module with several DFM — 2–4 weeks depending on code coupling.







