Delegates in Kotlin for Android App Development

Written by mkiriloff | Published 2023/11/08
Tech Story Tags: android | kotlin | kotlin-mobile-development | kotlin-delegates | programming | guide | tutorial | reusable-code

TLDRvia the TL;DR App

Introduction

Hi all, today, let's talk about Kotlin delegates and how they can be used in Android app development. In this article, let's try to understand how delegates work under the hood, see a ready-to-use Kotlin delegate, and try to write a new Kotlin delegate.

What is a Kotlin delegate?

First, let's understand what Kotlin delegate is. There is the keyword by in Kotlin, it means that this property uses Kotlin delegate under the hood. This means that a getter and setter property call will be delegated to some expression, with the expression being a delegate. This expression may contain logic different from the usual logic of setting and getting a property and specify some non-standard behavior of the property when a getter or setter is called. We delegate these method calls to this expression; that's why it's called a delegate, and as we can deduce, it's based on a delegation pattern. In the code, it looks like by <expression>. In Kotlin, a modern programming language, we can see delegates very often and in different layers of application, from UI to domain layer. It remains to be seen how this can benefit us in practice and how to apply it.

Property Delegation

Let's first try to understand the property delegates. Kotlin already has ready-to-use property delegates in object Delegates in package package kotlin.properties. Let's take a look at some of them.

val notNullString by Delegates.notNull<String>()

An IllegalStateException will be thrown when attempting to read this property. This logic is implemented in the delegate, which we use because of the keyword by.

val observableProperty by Delegates.observable(initialValue = true) { 
    property, oldValue, newValue -> 
    //is called only when the value of the property has been changed
}

The Observable delegate works like a classic observable pattern and notifies the subscriber, in our case, calls the lambda in case the value of the property changes.

One of the most powerful ready-to-use delegates is the lazy delegate. Let's take a closer look at it.

val text: String by lazy { 
    "lazy text" 
}

The lazy delegate lambda will be executed at the moment when the variable is first read. Thus, we postpone some time-consuming operations to the moment when the variable is read, and this is possible thanks to the use of the lazy delegate. In mobile development, where we try to save system resources, the lazy delegate is a very powerful tool. For example, it can be used in cases of deferred reading of Android resources.

val text: String by lazy {
    resources.getString(R.string.lazy_resource) 
}

Let's see how we can use lazy delegate in other cases. For example, when the calculation of some boolean flag can be executed with delay, on demand.

val firstCase: Boolean = true
val secondCase: Lazy<Boolean> = lazy { 
    users.any { it == "root" } 
}
val thirdCase: Lazy<Boolean> = lazy { 
    users.any { it == "admin" } 
}


if (firstCase) {

   doCaseOne()

} else if (secondCase.value) {   
// property secondCase will be calculated lazily

   doCaseSecond()

} else if (thirdCase.value) {    
// property thirdCase will be calculated lazily

   doCaseThird()

}


In this example, the values of thesecondCase and thirdCase properties may never be needed. So why compute these boolean flags? We can only do it when we really need it. And in that case, the lazy delegate is perfect for doing the resource-intensive calculations at the first read property, and all subsequent reads of the flag will return the value already calculated before. As a result, with the help of the delegate, we have postponed our time-consuming calculations, which may never be needed at all.

It is important to know that by default, this delegate works synchronously and provides thread-safe access to the field, which imposes additional costs when reading the variable. In the case of multi-threaded access, our thread may be blocked at the time of the first read. But luckily for us, the Kotlin developers have thought of everything for us and provided an API that allows us to set the delegate strategy and disable synchronized variable access when we don't need it. Let's see how to do that.

To do that, we need to specify LazyThreadSafetyMode, it can be SYNCHRONIZED, PUBLICATION, NONE. I will not dwell on each of them; you can read about their behavior in the documentation.

val lazyField = lazy(mode = LazyThreadSafetyMode.NONE) { 
    "not blocking property" 
}

So, we have learned about property delegates and one of the most popular and powerful delegates in Kotlin, Lazy. Now we know how powerful this tool is. In the next step, we will try to implement our own delegate.

Writing our property delegate

To write our own delegate, we need to create a class and inherit from ReadWriteProperty or from ReadOnlyProperty from package kotlin.properties. If we want to override read and write logic, we inherit from interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V>, and if we need to override only read logic, we can inherit from public fun interface ReadOnlyProperty<in T, out V>.

package kotlin.properties

import kotlin.reflect.KProperty

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
   /**
    * Returns the value of the property for the given object.
    * @param thisRef the object for which the value is requested.
    * @param property the metadata for the property.
    * @return the property value.
    */
   public override operator fun getValue(thisRef: T, property: KProperty<*>): V

   /**
    * Sets the value of the property for the given object.
    * @param thisRef the object for which the value is requested.
    * @param property the metadata for the property.
    * @param value the value to set.
    */
   public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

public fun interface ReadOnlyProperty<in T, out V> {
   /**
    * Returns the value of the property for the given object.
    * @param thisRef the object for which the value is requested.
    * @param property the metadata for the property.
    * @return the property value.
    */
   public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

So let's write our class.

class ObservableDelegate<T>(
   initValue: T,
   val onChanged: (old: T, new: T) -> Unit,
) : ReadWriteProperty<Any?, T> {

   private var currentValue: T = initValue

   override fun getValue(thisRef: Any?, property: KProperty<*>): T {
       return currentValue
   }

   override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
       val oldValue = currentValue
       currentValue = value
       onChanged(oldValue, value)
   }
}

Let's write a function for the convenience of using the delegate.

fun <T> observableDelegate(
   initValue: T,
   onChanged: (old: T, new: T) -> Unit
): ReadWriteProperty<Any?, T> {
   return ObservableDelegate(
       initValue = initValue,
       onChanged = onChanged
   )
}

As a result, we have implemented our own ObservableDelegate. The override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) is called when we assign a new value and the override fun getValue(thisRef: Any?, property: KProperty<*>): T when we read the field value. So, our delegate will call the onChanged function with the new value and the old value every time we set a new value. In our code, we just use the standard assignment operator, and our onChanged function is called every time we set a new value. We can also write some logic for the getValue function and do some actions, too, for example, increase the counter of reading the value.

private var observableProperty: String by observableDelegate("initValue") { oldValue, newValue ->
   Log.d("tag", "oldValue: $oldValue, new: $newValue")
}

observableProperty = "test1"
observableProperty = "test2"
observableProperty = "test3"

Yay, our delegate works, and now we know how to implement our own delegates.

Delegate for interface

The interface can also be implemented using a delegate. In this case, overriding public members’ methods of the interface becomes optional. And all such calls will be delegated to our delegate. This means that we can define a default implementation and use it as a delegate.

interface Player {
    
    fun play()
    
    fun stop()
}

class DefaultPlayerImpl : Player {

    override fun play() { print("default play impl") }

    override fun stop() { print("default stop impl") }
}

class AdminPlayer(player: Player = DefaultPlayerImpl()) : Player by player {

    override fun play() { print("admin play impl") }
}

class UserPlayer(player: Player = DefaultPlayerImpl()) : Player by player

Now AdminPlayer and UserPlayer classes do not need to implement more than one public function of the interface Player. Now AdminPlayer and UserPlayer delegate the call of these functions to the delegate. But if you still need to use a function implementation not from a delegate, you can simply override the required functions as usual.

Conclusion

In this article, we learned what Kotlin delegates are and how we can use them in modern android development. It's also worth mentioning that Kotlin delegates are now used almost everywhere in Android development. For example, there are a lot of delegates for Jetpack Compose out of the box, and there are also ready-made delegates in Kotlin in the Delegates object. Ideas for using delegates can be absolutely different, from fields with custom logic to getting some Android resources, services, or lazy loggers. So make sure you use Kotlin delegatesin your development and get nice, concise, and reusable code.




Written by mkiriloff | Software Engineer / Android Developer
Published by HackerNoon on 2023/11/08