Chapter 2: Setting Up Your Environment
In Chapter 1 we stayed in the world of ideas: UI as a function of state, description instead of mutation, recomposition instead of manual sync. This chapter is the opposite. We're going to install tools, read Gradle files line by line, and end with a Compose app running on a device. It's the least glamorous chapter in the book and one of the most important, because a misconfigured toolchain produces errors that look like your mistakes when they're really setup mistakes — and nothing erodes a beginner's confidence faster than a red squiggle that has nothing to do with the code they wrote.
So we'll go slowly and, more importantly, we'll explain why each piece exists rather than just listing magic incantations to paste. By the end you'll be able to read a Compose project's build configuration and know what every line is for. That understanding pays off later: when you migrate an old XML project (Part IX) or chase down a build failure, you'll know which knob to turn.
A note before we start. Compose ships on a fast cadence, and exact version numbers change every couple of months. Wherever this chapter pins a specific version, treat it as a snapshot taken in mid-2026, not a permanent truth. The structure of the setup — which plugins, which dependencies, what each does — is stable and is what you should actually learn. The numbers are just today's values.
The toolchain at a glance
When you build a Compose app, five distinct things cooperate. People often blur them together, and that blurring is the source of most "why won't this build" confusion. Let's separate them clearly:
- Android Studio — the IDE. It hosts the editor, the Compose preview, the emulator, and the build tooling. The preview and "live edit" features in particular are tied to the IDE version, so a too-old Studio is a common cause of "my preview doesn't work."
- The Android Gradle Plugin (AGP) — the thing that actually turns your project into an APK or App Bundle. It defines the
android { }block and orchestrates compilation, resource packaging, and signing. - Kotlin and the Compose compiler plugin — Compose is not a library you simply call; part of it is a compiler plugin that rewrites your
@Composablefunctions at build time. As of Kotlin 2.0 this plugin lives inside the Kotlin project itself, and its version is tied to your Kotlin version. This is the single biggest change from older tutorials, and we'll dwell on it. - The Compose libraries — the actual UI APIs (
Text,Column,Modifier, Material 3, and so on), pulled in as dependencies and version-aligned through the BOM (Bill of Materials). - The Android SDK levels —
compileSdk,minSdk, andtargetSdk, which decide what platform APIs you can call and which devices you support.
Keep this five-part picture in mind. Almost every setup problem is really a mismatch between two of these — a Studio too old for a Compose feature, a Kotlin version that doesn't match the compiler plugin, a compileSdk too low for a library. Naming the parts is half the battle.
Installing Android Studio
Download the latest stable Android Studio from the official site. At the time of writing the stable line is the Quail series (version 2026.1.x); by the time you read this it may have a different animal codename, and that's fine — the rule is simply "use a recent stable release." Compose's tooling (preview, live edit, the Layout Inspector) improves with each IDE release, so being a version or two behind is the most common reason a reader's preview behaves differently from the book's screenshots.
Avoid Canary and Beta builds while learning. They carry leading-edge features but are lightly tested, and a flaky IDE is the last thing you want while forming new mental models. Stick to stable.
During installation, let the setup wizard install the Android SDK, an emulator system image, and the platform tools. If you already have Android Studio installed for View-based work, you don't need a separate installation for Compose — the same IDE builds both. Compose support has been built in for years; there's no plugin to add.
Creating your first project
Open Android Studio and choose New Project. In the template chooser, pick Empty Activity. This is the modern Compose-first template; despite the plain name, it generates a Compose project, not an XML one. (Older Android Studio versions labeled it "Empty Compose Activity"; the Compose version is now simply the default Empty Activity, while XML templates have moved aside.)
On the configuration screen:
- Name / Package name / Save location: set as you like. The package name (e.g.
com.example.composebook) becomes your app's namespace. - Language: Kotlin is the only option, and that's expected — Compose is Kotlin-only by design. There is no Java path, because the compiler plugin that makes
@Composablework is a Kotlin plugin. - Minimum SDK: choose API 24 or higher. Compose's absolute floor is lower, but API 24 is a sensible modern baseline that covers effectively all active devices and avoids a few older-API headaches you don't need while learning.
Click Finish, let Gradle sync, and you'll land in a project that already runs. Before we run it, let's read what the wizard generated — because understanding these files now means you'll never be mystified by them later.
Reading the build configuration
Modern Android projects centralize versions in a version catalog at gradle/libs.versions.toml, then reference those entries from the Gradle build scripts. This keeps every version in one place. Let's look at the relevant parts.
The version catalog
# gradle/libs.versions.toml
[versions]
agp = "9.0.0"
kotlin = "2.3.21"
composeBom = "2026.06.00"
coreKtx = "1.16.0"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
# Note: the Compose libraries below have NO version — the BOM supplies it.
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Two things in here deserve your full attention.
First, the Compose library entries carry no version number. androidx-ui, androidx-material3, and their siblings just name a group and artifact. Their versions come from the BOM, which we'll cover in a moment. This is intentional and it's the feature that keeps a dozen separately-versioned Compose libraries from drifting out of sync.
Second, look at the [plugins] block. There's kotlin-android (the Kotlin Gradle plugin) and, right below it, kotlin-compose, whose id is org.jetbrains.kotlin.plugin.compose. Both reference the same kotlin version. That shared version reference is not a coincidence — it's a hard requirement, and it's the heart of how Compose builds today.
The Compose compiler plugin (the part people get wrong)
Recall the claim from Chapter 1 that changing state causes Compose to re-run your composable and update only what changed. That magic doesn't happen by ordinary library calls. The Compose compiler plugin rewrites every @Composable function during compilation: it threads an invisible Composer parameter through them, inserts the bookkeeping that lets the runtime track which composable read which state, and adds the logic that allows recomposition to skip functions whose inputs haven't changed. When you write a plain-looking @Composable fun Greeting(name: String), the compiled output is substantially more elaborate. That transformation is what turns "just a function" into a node the runtime can observe, re-run, and skip.
Here's the key historical point. For years, this plugin shipped separately from Kotlin, on its own version number, and you had to find the exact compiler version compatible with your Kotlin version — a notorious source of "this Compose compiler requires Kotlin X" build errors. Starting with Kotlin 2.0, the plugin moved into the Kotlin project itself. You now apply it as org.jetbrains.kotlin.plugin.compose with a version that simply equals your Kotlin version. The compatibility problem is gone by construction: if your Kotlin is 2.3.21, your Compose compiler plugin is 2.3.21.
If you ever follow an older tutorial that tells you to add this:
// OUTDATED — do not use with Kotlin 2.0+
composeOptions {
kotlinCompilerExtensionVersion = "1.5.x"
}
…stop. That composeOptions block belongs to the pre-2.0 world. With the modern setup it's unnecessary and will only cause confusion. The compiler plugin is applied as a Gradle plugin, not configured as a compose option.
The module build script
Now the app module's build.gradle.kts, with the parts that matter annotated:
// app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) // applies the Compose compiler plugin
}
android {
namespace = "com.example.composebook"
compileSdk = 36
defaultConfig {
applicationId = "com.example.composebook"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
compose = true // turns on Compose for this module
}
// compileOptions / kotlin { } for the JVM target also live here
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
// The BOM: align all Compose library versions through one number
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
// Core Compose UI + Material 3 (no versions — the BOM decides)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// Tooling: preview rendering + test manifest, debug builds only
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
// UI tests
androidTestImplementation(libs.androidx.ui.test.junit4)
}
Two lines do the heavy lifting for Compose specifically:
alias(libs.plugins.kotlin.compose) applies the compiler plugin we just discussed. Without it, your @Composable functions won't be transformed and the build will fail with errors about Compose not being enabled.
buildFeatures { compose = true } tells AGP that this module uses Compose, enabling the relevant compilation and tooling paths. You need both the plugin and this flag; they do different jobs (the plugin transforms code; the flag wires Compose into the Android build).
Why the BOM exists
The line val composeBom = platform(libs.androidx.compose.bom) followed by implementation(composeBom) deserves its own explanation, because the Bill of Materials is one of the cleverer pieces of the Compose ecosystem.
Compose is not one library — it's many, spread across several Maven groups (androidx.compose.ui, androidx.compose.foundation, androidx.compose.material3, androidx.compose.animation, and more), and each is versioned independently. A given UI version might be 1.11.3 while Material 3 is at 1.4.x and the adaptive libraries are on a different track entirely. Picking compatible versions by hand across all of them would be miserable and error-prone.
The BOM solves this. It's a single artifact whose only job is to declare a known-good set of versions that work together. You pin one number — 2026.06.00 — and every Compose dependency that you list without a version inherits the correct version from that set. Upgrade the BOM, and the whole family moves together. This is why the catalog entries for androidx-ui and friends had no versions: the BOM supplies them.
When you do need a version the BOM doesn't carry — say, an experimental library on an alpha track — you can override it by specifying a version on that one dependency. But for normal use, "pin the BOM, list the libraries bare" is the pattern, and it's the pattern this book uses throughout.
The SDK levels
Three SDK numbers appear in defaultConfig and android, and they mean different things:
compileSdkis the platform version you compile against — it determines which Android APIs your code can see. Here it's 36 (Android 16). Compose libraries impose a floor on this; you must compile against a recent enough SDK for the version of Compose you use.minSdkis the oldest Android version your app will install and run on (24 here). Lower means more devices but fewer guaranteed APIs.targetSdkdeclares the version you've tested against and built for, which affects certain runtime behaviors. Keep it equal tocompileSdkfor new apps.
The versions this book pins to
For reproducibility, every example in this book is written against a fixed toolchain. As of mid-2026:
- Android Studio: latest stable (the Quail series or newer)
- AGP: 9.0.0 (Compose's lint checks require at least AGP 8.8.2 if you're on an older toolchain)
- Kotlin: 2.3.21 — which means the Compose compiler plugin is also 2.3.21
- Compose BOM: 2026.06.00 (this maps to core Compose 1.11.x)
- compileSdk / targetSdk: 36 · minSdk: 24
One forward-looking note so this doesn't surprise you. The next core Compose release, 1.12, will raise the floor: it is slated to require compileSdk 37 and AGP 9. If you're tracking the latest, plan that toolchain bump. Nothing in this book requires 1.12 — everything runs on the 1.11 baseline above — but several of the newest, still-experimental APIs we'll preview (the mediaQuery system, the Grid/FlexBox/Styles layout primitives) live on that leading edge and may shift before they stabilize. Wherever we touch them, the text flags their experimental status explicitly.
If you let the New Project wizard generate your project, it will pick a mutually compatible set of these versions automatically. The value of reading the files above is that when a version does need changing, you'll know exactly which line controls what.
Running it
You haven't written any code yet, but the generated project is a complete, runnable app. Let's confirm the whole toolchain works end to end before we start changing things — a clean baseline run now saves you from debugging a tool problem and a code problem at the same time later.
Set up a target device, either of:
- An emulator: open Device Manager, create a virtual device with a recent system image, and let it download.
- A physical device: enable Developer Options and USB debugging, then plug it in.
Press Run (the green triangle). Gradle builds the app and installs it. You should see a screen with the text "Hello Android!" — the placeholder the template ships with. We'll dissect exactly how that text gets on screen in Chapter 3.
While the build runs, you can also open MainActivity.kt and look at the Preview panel on the right. If the project synced correctly, you'll see a rendering of the UI without running the app at all. That preview is one of Compose's best features, and it's the first thing to check if your environment is healthy: a working preview means the compiler plugin, the tooling dependency, and the IDE are all cooperating.
When setup goes wrong
A short field guide to the failures you're most likely to hit, framed around the five-part toolchain so you can localize the problem.
"Compose Compiler / Kotlin version" errors. Almost always a Kotlin–plugin mismatch. With the modern setup, the fix is to make sure the kotlin-android and kotlin-compose plugins reference the same version in the catalog. If you copied a composeOptions { kotlinCompilerExtensionVersion = ... } block from an old tutorial, delete it.
The preview doesn't render (or shows "Render problem"). Usually one of: a missing debugImplementation(...ui-tooling) dependency, a Studio too old for the Compose version, or a preview function that isn't actually previewable (we'll cover the rules in Chapter 3). Try Build → Rebuild Project, and confirm ui-tooling is present as a debugImplementation.
Unresolved Compose symbols (Text, Column, etc. show red). Typically a missing buildFeatures { compose = true }, a missing core dependency, or a Gradle sync that didn't complete. Re-sync Gradle and verify both the plugin and the build feature are present.
A library demands a higher compileSdk. Raise compileSdk (and targetSdk) to the version the error names. This is the expected consequence of Compose moving its floor forward over time — exactly the 1.12 → compileSdk 37 situation described above.
Gradle/AGP incompatibility. If AGP complains about the Gradle version, let Android Studio's AGP Upgrade Assistant align them, or match them by hand using the AGP–Gradle compatibility table.
The meta-lesson: when something breaks, ask which of the five parts is implicated. Setup errors feel personal, but they're structural, and structural problems have structural fixes.
Summary
We turned the abstract model of Chapter 1 into a working environment. We separated the five cooperating pieces — Android Studio, AGP, Kotlin with the Compose compiler plugin, the Compose libraries managed through the BOM, and the SDK levels — and saw the job each one does. We read a real project's version catalog and module build script line by line, and we singled out the two pieces that are specific to Compose and most often misconfigured: the org.jetbrains.kotlin.plugin.compose plugin (now part of Kotlin, versioned to match it, the thing that rewrites your @Composable functions so recomposition can work) and buildFeatures { compose = true }. We explained the BOM as the mechanism that keeps a sprawling family of independently-versioned Compose libraries in sync from a single pinned number, recorded the exact versions this book targets, and noted the upcoming 1.12 toolchain bump so it won't catch you off guard. Finally, we ran the generated app and used the preview as a health check for the whole chain.
You now have a working Compose environment and, more valuably, the ability to read and reason about a Compose build configuration rather than copy-pasting it on faith.
What's next
In Chapter 3 we finally write Compose by hand. We'll dissect the MainActivity the wizard generated — setContent, the theme, the Scaffold — then build our own composables from scratch, learn the @Composable annotation and the conventions that go with it, and get fluent with @Preview, the tool that lets you iterate on UI without ever launching the app. By the end of the next chapter, "Hello Android" will be something you wrote and shaped yourself.