Dynamic Delivery (Play Feature Delivery) lets you ship optional features as separate APK modules that users download on demand — reducing initial install size and opening advanced delivery strategies.
Dynamic Feature Module vs Regular Module
| Regular module | Dynamic feature module | |
|---|---|---|
| Bundled in base APK | Always | No — delivered separately |
| Install required | At install time | On demand, at install time, or conditionally |
Code access from :app | Direct import | Via reflection or interfaces |
| Use case | Shared code | Rarely used features (camera, AR, heavy SDK) |
Creating a Dynamic Feature Module
// In the feature module's build.gradle.kts:
plugins { id("com.android.dynamic-feature") }
android {
// No applicationId here — dynamic features share the base's ID
}
dependencies {
implementation(project(":app")) // always depends on base :app
}
<!-- AndroidManifest.xml of the dynamic feature -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution">
<dist:module
dist:instant="false"
dist:title="@string/feature_camera_title">
<dist:delivery>
<dist:on-demand /> <!-- or <dist:install-time /> or <dist:fast-follow /> -->
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
</manifest>
Requesting a Dynamic Feature at Runtime
class CameraViewModel : ViewModel() {
private val splitInstallManager = SplitInstallManagerFactory.create(context)
fun installCameraFeature() {
val request = SplitInstallRequest.newBuilder()
.addModule("feature_camera")
.build()
splitInstallManager.startInstall(request)
.addOnSuccessListener { sessionId ->
// Monitor install progress with sessionId
}
.addOnFailureListener { exception ->
when ((exception as SplitInstallException).errorCode) {
SplitInstallErrorCode.NETWORK_ERROR -> showOfflineError()
SplitInstallErrorCode.MODULE_UNAVAILABLE -> showNotAvailableError()
else -> showGenericError()
}
}
}
fun observeInstallState(sessionId: Int): Flow<InstallStatus> = callbackFlow {
val listener = SplitInstallStateUpdatedListener { state ->
if (state.sessionId() == sessionId) {
when (state.status()) {
SplitInstallSessionStatus.DOWNLOADING ->
trySend(InstallStatus.Downloading(state.bytesDownloaded(), state.totalBytesToDownload()))
SplitInstallSessionStatus.INSTALLED ->
trySend(InstallStatus.Installed)
SplitInstallSessionStatus.FAILED ->
trySend(InstallStatus.Failed(state.errorCode()))
}
}
}
splitInstallManager.registerListener(listener)
awaitClose { splitInstallManager.unregisterListener(listener) }
}
}
sealed class InstallStatus {
data class Downloading(val bytes: Long, val total: Long) : InstallStatus()
object Installed : InstallStatus()
data class Failed(val code: Int) : InstallStatus()
}
Accessing Code After Install
Since dynamic features aren't in the base APK, you can't directly import their classes at compile time. Use a factory pattern or service loader:
// In :app — define an interface in a shared module
interface CameraLauncher {
fun launch(context: Context)
}
// In :feature_camera — implement it
class CameraLauncherImpl : CameraLauncher {
override fun launch(context: Context) {
context.startActivity(Intent(context, CameraActivity::class.java))
}
}
// Load at runtime after installation
fun launchCamera(context: Context) {
try {
val clazz = Class.forName("com.example.camera.CameraLauncherImpl")
val launcher = clazz.newInstance() as CameraLauncher
launcher.launch(context)
} catch (e: ClassNotFoundException) {
// Feature not installed yet
installCameraFeature()
}
}
Delivery Modes
| Mode | When installed | Use case |
|---|---|---|
on-demand | User requests it | Rarely used features (AR, video editor) |
install-time | At initial install | Features most users will use |
fast-follow | Shortly after install (in background) | Features expected within first session |
| Conditional delivery | Based on device features/country | "Only if device has NFC" |
<!-- Conditional on NFC capability -->
<dist:delivery>
<dist:install-time>
<dist:conditions>
<dist:device-feature dist:name="android.hardware.nfc"/>
</dist:conditions>
</dist:install-time>
</dist:delivery>
Testing Dynamic Features Locally
# Build a local bundle
./gradlew bundleDebug
# Install with bundletool
bundletool build-apks --bundle=app/build/outputs/bundle/debug/app-debug.aab \
--output=local.apks --local-testing
bundletool install-apks --apks=local.apks
Key Takeaways
- Dynamic features reduce initial install size — useful when features are > 5 MB and used by < 50% of users
- On-demand delivery requires handling
SplitInstallManagerstate machine in your UI - Access dynamic feature code via interfaces defined in shared modules; instantiate via reflection
- Test locally with
bundletoolbefore releasing — device testing with Play Store staging takes time