For the past three years, the Android Open Source Project (AOSP) applications team has taken on the task of converting AOSP apps from Java to Kotlin. This pursuit was begun as part of Android’s commitment to increasingly develop with Kotlin-first. Kotlin is a safe, pragmatic, and concise language, with a number of language-specific advantages. Useful Kotlin language features include:
- Null Safety: Kotlin types are non-nullable unless explicitly specified. This is incredibly useful for developers in avoiding difficult to trace null-pointer exceptions.
- Conciseness: Kotlin allows developers to reduce the amount of boilerplate code, expressing more with less lines of code compared to Java.
- Java Interoperability: Interoperability between Kotlin and the Java programming language proved very helpful for this project as the migration could be done incrementally, and is useful for all developers working in mixed Java and Kotlin projects.
- Jetpack Libraries and Coroutines: Kotlin provides in-language support for lightweight, structured concurrency through coroutines. Additionally, Android development content is Kotlin-first and provides access to useful Jetpack libraries.
AOSP interns converted the AOSP Deskclock app in 2019 and Calendar app in 2020, detailing the migration process in similar articles. This year the AOSP team set out to fully convert the QuickSearchBox app as part of a summer 2022 intern project. Introduced in 2009 through the Android 1.6 release, the QuickSearchBox app allows users to search both their device and the web directly from their home screen, providing suggestions based on downloaded content, contacts, apps, and browser history. Over the course of 6 weeks, over 11,000 lines of Java code within the QuickSearchBox app were converted to Kotlin to showcase best practices in Android development and provide the functionality of the QuickSearchBox app with Kotlin-first in mind.
Automatic Conversion and Bug Fixing
To migrate the QuickSearchBox app to Kotlin, we utilized the Kotlin conversion tool that is included in Android Studio. Our process followed five steps:
- Copy the .java file to a .kt file of the same name using the command:
cp ExampleFile.java ExampleFile.kt
- Utilize the provided conversion tool to convert the Java code to Kotlin
- Add the .java file to an exclude_srcs property in the Android.bp file so that the conversion could occur incrementally, one file at a time
- Resolve compilation errors and any other bugs introduced by conversion
- Run and pass unit tests to check for correctness of the Kotlin based implementation, and run manual tests to check feature parity between the Kotlin and the legacy Java based app.
The git history of each of these steps was maintained to showcase the process of migration to outside developers, and can be viewed for each file in the master branch of AOSP QuickSearchBox. The bug-fixing step in this process was necessary due to several common issues found within Android Studio’s Java-to-Kotlin conversion tool, which occurred in the majority of AOSP QuickSearchBox files that were migrated.
These common problems and their solutions are as follows:
Common Problem: Required Import statements would frequently be removed in the converted Kotlin file
Solution: Manually convert in the needed import statements
Common Problem: The
override keyword was often not added to methods and variables tagged @Override
Solution: Manually add in
Common Problem: One larger problem encountered was the conversion of nullable variables to non-nullable variables in the Kotlin code. This caused a variety of errors, from type mismatches to large sections of QuickSearchBox code not being executed since logic relying on null checking was now unused. Converting one file at a time also meant that the use of inherently nullable types in the unconverted Java files would create issues when attempting to assign, override, or return those inherited variables in the Kotlin code.
Solution: In most cases, these properties actually needed to be nullable in order to achieve the desired functionality. The solution here was simply to use nullable types by adding `?` to the type declaration. This required adding in safe calls (`?.`) where appropriate and changing the expected types where necessary to avoid mismatch errors. While this may seem counterintuitive to the null safe nature of Kotlin, it was necessary for some properties to remain nullable since they were set asynchronously, and all variables that could be made non-nullable were.
Common Problem: Functions beginning with get and set were changed by the converter to variables with explicitly defined getters/setters, but many usages were unchanged and left undefined
Solution: Manually change calls to the converted functions to the variable name; eg. getSuggestions() -> suggestions
Common Problem: Usages of
getClass() were unchanged by the converter in the Kotlin code. Unlike Java, Kotlin does not support calling getClass() on objects to retrieve a Class type token.
Solution: Utilize Kotlin’s class reference syntax, changing usages of
::class to return the KClass token.
Common Problem: The QuickSearchBox project was built with the
-Werror flag turned on, and a common source of errors was the use of java.util.Collection in a Kotlin class. When migrating an app to Kotlin, it’s recommended for developers to utilize libraries built for Kotlin in order to maximize language-specific benefits.
Solution: Switch to using the Kotlin equivalent of the required class. This has the benefit of increased safety, as the class can be specified as mutable or immutable when necessary.
i.e. java.util.Collection -> kotlin.collections.Collection or kotlin.collections.MutableCollection
As the migration took place, the API level of the QuickSearchBox app was also updated to the latest version. This involved replacing several deprecated function calls with the new versions recommended by Android. An example of this was the deprecated use of AsyncTask within
AsyncTask was deprecated in API level 30, with Android documentation recommending the use of java.util.concurrent or Kotlin concurrency utilities. Since this project is geared towards showcasing the benefits of developing in Kotlin, we replaced the use of AsyncTask with a Kotlin coroutine scope and async block to asynchronously issue network requests. While, like Java, Kotlin supports the creation of threads, coroutines are non-blocking and stackless, allowing for lower memory usage.
Finally, one subtle source of bugs that developers should watch out for comes from the Kotlin converter feature that changes methods whose names begin with ‘get’ and ‘set’ to variables with explicitly defined getters and setters. When this change takes place, the converter will rename the migrated function to a variable of the same name, except without the preceding ‘get’ or ‘set’. However, the converter fails to check for existing usages of the same name, potentially introducing devastating bugs into the converted code. In large files with many usages of these variables, these bugs can be incredibly hard to catch. This was seen in the file
DelayingSuggestionsAdapter, where the line:
Was changed in the Kotlin code to:
This introduced a subtle runtime bug where an object would be released too early, generating an exception in a completely different file. While the fix was a simple one line change, tracking down all improper usages caused by the conversion can prove time consuming for developers, and ideally the converter would utilize more comprehensive code checking before creating new variables.
After completing the conversion, we ran some benchmark tests to analyze the changes that occurred within the AOSP QuickSearchBox app when migrating to Kotlin. Some interesting performance metrics we can use to compare the Java and Kotlin versions of the fully functioning QuickSearchBox app include:
After the migration was completed, we saw a sizable reduction of about 11.5% in the total # LOC (lines of code). It’s also worthwhile to note that the migration of the QuickSearchBox app focused on maintaining the existing functionality of the app when converting to Kotlin, without majorly refactoring the structure of the code. If we were to rewrite the entire QuickSearchBox app starting from Kotlin, we believe it would be possible to achieve an even greater reduction in size by better utilizing the concise nature of Kotlin.
We did see a rather significant increase in the APK size, however this increase can be attributed to the new inclusion of the kotlinx-coroutines and androidx.core libraries in this project. The androidx.core library was added in order to substitute several deprecated function calls with the recommended replacements in order for QuickSearchBox to target the latest API level, and would have occurred regardless of the Kotlin migration. Since the overall size of the QuickSearchBox APK was still quite small (less than a MB total) we believe this increase is justifiable.
Finally, the clean build time for the code was averaged over 10 trials for each language. These trials, and the previous benchmark tests, were all taken on a machine with 96 cores and 180 GB of RAM. We actually saw a slight decrease in total build time in the Kotlin version of the app, potentially due to the decrease in # LOC, switch to several updated API calls, and use of Kotlin’s coroutines.
In total, the migration of the AOSP QuickSearchBox app took approximately 6 weeks, with 1 intern working on the project. The rate of conversion greatly increased with familiarity of Git/Repo, Kotlin, and common errors found in Android Studio Kotlin tool. By the midpoint of the project, the process of converting the file, fixing errors, formatting, and maintaining the Git history was incredibly efficient, with up to 10 files being migrated per day. Ultimately, this project helped prove the benefits of converting an app to Kotlin, from more concise syntax to providing access to useful libraries and lightweight concurrency, and can act as an example for other developers seeking to migrate their own apps.
By Ryan O’Leary, Android Intern