Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 010 / 100

Kotlin vs Java for Android: patterns, pitfalls and fixes

Why most new Android code is Kotlin.

Expona Admin6 min read

Introduction

After ten days of building Android fundamentals, you've likely noticed that most modern codebases and official Google samples use Kotlin rather than Java. The question isn't whether Kotlin is trendy—it's whether the language trade-offs justify shifting from Java, which still powers millions of Android apps. This article explores Kotlin vs Java for Android: patterns, pitfalls and fixes, examining the practical differences that matter in production code.

Both languages compile to the same bytecode and run on the Android Runtime (ART), so choosing one over the other is about developer productivity, code safety, and maintainability. We'll compare key features, show you how to work with both in the same project, and walk through a real migration strategy so your team can adopt Kotlin incrementally without disrupting delivery.

Feature Comparison: Null Safety, Async, UI, and Boilerplate

The core differences between Kotlin and Java on Android centre around four areas: how each handles null references, asynchronous code, modern UI frameworks, and ceremony in the language itself.

Null Safety: Built-in vs Annotations

Java relies on annotations like @Nullable and @NonNull to document whether a reference can be null. These are hints for static analysis tools, not enforced by the compiler or runtime. A careless developer can ignore them, and null pointer exceptions still crash production apps.

Kotlin bakes null safety into the type system. A variable declared as String can never be null; only String? can. The compiler refuses to compile code that might cause a null dereference without an explicit check. No annotation ceremonies, no runtime surprises.

Feature Java Kotlin
Null handling Annotations + developer discipline Type system enforces it
Async code Callbacks, executors, threads Coroutines (suspend functions)
UI framework XML layouts + programmatic tweaks Jetpack Compose is Kotlin-only
Boilerplate Getters, setters, constructors Data classes, extension functions

In practice, Java's NullPointerException is still a leading cause of crashes in production Android apps. Kotlin eliminates entire classes of bugs at compile time.

Async: Coroutines vs Callbacks and Executors

Java code often chains callbacks or relies on ExecutorService to handle background work. The pattern works, but nesting callbacks (callback hell) becomes unmaintainable quickly. Thread management is manual and error-prone.

Kotlin's coroutines let you write asynchronous code that looks sequential. A suspend function pauses without blocking the thread, and the Kotlin runtime resumes it when the result is ready. This is not multithreading; it's cooperative multitasking with far less overhead.

kotlin
// Kotlin: clean, sequential-looking async code
suspend fun fetchUserData(userId: String): User = withContext(Dispatchers.IO) {
    api.getUser(userId)
}

// Called from a coroutine scope
viewModelScope.launch {
    val user = fetchUserData("123")
    updateUI(user)
}

The equivalent Java code involves callbacks, error handling chains, or reactive libraries that add complexity. Coroutines are simpler, more efficient, and part of the standard Kotlin library.

UI: Compose is Kotlin-Only

Google's modern UI framework, Jetpack Compose, is built for Kotlin and leverages language features like lambda syntax, extension functions, and coroutine integration. You can use traditional XML layouts with Java, but Compose—the future of Android UI—requires Kotlin.

A simple Compose button with click handling in Kotlin:

kotlin
@Composable
fun MyButton() {
    Button(onClick = { viewModel.handleClick() }) {
        Text("Click me")
    }
}

This would require significantly more boilerplate in Java, and Compose simply isn't optimized for Java interop at the UI level.

Boilerplate: Much Less in Kotlin

A Java model class with a few fields demands getters, setters, equals, hashCode, toString, and a constructor. A Kotlin data class does all of that in one line:

kotlin
// Kotlin: 1 line
data class User(val id: String, val name: String, val email: String)

// Java: ~30 lines
public class User {
    private String id;
    private String name;
    private String email;
    
    public User(String id, String name, String email) { ... }
    public String getId() { ... }
    public void setId(String id) { ... }
    // ... and so on
}

Over a large codebase, this difference translates to thousands fewer lines of code to maintain, review, and test.

Interoperability: Kotlin and Java Work in the Same Project

You don't have to rewrite your entire app to adopt Kotlin. The two languages share the same bytecode target and are fully interoperable. A Kotlin class can extend a Java class; a Java class can call a Kotlin method. Gradle compiles both in the same pass.

This is powerful for teams mid-migration. Your persistence layer might remain in Java while new features are written in Kotlin. A Java Activity can call a Kotlin ViewModel. No friction, no ceremony.

However, there are a few gotchas. Kotlin's default null-safety doesn't fully translate when Java calls a Kotlin function that returns a nullable type; Java treats it as just a reference that might be null. And Kotlin's extension functions don't compile to methods Java can call directly (Kotlin hides the synthetic methods). These edge cases rarely matter in practice, but keep them in mind during code reviews.

Migrate Gradually: Convert Files One at a Time

If you have an existing Java codebase, don't attempt a big bang rewrite. Instead, adopt a per-file, per-feature approach.

Step 1: Pick a low-risk Java file. Choose something without many dependencies—perhaps a model class or a utility. Avoid core business logic or files with many callers.

Step 2: Use Android Studio's converter. Right-click the file, select Code → Convert Java File to Kotlin File. The tool is decent, though it doesn't always produce idiomatic Kotlin. You'll need to review and refactor.

Step 3: Test and refactor. Run your tests. Clean up the generated code: remove redundant null checks, use data classes instead of plain POJOs, replace loops with map and filter. This is where Kotlin's idioms start to pay off.

Step 4: Repeat gradually. As your team gets comfortable, convert more files. Establish a pattern: new features in Kotlin, old Java code only touched when necessary. Within a few sprints, your codebase naturally shifts.

This approach avoids the risk of a half-finished rewrite. Every merged file is tested and production-ready. Your team learns Kotlin patterns in small, manageable chunks.

New Projects: Start with Kotlin

For a greenfield Android project, there's no reason to choose Java. The ecosystem, community examples, and official Google samples assume Kotlin. Your hiring pool is increasingly Kotlin-literate. Libraries like Retrofit, Room, and Hilt all integrate seamlessly with Kotlin and take advantage of its features for cleaner APIs.

Starting with Kotlin means you benefit from coroutines from day one, you can adopt Jetpack Compose as your UI framework, and you avoid the debt of retrofitting null safety later. The learning curve for Java developers picking up Kotlin is shallow—most pick it up in a week or two of hands-on work.

Common Mistakes

Writing Java code in Kotlin syntax. Just because Kotlin allows mutable vars doesn't mean you should use them everywhere. Prefer val and immutability; use var only when necessary. Over-mutable code is hard to reason about and often introduces bugs.

Ignoring IDE warnings about nullable types. Kotlin's compiler enforces null safety, but if you cast or suppress warnings carelessly, you reintroduce NullPointerExceptions. Trust the compiler; it's smarter than you.

Mixing paradigms haphazardly. Kotlin supports both object-oriented and functional styles. Don't mix them arbitrarily in the same function. Pick a consistent approach within a file or module.

Over-using extension functions. While powerful, extension functions can make code harder to discover and trace. Use them judiciously, not as a shortcut for every utility method.

Not testing Kotlin-Java boundaries. If you have interop code, write explicit tests. Kotlin's null safety doesn't magically protect you when Java ignores it.

Key Takeaways

  • Kotlin's type system enforces null safety at compile time, eliminating a major source of runtime crashes compared to Java's annotation-based approach.
  • Coroutines make asynchronous code cleaner and more efficient than Java callbacks or thread-based approaches.
  • Jetpack Compose, the modern UI framework, is Kotlin-only and represents the future of Android development.
  • Kotlin's data classes and extension functions eliminate boilerplate, reducing code volume and maintenance burden.
  • Kotlin and Java are fully interoperable; you can migrate gradually, file by file, without disrupting your release schedule.
  • For new projects, start with Kotlin. The ecosystem, tooling, and community support are mature and biased toward Kotlin.
  • When migrating existing code, use the IDE converter as a starting point, then refactor to idiomatic Kotlin rather than a literal Java port.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles