Android Project Setup in 2026 — Version Catalogs, KSP, Compose & CI
Set up a modern Android project with Kotlin 2.x, Compose, KSP, version catalogs, convention plugins, and GitHub Actions CI — a complete 2026 baseline.
Starting a new Android project in 2026 means Kotlin 2.x, Compose-first UI, KSP instead of KAPT, and version catalogs for dependency management. This post walks through the full setup — from libs.versions.toml to a CI pipeline that catches problems before they hit the main branch.
Baseline assumptions
- Kotlin 2.1+ with the Compose compiler plugin (no separate Compose compiler version needed)
- Compose-first — new screens use Compose, Views only where necessary
- minSdk 34, targetSdk 36 — modern API surface, no legacy workarounds
- KSP for annotation processing — KAPT is deprecated
- Version catalogs — single source of truth for dependencies
- Convention plugins — shared build logic across modules
Version catalog
Create gradle/libs.versions.toml:
[versions]
agp = "8.8.2"
kotlin = "2.1.10"
compose-bom = "2026.02.00"
coroutines = "1.10.1"
ksp = "2.1.10-1.0.31"
room = "2.7.1"
hilt = "2.55"
lifecycle = "2.9.0"
navigation = "2.9.4"
junit5 = "5.11.4"
mockk = "1.13.16"
[libraries]
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" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
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" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
junit5 = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit5" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
[bundles]
compose = ["compose-ui", "compose-material3", "compose-ui-tooling-preview", "lifecycle-runtime-compose", "lifecycle-viewmodel-compose", "navigation-compose"]
compose-debug = ["compose-ui-tooling", "compose-ui-test-manifest"]
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" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
Bundles group related dependencies so your module’s build.gradle.kts stays clean:
dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
debugImplementation(libs.bundles.compose.debug)
}
Convention plugins
Convention plugins eliminate duplicated build configuration across modules. Create a build-logic directory:
build-logic/
convention/
build.gradle.kts
src/main/kotlin/
AndroidLibraryConventionPlugin.kt
ComposeConventionPlugin.kt
settings.gradle.kts
build-logic/settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
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)
}
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 = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
}
}
}
Register it in build-logic/convention/build.gradle.kts:
gradlePlugin {
plugins {
register("androidLibrary") {
id = "myapp.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
}
}
Now any module can use plugins { id("myapp.android.library") } instead of repeating the configuration.
KSP over KAPT
KAPT runs a full Java stub generation step before annotation processing — slow and incompatible with Kotlin 2.x optimizations. KSP works directly with Kotlin symbols.
Libraries with KSP support: Room, Hilt/Dagger, Moshi, Kotlin Serialization. If a library still requires KAPT, check for a KSP migration guide or consider alternatives.
In your module’s build.gradle.kts:
plugins {
id("myapp.android.library")
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.bundles.room)
ksp(libs.room.compiler)
}
Compose-first module layout
:app — Application module, navigation host
:core:ui — Theme, shared composables, design tokens
:core:data — Repositories, data sources
:core:model — Shared data models
:core:network — Retrofit/Ktor setup, API interfaces
:feature:home — Home screen feature
:feature:profile — Profile feature
:feature:settings — Settings feature
Each feature module depends on :core:ui and :core:model. Only :app knows about all feature modules. This keeps build times fast — changing a feature only recompiles that module.
settings.gradle.kts:
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyApp"
include(":app")
include(":core:ui", ":core:data", ":core:model", ":core:network")
include(":feature:home", ":feature:profile", ":feature:settings")
Testing setup
// In convention plugin or module build.gradle.kts
dependencies {
testImplementation(libs.junit5)
testImplementation(libs.mockk)
testImplementation(libs.coroutines.test)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.ui.test)
}
tasks.withType<Test> {
useJUnitPlatform()
}
JUnit 5 gives you @Nested test classes, @ParameterizedTest, and better lifecycle control. MockK is the Kotlin-native mocking library — no more fighting with Mockito’s final class limitations.
CI with GitHub Actions
.github/workflows/android-ci.yml:
name: Android CI
on:
pull_request:
branches: [main, dev]
push:
branches: [dev]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew build
- run: ./gradlew testDebugUnitTest
This runs on every PR and push to dev. For a deeper dive into CI/CD pipelines, see the deployment post.
Common mistakes
- Still using KAPT — it’s deprecated and slows builds significantly. Migrate to KSP.
- No version catalog — hardcoded version strings across modules drift out of sync. Use
libs.versions.toml. - Skipping convention plugins — copy-pasting build config into every module is a maintenance burden. Extract shared config into plugins once you have 3+ modules.
- buildSrc instead of build-logic —
buildSrcinvalidates the entire build cache on any change. Use an included build (build-logic) instead.
For a deeper dive into Gradle configuration, see the Gradle Kotlin DSL Tips post.