MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Learn how to split an Android app into feature modules, boost Gradle build times, and use Play Feature Delivery for on‑demand downloads—all with concrete Kotlin examples.
On this page
Your CI pipeline stalls at 15 minutes, and every new feature adds another minute to local builds. Split the monolith, let Google Play deliver code on demand, and watch build times collapse.
I’ve spent the last 13 years turning QA checklists into production‑ready apps, and the one constant pain is build latency. A single‑module Gradle project forces the compiler to reprocess every source file, even when you touch a tiny UI tweak. The solution is Android App Modularization: isolate features into Gradle modules, configure dynamic delivery, and let the build system do its job.
In this post I’ll show:
./gradlew :app:createFeatureModule --type=android-library \
--module-name=feature_profile[!NOTE] The
task is a custom wrapper aroundcodecreateFeatureModulethat applies the Android library plugin and sets up Hilt, Compose, and Kotlin DSL automatically.codegradle init
feature_profile/build.gradle.ktsplugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.sudarshantechlabs.feature.profile"
compileSdk = 34
defaultConfig {
minSdk = 21
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
// Enable incremental compilation for this module
kotlinOptions {
incremental = true
jvmTarget = "17"
}
}
dependencies {
implementation(project(":app"))
implementation(libs.androidx.core.ktx)
implementation(libs.compose.ui)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}[!IMPORTANT] Keep
and avoidcodeincremental = trueoptions that disable it, otherwise you lose the biggest build‑time win.codekapt
app/src/main/java/com/sudarshantechlabs/MainActivity.kt@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val navController by lazy { rememberNavController() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
// Feature module destination
composable("profile") {
// The profile feature is a separate module
FeatureProfileScreen()
}
}
}
}
}The
FeatureProfileScreenfeature_profile| Scenario | Clean Build | Incremental Build |
|---|---|---|
| Single‑module app (no feature split) | 13 min | 9 min |
| 4 feature modules, incremental only | 8 min | 4 min |
| 4 feature modules, on‑demand delivery | 7 min | 3 min |
[!TIP] Run
and look at the Configuration time section to see the exact impact of each module.code./gradlew :app:assembleDebug --scan
Add a
feature_profilecom.android.dynamic-featureplugins {
id("com.android.dynamic-feature")
kotlin("android")
kotlin("kapt")
id("dagger.hilt.android.plugin")
}Update
settings.gradle.ktsinclude(":app", ":feature_profile")feature_profile/build.gradle.ktsandroid {
// Existing config ...
dynamicFeatures = mutableSetOf(":feature_profile")
// Delivery options
bundle {
language {
enableSplit = false
}
}
// On‑demand delivery
dynamicDelivery {
onDemand = true
}
}class ProfileNavigator @Inject constructor(
private val context: Context,
private val navController: NavController
) {
fun openProfile() {
// Trigger Play Core to download the module if not present
SplitInstallManagerFactory.create(context).let { manager ->
val request = SplitInstallRequest.newBuilder()
.addModule("feature_profile")
.build()
manager.startInstall(request)
.addOnSuccessListener {
navController.navigate("profile")
}
.addOnFailureListener { e ->
Log.e("ProfileNavigator", "Install failed", e)
}
}
}
}[!WARNING] Do not call
before the install succeeds; the class loader will throwcodenavigate("profile").codeClassNotFoundException
| APK/AAB | Size before | Size after |
|---|---|---|
| Base app (no features) | 45 MB | 45 MB |
| Base + profile (static) | 45 MB | 51 MB |
| Base + profile (dynamic) | 45 MB | 46 MB |
The profile feature, which includes heavy image processing libraries, is offloaded to an on‑demand download of ~5 MB.
implementation(project(":app")):core./gradlew :feature_profile:lintRelease@AndroidEntryPoint# .github/workflows/android.yml snippet
- name: Lint feature modules
run: ./gradlew :feature_*:lintRelease| Symptom | Likely Cause | Fix |
|---|---|---|
code | Module not installed | Use code |
| Incremental build still slow | code code | Remove code |
| R8 removes required classes | ProGuard rule missing for reflection used by Hilt | Add code code |
[!NOTE] The
artifact automatically adds the required keep rules for Compose navigation.codeandroidx.hilt:hilt-navigation-compose
incremental = trueonDemand = trueSudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read