Summary: Today, we continue the eleventh Kotlin original series, to reveal the beautiful coat of Kotlin property broker. Property broking is one of Kotlin’s unique and powerful features, especially useful for framework developers because it often involves changing the way properties are stored and modified. For example, Kotlin’s SQL framework Exposed source code makes extensive use of property broking. We believe you have also used a custom property proxy such as Delegates.Observable (), Delegates.notnull (), Delegates.vetoable() in your code. If you’re still using it or a little unfamiliar with it, don’t worry that this article will solve almost all of your doubts. Without further further, let’s move on to a wave of chapter maps:

First, the basic definition of property broker

  • 1. Basic definitions

Property broking is based on the proxy design pattern, which, when applied to a property, can proxy the accessor’s logic to a helper object.

It can be simply understood that the internal implementation of the setter and getter of the property is implemented by a proxy object, which is equivalent to replacing the original simple reading and writing process of the property field with a proxy object, while the operation of exposing the external property remains unchanged, which is the same as the assignment and reading of the property. It’s just that the internal implementation of the setter and getter has changed.

  • 2. Basic syntax format
class Student{
    var name: String by Delegate()
}

class Delegate{
    operator fun <T> getValue(thisRef: Any? , property:KProperty< * >): T{
        ...
    }
    operator fun <T> setValue(thisRef: Any? , property:KProperty<*>, value: T){... }}Copy the code

The name property delegates the logic of its accessor to the Delegate object, which is evaluated against the expression Delegate() with the by keyword. Anything that conforms to the property broker rules can use the BY keyword. The property proxy class must follow the convention of getValue() and setValue() methods. GetValue and setValue methods can be either normal or extended methods, and all methods support operator overloading. The getValue() method is all you need if the property is decorated by val.

The basic flow of the property broker is that the getValue() method in the proxy class contains the logical implementation of the getter accessor for the property, and the setValue() method contains the logical implementation of the setter accessor for the property. The setValue() method of the delegate object is then called inside the setter accessor when the name property performs an assignment. When the read property name operation is performed, the delegate object’s getValue method is called in the getter accessor.

  • 3. Keyword by

The by keyword is actually a symbol overloaded by the property broker operator. Any class with property broker rules can use the BY keyword to proxy properties.

Two, common property proxy basic use

The property broker is a unique feature of Kotlin. We can customize the property broker ourselves, but Kotlin also provides several common implementations of the property broker. For example: Delegates.notnull (), Delegates.Observable (), Delegates.vetoable()

  • 1, Basic use of Delegates. NotNull ()

The delegator.notnull () agent is used for var properties that can be delayed until later rather than initialized at constructor time. It is similar to the LateInit function, but there are a few differences. One thing that needs to be noticed is that the property lifecycle is controlled by the developer. Make sure that the property is initialized before the property is used, otherwise an IllegalStateException will be thrown.

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Teacher {
    var name: String by Delegates.notNull()
}
fun main(args: Array<String>) {
    val teacher = Teacher().apply { name = "Mikyou" }
    println(teacher.name)
}
Copy the code

For those of you who don’t see the great use of notNull(), let’s give you some context.

Background: Kotlin development is different from Java in that it is necessary to initialize properties when defining and declaring them, otherwise the compiler will prompt an error. Unlike Java, it is OK to define and initialize properties. Let me explain and this is one of the things that Kotlin has over Java, right, is empty type safety, which is that Kotlin, when you write code, lets you know if a property is initialized or not, so you don’t leave that undefined definition behind at runtime. If you forget to initialize in Java, you’ll get a null pointer exception at runtime.

Here’s the problem: With that background out of the way, Kotlin attributes are defined with the extra work of initializing attributes compared to Java attributes. But maybe you don’t know the value of a property when you define it, and you need to perform logic later to get it. At this point, there are several solutions:

Method A: Start initialization by assigning A default value to the property

Method B: Uses the Delegates.notnull () property proxy

Mode C: Use the lateinit attribute

The limitations of all three of these methods are that method A is very aggressive in assigning default values, which is fine for basic types, but for properties that reference types, assigning A default reference type object feels inappropriate. Mode B applies to both basic data types and reference types, but the existence of property initialization must be a prerequisite before the property can be used. Mode C applies only to reference types, but there is also a condition that property initialization must be made before the property is used.

Advantages and disadvantages analysis:

Attribute usage advantages disadvantages
Method A(initializing default values) Simple to use, there is no problem that the property initialization must be before the property is used Applies only to basic data types
Method B(Delegates.notnull () property proxy) Applies to both basic data types and reference types 1, there is a problem that the property initialization must be used before the property;

2. Injecting it directly into Java fields is not supported by external injection tools
Mode C(lateinit modifier attribute) Applies only to reference types 1, there is a problem that the property initialization must be used before the property;

Basic data types are not supported

Recommended use: if the property life cycle is well controlled and there is no need to inject external fields, use mode B. Another good suggestion is the combination of way A plus way C, or way A plus way B. See the actual scenario requirements.

  • 2, Basic use of Delegates.Observable ()

Delegates.Observable () is used to monitor changes in property values, much like an observer. A change callback is thrown when the property value is modified. It takes two arguments, one is the value initialized by initValue, and the other is a callback to lamba, calling back property, oldValue, and newValue.

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.observable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")})}fun main(args: Array<String>) {
    val person = Person().apply { address = "ShangHai" }
    person.address = "BeiJing"
    person.address = "ShenZhen"
    person.address = "GuangZhou"
}
Copy the code

Running result:

property: address  oldValue: NanJing  newValue: ShangHai
property: address  oldValue: ShangHai  newValue: BeiJing
property: address  oldValue: BeiJing  newValue: ShenZhen
property: address  oldValue: ShenZhen  newValue: GuangZhou

Process finished with exit code 0

Copy the code
  • 3, Basic use of Delegates.vetoable()

The Delegates.vetoable() agent is used to monitor changes in the property value. It acts as an observer, throwing a callback when the property value is modified. It takes two arguments, one is the value initialized by initValue, and the other is a callback to lamba, calling back property, oldValue, and newValue. Unlike Observable, this callback returns a Boolean value that determines whether the property value should be modified this time.

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.vetoable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")
        return@vetoable newValue == "BeiJing"})}fun main(args: Array<String>) {
    val person = Person().apply { address = "NanJing" }
    person.address = "BeiJing"
    person.address = "ShangHai"
    person.address = "GuangZhou"
    println("address is ${person.address}")}Copy the code

Three, common property agent source analysis

We’ve covered the basic use of common property brokering, and it’s a bit low to just stay at the use stage, so let’s take a look at them first. Let’s start with the common property broker package structure found in the Kotlin standard library source code.

  • 1, source package structure

  • 2. Relationship class diagram

Delegates: is a proxy singleton with notNull, Observable, vetoable static methods, each returning a different type of proxy object

NotNullVar: The notNull method returns the class of the proxy object

ObserableProperty: Observable, vetoable methods return the class of the proxy object

ReadOnlyProperty: a generic interface for a read-only property proxy object

ReadWriteProperty: generic interface for reading and writing property proxy objects

  • 3, Delegates. NotNull () source analysis

NotNull () is first a method that returns an instance of the NotNullVar property proxy; So the core logic it handles is the setValue and getValue methods inside NotNullVar. Take a look.

  public override fun getValue(thisRef: Any? , property:KProperty< * >): T {
        returnvalue ? :throw IllegalStateException("Property ${property.name} should be initialized before get.")}public override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
        this.value = value
    }
Copy the code

As you can see from the source code, once the value in getValue is null, an IllegalStateException is thrown, i.e. the property is not initialized before it is used. You can actually understand a proxy implementation that adds a null level to the accessor getter.

  • 4, Delegates. Observable () source analysis

Observable () is a method that returns an ObservableProperty proxy instance; So how does it tell the outside when the property value changes? Well, it’s pretty simple. First of all, it keeps an oldValue inside for the last time, Then after the setValue method ObservableProperty classes really assignment to outside again throws a afterChange callback, and put the oldValue, newValue, property callback to the outside, Finally, the onChange method is used to call back to the outermost layer.

 public override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
        val oldValue = this.value
        if(! beforeChange(property, oldValue, value)) {return
        }
        this.value = value
        afterChange(property, oldValue, value)
    }
 public inline fun <T> observable(initialValue: T.crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Any? , T> =object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
 }    
Copy the code
  • Vetoable () is a method that returns an ObservableProperty proxy instance;… vetoable() will return an ObservableProperty proxy instance; As you can see from the source code above, before the actual assignment is performed in the setValue method, there is a judgment logic, based on the Boolean returned by the beforeChange callback method to determine whether to proceed with the actual assignment. If beforChange() returns false and terminates the assignment, the Observable does not get the callback. If beforChange() returns true, the assignment continues and the Observable callback is performed.

Four, the principle behind the property proxy and source code decompile analysis

If section 3 takes away the first layer of property brokering, section 4 will take away the last layer of property brokering, and you’ll see the real principle behind property brokering. It’s pretty simple. Without further ado, let’s do a simple example

class Teacher {
    var name: String by Delegates.notNull()
    var age: Int by Delegates.notNull()
}
Copy the code

In fact, the above line of code goes through two steps:

class Teacher {
    private val delegateString: ReadWriteProperty<Teacher, String> = Delegates.notNull()
    private val delegateInt: ReadWriteProperty<Teacher, Int> = Delegates.notNull()
    var name: String by delegateString
    var age: Int by delegateInt
}
Copy the code

Kotlin decompiled Java source code

public final class Teacher {
   // $FF: synthetic field
   // Key point 1
   static finalKProperty[] ? delegatedProperties =new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "name"."getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "age"."getAge()I"))};
   // Key point 2
   @NotNull
   private final ReadWriteProperty name$delegate;
   @NotNull
   private final ReadWriteProperty age$delegate;

   // Key point 3
   @NotNull
   public final String getName(a) {
      return (String)this.name$delegate.getValue(this, ?delegatedProperties[0]);
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "
      
       "
      ?>);
      this.name$delegate.setValue(this, ?delegatedProperties[0], var1);
   }

   public final int getAge(a) {
      return ((Number)this.age$delegate.getValue(this, ?delegatedProperties[1])).intValue();
   }

   public final void setAge(int var1) {
      this.age$delegate.setValue(this, ?delegatedProperties[1], var1);
   }

   public Teacher(a) {
      this.name$delegate = Delegates.INSTANCE.notNull();
      this.age$delegate = Delegates.INSTANCE.notNull(); }}Copy the code

Analysis process:

  • 1. First, the name and age properties of the Teacher class will automatically generate the corresponding setter and getter methods, and the corresponding name$delegate and age$delegate delegate objects will be automatically generated, as shown in key point 2.
  • 2, Then? DelegatedProperties KProperty array will be saved through the Kotlin reflecting the current the Teacher in the class name, the age attribute, reflected corresponding stored in KProperty separately for each attribute in the array.
  • 2. The setValue and getValue methods of the name$delegate and age$delegate object are delegated to the setValue and getValue methods of the name$delegate and age$delegate objects.
  • 3. Finally, pass in the setValue and getValue methods of the delegate object the corresponding reflected property and the corresponding value.

Five, to achieve their own property agent

With the above introduction, it should be very simple to write a custom property proxy. The basic shelf for implementing a simple property broker is the setValue and getValue methods without implementing any interfaces.

SharedPreferences is actually a good scenario in Android because it involves storing and reading properties. Android SharedPreferences can be implemented directly by the ReadWriteProperty interface, or you can write a class and define the setValue and getValue methods.

class PreferenceDelegate<T>(private val context: Context, private val name: String, private val default: T, private val prefName: String = "default") : ReadWriteProperty<Any? , T> {private val prefs: SharedPreferences by lazy {
		context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
	}

	override fun getValue(thisRef: Any? , property:KProperty< * >): T {
        println("setValue from delegate")
        return getPreference(key = name)
	}

	override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
        println("setValue from delegate")
		putPreference(key = name, value = value)
	}

	private fun getPreference(key: String): T {
		return when (default) {
			is String -> prefs.getString(key, default)
			is Long -> prefs.getLong(key, default)
			is Boolean -> prefs.getBoolean(key, default)
			is Float -> prefs.getFloat(key, default)
			is Int -> prefs.getInt(key, default)
			else -> throw IllegalArgumentException("Unknown Type.")}as T
	}

	private fun putPreference(key: String, value: T) = with(prefs.edit()) {
		when (value) {
			is String -> putString(key, value)
			is Long -> putLong(key, value)
			is Boolean -> putBoolean(key, value)
			is Float -> putFloat(key, value)
			is Int -> putInt(key, value)
			else -> throw IllegalArgumentException("Unknown Type.")
		}
	}.apply()

}
Copy the code

Six, the concluding

So that’s the end of the property broker, but if you think Kotlin language sugar design is very clever. While many people are averse to grammar sugar, there is no denying that it has greatly improved the efficiency of our development. Sometimes we need to look beyond the grammatical sugar to see the principle behind it, to understand the whole grammatical sugar design ideas and techniques, and to look at it from a global perspective, it will be so. Finally, thanks to the bennyHuo, I first saw his sharedPreferences example and felt very good, then decided to explore the attributes of the extension, this should have a deeper understanding of Kotlin attributes extension.

Welcome to the Kotlin Developer Alliance, where you can find the latest Kotlin technical articles. We will translate one foreign Kotlin technical article every week. If you like Kotlin, please join us ~~~