User:DBrant (WMF)/Kotlin

From mediawiki.org

Getting serious about Kotlin[edit]

This is a collection of guidelines and best practices for development using Kotlin and its various affordances, specifically as it relates to building clean and performant Android apps.

General thoughts[edit]

As we all know, the Android platform gives us a hundred different ways of doing the same thing, so we have to be very mindful and careful about picking a single way and remaining consistent throughout our code.

View bindings[edit]

Currently our code uses several distinct ways to bind views at runtime:

  • ButterKnife, which creates binding classes at compile time and requires us to hook into them at runtime.
  • Plain old findViewById() in certain cases.
  • Kotlin synthetics, which are convenient for our purposes, but are now deprecated!

So then, the new One True Way of binding views seems to be the View Binding library of the Jetpack suite. Let's work towards updating all of our layouts to use these bindings, and remove the usage of ButterKnife and synthetics. This will have the benefit of using fewer dependencies, and probably fewer total methods and cleaner code.

Use @Parcelize when passing objects between activities[edit]

There are many cases where we have to exchange information between activities. In the cases where the information is a complex object, we usually resort to serializing it to JSON, and then deserializing it from JSON in the destination activity. This is a pretty expensive operation.

Android provides the "Parcelable" interface which is supposed to solve this issue, but the problem is that it requires a lot of boilerplate code to make a class be Parcelable. Fortunately Kotlin now offers the Parcelize annotation which supposedly auto-generates all the necessary boilerplate.

Let's investigate the Parcelize annotation and use it whenever we transfer complex objects between activities.

Use kotlinx.serialization to serialize objects to and from the server[edit]

The new kotlinx.serialization library seems to be much more powerful and more appropriate for our purposes than Gson, so let's work towards adopting it. A huge benefit is that this library is reflectionless, which should make it much more efficient. It also has nice conveniences like a @Required annotation.

Use lateinit liberally[edit]

If you have an Activity that contains fields that are initialized in onCreate(), make sure they are defined as lateinit var. In other words, avoid using nullable types as much as possible.

Use 'by lazy' sparingly[edit]

The by lazy annotation allows you to lazily initialize a field, but beware: this comes at a nonzero cost. The by lazy annotation itself will become an object that will "contain" the lazy initialization code and run it in a thread-safe way when it's first requested. Therefore only objects that are truly heavy and expensive should be initialized this way.

Data classes[edit]

When possible, model classes (e.g. objects that are deserialized from server responses, objects exchanged between activities, etc.) should be declared as data class, to be maximally clear and concise.

Use java.time everywhere[edit]

The java.util.Date and related classes have been deprecated for a while, and have been superseded by java.time. We should adopt it everywhere we use dates, time ranges, etc.

Things to check after conversion[edit]

The automatic converter offered by Android Studio that converts Java files to Kotlin is good, but not perfect. After you convert a file, check the following things for possible improvement or optimization:

If-not-null-then constructs[edit]

Very often we might have a Java construct like this:

if (object != null) {
   object.method();
}

When converted to Kotlin, this can become:

object?.method();

Usages of TextUtils.isEmpty()[edit]

We have numerous usages of TextUtils.isEmpty() which checks whether a string is empty or null. In Kotlin this is easily replaceable by:

[string].isNullOrEmpty()

Other string operations[edit]

We have other usages of TextUtils and StringUtils functions that are now part of the Kotlin standard library. For example, string joins can now be done by:

[string array].joinToString(",")

Things to watch out for[edit]

Chained safe-call operators with null-check[edit]

Beware of constructs like this:

parentObject?.childObject?.childChildObject!!.method()

In the above line, if either parentObject or childObject is null, it will cause a crash. This is because the !! operator will operate on any value that comes out of the chain, even if it's null! (i.e. the execution will not "bail out" if something in the chain leading up to the !! operator happens to be null.)

Instead, always prefer using safe-call operators the whole way through, and if this is impossible, try using a let construct:

parentObject?.childObject?.let {
  it.childChildObject!!.method()
}

Use @JvmOverloads carefully[edit]

The @JvmOverloads annotation makes code a little bit cleaner, but it can actually cause issues when creating custom Views and using @JvmOverloads to overload the constructors. This is because the View constructors (usually three of them; with one, two, and three parameters) don't always call through to each other in the same way that @JvmOverloads assumes. This can result in styling issues in your custom View.

The truly proper solution is to keep three separate constructors, making each constructor call super() with the same parameter signature, and avoid using @JvmOverloads for custom Views.

String split preserves empty strings[edit]

The string split() function returns empty substrings as valid items. So for example, if you have a string "one,,three", then split(",") will return the list "one", "", "three".

More importantly, if you have an empty string "", splitting it by any delimiter will return a non-empty list with one item in it, which is "". Kotlin does not seem to provide a way to suppress empty strings when splitting. The proper way to do this would be something like this:

string.split(",").filter { it.isNotEmpty() }