# Property Delegation Tools

The standard library contains special property delegation operations.

Map is one of the few types in the Kotlin library that is preconfigured to be used as a delegated property. A single Map can be used to store all the properties in a class. Each property identifier becomes a String key for the map, and the property’s type is captured in the associated value:

// DelegationTools/CarService.kt
package propertydelegation
import atomictest.eq

class Driver(
  map: MutableMap<String, Any?>
) {
  var name: String by map
  var age: Int by map
  var id: String by map
  var available: Boolean by map
  var coord: Pair<Double, Double> by map
}

fun main() {
  val info = mutableMapOf<String, Any?>(
    "name" to "Bruno Fiat",
    "age" to 22,
    "id" to "X97C111",
    "available" to false,
    "coord" to Pair(111.93, 1231.12)
  )
  val driver = Driver(info)
  driver.available eq false
  driver.available = true
  info eq "{name=Bruno Fiat, age=22, " +
    "id=X97C111, available=true, " +
    "coord=(111.93, 1231.12)}"
}

Notice that the original Map info is modified when setting driver.available = true. This works because the Kotlin standard library contains Map extension functions getValue() and setValue() that enable property delegation. These simplified versions show how they work:

// DelegationTools/MapAccessors.kt
package delegationtools
import kotlin.reflect.KProperty

operator fun MutableMap<String, Any>.getValue(
  thisRef: Any?, property: KProperty<*>
): Any? {
  return this[property.name]
}

operator fun MutableMap<String, Any>.setValue(
  thisRef: Any?, property: KProperty<*>,
  value: Any
) {
  this[property.name] = value
}

To see the actual library definitions, put the cursor on the by keyword in IntelliJ IDEA or Android Studio and invoke “Go to Declaration” (opens new window).

Delegates.observable() observes modifications of a mutable property. Here, we trace old and new values:

// DelegationTools/Team.kt
package delegationtools
import kotlin.properties.Delegates.observable
import atomictest.eq

class Team {
  var msg = ""
  var captain: String by observable("<0>") {
    prop, old, new ->
      msg += "${prop.name} $old to $new "
  }
}

fun main() {
  val team = Team()
  team.captain = "Adam"
  team.captain = "Amanda"
  team.msg eq "captain <0> to Adam " +
    "captain Adam to Amanda"
}

observable() takes two arguments:

  1. The initial value for the property; "<0>" in this case.
  2. A function which is the action to perform when the property is modified. Here, we use a lambda. The function arguments are the property being changed, the current value of that property, and the value it’s being changed to.

Delegates.vetoable() allows you to prevent a change to a property if the new property value doesn’t satisfy the given predicate. Here, aName() insists that the team captain’s name begin with the letter “A”:

// DelegationTools/TeamWithTraditions.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

fun aName(
  property: KProperty<*>,
  old: String,
  new: String
) = if (new.startsWith("A")) {
  trace("$old -> $new")
  true
} else {
  trace("Name must start with 'A'")
  false
}

interface Captain {
  var captain: String
}

class TeamWithTraditions : Captain {
  override var captain: String
    by Delegates.vetoable("Adam", ::aName)
}

class TeamWithTraditions2 : Captain {
  override var captain: String
    by Delegates.vetoable("Adam") {
      _, old, new ->
        if (new.startsWith("A")) {
          trace("$old -> $new")
          true
        } else {
          trace("Name must start with 'A'")
          false
        }
    }
}

fun main() {
  listOf(
    TeamWithTraditions(),
    TeamWithTraditions2()
  ).forEach {
    it.captain = "Amanda"
    it.captain = "Bill"
    it.captain eq "Amanda"
  }
  trace eq """
    Adam -> Amanda
    Name must start with 'A'
    Adam -> Amanda
    Name must start with 'A'
  """
}

Delegates.vetoable() takes two arguments: the initial value for the property, and an onChange() function, which is ::aName in this example. onChange() takes three arguments: property: KProperty<*>, the old value currently held by the property, and the new value being placed in the property. The function returns a Boolean indicating whether the change is successful or prevented.

TeamWithTraditions2 defines Delegates.vetoable() using a lambda instead of the function aName().

The remaining tool in properties.Delegates is notNull(), which produces a property that must be initialized before it can be read:

// DelegationTools/NeverNull.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates

class NeverNull {
  var nn: Int by Delegates.notNull()
}

fun main() {
  val non = NeverNull()
  capture {
    non.nn
  } eq "IllegalStateException: Property " +
    "nn should be initialized before get."
  non.nn = 11
  non.nn eq 11
}

Trying to read non.nn before nn has been assigned a value produces an exception. After nn has been assigned, you can successfully read it.

Exercises and solutions can be found at www.AtomicKotlin.com.