Skip to content

4.) The Tricky Stuff

Gabor Varadi edited this page Jan 8, 2019 · 15 revisions

Kotlin properties and encapsulation

When you first see Kotlin properties in a class, your first reaction is probably: "I can get rid of all those private field/constructor/getters/setters".

Why did we need getters and setters? The idea was: encapsulation. The way Java made us implement this idea was verbose, but the idea itself is valuable.

Kotlin doesn't get rid of getters and setters. Instead, the compiler generates them for us!.

While in many cases we did not actually use them in any way, for the remaining cases that are complex, Kotlin has the following features:

  • Custom getter methods
  • Manipulating the backing field
  • Built-in delegates
  • Delegating to a map
  • Custom delegates

Custom getter method

A val property can be backed by a getter method instead of private field.

val hello : String = "Hello at time ${System.currentTimeMillis()}!"

val hello: String
    get() = "Hello at time ${System.currentTimeMillis()}!"

There is an important difference between those two properties.

The first is backed by a final field and will be initialized once and then always return the same value.

The second property is backed by a getter method and will deliver a different value each time.

Setter methods and backing field

It is also possible to define a custom setter method where you manipulate the private field generated by the compiler.

var name: String = ""
     private set(value) {
         field = value
         println("Name was set to $value")
     }

Intercepting accessors and sharing logic between getters/setters with delegates

Calling a function when the field is changed is actually built-in in Delegates.observable

import kotlin.properties.Delegates

var name: String by Delegates.observable("") { property, oldValue, newValue ->
    println("Property '${property.name}' was changed to $newValue")
}

fun main() {
    name = "Jake"
    //Prints: Property 'name' was changed to Jake
}

Delegates.vetoable is another built-in that allows to forbid (veto) to modify a property if some condition is not fulfilled.

Lazy delegates

lazy lets you "defer" the instantiation of the class to first access. Meaning you define that "this will happen at some point", give it an initializer it will invoke, and the default behavior also uses a double-checked lock around the initializer for the sake of thread-safety.

val myView by lazy { view.findViewById(R.id.myView) }

Please note that this is actually not free because of the double-checked lock, so using it for view references like this would be fairly slow. An unsafe version would be preferred.

val myView by lazy(LazyThreadSafetyMode.NONE) {
    view.findViewById(R.id.myView)
}

Custom delegates

You are not limited to the built-in properties delegate.

Library authors can define their own functions that return a ReadOnlyProperty or ReadWriteProperty.

This allows you for example to abstract away how things are stored in your SharedPreferences.

interface CurrentUser {
    var userId: String
    var fullName: String
}

// Implementation on Android with the kotlin-jetpack library
// https://github.com/nsk-mironov/kotlin-jetpack#preferences-bindings
class CurrentUserSharedPrefs(
    val preferences: SharedPreferences
): CurrentUser {
    override var userId by preferences.bindPreference("", "user_id")
    override var fullName by preferences.bindPreference("", "fullName")
}

Note that each comes at a cost because a ReadOnlyProperty is used for each, so use it when it makes sense.

Once you are using a delegate, wrapping it with another delegate is tricky.

Delegates by map

One common use case is storing the values of properties in a map. This comes up often in applications like parsing JSON or doing other “dynamic” things.

Delegating to a map avoid the annoying use of magic strings and casting and is optimized by the compiler!

class User(
    val map: Map<String, Any?>
) {
    val name: String by map
    val age: Int     by map
}

val user = User(convertSomeJsonToAMap())
println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

// Instead of
println(user.map["name"] as String)
println(user.map["age"] as Int)

Beware though that in both case a map is being used behind the scene. Your code will crash if your map contains no name. You may want to wrap your map with map.withDefault { ... }

delegation by class

In Kotlin, it's possible to pass over implementations of a given interface to a value that is passed in to the class via its constructor.

Suppose you use an API to fetch a list of users. Your backend developer has wrapped the actual list in an object to be more flexible:

{ 
 "users" : [ 
  { "id": 1, "name": "Jake"}
 ] 
}

But really it's a list of users. Here is a possible trick you can use to use the inner list directly:

data class GetUsersResponse(
    val users: List<UserDTO>
) : List<UserDTO> by users

data class UserDTO(val id: Int, val name: String)

// usage
val users: GetUsersResponse = api.fetchUsersSync()
println(users.first().name) // INSTEAD OF:  users.users.first().name

JVM annotations: @JvmOverloads, @JvmField, @JvmStatic, @JvmSuppressWildcards, @get: and @set: and @field:

It's VERY important to know about the @Jvm* annotations, which manipulates what kind of Java code will be available as a result of having written your Kotlin code.

For example, Dagger requires @JvmSuppressWildCards to generate Map<? extends Class<?>, Provider<ViewModel>> instead of Provider<? extends ViewModel>-

Parcelable.CREATOR needs to be a public static final field, so we must apply @JvmField inside the companion object {.

If we don't want to call MyObject.INSTANCE.someMethod() to access a method inside an object, we need to use @JvmStatic. It's also required for binding adapters (data-binding) and static provider methods (Dagger modules).

@JvmOverloads can be used to generate multiple constructors from a Kotlin constructor that has multiple arguments. However, it should generally NOT be used for Android View constructors (even if your IDE says so).

@get: and @set: and @field: lets you define what an annotation is applied to when you put it on a property.

annotation processing with Kotlin: kapt and apply plugin: 'kotlin-kapt'

If you use Java's annotation processing with Kotlin on classes written in Kotlin, then you HAVE TO apply kotlin-kapt plugin.

You don't really get any real errors beyond "things not working" if you forget.

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'