Runtime, build and Gradle cheat sheet
The 10 Runtime, build and Gradle interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Kotlin → bytecode → DEX → ART with profiles.
- R8 crashes are almost always reflection; keep narrowly.
- KSP over kapt.
- Measure builds before tuning them.
- @Volatile is visibility, not atomicity.
ART: interpreter, JIT and AOT
ART starts with code that hasn't been compiled: it interprets it, and the JIT compiles methods that get hot. While doing so it records those hot methods in a profile. Later, when the device is idle and charging, a background job (dex2oat) compiles the profiled methods ahead of time, so later launches run machine code. Baseline Profiles and Play's Cloud Profiles give ART that list at install time, so even the first launch is fast.
R8, shrinking and keep rules
R8 shrinks (removes unused code and resources), optimises (inlining, removing dead branches), and obfuscates names in release builds. It reads ProGuard-format keep rules. Crashes happen when code is reached through reflection, such as serialisation or JNI, and R8 removed or renamed it. Fix with targeted keep rules or reflection-free libraries; libraries ship their own consumer rules.
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}kapt vs KSP
Both run annotation processors such as Room, Dagger/Hilt and Moshi. kapt generates Java stubs from Kotlin so Java annotation processors can run, which is slow. KSP (Kotlin Symbol Processing) reads Kotlin symbols directly and is often about twice as fast. Room, Hilt and most popular libraries support KSP, so new projects should use it and drop kapt where possible.
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
}Speeding up Gradle builds
Measure first with a Build Scan or the Build Analyzer. Then the usual wins: turn on the configuration cache and build cache, replace kapt with KSP, split code into modules with implementation dependencies so changes recompile less, avoid work at configuration time, keep plugins and AGP up to date, and give Gradle enough memory. Non-transitive R classes also cut recompilation.
Build types, flavors and variants
Build types describe how you build, such as debug and release (signing, minification, debuggable). Product flavors describe what you build, such as free and paid, or staging and production, each with its own applicationId suffix, resources or code. Gradle combines them into variants, like freeRelease. Each variant can have its own source set, src/free/.
Version catalogs and convention plugins
Put versions and coordinates in a version catalog, gradle/libs.versions.toml, and refer to them as libs.androidx.room.runtime. Use BOMs for families like Compose and Firebase. Put shared build setup (compileSdk, Kotlin options, Compose, test setup) in convention plugins in an included build-logic build, so each module applies one plugin instead of copying blocks.
[versions]
room = "2.8.5"
[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
compose-bom = { module = "androidx.compose:compose-bom", version = "2026.09.00" }Garbage collection on Android
ART frees objects that are no longer reachable from GC roots such as threads, static fields and JNI references. Modern ART uses a concurrent, generational collector, so pauses are short, but allocating lots of short-lived objects in hot paths (onDraw, scroll, tight loops) still costs CPU and can cause jank. Leaks happen when a long-lived root keeps something like an Activity reachable.
Threads, @Volatile and synchronized
When threads share mutable data, you need visibility and atomicity. @Volatile makes writes visible to other threads but doesn't make count++ atomic. synchronized (or a lock) makes a block atomic and visible. Atomic classes like AtomicInteger do single-value updates without locks. With coroutines, prefer Mutex, confinement to one thread, or a StateFlow's update instead.
class HitCounter {
private val hits = java.util.concurrent.atomic.AtomicInteger(0)
fun hit(): Int = hits.incrementAndGet()
val total: Int get() = hits.get()
}OOP principles in Kotlin
Encapsulation hides state behind an API, such as a private val _state exposed as read-only StateFlow. Inheritance reuses behaviour from a parent class, which Kotlin makes opt-in with open. Polymorphism lets callers use one type for many implementations, such as a Repository interface with fake and real versions. Abstraction exposes what a thing does, not how.
Interface vs abstract class
An interface declares a contract and can have default method bodies and abstract properties, but no state; a class can implement many. An abstract class can hold state and constructor parameters, but a class extends only one. Use interfaces for capabilities and seams for testing; use an abstract class when implementations share real state or setup.
Quick facts
- DEX limit
- A single DEX file can reference at most 65,536 methods; multidex is built in from Android 5.0.
- D8
- D8 is the dexer that turns Java bytecode into DEX; R8 does the same while shrinking and optimising.
- KSP
- KSP processes Kotlin symbols directly and is often about twice as fast as kapt.
- Configuration cache
- Gradle's configuration cache reuses the task graph when build scripts haven't changed.
- ART
- ART replaced Dalvik as the only runtime in Android 5.0.