The Multi-Module Build Configuration Problem
Version Catalogs: Single Source of Truth for Dependencies
# gradle/libs.versions.toml
[versions]
agp = "8.8.2"
kotlin = "2.1.10"
compose-bom = "2026.03.00"
coroutines = "1.10.1"
hilt = "2.54.1"
room = "2.7.1"
ktor = "3.1.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.15.0" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
[bundles]
compose = ["compose-ui", "compose-material3", "compose-ui-tooling-preview"]
room = ["room-runtime", "room-ktx"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
room = { id = "androidx.room", version.ref = "room" }Convention Plugins: Extracting Shared Build Logic
// build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
compileSdk = 36
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
// Kotlin JVM target
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
freeCompilerArgs.addAll(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlin.ExperimentalStdlibApi"
)
}
}
}
}
// build-logic/convention/build.gradle.kts
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.kotlin.gradle.plugin)
compileOnly(libs.compose.gradle.plugin)
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "myapp.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidCompose") {
id = "myapp.android.compose"
implementationClass = "ComposeConventionPlugin"
}
register("androidHilt") {
id = "myapp.android.hilt"
implementationClass = "HiltConventionPlugin"
}
}
}Wiring It Together: The Feature Module Build File
// feature/search/build.gradle.kts
plugins {
alias(libs.plugins.myapp.android.library)
alias(libs.plugins.myapp.android.compose)
alias(libs.plugins.myapp.android.hilt)
}
android {
namespace = "com.myapp.feature.search"
}
dependencies {
implementation(projects.core.data)
implementation(projects.core.designSystem)
implementation(libs.bundles.compose)
implementation(libs.bundles.room)
implementation(libs.coroutines.android)
implementation(libs.ktor.client.core)
testImplementation(libs.coroutines.test)
testImplementation(libs.turbine)
}Migration Strategy: Incremental Adoption
Key Takeaways
- 1Version catalogs (libs.versions.toml) centralize dependency coordinates in one TOML file with type-safe, IDE-autocompleted accessors — eliminating version drift across modules.
- 2Convention plugins extract repeated build logic (compileSdk, Compose config, Hilt setup) into reusable Gradle plugins that modules apply with a single line.
- 3The build-logic included build is a regular Kotlin project: full IDE support, type safety, and testable build configuration.
- 4Layered convention plugins (android-library + compose + hilt + test) let feature modules compose only the build configuration they need.
- 5Migration is incremental: adopt version catalogs first, then create convention plugins, then expand and enforce — each phase is independently mergeable.
- 6A new feature module build.gradle.kts shrinks from 60-80 lines to roughly 10: plugin applications and module-specific dependencies.
Frequently Asked
What problem do Gradle version catalogs solve?
They centralize dependency coordinates in one libs.versions.toml with type-safe, IDE-autocompleted accessors, which eliminates version drift between modules.
What is a convention plugin?
A reusable Gradle plugin that holds build logic repeated across modules -- compileSdk, Compose config, Hilt setup -- so a module applies it in one line instead of restating it.
Do I have to migrate the whole build at once?
No. Adopt version catalogs first, then extract convention plugins, then expand and enforce. Each phase is independently mergeable, and a feature build file drops from 60-80 lines to roughly 10.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.