How Kotlin took over
Kotlin was announced as first-class at I/O 2017 and Kotlin-first at I/O 2019. That is an unusually fast adoption curve for a language, and it happened because Kotlin solved problems Java on Android could not, without asking anyone to rewrite anything.
Most language migrations take a decade and fail. Kotlin's took about two years to become the default and five to become nearly unavoidable, and the reason is not that it is a prettier language. It is that it fixed three specific Android problems at the exact moment Android had them.
TL;DR#
- 2017: Google announces first-class Kotlin support at I/O. Kotlin compiles to the same bytecode as Java, so adoption required no platform change.
- 2019: Google says new Android APIs and documentation will be Kotlin-first.
- The three problems it solved: null safety, asynchronous code (coroutines), and boilerplate (data classes, properties, extension functions).
- Compose closed the door. Jetpack Compose is a Kotlin compiler plugin; there is no Java version of it.
- The migration was never a rewrite. Kotlin and Java compile to the same thing and interoperate in both directions, so a file could move one at a time.
Mental model#
Think of a city where everyone drives on the left and a new car manufacturer arrives that offers vehicles designed for left-hand driving.
The roads do not change. The fuel is the same. The licensing authority accepts the same tests. But the cars are built around the way people actually drive here: the steering wheel on the right, the mirrors placed for left-side traffic.
Kotlin is that manufacturer. The roads are ART and DEX, which never changed. The fuel is JVM bytecode, which is identical. The licence is the existing build toolchain, which already knew how to compile it.
Where the analogy breaks#
A new car is a replacement. Kotlin is additive: a project can have Kotlin and Java files calling each other in the same module, and the compiler does not care which is which. That is why the migration could be incremental, and it is the single most important reason it succeeded where rewrite-based migrations fail.
The big picture#
Text description of this diagram
Nine rows comparing Java and Kotlin at the language level. Nullability: every Java reference can be null, while Kotlin distinguishes String and String with a question mark. The null check cost: Java checks by convention, Kotlin still emits a check where a Java platform type crosses the boundary. Data holders: handwritten or generated in Java, generated by the data class keyword in Kotlin. Extension functions: static utility classes in Java, static methods with a receiver in Kotlin. Properties: explicit fields and getters in Java, generated from val and var in Kotlin. Lambdas: anonymous inner classes or invokedynamic in Java, function types or invokedynamic in Kotlin. Async: callbacks or a library in Java, suspend functions with a generated state machine in Kotlin. Checked exceptions: enforced in Java, absent in Kotlin. Interop: fully two-way, with Java seeing Kotlin classes as ordinary classes and Kotlin seeing Java types as platform types.
The last row is the one that made adoption possible. Everything above it is a reason to prefer Kotlin; the last row is the reason you could try it on one file.
Deep dive#
2017: first-class, not mandatory#
The I/O 2017 announcement was careful. Kotlin was not replacing Java; it was becoming an officially supported language, with tooling, documentation and samples. The Kotlin compiler produced the same bytecode as javac, so the platform did not need to change at all.1
That framing mattered politically and technically. Developers were told they could adopt it incrementally, and they could, because:
- A Kotlin class is a JVM class; Java can call it.
- Java classes are visible from Kotlin as platform types — a type whose nullability is unknown.
- The build toolchain already had a Kotlin compile task because Kotlin had been usable as a Gradle plugin for years.
2019: Kotlin-first#
Two years later, Google went further: new Android documentation, samples and APIs would be Kotlin-first. Not Kotlin-only — the APIs remained Java-callable — but Kotlin was now the assumed reader.1
The practical consequences showed up in the Jetpack libraries. Artifacts gained Kotlin extension
functions (core-ktx), coroutine-friendly APIs (lifecycle-runtime-ktx, room-ktx), and, later,
Compose entirely.
The three problems, in order of importance#
1. Null safety.
Java's String can be null, and the type says nothing. Android's framework is full of methods that
return null on failure — getSystemService returns null for an unknown name, findViewById returns
null for a missing view, Intent.getStringExtra returns null for a missing key.
// Java: the type system will not help you.
String name = intent.getStringExtra("name");
int length = name.length(); // NullPointerException if the key was absent// Kotlin: the type is String?, and the compiler refuses the call.
val name: String? = intent.getStringExtra("name")
val length = name.length // compile error
val length = name?.length ?: 0 // explicit decisionThis is not a runtime feature. The compiler inserts a null check where a platform type crosses the boundary, and refuses to compile code that ignores nullability. The cost is zero at runtime for values that were already non-null.2
2. Asynchronous code.
Before coroutines, Android async was callbacks. AsyncTask was the platform's answer and it was
awkward; RxJava was the powerful answer and it was a large conceptual investment.
Coroutines made async code look synchronous:
// Reads top to bottom, but suspends rather than blocking.
viewModelScope.launch {
val user = repository.loadUser() // suspends
val avatar = repository.loadAvatar(user.avatarId) // suspends
_state.value = ProfileState.Ready(user, avatar)
}viewModelScope cancels the work when the ViewModel clears. That is structured concurrency: the
work has a parent, and when the parent dies, the children die. Every leak of a background task
calling into a destroyed Activity is a bug this design prevents by construction.3
3. Boilerplate.
data class User(val name: String, val age: Int) generates a constructor, getters, equals,
hashCode, toString, copy and componentN. The Java equivalent is about forty lines, or an
annotation processor, or a code generator. Multiply by every DTO in an app and the difference is
real, but it is the least important of the three — it saves typing, not bugs.
Why Compose ended the debate#
Compose is a Kotlin compiler plugin. The @Composable annotation changes how the compiler
transforms the function body; there is no equivalent for javac, and no plan to build one. An app
that adopts Compose is a Kotlin app, permanently.
That single fact moved Kotlin from "preferred" to "required for the recommended UI toolkit", and it is why new Android projects are Kotlin by default in every template.4
What the adoption did not change#
It is worth being precise about what stayed the same, because the marketing sometimes obscures it:
- ART still executes DEX. Kotlin does not have its own runtime on Android.
- The framework is still Java-shaped. Every
android.*class is a Java class with Java semantics, including platform types and checked-exception-free APIs. - Startup cost did not disappear. Kotlin's standard library adds a small amount of code; R8 removes what is unused, but a hello-world Kotlin app is not free.
- Reflection still works the same way, and R8 still needs the same keep rules for it.
The migration that did not happen#
It is worth naming what Kotlin's adoption was not. It was not a rewrite, and the industry has a long record of rewrites that failed.
The mechanism that made it work was file-level coexistence. A module can contain User.kt and
UserDao.java, and the compiler handles both in one compilation. That means a team can convert one
file, ship it, and convert the next one when it is touched for another reason. The migration cost is
spread over years and paid in small increments.
The cost of that approach is a codebase that is permanently bilingual, which has real downsides: two idioms, two review checklists, and Java files that never get converted because they work. Most large Android codebases today are in exactly that state, and the honest framing is that the migration is finished for new code and indefinite for old code.
The tooling followed the language#
Kotlin's adoption was accelerated by the fact that the ecosystem moved with it, and the order is instructive.
First came the IDE: conversion actions, inspections and refactors that made Kotlin feel native in
Android Studio. Then the libraries: AndroidX gained Kotlin extension artifacts (-ktx), then
coroutine-aware APIs, then Compose-only APIs. Then the documentation: samples and guides switched
their default language.
Each step lowered the cost of the next. By the time Compose made Kotlin mandatory for new UI, the language was already the path of least resistance for everything else.
What the Kotlin-first era did to documentation#
The least visible but most consequential effect of Kotlin-first was on the documentation itself.
API references gained Kotlin samples; guides were rewritten around idiomatic Kotlin rather than
transliterated Java; and the extension-function pattern (core-ktx) became the assumed way to call
platform APIs.
For a developer joining Android after 2019, that is simply the environment. For anyone maintaining code written before it, the mismatch is real: the docs show a coroutine and a scope function, and the codebase has a callback and a null check. Part of learning the platform today is learning to read both dialects, because production code contains both.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 4 | 1.6 Donut |
|
| 5 | 2.0 Eclair |
|
| 8 | 2.2 Froyo |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 13 | 3.2 Honeycomb |
|
| 14 | 4.0 Ice Cream Sandwich |
|
| 16 | 4.1 Jelly Bean |
|
| 17 | 4.2 Jelly Bean |
|
| 18 | 4.3 Jelly Bean |
|
| 19 | 4.4 KitKat |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 22 | 5.1 Lollipop |
|
| 23 | 6.0 Marshmallow |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 27 | 8.1 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 32 | 12L |
|
| 33 | 13 Tiramisu |
|
| 34 | 14 Upside Down Cake |
|
| 35 | 15 Vanilla Ice Cream |
|
| 36 | 16 Baklava |
|
| 37 | 17 Cinnamon Bun |
|
Under the hood#
- the Kotlin compiler as vendored into the platform build
- Soong's Kotlin support, which compiles platform modules written in Kotlin
- Activity.javaa representative framework class: Java semantics, platform types, and nullability annotations
The external/kotlin entry is a useful reminder that the platform itself is written in a mixture of
Java, Kotlin, C++ and Rust, and that Kotlin's role inside AOSP is smaller than its role in apps.
Curiosity corner#
- Kotlin was not designed for Android. It was a general-purpose JVM language from JetBrains, and Android's needs happened to align with its design goals.
- The Kotlin standard library is small on purpose. Much of what feels like the language —
map,filter,firstOrNull— is extension functions over existing Java collections, not new runtime classes. - Java is still supported and still compiles. "Kotlin-first" never meant "Java deprecated", and a Java-only app built today is not doing anything wrong.
Try it#
javap -p app/build/tmp/kotlin-classes/debug/com/example/User.class
Example output
public final class User {
private final java.lang.String name;
private final int age;
..../gradlew :app:compileDebugSources
Example output
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavacBuild it#
Take a Java class in an existing project and convert it with the IDE's "Convert Java file to Kotlin" action. Then call it from a Java file. Nothing in the build needs to change. That lack of friction is the whole story of the adoption.
Check yourself#
Check yourself
Why did Android choose the Java language but not the JVM?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsAndroid for Kotlin developersdeveloper.android.comaccessed 2026-10-27↩
- docsKotlin documentationkotlinlang.orgaccessed 2026-10-27↩
- docsKotlin coroutines on Androiddeveloper.android.comaccessed 2026-10-27↩
- docsJetpackdeveloper.android.comaccessed 2026-10-27↩
- specGoogle LLC v. Oracle America, Inc., 593 U.S. (2021)Supreme Court of the United Statesaccessed 2026-10-27↩