Kotlin - How to create property delegates

dimitrilc 3 Tallied Votes 147 Views Share

Introduction

When working on Android with Kotlin, you might have ran into property delegates on a few occasions, such as activityViewModels() from androidx.fragment.app or remember() from androidx.compose.runtime. In this tutorial, we will learn what property delegates are and how to create them ourselves.

Goals

At the end of the tutorial, you would have learned:

  1. How to create final delegates.
  2. How to create non-final delegates.

Tools Required

A Kotlin IDE such as IntelliJ IDEA version 2021.3.2 (Community Edition).

  1. The Kotlin version used in this tutorial is 1.6.10.

Prerequisite Knowledge

  1. Basic Kotlin.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Kotlin project using Gradle as the build system.

  2. For the Project JDK, use JDK 17.

  3. Add the kotlin-reflect dependency below into your build.gradle.

     implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")

Final Property Delegate

In Kotlin, to delegate a property means letting another class provide or set the value for that property. There is a special syntax to use a delegate, which is:

val/var propertyName: Type by expression

In real code, the explicit Type on the variable can be omitted because it can be inferred from the delegate getter.

If the reference is a val, the delegate must provide a getter with the syntax below.

operator fun getValue(thisRef: Any?, property: KProperty<*>): Type

The function signature Type must be of the same or a subtype of the variable declaration. thisRef will contain a reference to the enveloping class of the property. If the property is a top-level declaration, then thisRef will be null.

To see how this works, let us write some code.

  1. Open Main.kt. All of the code used in this tutorial can be written in this file.

  2. Import this class into the file.

     import kotlin.reflect.KProperty
  3. Create a delegate for the final-variable use case.

     class FinalDelegate {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
            if (thisRef === null) {
                "${property.name} is top-level"
            }
            else "${property.name} is inside $thisRef"
     }
  4. Add a top-level val property and delegate its value to FinalDelegate.

     val topLevelFinalProp: String by FinalDelegate()
  5. Add a class with an inner val property like below.

     class FinalPropContainer {
        val innerFinalProp: String by FinalDelegate()
     }
  6. In main(), call the code below.

     println(topLevelFinalProp)
     println(FinalPropContainer().innerFinalProp)
  7. Run the program. We can see that the output prints whether the property is top-level or an inner property of another class.

     topLevelFinalProp is top-level
     innerFinalProp is inside FinalPropContainer@73a8dfcc

Non-final Property Delegate

A delegate for a var reference is different to a delegate for a val because in addition to providing a getter using the same syntax above, it must also provide a setter with a special syntax.

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Type)

The Type must be of the same or a super type of the property. To see how this works, let us write some more code.

  1. Inside Maint.kt, create another delegate for non-final var references. This delegate is a little bit different from FinalDelegate because it is a stateful class (the delegate is responsible for storing its state).

     class NonFinalDelegate(private var num: Int = 1) {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = num
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            num = value
        }
     }
  2. Add a var reference and delegate it to NonFinalDelegate.

     var nonFinalProp: Int by NonFinalDelegate()
  3. In main(), call the code below. The assignment expression = will invoke the setter.

     println(nonFinalProp)
     nonFinalProp = 2
     println(nonFinalProp)
  4. We can see that after invoking the setter, nonFinalProp now contains a new value. The output of the code above is:

     1
     2

(Optional) How does remember() work?

This section is mostly only relevant towards Android development.

If we look at the function signature of one of the remember() variant,

@Composable
inline fun <T : Any?> remember(calculation: (@DisallowComposableCalls () -> T)?): T

then we can see that it basically just returns T from the lambda argument. Usually remember() is used in conjunction with lambdas that return a State (androidx.compose.runtime) object, such as mutableStateOf(). State itself does not contain a getValue() function, so how can it be used as a delegate? The answer lies in one of its extension functions, getValue().

inline operator fun <T : Any?> State<T?>?.getValue(thisObj: Any?, property: KProperty<*>?): T

Even though State does not include a getValue() function, it can still be used as a delegate if this extension function is imported into your source file.

Solution Code

Main.kt

import kotlin.reflect.KProperty

fun main() {
//    println(topLevelFinalProp)
//    println(FinalPropContainer().innerFinalProp)
   println(nonFinalProp)
   nonFinalProp = 2
   println(nonFinalProp)
}

val topLevelFinalProp: String by FinalDelegate()

class FinalPropContainer {
   val innerFinalProp: String by FinalDelegate()
}

class FinalDelegate {
   operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
       if (thisRef === null) {
           "${property.name} is top-level"
       }
       else "${property.name} is inside $thisRef"
}

var nonFinalProp: Int by NonFinalDelegate()

class NonFinalDelegate(private var num: Int = 1) {
   operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = num
   operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
       num = value
   }
}

build.gradle

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
   kotlin("jvm") version "1.6.10"
   application
}

group = "me.dimitri"
version = "1.0-SNAPSHOT"

repositories {
   mavenCentral()
}

dependencies {
   implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")
   testImplementation(kotlin("test"))
}

tasks.test {
   useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
   kotlinOptions.jvmTarget = "1.8"
}

application {
   mainClass.set("MainKt")
}

Summary

We have learned how to create property delegates in Kotlin in this tutorial. In some situations where you do not have to use extension functions, then you can also implement the ReadOnlyProperty or ReadWriteProperty interfaces to create delegates.

The full project code can be found at https://github.com/dmitrilc/DaniwebKotlinPropertyDelegate.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.