Duolingo for Android was a Java app for its first five years of existence. Two years later, it’s now 100% Kotlin! This migration proved to be a huge success for us in terms of developer productivity and happiness.
There are already plenty of resources online for learning Kotlin, so in this post we’ll focus on our own experience of deploying Kotlin code to millions of users.
Why Kotlin
When we first started considering Kotlin in early 2018, Android support for it was less than a year old. It wasn’t yet clear that Kotlin would ever reach its current level of popularity or go on to unseat Java as Google’s preferred language for Android development.
The main benefits we anticipated:
- Productivity. Kotlin is far less verbose than Java, making it faster and easier to both write and (more importantly!) maintain. Its seamless interoperability with Java and conservative approach to adding new language features make it a breeze for any Android developer to pick up.
- Stability. Our Android repo’s history contains over 100 commits from its Java days along the lines of “Fix NullPointerException crash”. Kotlin’s null safety features prevent more NPEs from reaching users and allow us to focus on other problems during code review since there’s so much less boilerplate to sift through.
- Developer happiness. Kotlin was among Stack Overflow users’ most loved languages of 2018, second only to Rust. Our own developers had already reacted positively to similar language upgrades on our two other major platforms: support for Swift in our iOS app and our complete rewrite of duolingo.com in TypeScript.
There were some risks too, primarily that a Kotlin migration might not be worth its opportunity cost in developer time. Another concern was that Kotlin, like CoffeeScript, might eventually be made obsolete by backported improvements to the very language it set out to improve.
Ultimately our Android developers decided unanimously that the benefits were valuable enough to justify a policy of writing all new Android code in Kotlin, although we weren’t yet ready to commit further to a total migration of all existing code.
Getting developers up to speed
All Android developers at Duolingo meet biweekly for discussion of recent and upcoming platform changes, informal postmortems, and Q&A. The first few of these assemblies were dedicated to introductory Kotlin presentations based on sources such as the official language guide, Kotlin Koans, the official Android docs, and the MindOrks cheat sheet.
Each Android developer was then assigned some Java code to port to Kotlin. We created a new “Kotlin checker” role for more experienced Kotlin developers to share best practices during code review; this role’s membership gradually increased until it contained all of our Android developers and was no longer necessary.
Kotlin tooling
From the start, we’ve always ensured code consistency by dockerizing our Kotlin tooling and enforcing it in both pre-commit and GitHub pull request status checks.
We lint all Kotlin code with detekt, IntelliJ inspections, Android Lint, and our own regex-based linter, Splinter.
For automatic code formatting, we run ktlint as part of a common pre-commit hook shared across all repos in the company. (The other main contender was the IntelliJ formatter, which we found to be slower and a bit more finicky to run in Docker.)
Once we got down to roughly 10% Java, we removed PMD, SpotBugs, and most inspections from our CI pipeline. Continuing to run these Java-specific tools would’ve slowed down our development speed while no longer offering much value.
Converting old Java
In order to make code reviewing Kotlin conversions as painless as possible, we recommended handling each source file in its own pull request containing at least three separate commits:
- Run the IDE’s autoconverter. This commit is responsible for most of the LOC churn and doesn’t need to be reviewed carefully since it’s generally safe as far as runtime errors go, although it may introduce compile-time errors.
- Fix compilation errors. The fixes are typically straightforward to implement, e.g. adding
@JvmStatic
annotations where necessary. - Refactor. The author should satisfy the linters and refactor code to be more idiomatic in Kotlin, e.g. using
sumBy
instead of a for-loop.
We found that converting a Java file to Kotlin reduced its line count by around 30% on average and by as much as 90% in some cases!
While porting old code fit comfortably into the scope of our Android platform engineer’s role, we expected it to be tougher for our product teams to prioritize. We encouraged product teams’ developers to convert their own most frequently touched files whenever they found themselves with spare time, and - true to the spirit of Duolingo - we gamified that process by running a contest with a daily leaderboard. In the end, product developers accounted for about half of all conversions.
Stumbling blocks
Kotlin’s tooling ecosystem is much smaller than Java’s. It’s more than sufficient for our needs, though - we lint our Kotlin code about as aggressively as we were already linting our Java code.
We occasionally still get NullPointerExceptions and IllegalArgumentExceptions from third-party Java dependencies (such as the Android framework itself) that don’t follow the best practice of using nullability annotations, leaving the Kotlin compiler with no way to know whether their methods’ parameters or return values can ever be null. This situation has been improving over time as Google goes back and annotates their public APIs.
Kotlin still has no native support for a few Java features that range from the uncommon (calling a superclass’s static protected methods) to the arcane (qualified superclass constructor invocations), but issues like these have been easy enough to work around.
Results
Our Android codebase’s line count was growing 46% year over year until we introduced Kotlin in early 2018. Two years, many new product features, and more than twice the number of active contributors later, our codebase is almost exactly the same size now as it was back then!
Developer happiness according to NPS increased by 129 points for Android during this time, with most developers citing Kotlin (and our tooling around it) as a major factor.
We also now support Kotlin alongside Python and Java as a first-class language for building backend services at Duolingo, which has required little additional effort since we can reuse Java code from our existing services and Kotlin tooling from our Android repo.
Overall we’re very happy that we migrated to Kotlin when we did, and we’re excited to see its usage continue to grow both within our company and throughout the software industry!
Interested in software engineering at Duolingo? We’re hiring!