Make It Kotlin! — Migrating a Purely Java Android App at Scale

Android’s new best friend; null-proof and ready for production

Nikolas Alvelo
DraftKings Engineering
9 min readJun 30, 2021

--

The Kotlin programming language has become the industry standard for writing Android applications. With first-class Google support and a JetBrains-driven open source community, Kotlin brings idiomatic, functional, and interoperable code to the previously Java-dominated Android development landscape.

The benefits of Kotlin are numerous, from its core philosophies of maximizing readability and reducing boilerplate to the powerful ease of use in its lambdas, extensions, and list operations. However, the convincing motivations for migrating your Java Android application to Kotlin are well-documented, generally to this enthusiastic tune:

“Kotlin will be the language used to develop on the Android platform for the foreseeable future and you should have started replacing your Java files yesterday.” — every Medium article on the subject

How does a Kotlin migration pan out in real life and at scale?

In an ever-changing industry with the priority balancing act of agile development, a line-by-line rewrite may be relegated to the bottom of the backlog. Why would we even want to begin this process?

At DraftKings, our Android developer cohort began a gradual migration to Kotlin in July 2019. A small, low-risk project was selected for implementation in Kotlin to test its cohesion with our existing codebase and performance in production. After the success of this implementation, we were confident moving forward with the Kotlin-first ideology to reap the language’s apparent benefits across the application. New engineers began to join the organization, learning Android and Kotlin as inseparable entities. We knew we would need to define a set of best practices and shared motivations to reshape our historically Java codebase.

These are some guiding principles that have worked well for us:

If adding a new file, it should be written in Kotlin.

We cannot reduce the percentage of our codebase written in Java if we allow new Java files to be added. New code should be Kotlin code by default, and when a small file can be converted and improved upon, we encourage doing so. This in-the-moment decision is a question of scope inflation and if it will improve the codebase long-term to convert the code now, without costing excessive engineering time. We challenge our developers to find creative workarounds if they run into a situation where conversion feels impossible, as they may be trailblazing a question of interoperability that has yet to be encountered.

However, direct conversions of existing Java code to Kotlin code are not inherently worthwhile.

The Kotlin migration should focus on time-saving and putting out the right fires with the right attention to detail. Android Studio has the built-in functionality to migrate Java to Kotlin (Convert Java File to Kotlin File), and in theory, we could highlight the entirety of our codebase, cross our fingers, and run that function. We’d be finished with the migration in an hour that way, but the code would be functionally identical, technical debt included. The automatic conversion also can often lead to the usage of nullable force-unwrapping (!!) and other Kotlin anti-patterns that adopt Java’s ambivalence to nullability, amongst other issues.

We’ve taken the stance that converted Kotlin code with the same runtime complexity, potential bugs, and design pattern as the original Java code is not fundamentally better. We’d prefer to see that our Kotlin code was written thoughtfully with language-specific improvements enhancing the Java implementation it seeks to replace.

Completeness is not the goal of the migration — software maturity is.

A glance at our current codebase shows that our most modernized classes are written in Kotlin. These domains have applied our current architectural standards of MVVM (Model-View-ViewModel) and are supported by unit testing to a higher degree than the older Java code. We made the parallel decision to be more intentional about our application’s architecture when we chose Kotlin as our primary language going forward. When planning code refactoring initiatives, we can use the presence of Java as a simple signifier of an area that most likely needs improvement. This search often points to natural areas for increasing developer happiness, as well. We lose this context if we seek migration from Java to Kotlin solely for completeness.

We will have more pride in a multi-year migration that leaves our codebase well-tested, modular, error-tolerant, and easier to work in. These are the characteristics of mature software, and we have found the Kotlin language to be a reliable avenue to achieve that maturity.

DraftKings Engineering plus Android with Kotlin equals Happiness
That’s just math.

But if the trophy doesn’t rush us to the finish line, is the race worth running?

Short answer? Yes. Kotlin is simultaneously accessible and sophisticated in ways that are saving our engineers’ precious time. Below are just a few of the language features we find most compelling with immediate positive impact for us on a day-to-day basis.

Nullable and Non-Nullable Types

The malevolent NullPointerException is a classic example of an avoidable yet common mistake in Java code, and Kotlin has baked the avoidance of this error into its compilation time. We can more confidently write code that handles errant data quality scenarios from both API response data and internal functionality with the compile-time security of nullable and non-nullable types. We have generally banned the usage of the aforementioned force-unwrap in our codebase, as it almost always points to an opportunity for better null handling and smart-casting.

Data Classes

Kotlin data classes are beneficial for rapidly generating complex objects without the complications and requirements of writing a Java class with the same functionality. Due to the complexity of our application, it is critical that we can build stable systems of interacting entities, and data classes provide a reliable shortcut to delivering on this complexity. We can also use the copy() method in our Unit Tests to quickly alter our test objects into a variety of states.

Extension Functions

Kotlin extension functions enable us to reuse subroutines that are common to separate implementations in various areas of the application. For example, these can take the form of RxJava shortcuts or enhanced versions of Kotlin List and Map operators that save us time and can be tested in isolation. Whenever we find ourselves rewriting the same few lines of code in many areas, or we think a chunk of code solves a problem that could be encountered again in the future, we convert that snippet into a Kotlin extension function and share it with our developer cohort.

Open Source

The community around both Kotlin and Android continues to thrive and innovate. The migration to Kotlin would be incomplete without taking advantage of the power of open source to speed up development time with existing solutions.

Here are a few projects, for example, that we love and we hope you will, too:

  • RxKotlin — convenient extensions on RxJava that cut down on the excessive type parameters and boilerplate traditionally associated with implementing reactive streams in Java
  • MockK —straightforward library for providing mocked dependencies and spying on internal method calls in Unit Testing, a Kotlin-friendly replacement for Mockito
  • Ktlint — a Kotlin linter and auto-formatter introduced to our build process to reinforce customizable Kotlin coding style standards and maintain stylistic quality at build-time

These, amongst a plethora of others, create an exciting landscape of adaptable Kotlin libraries that serve as a toolbox of bleeding-edge answers to questions that you may encounter.

Kotlin speeds up Android development by allowing you to develop more functionality in less time by writing code that does more in fewer lines. But what about the interoperability with Java? If there will be a several-year period where two languages are present in the codebase, how can we reduce friction where the two meet and cut down on time spent in switching contexts? An all-Kotlin codebase seems like a far-off pipe dream, and the idea of increasing developer overhead in transition seems inadvisable at first glance.

Your burning interoperability questions… answered.

The interoperability of Java and Kotlin is a gold-star selling point for the Kotlin language. At its core, Kotlin is designed to supersede and replace Java. The language creators have made this junction between technologies as smooth as possible. This allows us to focus on writing Kotlin for a Kotlin-only world, with all of its unique paradigms and idioms. With a few changes to your existing Java code, we can consume Kotlin where we need to. We have found some of these solutions to be most applicable in migrating our Android codebase:

How can I use my Kotlin extension functions in my Java code?

Let’s say you have written a new extension function, and you want all usages of this subroutine to go through this function now, whether the usage is in Java or Kotlin. The @file:jvmName annotation makes this quick and easy. Please take a look at this example for a simple orDefault extension that accepts a nullable type and maps it to a non-nullable type, falling back to the provided default value in the null case via the Kotlin elvis operator.

Adding the annotation allows all extension functions in this file to be accessible from Java code.
Append the JVM name to the beginning of the method call and provide the receiving object as the first parameter of the method call.
Luckily, this changes nothing about the extension’s usage in Kotlin and is purely for Java interoperability.

This process became a natural jumping-off point for migrating our large Java utility classes, meant for providing simple operations on primitives. Each utility method becomes a Kotlin extension, often achieved in shorter, more readable code and improved null safety.

How do we migrate static Java methods and classes?

Static is a keyword that is absent from the Kotlin language, which, at first, can complicate the transition from this type of code in Java to a natural Kotlin equivalent. We use three Kotlin language features to migrate our static Java code: object, companion object, and the @JvmStatic annotation.

The static PlayerUtil library class contains static methods, which could be operations on non-static Player objects or other utility functions.
A Kotlin object replaces the Java static class, and we add the @JvmStatic annotation to the Kotlin methods that were once static Java methods.

This migration requires no changes in the Java callers. You can imagine more complex examples where the new Kotlin version can leverage deeper benefits of the Kotlin language where the Java code may have been restricted or more complicated. The changes needed for static methods within non-static classes are very similar:

The same setup as the previous example, but instead of PlayerUtil, we’ve added a static method to the Player class itself, giving it static accessibility from the class name.
We leverage Kotlin’s companion object to provide a static context in which to host the static methods of the non-static Player class; the @JvmStatic annotation is used again to allow static Java usage.

What about Kotlin lambdas? Does Java handle Unit?

Our Java code has historically used faux-functional interfaces for providing lambdas to method parameters. We use these for click listeners, RxJava handlers, and a variety of other callback patterns. Kotlin is motivated heavily by the functional programming paradigm, and as such, lambdas have first-class support in the type system.

In Kotlin, the analog to Java’s void return type is Unit. There is a simple solution to creating an equivalent to a () -> Unit lambda type in Java code, as this would be the classic type applied to an anonymous lambda with no return.

We can freely expose Kotlin’s lambda types, regardless of whether the caller is Kotlin or Java; a huge benefit.
An option here is to add a small Java method of return type Unit that calls the original void method and returns Unit.INSTANCE, passing this to the Kotlin interface and leaving any callers of the original void method unchanged.

How can our Kotlin code safely consume possibly “null” values from Java?

You will often call upon Java code from Kotlin code; this will be unavoidable during a dual-language migration period. The difficulty that arises is that, by default, a Java type T consumed by Kotlin is given the possibly-null type T!, meaning “either T or T?”. This ambiguity requires your Kotlin code to consume that value with excessive caution, first assuring that the value is non-null and handling it properly if it is. To reinforce one or the other, so that the Kotlin code can avoid this unknown type, you can add @Nullable or @NonNull annotations to your Java functions and fields. This enables Kotlin’s compile-time errors for nullability when consuming the Java code.

Add @Nullable or @NonNull annotations to your Java code to provide compile-time null safety to Kotlin code that is calling these methods.
We can successfully declare the Player? and List<Player> types from Kotlin code after annotating the methods in the Java code.

All of these translations become second nature over time, meaning we can spend more time focused on writing strong Kotlin code that delivers on our Android needs without losing significant development time to the presence of two programming languages.

This stable interoperability means there is no rush to completion for the Kotlin migration of your Android application. Kotlin becomes a threshold for modernization that can slowly grow throughout your codebase until you are ready to move the technical goalposts once more. We dream of the day that our Kotlin code is the new pariah, naive and begging to be re-written in even better Kotlin.

Where does our migration stand after two years?

The MAD (Modern Android Development) Scorecard provided by Google tells us that our current app code (+ compiled code) is 43% Kotlin! 🎊

We have a long way to go, but the improvements and architectural changes that Kotlin has enabled us to achieve with a small group of Android engineers are worth celebrating.

We have used the migration from Java to Kotlin as an empowering and methodical process that has pushed us to ask what is truly legacy about our legacy code and how we can leave every migrated line better than it was, not simply translating our technical debt from one language to another. We take advantage of the dual-language interoperability where we have to, and we unforgivingly rip out Java code that no longer suits our needs where we can.

We challenge aspiring Kotlin developers to explore the powerful complexities of this language and Java Android developers to investigate how they, too, can introduce Kotlin into their codebase and spend more time building great apps.

“O Kotlin! My Kotlin! Our fearful migration is done, the app has weather’d every rack, the prize we sought is won” — Walt Whitman (mostly)

--

--