Java is dead, long live Kotlin

By: Mariano Simone

Thumbtack Engineering
Thumbtack Engineering
5 min readAug 13, 2018

--

When official Android support for Kotlin was announced on May 2017, I got really excited. Don’t get me wrong, I love Java: it was the first language I used professionally, and it has a very strong community, a myriad libraries to use, and some of the best tooling out there. However… it also has its problems: it’s verbose, until the latest versions didn’t have a nice way to deal with optional or nullable values, and a lot of its progress gets slowed down by backwards compatibility with decisions made two decades ago. Kotlin came as a breath of fresh air.

Thumbtack’s transition strategy

We merged our first line of Kotlin back in July 2017. Since then, we’ve been slowly but surely adding more and more Kotlin, and writing less and less Java (as of August 2018, a little over 60% of our Android repo is Kotlin, and most — if not all — new code is written in it instead of Java).

One of the things that made the transition easy is that , except for a couple of quirks here and there, the interoperability between the two languages is seamless: we still use a lot of third-party libraries (most of which are not written in Kotlin), and our new code calls and gets called by our legacy code without changes.

Our strategy to adoption was roughly:

  • Start writing tests in Kotlin: this gave us some confidence on the build process and allowed us to have a playground to get used to the language
  • Migrate trivial code from Java to Kotlin , to see what the benefits were (classes that could be data classes were a popular candidate for this step)
  • Implement new features in Kotlin
  • Write tests for pieces of code that were suffering some of Java’s problems (nullability issues, verbosity)…
  • … and then migrate those pieces of code to Kotlin

What we struggled with

1. Null Safety

In Kotlin, null safety is a first class citizen. For example:

val nonNull: String = null // compilation error
val possiblyNull: String? = "but not null" // this works!
// compilation error, String method call on a nullable String
val uppercase = possiblyNull.toUpperCase()

This is great because it forces you to think about what types of values your methods return, and callers to handle potential null cases appropriately.

However, interoperability with Java means that the compiler won’t know if a type is nullable or not unless the declaration is marked with one of the compatible nullability annotations. Right now, most of the third-party libraries we use (including the Android APIs!) and some of our own legacy code are not annotated, and the Kotlin compiler will happily let us do (dangerous) things like:

In Java:

class Foo {
private static boolean shouldTrollKotlin() {
return true;
}
public static String bar() {
if (shouldTrollKotlin()) {
return null;
}
retrun "I'm cool";
}
}

In Kotlin:

val myValue = Foo.bar.toUpperCase() // NullPointerException at runtime!

Mitigation steps

  • Go back to your legacy Java code that’s called from Kotlin and annotate the nullability of return types
  • When calling non-annotated code, go through the code and understand if the returned values can be null. If so, declare the type on the Kotlin side, so from that point on, type information is more complete. So, for the previous example :
// Do this:
val safe: String? = Foo.bar()
// Instead of:
val unsafe = Foo.bar()
  • Enable warnings for platform types in Android Studio (Settings > Editor > Inspections > Kotlin > Java interop issues > Function or property has platform type)
  • Bonus: Create tickets (or vote for them!) for 3rd party libraries you use to request nullability annotations. Even better, submit a Pull Request adding them yourself!

2. Styleguide

Our Java style guide is a mix of the AOSP Java Code Style, the Google Java Style Guide, rules inherited from other Thumbtack codebases, and a couple of additions and exceptions we’ve added to all of that through the years. Apart from being hard to keep all of those rules in mind, most of them are not enforced in any way (except by some of us who are suckers for consistency and point out nitpicks during code reviews, which is not productive at all).

When we started writing Kotlin, there was neither a consolidated style guide nor coding conventions for it, and we mainly carried over a lot of what we’d been doing in Java. Since then, the official Kotlin coding conventions have evolved, and we’ve adopted them for new code (and try to re-format legacy code when we are working on it). What’s more, we are using detekt to fail the build if some of the rules are not met (this is still work in progress, and we envision a future where auto-formatting is also applied before commiting).

Mitigation steps

  • Don’t blindly follow your (or other’s) style guides for other languages (nor for Kotlin!). Understand each rule, measure its value, and advocate for it…
  • … but don’t add exceptions to them liberally, even if you don’t completely see the value in them. Each new exception means higher onboarding costs for new engineers and, potentially, custom configuration to tooling.

3. Writing Kotlin, the “Java Way”

Even if given O(1) access to the full vocabulary of a natural language you don’t know, you wouldn’t be able to speak it; you could certainly get by, but your speech wouldn’t sound natural. Something similar happens when you write Kotlin, but think in Java. Especially at the beginning, we were sometimes writing stuff like:

val nonEmptyStrings = mutableListOf<String>()
allTheStrings.forEach {
if (it.isNotEmpty()) {
notEmptyStrings.add(it)
}
}

Which can be easily made more idiomatic by using the preferred functional style:

val notEmptyStrings = allStrings.filter { it.isNotEmpty() }

Or, for something a bit more complicated, we were doing:

val allStrings = mutableListOf<String>()
val currentNode = root
while (currentNode != null) {
allStrings.add(currentNode.value)
currentNode = currentNode.next
}

When the same can be accomplished with Kotlin’s built-in generateSequence:

val allStrings = generateSequence(root) { it.next }.map { it.value }

Mitigation steps

  • Learn! Read! Write! In a lot of cases, Kotlin is not only a new language, but also a new paradigm: it provides a lot of APIs that will make your code less verbose and less error prone by embracing functional programming, immutability, and null safety. To take advantage of all the benefits, you have to use those APIs, though. There are great articles and books about doing the jump (from there to there, and from here to there…).
  • Share! When we reached the 50% Kotlin mark, we organized a celebration that included cake (if there’s anything the Thumbtack mobile team loves more than programming, it’s food ) and, more importantly, lightning talks on topics like immutability, functional programming, and linting rules. Team members that hadn’t been using Kotlin for much were sold on the advantages and started using it more.
  • Upgrade often. Both Kotlin and Android Studio are continuously releasing new versions that improve stability and add useful inspections to guard against potential bugs.

At the end of the day, the language is just a tool to get things done. However, once you are using one tool, it’s best to learn how to use it effectively and take advantage of all its benefits. If you are always trying to improve your craft, and looking for ways to put it to use solving problems for real people, come and join us!

Originally published at https://engineering.thumbtack.com on August 13, 2018.

--

--

Thumbtack Engineering
Thumbtack Engineering

We're the builders behind Thumbtack - a technology company helping millions of people confidently care for their homes.