Irbisa · Cheatsheet

Gradle & Modularization

Gradle KTS, version catalogs, convention plugins, KSP, build variants, configuration cache, modules

Senior Developer · 20 questions

  1. 01

    Which Gradle files make up an Android project, and what does each one do?

    settings.gradle.kts declares which modules exist and where plugins and libraries are downloaded from, each module's build.gradle.kts configures that module, and the Gradle wrapper pins one Gradle version for every machine.

    // settings.gradle.ktspluginManagement {    repositories {        google()        mavenCentral()        gradlePluginPortal()…
  2. 02

    What is Android Lint, and how do you make it fail the build instead of producing a report nobody reads?

    Android Lint is the static analyzer built into AGP: it checks code, resources and the manifest for real bugs, such as calling an API above minSdk, a missing permission or an unused resource.

    // app/build.gradle.ktsandroid {    lint {        abortOnError = true                   // the default: errors fail the task        warningsAsErrors = true        baseline = file("lint-baseline.xml")  // report only issues newer than this file…
  3. 03

    What is multidex, why did it exist, and do you still need it?

    One classes.dex file can reference at most 65,536 methods, so larger apps are split into classes2.dex, classes3.dex and so on.

  4. 04

    What does ./gradlew hello print for this build script, and what does ./gradlew help print?

    // build.gradle.ktsprintln("1: script body") tasks.register("hello") {    println("2: configuring hello")    doLast { println("3: running hello") }} tasks.register("cleanup") {    println("4: configuring cleanup")} println("5: end of script")

    ./gradlew hello prints 1, 5, 2, 3 and ./gradlew help prints only 1 and 5.

  5. 05

    What is the difference between implementation, api, compileOnly and runtimeOnly?

    They decide which classpaths a dependency lands on.

    // core/network/build.gradle.ktsdependencies {    api(project(":core:model"))         // `User` from :core:model is in this module's public API    implementation(libs.retrofit)       // used inside, never exposed    compileOnly(libs.javax.annotation)  // annotations only, not packaged    runtimeOnly(libs.slf4j.android)     // a backend nobody imports…
  6. 06

    What is a version catalog, and why do you still need a BOM next to it?

    A version catalog (gradle/libs.versions.toml) is one shared list of libraries, plugins and versions with type-safe accessors such as libs.retrofit.

    // gradle/libs.versions.toml// [versions]// composeBom = "2025.09.00"// retrofit = "3.0.0"//// [libraries]…
  7. 07

    What is the difference between build types and product flavors, and how do you ship a staging app with its own API URL and icon next to production?

    Build types describe how the app is built (debuggable, signed, minified), product flavors describe which app is built (free or paid, staging or prod), and a variant is one combination.

    android {    namespace = "com.example.app"    buildFeatures { buildConfig = true }   // off by default since AGP 8     defaultConfig {        applicationId = "com.example.app"…
  8. 08

    How many build variants does this configuration produce, and which source sets does freePlayDebug compile?

    android {    flavorDimensions += listOf("tier", "store")    productFlavors {        create("free") { dimension = "tier" }        create("paid") { dimension = "tier" }        create("play") { dimension = "store" }        create("huawei") { dimension = "store" }    }    buildTypes {        getByName("debug")        getByName("release")        create("qa") { initWith(getByName("release")) }    }}

    Twelve: 2 tiers × 2 stores × 3 build types, each with its own set of tasks.

  9. 09

    What happens between ./gradlew assembleRelease and a signed APK?

    Sources compile to .class files, D8 (or R8 when isMinifyEnabled = true) turns them into classes.dex, AAPT2 compiles the resources and the merged manifest, and the result is zipped, aligned and signed.

  10. 10

    What is the difference between kapt and KSP, and what do you gain by migrating?

    kapt runs Java annotation processors by first generating Java stubs for every Kotlin file and compiling them with javac; KSP is a Kotlin API that reads the compiler's symbols directly.

    // before://   plugins { id("org.jetbrains.kotlin.kapt") }//   kapt(libs.room.compiler) plugins {    alias(libs.plugins.android.library)…
  11. 11

    How does Gradle resolve dependency version conflicts, and how do you track down a "Duplicate class" error?

    When two paths request different versions of the same module, Gradle picks the highest one, unlike Maven's nearest-wins rule.

    // Who asked for it, and why did this version win?//   ./gradlew :app:dependencyInsight --configuration releaseRuntimeClasspath --dependency okio dependencies {    // one family, one version    implementation(platform(libs.okhttp.bom))…
  12. 12

    What does a Java toolchain do in an Android build, and what does "Inconsistent JVM-target compatibility" mean?

    A toolchain pins the JDK that compiles your code, whatever JDK happens to run Gradle.

    kotlin { jvmToolchain(17) } android {    compileOptions {        // both must name the same level        sourceCompatibility = JavaVersion.VERSION_17…
  13. 13

    Why does every edit in buildSrc reconfigure the whole project, and what changes when convention plugins live in an included build?

    buildSrc is on the classpath of every build script, so any change to it makes every compiled script stale and forces full reconfiguration.

    // settings.gradle.ktspluginManagement {    includeBuild("build-logic")   // a separate build, not a project dependency} // build-logic/convention/build.gradle.kts…
  14. 14

    This code generation task reruns on every build and fails once the configuration cache is on: what is wrong with it, and how do you fix it?

    tasks.register("generateConfig") {    doLast {        val dir = project.layout.buildDirectory.dir("generated/config").get().asFile        dir.mkdirs()        val url = System.getenv("API_URL") ?: "https://api.example.com"        File(dir, "Config.kt").writeText("package com.example\n\nconst val API_URL = \"$url\"\n")    }} tasks.named("preBuild") { dependsOn("generateConfig") }

    The task declares no inputs or outputs, so Gradle cannot ever consider it up to date, and its action reaches for project at execution time, which the configuration cache does not allow.

  15. 15

    What is the difference between the build cache and the configuration cache, and what stops each of them from hitting?

    The configuration cache stores the finished task graph and skips the whole configuration phase; the build cache stores task outputs keyed by task inputs and skips the tasks themselves.

  16. 16

    A Gradle build is slow: how do you find out why, and which changes usually give the biggest win?

    Measure before changing anything: a build scan or Android Studio's Build Analyzer on a realistic incremental build shows whether time goes to configuration, to a few slow tasks, or to a long chain of modules waiting on each other.

    # gradle.properties - change one thing at a time and measure againorg.gradle.caching=trueorg.gradle.configuration-cache=trueorg.gradle.parallel=trueorg.gradle.jvmargs=-Xmx6g -XX:+UseParallelGCkotlin.daemon.jvmargs=-Xmx3g…
  17. 17

    When does splitting a module into :api and :impl speed up the build, and when is it just ceremony?

    It pays off when many modules use a service whose implementation changes often: consumers compile against a small, stable :api module, so editing the implementation recompiles only :impl and :app.

  18. 18

    How do you make dependency resolution reproducible, so the same commit resolves the same artifacts next month?

    Ban moving versions, lock the resolved graph and verify artifact checksums.

  19. 19

    What do you have to get right when publishing an Android library module as an AAR to a Maven repository?

    Beyond applying maven-publish: publish an explicit variant so consumers get the right artifact and metadata, ship consumer R8 rules inside the AAR, and guard the public API so you notice a breaking change before your consumers do.

    // core/network/build.gradle.ktsplugins {    alias(libs.plugins.android.library)    `maven-publish`}…
  20. 20

    How does a dynamic feature module change the dependency graph, and how does the base app open a screen from it?

    The arrow is reversed: the dynamic feature depends on :app, not the other way round, so the base cannot import any feature class.

    // app/build.gradle.ktsandroid {    dynamicFeatures += setOf(":feature:scan")} // feature/scan/build.gradle.kts - note the direction of the dependency…