Illustration by Virginia Poltrack

Re-writing the AOSP DeskClock app in Kotlin

Colin Marsch
Android Developers

--

One of the main goals of Android Open Source Project (AOSP) applications is to serve as an example to developers on how to build Android applications. As a part of this commitment, the AOSP apps are developed with Android best practices in mind, including the use of Kotlin as their development language. Recently, in pursuit of this goal, we began refactoring the AOSP DeskClock from Java to Kotlin. Through this process, much was learned about what developers could expect when converting their own apps. This article touches on some hurdles that were encountered during this process and provides tips that you can use to reduce the workload for a similar conversion. You’ll also learn about the benefits of converting from Java to Kotlin, focusing on improvements that were seen as a result of the DeskClock conversion.

Why Kotlin?

We chose to convert the AOSP DeskClock app in part due to Kotlin’s many benefits over Java, including the following:

  • Built-in null safety, which can substantially reduce null pointer exceptions in your app.
  • Conciseness, or writing less code to do more work.
  • Google’s “Kotlin-first” support for Android development means that new Android development tools and content such as Jetpack libraries and online training will be built with Kotlin users first in mind.

Additionally, many developer surveys have ranked Kotlin as one of the most loved languages to work with in terms of developer satisfaction. All of these benefits together can lead to a much smoother process when maintaining your codebase in Kotlin rather than Java.

The Conversion Process

The DeskClock app is quite large, containing 160 Java files with a total of 31,914 lines of code before conversion began. Because of its size, the code had to be converted incrementally. In AOSP, this was done by creating a separate Soong build target (DeskClockKotlin), excluding any Java files with Kotlin equivalents. Kotlin’s interoperability with Java made this process straightforward by allowing the DeskClockKotlin target to continually include more Kotlin files as the corresponding Java files were converted.

The first conversion step was to run the automatic Kotlin conversion tool from the Kotlin plugin in Android Studio. This plugin automatically converts code from Java to Kotlin and generally works well, though you might see some common issues that must be manually corrected to make the Kotlin code more idiomatic and robust. The incremental conversion helped to ensure the correctness of the converted code by targeting only a small portion of app functionality at a time. In addition to manual testing, we also ran the Compatibility Test Suite (CTS) for the DeskClock app to check for possible feature regressions.

Manual Work To Be Done

During the conversion process, we ran into some issues that required more manual work after running the auto-conversion tool. This section discusses a few of the more complex issues in depth, followed by short summaries of additional minor issues.

Unable to Run Auto-Conversion on Some Files

One of the most time-consuming issues we encountered was the inability to run the Kotlin plugin’s auto-conversion tool on certain Java files. This was not a common issue, occurring only twice in the first ~75 files converted. However, we did need to either rewrite the Java code completely in Kotlin or spend time investigating the issue.

We first encountered this problem with the ClockDatabaseHelper.java file. In that file, a try-with-resources block was causing the issue.

try (Cursor cursor = db.query(…)) {
// some code in here
}

This try-with-resources statement does not exist within Kotlin, though it does have an equivalent in the use function. The try-with-resources statement in Java handles the releasing of the given resource once it is no longer in use (i.e. after the try block). Similarly, the Kotlin use function takes a lambda expression and disposes of the resources that use was called on once the lambda has been executed. One way to write the code from above in Kotlin would be as follows:

val cursor = db.query(…)
cursor.use {
// some code in here
}

The converter could not automatically make this change. In this case, we manually updated the Java code to declare the Cursor before the try block, instead of in the try-with-resources statement, which allowed the conversion tool to run successfully.

The second file that was unable to be converted automatically was the LogEventTracker.java file. This file contained a function defined as follows:

private String safeGetString(@StringRes int resId) {
return resId == 0 ? null : mContext.getString(resId);
}

The conversion tool was unable to handle the ternary expression being returned from this function. It seems that the tool is unable to deduce the correct return type of the corresponding Kotlin function (String? in this case). We manually rewrote this function as follows:

private fun safeGetString(@StringRes resId: Int): String? {
return if (resId == 0) null else context.getString(resId)
}

In both of these cases, the manual work to rewrite the Java code is minimal. However, the converter skips the entire file when encountering issues like these. This means that for large files that contain minor issues, without knowing the root cause beforehand, manual investigation into the source of the issue is required.

One method that we used to discover issues was to comment out the majority of the code in the file and then incrementally un-comment code until encountering the issue. This can be a time-effective way to figure out what code snippet is responsible.

Static Constant Inheritance

An additional complex issue we encountered was the difference in inheritance abilities between Java and Kotlin. This issue occurred in the ClockContract.java file, where static constant values are defined within interfaces that inherit from each other. This is not a problem in itself. However, many places in the codebase attempted to reference the constants through children of these interfaces, which is not possible in Kotlin.

A common way to define static constants in Kotlin to be referenced in Java code is within companion objects, in this case within the interfaces the constants are to be defined in. Due to the incremental conversion, the ClockContract interfaces were referenced from Java code, so this companion object approach was used. The difference between Java and Kotlin here is that companion objects are not inherited from the parents of an interface. For example, a static constant defined in the companion object of an interface A, where interface B inherits from A, would not be able to be accessed by calling B.CONSTANT_NAME. This method of access would work fine in Java.

To solve this problem, we chose to replace the affected usages of these interfaces with calls to the constants in the interfaces where they are defined. Continuing the example from the last paragraph, we would instead refer to A.CONSTANT_NAME, instead of trying to refer to B.CONSTANT_NAME, since CONSTANT_NAME is defined in interface A. This problem could perhaps be solved differently by changing the overall structure of the inheritance of these constants, likely not using interfaces in Kotlin. However, the approach taken was straightforward to implement.

Manual Nullability Fixes

One other common issue that came up during the conversion process was the occasional inaccuracy of nullable types in the converted Kotlin code. For example, converted code incorrectly specified that a function accepts a String instead of a String? parameter, which could result in runtime errors. These issues can be avoided if the converter tool is able to deduce from the Java code whether or not a parameter or return type is able to be null. You can annotate Java code with @Nullable and @NonNull to give the IDE the information needed to make this decision correctly. Otherwise, it’s possible to manually fix these nullability issues in the resulting Kotlin by tracing through the code and determining whether it is possible to receive or return a null value.

Additional Issues

In addition to these more complex issues, we encountered multiple smaller issues during the conversion process. Each issue had a quick fix that didn’t take much time to address.

The generated Kotlin code sometimes included the deprecated uses of “===” and “!==” while comparing two Int values. Using “==” or “!=” suffices for comparing Int values.

Some @NonNull annotations present in the Java code are not removed in the resulting Kotlin code. These annotations are not needed in Kotlin due the nullability safety the language provides, so they were removed.

Occasionally, single line comments within Java functions appeared in two different places in the resulting Kotlin code. These comments appeared both in their proper place and also in general class scope outside of the function they are meant to be in. We removed these second instances.

The generated Kotlin code excluded blank lines that were present in the equivalent Java code. This can result in some functions that were spaced out to separate portions of logic to have all their lines together in the resulting Kotlin code.

When functions that accept Long parameters are instead passed an Int value, the generated Kotlin code sometimes adds “as Long” to attempt to cast the Int to a Long. However the correct approach is to call toLong() on the Int. This also applies when used with other number types (e.g. Float, etc.).

In Java, you can reassign function parameters. In Kotlin, however, function parameters are defined as val behind the scenes and thus cannot be reassigned. In the generated Kotlin code for this scenario, a local var is defined within the function having the same name as the function parameter. To avoid shadowing of a parameter’s name by the local var, it could be beneficial to rename the local var so as to increase the function’s readability by not sharing the same name between a parameter and local variable.

When a private Java property with a private getter method is converted to Kotlin, the private keyword sometimes remains on the get() accessor function for the property. This is unnecessary, however, since the property being declared private is enough.

Quick Conversion Tips

The following list aims to quickly summarize the more detailed explanations of the issues encountered above into actionable items to help speed up the conversion process:

  • Ensure usages of “===” and “!==” are used for checking referential equality
  • Remove any @NonNull or @Nullable annotations in the Kotlin code
  • Check that single line comments are located in their desired places and not duplicated
  • Add back removed blank lines within functions to match the style of the Java code
  • Fix any incorrect casting attempts between number values
  • Improve code readability by renaming local variables that shadow names of function parameters
  • Remove unnecessary private keywords on get() accessors on private properties

Outcomes

Right now, approximately half of the DeskClock app’s Java files have been converted to Kotlin. We’ve replaced 15,886 lines of Java code with an equivalent 15,240 lines of Kotlin code. The conversion of these Java files took roughly one month of engineering to complete. Through this Kotlin conversion process, as well as this article, the AOSP DeskClock app is better able to fulfill its goal of being an example for Android development best practices.

After converting roughly half of the DeskClock app to Kotlin, we’re already seeing benefits in the number of lines of code to do equivalent work. The total number of lines of code has dropped from 15,886 to 15,240, representing a ~5% decrease. While this is not a tremendous decrease so far, it is expected that as more Java files are converted to Kotlin, some additional boilerplate code can be removed, as the Kotlin files have less interaction with Java files.

We also examined the differences in build times and APK size between the original Java app and the half-converted app. The APK size of the partially converted Kotlin app was 6237217 bytes, compared to 6117353 bytes for the original Java app’s APK size. This very slight size increase, approximately 2%, shows that the introduction of Kotlin to the app did not dramatically affect the APK size. When looking at the change in build times between these two versions of the app, the Java app had an average build time of ~33 seconds, whereas the partially-converted Kotlin app had an average build time of ~57 seconds. These build times were both recorded while performing clean builds with no previously compiled class files on a 48 core machine with 128 GB of RAM.

In addition to the results found from the conversion of the DeskClock app, other developers have similarly seen benefits from converting to Kotlin. Duolingo completed a full Kotlin migration of their app and reduced their line count by 30%. As well, the Google Home app began incorporating Kotlin into their codebase, reducing their NullPointerExceptions by 33%. 70% of the top 1k apps include Kotlin code, as well, and 60% of professional Android developers use Kotlin.

Next Steps

To learn more about the Java to Kotlin conversion process for Android, check out the Get Started with Kotlin on Android documentation or the Kotlin for Java Developers Pathway. If you’d like to look at the codebase, you can check out the AOSP source code by following the Downloading the Source documentation.

--

--