Why Download the "Payment" Feature for Users Who Only Browse? Implementing Dynamic Delivery
A Senior Engineer’s Guide to Reducing App Install Size by 50% Using On-Demand Feature Modules and Navigation Contracts.
TL;DR
- Use Dynamic Feature Modules for large, heavy features with low usage or regional constraints.
- Configure delivery in the feature module’s
AndroidManifest.xml, not Gradle. - Invert dependencies: Feature modules depend on the Base; use Navigation Contracts and Service Discovery to communicate.
- Treat downloads like network calls: Wrap
SplitInstallManagerin a Repository to handle state and idempotency.
You’ve spent months optimizing your Android app. You audited dependencies, converted PNGs to WebP, and fine-tuned R8. Your APK is lean — but then, a new requirement arrives: a heavy 3D rendering engine for a “Virtual Try-On” or a specialized KYC verification flow that only 5% of your users will ever touch.
Suddenly, your 15MB app jumps to 45MB.
For users in regions with expensive data or limited storage, that 30MB increase is a high barrier to entry. Senior engineers don’t accept this bloat; they use Dynamic Delivery.
1. Identifying “On-Demand” Candidates
Modularization for the sake of modularization is a technical debt trap. To find features that truly deserve to be “Dynamic,” look for these three red flags in your monolith:
- High Weight / Low Frequency: A 10MB “Customer Support Chat” used by 2% of users.
- Conditional Access: Features locked behind a “Premium” paywall.
- Distribution Constraints: A payment gateway that only functions in India shouldn’t be downloaded by a user in Brazil.
2. Defining Delivery: Manifest vs. Business Logic
In modern Android Gradle Plugin (AGP) versions, delivery is controlled via the AndroidManifest.xml of the feature module.
The Feature Module Manifest (:photo-editor)
Use the <dist:module> tag to define the delivery contract.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/res-auto"
package="com.example.photo_editor">
<dist:module
dist:onDemand="true"
dist:title="@string/title_photo_editor">
<dist:fusing dist:include="true" />
<dist:conditions>
<dist:device-country dist:code="IN"/>
</dist:conditions>
</dist:module>
</manifest>3. Architecture: Navigation & Discovery
In Dynamic Delivery, the Feature Module depends on the Base Module. This creates a visibility problem: the Base cannot “see” the classes in the Feature.
The Discovery Mechanism
To avoid fragile, string-based Intents, use a discovery pattern:
- Hilt EntryPoints: The gold standard for DI-heavy apps.
- ServiceLoader: A clean, standard Java/Kotlin way to discover implementations.
- Navigation Contracts: Define an interface in your Base Module that the Feature Module implements.
4. Implementing a Production-Grade Install Flow
Don’t let your UI talk directly to the SplitInstallManager. Instead, encapsulate the logic. This repository becomes the single source of truth for feature installation state across the app.
The FeatureInstallRepository
class FeatureInstallRepository(private val manager: SplitInstallManager) {
fun installModule(moduleName: String): Flow<InstallState> = callbackFlow {
// Note: moduleName must match the Gradle module name, not the package name.
if (manager.installedModules.contains(moduleName)) {
trySend(InstallState.Installed)
close()
return@callbackFlow
}
val request = SplitInstallRequest.newBuilder().addModule(moduleName).build()
val listener = SplitInstallStateUpdatedListener { state ->
trySend(state.toDomainState())
}
manager.registerListener(listener)
manager.startInstall(request)
awaitClose { manager.unregisterListener(listener) }
}
}⚠️ When NOT to use Dynamic Delivery
Dynamic delivery is powerful, but it introduces complexity. Avoid it if:
- Core Logic: The feature is required for the app to boot or perform its primary function.
- High Usage: If >80% of your users use the feature, the overhead of the download flow outweighs the size savings.
- Cold Start Features: Anything needed immediately upon clicking the app icon.
- Lack of Testing Infrastructure: If your team isn’t prepared to maintain a complex CI/CD pipeline that handles multi-module instrumentation.
🙋♂️ Frequently Asked Questions (FAQs)
Can I uninstall a module to save space?
Yes! Use splitInstallManager.deferredUninstall(listOf("onboarding")). Google Play prunes the code during the next background maintenance cycle.
Does this affect Hilt?
Yes. Since the Base doesn’t know about the Feature, you must use Hilt EntryPoints to “peek” into the feature’s component once it’s installed.
What is the “Install Size” benefit?
Google Play case studies report that apps using Dynamic Delivery see an average 20% to 50% reduction in initial install size, which significantly improves conversion rates in emerging markets.
💬 Let’s Discuss!
- Analyze your APK: Open your latest build in the APK Analyzer. What is the single largest package that isn’t required for the app’s first five minutes?
- Degraded Navigation: How does your app behave if the user clicks a dynamic feature while offline?
- Discussion: What has been your biggest hurdle with dynamic modules — DI, navigation, or CI/CD?
Video References
- Customizing App Delivery with the App Bundle — Official Google I/O session.
📘 Master Your Next Technical Interview
Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.
- E-book (Best Value! 🚀): $1.99 on Google Play
- Kindle Edition: $3.49 on Amazon
- Also available in Paperback & Hardcover.

Comments
Post a Comment