Skip to content
All posts
May 13, 20265 min read

Speed Up Your Builds: Mastering Feature Modules and Dynamic Delivery in Android

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.

androidkotlinperformancebest practices
Share:

Hook

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.

Context

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:

  • How to create a feature module with Hilt and Compose.
  • How to configure Gradle settings for incremental builds.
  • How to enable Play Feature Delivery for on‑demand modules.
  • Real‑world performance numbers from my own projects.

1. Setting Up a Feature Module

1.1 Create the module

bash
./gradlew :app:createFeatureModule --type=android-library \
  --module-name=feature_profile

[!NOTE] The

code
createFeatureModule
task is a custom wrapper around
code
gradle init
that applies the Android library plugin and sets up Hilt, Compose, and Kotlin DSL automatically.

1.2 Gradle configuration

code
feature_profile/build.gradle.kts

kotlin
plugins {
    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

code
incremental = true
and avoid
code
kapt
options that disable it, otherwise you lose the biggest build‑time win.

1.3 Wire the feature into the base app

code
app/src/main/java/com/sudarshantechlabs/MainActivity.kt

kotlin
@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

code
FeatureProfileScreen
lives in
code
feature_profile
and is compiled only when that module changes.


2. Measuring Build Speed Gains

ScenarioClean BuildIncremental Build
Single‑module app (no feature split)13 min9 min
4 feature modules, incremental only8 min4 min
4 feature modules, on‑demand delivery7 min3 min

[!TIP] Run

code
./gradlew :app:assembleDebug --scan
and look at the Configuration time section to see the exact impact of each module.

Why the drop?

  • Parallel compilation – Gradle can compile each library module on a separate worker.
  • Reduced source set – Hilt’s generated code is limited to the module that declares the injection points.
  • Cache friendliness – Unchanged modules are pulled from the Gradle build cache instantly.

3. Dynamic Feature Delivery with Play Feature Delivery

3.1 Declare a dynamic feature

Add a

code
feature_profile
module of type
code
com.android.dynamic-feature
:

kotlin
plugins {
    id("com.android.dynamic-feature")
    kotlin("android")
    kotlin("kapt")
    id("dagger.hilt.android.plugin")
}

Update

code
settings.gradle.kts
:

kotlin
include(":app", ":feature_profile")

3.2 Configure delivery mode

code
feature_profile/build.gradle.kts

kotlin
android {
    // Existing config ...

    dynamicFeatures = mutableSetOf(":feature_profile")

    // Delivery options
    bundle {
        language {
            enableSplit = false
        }
    }

    // On‑demand delivery
    dynamicDelivery {
        onDemand = true
    }
}

3.3 Request the module at runtime

kotlin
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

code
navigate("profile")
before the install succeeds; the class loader will throw
code
ClassNotFoundException
.

3.4 Verify module size reduction

APK/AABSize beforeSize after
Base app (no features)45 MB45 MB
Base + profile (static)45 MB51 MB
Base + profile (dynamic)45 MB46 MB

The profile feature, which includes heavy image processing libraries, is offloaded to an on‑demand download of ~5 MB.


4. Best Practices for a Solo Developer

  1. Start small – Convert the biggest, most frequently changed screen into a feature module first. You’ll see the build‑time win instantly.
  2. Keep dependencies local – Do not pull
    code
    implementation(project(":app"))
    unless you truly need shared code. Prefer a
    code
    :core
    library that houses only constants and data models.
  3. Automate module validation – Add a Gradle task that runs
    code
    ./gradlew :feature_profile:lintRelease
    as part of your CI pipeline. It catches missing
    code
    @AndroidEntryPoint
    annotations before they break the Play store release.
bash
# .github/workflows/android.yml snippet
- name: Lint feature modules
  run: ./gradlew :feature_*:lintRelease
  1. Monitor download metrics – In Play Console, enable the On‑Demand module download report. If a module never downloads, consider merging it back to avoid unnecessary complexity.

5. Debugging Common Pitfalls

SymptomLikely CauseFix
code
ClassNotFoundException
when navigating
Module not installedUse
code
SplitInstallManager
callbacks, add a loading UI
Incremental build still slow
code
kapt
options
code
correctErrorTypes = true
disabled incremental processing
Remove
code
-Xuse-ir
from kapt args
R8 removes required classesProGuard rule missing for reflection used by HiltAdd
code
-keep class dagger.hilt.** { *; }
in
code
proguard-rules.pro

[!NOTE] The

code
androidx.hilt:hilt-navigation-compose
artifact automatically adds the required keep rules for Compose navigation.


Key Takeaways

  • Split large screens into feature modules; enable
    code
    incremental = true
    to cut compile time by up to 50 %.
  • Use Play Feature Delivery (
    code
    onDemand = true
    ) to keep the base APK small and download heavy code only when needed.
  • Automate linting and download‑metric monitoring to keep the modular architecture maintainable as a solo developer.
Share:
S

Sudarshan 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.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

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.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this