Hundreds of millions of downloads. 10 years of Java. Where do you begin the process converting to Kotlin?
Strategy and a plugin for supercharging your conversion
When I arrived at Pandora, in October 2018, our Android codebase was only nominally Kotlin. As of this writing, we’re fast approaching half of our codebase. That’s six months to quadruple the Kotlin in our Android codebase. And while we expect the pace to slow, we’re not stopping.
Kotlin has several advantages, but converting Java to Kotlin isn’t without risk. I have had to fix a few bugs I’ve created in this process, almost always related to nullability.
For example, a Java string may or may not be null. When the auto-conversion tool migrates the Java string to Kotlin, it optimistically assumes that the string will not support null - but, unfortunately, this isn’t always the case. We just introduced a bug that you may only find when the code is in production.
We already write all new code in Kotlin, but working in existing Java files we are much more cautious about introducing problems just for the sake of conversion.
How do you convert hundreds of thousands of lines of code to Kotlin while building new features and supporting so many users? Very carefully. We take several approaches to our conversion process:
Convert individual files you are working on as it makes sense.
Convert smaller modules and packages in a single-go.
Convert files by size, smallest first.
Convert all test classes.
Each of these ways has its own caveats and best practices we use.
Individual files, as you go
This is the slowest part, and some reason that the percentage of Kotlin in our codebase stayed relatively flat for a long time. You don’t edit as many files on a day-to-day basis as you think in your codebase. A lot of the high-touch files are also the highest risk for conversion. The speed with which you convert this way has a lot to do with your risk tolerance. On the plus side, this conversion is good because it’s done by an author who has high knowledge of the file they’re converting. This means that nullability inference is probably better than somebody who doesn’t know the particular class they’re trying to convert.
Small modules and packages
Depending on how you have structured your application, finding small, self-contained areas of your code convertible with little change to calling code. For us, there were several places we could quickly convert with no changes in calling code that gave us a quick boost.
The key here is to identify the developer who has the most knowledge of the module. And, it’s even better if you have good unit tests around the nullability and/or annotations on the methods already.
Class by lines of code, small to large
This is self-explanatory, but the reasoning has a lot to do with Kotlin itself. Tiny Java classes often convert easily to data classes.
All the tests!
There are a couple great reasons for converting tests, even if you aren’t converting the classes under test. The first is that it’s code that doesn’t end up in production, and tests that pass before and after are self-validating. The bigger reason is that converting a test class and the class under test to Kotlin at the same time carries the increased risk of missing a nullable incompatibility. Converting a test class ahead of time adds gravity to converting the class under test for the same reason: If the tests pass before and after you convert the class, then you have higher confidence in the conversion.
While converting tests may seem as if you are cheating to bring the percentage of Kotlin in your codebase, it’s a necessary step, and increases the likelihood that the code it tests is converted.
One final word on test conversions; if you use Mockito, you’ll want to use a Kotlin specific overlay, such as mockito-kotlin. The big gains here are not having to back-tick the Mockito when calls, and proper understanding of nullable, and non-nullable types for any() and eq() calls.
How to find the files to convert next?
Converting the files you’re working on, or specific modules/packages is straight-forward. Converting tests en masse is, or finding files by size becomes trickier. When we started it was a manual process. Run a script to find the candidate files, paste the results into a shared spreadsheet, mark the spreadsheet when you are working on or have completed the conversion of a file. This is admittedly not a good solution. Especially given the size of our codebase, and the number of engineers actively working on it.
So, I had a thought: A PLUGIN! And from that idea was born the Multiple FIle Kotlin Converter (source code). This is a JetBrains IDE specific plugin that works in IntelliJ and Android Studio. It has useful features beyond the built-in conversion option.
The plugin allows you to:
Paste a list of files, which we’ve used a fair bit based on our identification of easy to convert files with our scripts
Search for files by size, or file name matching.
The default conversion also drops the ball sizeably. If you use a version control system, like git, then you will lose all the file history. This was a big problem for us in pushing for more conversion, because we have a lot of history we want to maintain. Because the file extension changes at the same time as the file content, git views this as a delete and an add. To get around this we play a trick on git:
Make a commit that renames the file to the .kt extension, but changes none of the code.
Change the file extension back to .java, so that the JetBrains conversion action recognizes the file
Apply the JetBrains conversion action on the Java file which renames the file to .kt
Since we make a separate commit for the file extension and then perform the conversion, git maintains the history. As you would expect, the plugin allows you to customize the commit message.
The plugin is open-source, so look at the code and submit PRs, or make feature suggestions.
How much of your codebase is Kotlin? 0%? 100%? How have you found the process? What tips and tricks do you have?