If you’re starting an Android project today, one of the first decisions is which language to write it in. For years that meant Java. Since 2017 Kotlin has been an official Android language, and since 2019 Google has made it its preferred and recommended language for Android development. That shift matters - but it doesn’t mean Java has disappeared. This guide explains what actually separates the two, why Kotlin has become the default, and where you’ll still meet Java in the real world.

What makes Kotlin stand out

Kotlin was designed to fix long-standing pain points in Java while staying fully compatible with the JVM. On Android, a handful of features do most of the heavy lifting.

Null safety built into the type system

The infamous NullPointerException has caused a huge share of Android crashes over the years. Kotlin distinguishes nullable (String?) from non-nullable (String) types at compile time, so many null-related bugs are caught before the app ever runs.

Concise, expressive syntax

Kotlin removes a lot of boilerplate. A data class replaces dozens of lines of getters, setters, equals() and hashCode(). Type inference, smart casts and named arguments make code shorter and easier to read, which means fewer places for bugs to hide.

Coroutines for asynchronous work

Kotlin Coroutines make asynchronous and concurrent code read like sequential code, without callback pyramids. Combined with Flow for streams of data, they’re the modern way to keep work off the UI thread - a cornerstone of a responsive Android app.

Extension functions and more

Extension functions let you add behaviour to existing classes without inheritance, keeping code expressive and discoverable. Together with higher-order functions, sealed classes and a rich standard library, they make everyday Android code cleaner.

Kotlin in practice

A quick example shows the difference. Here’s a data class plus a coroutine that fetches a user off the main thread and safely handles a nullable result:

// A data class: equals(), hashCode(), toString() and copy() for free
data class User(val id: Long, val name: String, val email: String?)

class UserRepository(private val api: ApiService) {

    // suspend function runs on a background dispatcher, off the UI thread
    suspend fun loadUser(id: Long): User? = withContext(Dispatchers.IO) {
        val user = api.fetchUser(id)          // may return null
        // Safe-call + Elvis operator: no NullPointerException risk
        user?.copy(name = user.name.trim()) ?: null
    }
}

The same behaviour in Java would need an explicit model class with generated boilerplate, a manual threading mechanism, and defensive null checks scattered through the code. Kotlin expresses it in a fraction of the lines, with null safety enforced by the compiler.

Kotlin doesn’t ask you to abandon everything Java taught the platform - it builds on it and removes the sharp edges. - The Solution On IT engineering team

Java’s continuing role

Java is far from irrelevant. The Android framework itself has decades of Java heritage, and an enormous amount of production Android code - and countless libraries and SDKs - is written in Java. Many mature apps have large, well-tested Java codebases that work perfectly well and don’t need rewriting.

You’ll also still meet Java when maintaining legacy projects, integrating older third-party SDKs, or working on teams with deep Java expertise. Java remains a capable, stable language with a vast ecosystem; the point isn’t that Java is bad, but that Kotlin is the more productive default for new Android work.

Interop and mixed codebases

The reason this isn’t an either-or decision is interoperability. Kotlin and Java both compile to the same JVM bytecode and can call each other directly. In one project you can have Kotlin classes calling Java classes and vice versa, with no wrappers or glue code. Mixed Kotlin/Java codebases are completely normal and fully supported by Android Studio and the build tools.

This is exactly what makes gradual adoption practical: a team can keep its existing Java code running while writing every new feature in Kotlin, converting older files only when it makes sense.

Migration considerations

If you decide to move an existing Java app toward Kotlin, you don’t have to do it all at once. Android Studio ships a Java-to-Kotlin converter that gives you a fast starting point, though its output should always be reviewed - automatic conversions can produce non-idiomatic Kotlin or overly defensive nullability. A sensible approach is to convert file by file, starting with lower-risk classes, and to lean on your test suite to catch regressions along the way.

Weigh the effort honestly: migrating stable, rarely touched Java code often isn’t worth it, whereas modules under active development benefit quickly from Kotlin’s safety and brevity.

Common mistakes to avoid

  • Writing “Java in Kotlin.” Porting Java patterns line for line misses the point; learn idiomatic Kotlin to get the real benefits.
  • Overusing !!. The not-null assertion operator throws away Kotlin’s null safety; prefer safe calls, ?.let and the Elvis operator.
  • Big-bang migrations. Trying to convert an entire codebase at once is risky; incremental conversion is safer and cheaper.
  • Ignoring nullability at the boundary. Java code has no null information, so annotate or wrap it carefully when Kotlin consumes it.

Best practices for Kotlin on Android

  1. Default to Kotlin for new code. It’s Google’s recommendation and the direction the ecosystem is moving.
  2. Embrace coroutines and Flow. Use them for all asynchronous work to keep the UI thread responsive.
  3. Model data with data classes. Let the compiler generate the boilerplate you’d otherwise hand-write.
  4. Respect nullability. Design APIs so nullable and non-null intent is explicit and honest.
  5. Keep interop clean. When mixing languages, define clear boundaries and annotate Java APIs used from Kotlin.

Frequently asked questions