Skip to main content
PCSalt
YouTube GitHub
Back to Android
Android · 2 min read

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-logicbuildSrc invalidates 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.