“Neist Point at sunset — Isle of Skye” by Edoardo Brotto — on flickr

Kotlin and Android #3 — know your properties

--

tl;dr Kotlin properties are awesome and super powerful, but each form comes with a bunch of gotchas. Make sure you fully understand them before deciding what type of property you use!

Kotlin boasts an excellent support for properties. No more you are limited to the bare-bones fields that Java offered; no more envying C# developers for their fully-fledged properties!

Just look at this non-exhaustive list of ways to declare a property:

But what if I told you that there’s some non-trivial implications that depend on the way a property is declared?

1-2. Simple vals and vars

The simplest and most common way to declare a property is to have a val or var in a field-like fashion:

The first example is a read-only property whose value is a constant pistacchio-flavoured IceCream instance. The second example is a read-write property, initialised with another pistacchio-flavoured IceCream. The property values are initialized at the container’s init time for a class property, or whenever the backing static class is first accessed for a top-level property.

Note that vals are not immutable. This is a common misconception; vals are read-only properties in the sense that their reference cannot be changed once they’re assigned, but nothing prevents their value from mutating internally.

For example, a private val items = mutableListOf(...) will always point to the same instance of MutableList<>, but you can change the contents of that list at your heart’s content. This goes for any instance of a class or object that contains mutable state of any kind.

A simple property like these in a class is the closest to a field in Java. The biggest difference is that you can have top-level properties in Kotlin, but no top-level fields in Java.

3. Custom getters

A property can have a custom getter. This effectively makes accessing this property like invoking a function, since the getter is a function without arguments.

The not-so-obvious implication is that the value you obtain from the property in this example is a new instance every time. If IceCream does not have equals and hashcode functions, you might be in for some subtle and extremely annoying bugs where two seemingly identical instances obtained from the property are not equal since they are compared by reference:

On the other hand, you can exploit this “always fresh” property of custom getters for conveniently accessing something that is not yet available at init time, such as anything Context-related in an activity:

This is particularly useful when handling things that can change over time, like an activity’s intent: when onNewIntent() is called, you can update the intent field value with the new one. A Lazy property wouldn’t reflect the change as they can only be assigned a value once, so a custom getter will be a better fit. Obviously you need to ensure that these properties are only accessed when their backing value is present, or your app will crash — just like for lateinit and Lazy.

Lastly, make sure you don’t use custom getters to mask expensive operations behind a property, as that’s not the expected behaviour of a property. Prefer functions instead, in that case.

4. Custom setter with backing field

If you want to customize the assignment logic, you can use a property with a custom setter and the backing field:

You could technically do without assigning the backing field, but I cannot think of a scenario in which it would make sense to have a custom setter and not assigning the backing field.

This example shows one of the possible usages of custom setters, executing some code before saving the new value. A common case is to do some validation against the new value, but sometimes you’ll see a custom setter used to trigger some side-effect; in this case, respectively refusing misspelled pistacchio, and logging.

In general I’d advise against having custom setters to trigger side-effects; rather, use a function and set the value from that:

It’s clearer and provides a better indication that it’s not a trivial field-like assignment to users of the property, which would otherwise have no way to know it.

5–6. Property delegates and lateinit

Since there’s a whole article on the topic in this series, I’m going to keep it short here. We’ll consider the Lazy delegate as it’s the most common; other delegates can have different gotchas.

Both lateinit and Lazy-delegated properties start without an actual value at init time. lateinit requires an explicit initialisation before it can be accessed, and since Kotlin 1.2 you can check whether the initialisation has been performed already by using ::iceCream.isInitialized. Lazy-delegated properties will automatically lazy-initialise their value on their first access, and there is no way to tell whether they have been assigned a value already or not.

For lazy properties, once the value has been initialised, it can’t change; lateinit properties, being read-write properties, can be reassigned after the initial initialisation. A notable limitation of lateinit properties is that they can’t have nullable types, nor types backed by primitive types such as Int, Float, Double etc.

Hopefully it’s now clearer when to use, and when not to use, each of the style of properties. If you have other good or bad examples, make sure to let me know, I’d like to grow the collection!

--

--

"It depends" 🤷‍♂️ - UXE on Android Studio at Google. A geek 🤓 who has a serious thing for good design ✨ and for emojis 🤟 Personal opinions only 😁