CrikStats is an Android application built to demonstrate Dynamic Feature Modules (DFM), clean modular architecture, and dependency injection across on-demand features using Hilt + Dagger component dependencies.
The project is structured into two independent modules to enforce strict separation of concerns and support dynamic delivery.
app (Base Module)
- Hosts the Singleton NetworkModule
- Contains shared UiState
- Owns the navigation graph
- Installs and initializes SplitCompat
- Acts as the dependency provider for dynamic features
feature-player (Dynamic Feature Module)
- Downloaded on-demand
- Contains Player UI and Player Repository
- Not included in the initial APK install
This structure ensures scalability, reduced initial APK size, and clean ownership boundaries.
The dynamic feature module is registered at the project level:
rootProject.name = "CrikStats"
include(":app")
include(":featureplayer")The Dynamic Feature plugin is declared in the root build.gradle.kts so it can be applied by feature modules:
plugins {
id("com.android.application") version "8.1.4" apply false
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
id("com.android.dynamic-feature") version "8.1.4" apply false
}AndroidX and Jetifier are enabled to avoid duplicate class issues:
android.useAndroidX=true
android.enableJetifier=trueThe base app declares the dynamic feature and includes the Play Feature Delivery dependencies required to manage on-demand installs:
android {
dynamicFeatures += setOf(":featureplayer")
}
dependencies {
implementation("com.google.android.play:feature-delivery:2.0.0")
implementation("com.google.android.play:feature-delivery-ktx:2.0.0")
}These libraries provide access to SplitInstallManager, request handling, and install state callbacks.
The feature module applies the dynamic-feature plugin and depends on the base app module:
plugins {
id("com.android.dynamic-feature")
id("org.jetbrains.kotlin.android")
}
dependencies {
implementation(project(":app"))
}The on-demand delivery behavior is defined in
featureplayer/src/main/AndroidManifest.xml:
<dist:module
dist:instant="false"
dist:title="@string/title_feature_player">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>This ensures the feature module is excluded from the base install and downloaded only when explicitly requested at runtime.
Standard Hilt injection fails with Dynamic Feature Modules because the feature module cannot access the app module’s generated Hilt components.
To solve this, the Component Dependency pattern is used.
In the Base Module, a PlayerModuleDependencies interface is created and annotated with @EntryPoint to expose shared dependencies such as the Retrofit instance.
In the Feature Module, instead of using @AndroidEntryPoint, a custom Dagger @Component is created which depends on PlayerModuleDependencies.
Dependencies are retrieved manually from the application context using EntryPointAccessors.
This approach allows the dynamic feature to reuse the same OkHttp and Retrofit instances from the base app, preventing memory leaks and avoiding duplicate networking stacks.
Mock APIs are served using Mocky.io. To run the project, add the base URL to the local.properties file:
BASE_URL="https://mocki.io/"This allows easy switching between mock and real backends without code changes.
The standard Run button in Android Studio often installs all modules at once, which does not accurately simulate dynamic feature downloads. To properly test on-demand delivery, Bundletool local testing is used.
Bundletool
Download bundletool-all-1.18.2.jar and place it in the project root. Do not rename the file.
Direct download link:
https://github.com/google/bundletool/releases/download/1.18.2/bundletool-all-1.18.2.jar
If you prefer to use Bundletool installed globally via a package manager, the deploy_test.sh script must be modified accordingly. Using a globally installed Bundletool binary is possible, but would require modifying the bash script accordingly. The current script assumes usage of the local bundletool-all-1.18.2.jar file and invokes it using java -jar.
If the JAR file is not present, deploy_test.sh will exit early with an error.
Physical Device
Enable USB debugging on a physical device. Emulator testing is not recommended for local split installs.
A shell script (deploy_test.sh) is provided to automate deployment. Run the following command from the project root:
./deploy_test.sh- Builds the Android App Bundle (.aab)
- Uses Bundletool with the
--local-testingflag to generate device-specific APK splits - Installs these APKs directly onto the connected device
This triggers Fake Split Install mode. When the user taps “Download Module” in the app, the SplitInstallManager intercepts the request and installs the dynamic feature instantly from the device’s local storage, without requiring the Play Store. This verifies that the dynamic delivery logic works exactly as it would in production.
CrikStats demonstrates clean modular Android architecture, real-world Dynamic Feature Module implementation, advanced dependency injection across DFMs, and proper local testing of on-demand delivery without relying on the Play Store.