Kotlin migration of AnkiDroid

AnkiDroid successfully converted its entire project from Java to Kotlin. In this first AnkiDroid dev blog post, we’ll explain our goal, our methods, and what remains to do.

Our goals

The main goal of the migration is to improve developers' experience. Kotlin code is easier to read, write and review than Java. Anki AnkiDroid is 13 years old. If we are still here in 13 years, we hope that the time spent during this migration year will be entirely saved. We also hopefully will decrease the number of bugs and get more readable codes using Kotlin features, such as better nullability check, scope function, and high-order function over collections.

Here is the extra goals we discovered along the way:

  • git history should still allow us to track the origin of each code line.
  • Each commit should compile
  • New contributors should be able to help us easily
  • Kotlin migration should not introduce bug

How we achieved them

Git history

By default, if you just migrate a file foo.java to foo.kt, the file changed a lot, and git believe a file was deleted and a file was added. Instead, we renamed the file to foo.kt in the first commit, and converted its content to actual Kotlin code in the second commit.

This way, git history sees a rename and then a file change. Hence, if a line of code is surprising and we need to understand why it was introduced in the codebase, we could look at git history, for example through `gitk`, find the commit that did the migration, then find the original java line, and finally find where this java line was introduced.

Each commit compiles and tests passes

Being able to compile at each commit is important for `git bisect`[1].

Since we had a java file called `foo.kt`, we needed to let gradle, the tool used to build AnkiDroid, know that it must consider that this file is actually a java file. This was essentially done by changing compilation in the following way:

  • Ignoring foo.kt
  • Copying foo.kt to foo.java
  • Actually compiling
  • Deleting foo.java

As far as I can tell, this migration tool was the work of David-Allison, MikeHardy and Arnold2381[2].

This gradle file also has to contain explicitly what was the actual value of `foo.java`, and whether this file was part of the production code, the unit test code or the android test code.

Minimizing the risk of bug

As should be obvious to any developer that worked with both Java and Kotlin, a java file migrated to Kotlin is not actually a “Kotlin file”. Same as a French text automatically translated to English would not be proper English, but still be relatively useful and understandable.

Thas is, translating to Kotlin would require a lot of extra manual work.

We could require it to be done at the same time as the migration. We decided against it because,

  • This would take a lot of extra time, making the migration even longer
  • It is hard to check that the code change do not change the behavior when you can only compare the change to java code,
  • Even if you did the change in a third commit, you would still risk to disagree during review, which would block from merging the migration itself,
  • Some rewrites would require to touch multiple files simultaneously, which makes the review harder and the risk of merge conflict greater.

Instead, we simply added a `@KotlinCleanup` annotation, that would indicate what needs to be done. The details of the migration can always be discussed in individual PR later.

Ensuring single-file migration is easy

IntelliJ has a really great tool that converts a java file to kotlin almost flawlessly[3]. However, the changes to the build file and the two commit rules makes the process of submitting a PR long. Especially when it had to be done 861 times and 20[4] people[5] had to understand and apply the process. So Arthur-Milchior introduced a migration shell script that Codingtosh ported to Mac and generally improved.

This way, any contributors only had to use the IntelliJ tool to convert a file, ensure the test passes, and run this script to create the two commits for the pull request.

Also, the KotlinCleanup annotation ensures that any new developer that wants to start contributing with us can just look for KotlinCleanup annotation, and apply them, letting simple tasks for them to start.

After the migration

Here are a few changes the migration allowed to us. A lot of them could actually be started before the migration was complete. However, waiting until the end of the migration ensured that we could do it in a single change instead of having to redo it regularly when new files were converted.

Introducing coroutine.

This work is still in progress. The migration from AsyncTask, that Ankidroid used previously, is Divyansh’s 2022 Google Summer of Code project. We also plan to use coroutine for all access to our data, which was not yet the case.

Using Kotlin data structures.

In particular, HashSet/Map and ArrayList are the same as java, but with better typing around nullability. Also, in a lot of case, using map, filter, forEach, is really nice. `listOf` and other functions that creates collections from a set of explicit elements are great too.

Also, formatted strings are great in terms of readability.

Update Material Dialogs library

Newer versions of the library didn’t support Java anymore.

Conclusion

This is a very personal conclusion by Arthur Milchior - main author of this post, but certainly not of the migration.

I find this process is nothing short of amazing. Be it on the technical or on the human side. On the technical side, JetBrains was able to create a language that is greater than Java to code with, and still entirely compatible, ensuring a smooth migration. On top of that, we edited our build tool to ensure that a .kt files would be compiled as java to improve the migration. And if that was not already meta enough, we also created a script that would edit the build tool automatically.

Humanly, it’s even more impressive. You have a bunch of 20 people across the world, who never met, and who collaborated on a goal to help potentially a hundred or two fellow developers in the future years (or decade ?), improving their experience, so that they could help the millions of users relying on AnkiDroid.

Notes

[1] Git bisect allows us to give it a list of commits to ignore, but we should compile this list and this is extra complexity on an already complex debugging process.

[2] Checking for the authors of this file is harder than any other file, since each migration commit had to two this file to add the name foo.java. So I could not trust github statement that this file had 19 contributors.

[3] The only exception I encountered was with the String’s split method. In Kotlin, by default, split remove empty string starting and ending a split. While in Java, those empty strings are kept. JetBrains answered it was corrected in a version of the tool that I didn’t had yet installed.

[4] According to `git log --format='%aN %s' |egrep -i "to (kotlin|.kt)"|awk '{print $1}' |sort -u |wc -l`

[5] Arthur, Brayan, codingtosh, Damien, David, Divyansh, dorrin-sot, Kilian, lukstbit, MacAndSwiss, Mani, Nishant, oyeraghib, Piyush, Prateek, Prince, puranjayK, Shridhar and Timo.

Add a comment

HTML code is displayed as text and web addresses are automatically converted.

Add ping

Trackback URL : http://www.milchior.fr/blog_en/index.php/trackback/783

Page top