androidengineers.Book a session

App Modularization

Feature Modules & Dynamic Delivery

article25 minHard

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 moduleDynamic feature module
Bundled in base APKAlwaysNo — delivered separately
Install requiredAt install timeOn demand, at install time, or conditionally
Code access from :appDirect importVia reflection or interfaces
Use caseShared codeRarely 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

ModeWhen installedUse case
on-demandUser requests itRarely used features (AR, video editor)
install-timeAt initial installFeatures most users will use
fast-followShortly after install (in background)Features expected within first session
Conditional deliveryBased 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 SplitInstallManager state machine in your UI
  • Access dynamic feature code via interfaces defined in shared modules; instantiate via reflection
  • Test locally with bundletool before releasing — device testing with Play Store staging takes time

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Feature Modules & Dynamic Delivery | Android System Design | Android Engineers