Rewriting the AOSP Calendar app using Kotlin

Rewriting the AOSP Calendar app using Kotlin

Two years ago, the Android Open Source Project (AOSP) app team began to refactor AOSP apps to use Kotlin instead of Java. There were two main reasons for the refactoring: one was to ensure that AOSP apps followed Android best practices, and the other was to provide a good example of using Kotlin first for app development. One of the reasons why Kotlin is so attractive is its concise syntax. In many cases, the amount of code written in Kotlin is less than the amount of code in Java for the same function. In addition, Kotlin, an expressive programming language, has various other advantages, such as:

[[424403]]

  • Null safety: This concept is arguably built into Kotlin, helping to avoid destructive NullPointerExceptions;
  • Concurrency: As described in the Android talk at Google I/O 2019, structured concurrency allows for the use of coroutines to simplify background task management;
  • Compatibility with Java: Especially in this refactoring project, the compatibility of Kotlin with the Java language allows us to convert Kotlin file by file.

Last summer, the AOSP team published a post detailing the conversion of the AOSP Desktop Clock app. This year, we converted the AOSP Calendar app from Java to Kotlin. Prior to the conversion, the app had over 18,000 lines of code, and after the conversion the code base was reduced by about 300 lines. For this conversion, we followed similar techniques as the AOSP Desktop Clock conversion process, taking advantage of the interoperability of the Kotlin and Java languages, converting the code files one by one, and using independent build targets to replace Java code files with corresponding Kotlin code files in the process. Because there were two people on the team doing this work, we created an exclude_srcs property for each person in the Android.bp file so that both people could refactor and push code at the same time while reducing code merge conflicts. In addition, this allows us to perform incremental testing and quickly locate which files the errors appear in.

When converting any given file, we first use the automatic conversion tool from Java to Kotlin provided by the Android Studio Kotlin plugin. Although the plugin successfully helps us convert most of the code, there are still some problems that need to be manually solved by the developer. The parts that need to be manually changed will be listed in the following sections of this article.

After converting each file to Kotlin, we manually tested the calendar app UI, ran unit tests, and ran a subset of the Compatibility Test Suite (CTS) for functionality verification to ensure that no regression tests were required.

Steps after automatic conversion

As mentioned above, there are some recurring issues that need to be manually located and resolved after using the automatic conversion tool. In the AOSP Desktop Clock article, some of the issues encountered and their solutions are described in detail. The following is a list of some of the issues encountered during the AOSP calendar conversion process.

Mark the parent class with the open keyword

One of the issues we encountered was calling Kotlin parent and child classes. In Kotlin, to mark a class as inheritable, you must add the open keyword to the class declaration, and do the same for methods in the parent class that are overridden by the child class. However, inheritance in Java does not require the use of the open keyword. Since Kotlin and Java can call each other, this problem did not appear until most of the code files were converted to Kotlin.

For example, in the following code snippet, a class that inherits from SimpleWeeksAdapter is declared:

  1. class MonthByWeekAdapter(context: Context?, params:
  2. HashMap<String?, Int ?>): SimpleWeeksAdapter(context as Context, params) {//Method body}

Since the code file conversion process is done one file at a time, even if you completely convert the SimpleWeeksAdapter.kt file to Kotlin, the open keyword will not appear in its class declaration, which will cause an error. Therefore, you need to manually add the open keyword so that the SimpleWeeksAdapter class can be inherited. This special class declaration is as follows:

  1. open class SimpleWeeksAdapter(context: Context, params: HashMap?) {//Method body}

The override modifier

Likewise, methods in a subclass that override a superclass must also be marked with the override modifier. In Java, this is done with the @Override annotation. However, while there is a corresponding annotation implementation in Java, the automatic conversion process does not add the override modifier to Kotlin method declarations. The solution is to manually add the override modifier in all appropriate places.

Override properties in the parent class

During the refactoring process, we also encountered an unusual problem of property overriding. When a subclass declares a variable and there is a non-private variable with the same name in the parent class, we need to add an override modifier. However, even if the subclass variable is of a different type than the parent class variable, the override modifier must still be added. In some cases, adding override still does not solve the problem, especially when the subclass type is completely different. In fact, if the types do not match, adding the override modifier before the subclass variable and adding the open keyword before the parent class variable will result in an error:

  1. type of *property name * doesn't match the type of the overridden var-property

This error is confusing because in Java, the following code compiles fine:

  1. public class Parent {
  2. int num = 0;
  3. }
  4.  
  5. class Child extends Parent {
  6. String num = "num" ;
  7. }

The corresponding code in Kotlin will report the error mentioned above:

  1. class Parent {
  2. var num: Int = 0
  3. }
  4.  
  5. class Child : Parent() {
  6. var num : String = "num"  
  7. }

This is an interesting question, and we currently circumvent this conflict by renaming the variables in the subclass. The Java code above will be converted to problematic Kotlin code by the code converter currently provided by Android Studio, which has even been reported as a bug.

Import Statement

In all the files we converted, the automatic conversion tool tended to truncate all import statements in the Java code to the first line of the Kotlin file. This caused some very frustrating errors at first, with the compiler complaining about "unknown references" throughout the code. After realizing this problem, we started manually pasting import statements from Java into the Kotlin code files and converting them individually.

Exposing member variables

By default, Kotlin automatically generates getter and setter methods for instance variables in a class. However, sometimes we want a variable to be just a simple Java member variable, which can be achieved by using the @JvmField annotation.

The @JvmField annotation is used to "instruct the Kotlin compiler not to generate getter and setter methods for this property, and to allow it to be publicly accessible as a member variable." This annotation is particularly useful in the CalendarData class, which contains two static final variables. By using the @JvmField annotation on read-only variables declared with val, we ensure that these variables can be accessed as member variables by other classes, thus achieving compatibility between Java and Kotlin.

Static methods in objects

Functions defined in Kotlin objects must be marked with @JvmStatic to allow them to be called from Java code by method name instead of instantiation. In other words, this annotation makes them behave like Java methods, i.e., they can be called by class name. According to the Kotlin documentation, "The compiler generates a static method for the outer class of the object and an instance method for the object itself." We encountered this problem in the Utils file, and when the conversion is completed, the Java class becomes a Kotlin object. Subsequently, all methods defined in the object must be marked with @JvmStatic, which allows them to be called from other files using syntax such as Utils.method(). It is worth mentioning that using .INSTANCE between the class name and the method name (i.e. Utils.INSTANCE.method()) is also an option, but this does not conform to the common Java syntax and requires changing all calls to Java static methods.

Performance evaluation analysis

All benchmarks were performed on a 96-core machine with 176 GiB of RAM. The main metrics analyzed in this project were the number of lines of code reduced, the file size of the target APK, the build time, and the time from launch to first screen display. While analyzing each of the above factors, we also collected data for each parameter and presented it in a table.

Reduced lines of code

After fully converting from Java to Kotlin, the number of lines of code was reduced from 18,004 to 17,729. This is about 1.5% less than the original Java code. Although the reduction in code volume is not significant, for some large applications, this conversion may have a more significant effect on reducing the number of lines of code, as shown in the example given in the AOSP desktop clock article.

Target APK size

The APK size of an app written in Kotlin is 2.7 MB, while the APK size of an app written in Java is 2.6 MB. This difference is negligible, and the increase in APK size is actually expected due to the inclusion of some additional Kotlin libraries. This size increase can be optimized by using Proguard or R8.

Compile time

Kotlin and Java app build times are calculated by taking the average of 10 full builds from scratch (excluding outliers), with an average build time of 13 minutes and 27 seconds for Kotlin apps and 12 minutes and 6 seconds for Java apps. According to some sources (such as "Differences between Java and Kotlin" and "Compile times for Kotlin and Java"), Kotlin compile times are actually longer than Java, especially for builds from scratch. Some analyses assert that Java compiles 10-15% faster, while others claim 15-20%. For our example full build from scratch, Java compiles 11.2% faster than Kotlin, although this small difference is not in the above range, which may be because AOSP Calendar is a relatively small application with only 43 classes. Although full builds from scratch are slower, Kotlin still has advantages in other aspects that should be taken into account. For example, Kotlin's more concise syntax generally results in less code than Java, which makes Kotlin codebases easier to maintain. In addition, since Kotlin is a safer and more efficient programming language, we can assume that the slower full build time is negligible.

First screen display time

We used this method to test the time it takes for an app to fully display the first screen from startup, and after 10 trials, we found that the average time for Kotlin apps was about 197.7 milliseconds, while that for Java was 194.9 milliseconds. These tests were all performed on Pixel 3a XL devices. From this test result, it can be concluded that Java apps may have a slight advantage over Kotlin apps; however, since the average times are very close, the difference is almost negligible. Therefore, it can be said that the conversion of the AOSP Calendar app to Kotlin did not negatively affect the initial startup time of the app.

in conclusion

Converting the AOSP Calendar app to Kotlin took about 1.5 months (6 weeks) with 2 interns working on the project. It will definitely get more efficient once we get more familiar with the code base and get better at solving recurring compile-time, runtime, and syntax issues. Overall, this particular project successfully demonstrated how Kotlin can impact existing Android apps and was a solid step forward in converting AOSP apps.

<<:  A senior designer from Tencent tells his story: How did I grow in the past 8 years at Tencent?

>>:  WeChat launches "One ID Check": quickly check how many phone numbers you have

Recommend

Are frost and frost the same thing? Here are 7 tips to deal with frost!

Today is Frost Descent, the eighteenth of the twe...

A century-old puzzle in statistical physics and its solution

The new theory can not only give the results that...

Stock market practice course: from novice to professional trader

Episode 1: Three major trends talk about entry-tr...

Infected and in quarantine? Keep this list of items to keep!

Although the new coronavirus is rampant, don’t pa...

The legendary snail that disappeared for a century reappears in the world!

Recently, Chinese researchers discovered sessile ...

Hu Quan: Killer Applications in the Industrial 4.0 Era

In the era of Industry 4.0, from the perspective ...

How can a product novice do a good competitive product analysis?

Before a product design is implemented, the produ...