Dmitry Akishin,Android Developer at FINCH, Moscow

Kotlin Delegates in Android: Utilizing the power of Delegated Properties in Android Development

Kotlin is a beautiful development language and has some great features that make Android development fun and exciting. Delegate properties are one of them, and in this article we’ll see how delegates make Android development much easier.

basis

First of all, what is a delegate? How does it work? While delegates may seem like magic, they are not as complicated as they might seem.

A delegate is a class that provides values for attributes and handles changes in values. This allows us to move the getter-setter logic for a property from where the property is declared to (or delegate to) another class for logical reuse purposes.

For example, if we have a String attribute called param, the value of this attribute needs to be trimmed with Spaces (trim). We can do this in the setter for the property:

class Example {
    var param: String = ""
        set(value) {
            field = value.trim()
        }
}
Copy the code

If you are not familiar with the syntax, refer to the Properties section of the Kotlin documentation.

What if we want to reuse this logic in other classes? This is where the delegate comes in.

class TrimDelegate : ReadWriteProperty<Any? , String> {

    private var trimmedValue: String = ""

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

    override fun setValue(
        thisRef: Any? , property:KProperty<*>, value: String
    ) {
        trimmedValue = value.trim()
    }
}
Copy the code

A delegate is a class that has two methods that read and set the value of a property. More specifically, an example of the KProperty class represents the delegated property, and thisRef is the object that owns this property. That’s all. We can use the delegate we just created like this:

class Example {
    // Use the by keyword
    var param: String by TrimDelegate()
}
Copy the code

The above code has the same effect as the following:

class Example {

    private val delegate = TrimDelegate()

    var param: String
        get() = delegate.getValue(this, ::param)
        set(value) {
            delegate.setValue(this, ::param, value)
        }
}
Copy the code

::param is an operator that returns a KProperty instance for a property.

As you can see, there’s nothing magical about delegate properties. But while it’s simple, it’s very useful. Let’s look at some examples in Android development.

You can learn more about delegate properties in the official documentation.

To Fragment the refs

We often need to pass some parameters to the Fragment, which usually looks like this:

class DemoFragment : Fragment() {
    private var param1: Int? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState) arguments? .let { args -> param1 = args.getInt(Args.PARAM1) param2 = args.getString(Args.PARAM2) } }companion object {
        private object Args {
            const val PARAM1 = "param1"
            const val PARAM2 = "param2"
        }

        fun newInstance(param1: Int, param2: String): DemoFragment =
            DemoFragment().apply {
                arguments = Bundle().apply {
                    putInt(Args.PARAM1, param1)
                    putString(Args.PARAM2, param2)
                }
            }
    }
}
Copy the code

We pass arguments to the newInstance method used to create the Fragment instance, and within the method we pass arguments to the Fragment arguments so that they can be retrieved from onCreate.

We can make our code look better by moving arguments-related logic into getters and setters for properties.

class DemoFragment : Fragment() {
    private var param1: Int?
        get() = arguments? .getInt(Args.PARAM1)set(value) { value? .let { arguments? .putInt(Args.PARAM1, it) } ? : arguments? .remove(Args.PARAM1) }private var param2: String?
        get() = arguments? .getString(Args.PARAM2)set(value) { arguments? .putString(Args.PARAM2, value) }companion object {
        private object Args {
            const val PARAM1 = "param1"
            const val PARAM2 = "param2"
        }
        fun newInstance(param1: Int, param2: String): DemoFragment =
            DemoFragment().apply {
                this.param1 = param1
                this.param2 = param2
            }
    }
}
Copy the code

But we still have to write repetitive code for each attribute, too many attributes would be too tedious, and too much arguments-related code would seem too messy.

So is there another way to beautify the code further? The answer is yes. As you can guess, we’re going to use the delegate property.

First, we need to make some preparations.

Fragment arguments are stored using Bundle objects. Bundles provide many methods for storing different types of values. So let’s write an extension function that stores a value of a certain type in the Bundle and throws an exception if the type is not supported.

fun <T> Bundle.put(key: String, value: T) {
    when (value) {
        is Boolean -> putBoolean(key, value)
        is String -> putString(key, value)
        is Int -> putInt(key, value)
        is Short -> putShort(key, value)
        is Long -> putLong(key, value)
        is Byte -> putByte(key, value)
        is ByteArray -> putByteArray(key, value)
        is Char -> putChar(key, value)
        is CharArray -> putCharArray(key, value)
        is CharSequence -> putCharSequence(key, value)
        is Float -> putFloat(key, value)
        is Bundle -> putBundle(key, value)
        is Parcelable -> putParcelable(key, value)
        is Serializable -> putSerializable(key, value)
        else -> throw IllegalStateException("Type of property $key is not supported")}}Copy the code

Now we can create the delegate.

class FragmentArgumentDelegate<T : Any> 
    :ReadWriteProperty<Fragment, T> {

    @Suppress("UNCHECKED_CAST")
    override fun getValue(
        thisRef: Fragment,
        property: KProperty< * >): T {
        //key is the attribute name
        val key = property.name
        returnthisRef.arguments ? .get(key) as? T ? :throw IllegalStateException("Property ${property.name} could not be read")}override fun setValue(
        thisRef: Fragment,
        property: KProperty<*>, value: T
    ) {
        valargs = thisRef.arguments ? : Bundle().also(thisRef::setArguments)val key = property.name
        args.put(key, value)
    }
}
Copy the code

The delegate reads the value from the Fragment’s arguments, gets the Fragment’s arguments when the attribute value changes (or creates a new one and sets it to the Fragment if it doesn’t), and stores the new value via the extension function bundle.put you just created.

ReadWriteProperty is a generic interface that accepts two types of parameters. We set the first to be a Fragment, ensuring that this delegate can only be used on the Fragment properties. This allows us to get the Fragment instance and manage its arguments via thisRef.

Since we use the name of the property as the key when arguments is stored, we don’t need to write the key as a constant anymore.

ReadWriteProperty The second type parameter determines which types of values the property can have. We set this type to non-empty and throw an exception if it cannot be read. This allows us to fetch non-empty values in the Fragment, avoiding null checking.

But sometimes we do need some properties that can be null, so let’s create a delegate that does not throw an exception when no value is found in Arguments but returns NULL.

class FragmentNullableArgumentDelegate<T : Any?> :
    ReadWriteProperty<Fragment, T?> {

    @Suppress("UNCHECKED_CAST")
    override fun getValue(
        thisRef: Fragment,
        property: KProperty< * >): T? {
        val key = property.name
        returnthisRef.arguments? .get(key) as? T
    }

    override fun setValue(
        thisRef: Fragment,
        property: KProperty<*>, value: T?). {
        valargs = thisRef.arguments ? : Bundle().also(thisRef::setArguments)val key = property.name
        value?.let { args.put(key, it) } ?: args.remove(key)
    }
}
Copy the code

Next, we create some functions for ease of use (not necessary, just to make the code look nice) :

fun <T : Any> argument(a): ReadWriteProperty<Fragment, T> =
    FragmentArgumentDelegate()

fun <T : Any> argumentNullable(a): ReadWriteProperty<Fragment, T? > = FragmentNullableArgumentDelegate()Copy the code

Finally, let’s use delegates:

class DemoFragment : Fragment() {
    private var param1: Int by argument()
    private var param2: String by argument()
    companion object {
        fun newInstance(param1: Int, param2: String): DemoFragment =
            DemoFragment().apply {
                this.param1 = param1
                this.param2 = param2
            }
    }
}
Copy the code

SharedPreferences commissioned

We often need to store some data so that it can be quickly retrieved the next time the App launches. For example, we might want to store some user preferences so that users can customize the functionality of the application. A common approach is to use SharedPreferences to store key-value pairs.

Suppose we have a class user that reads and stores three parameters:

class Settings(context: Context) {

    private val prefs: SharedPreferences = 
        PreferenceManager.getDefaultSharedPreferences(context)

    fun getParam1(a): String? {
        return prefs.getString(PrefKeys.PARAM1, null)}fun saveParam1(param1: String?). {
        prefs.edit().putString(PrefKeys.PARAM1, param1).apply()
    }

    fun getParam2(a): Int {
        return prefs.getInt(PrefKeys.PARAM2, 0)}fun saveParam2(param2: Int) {
        prefs.edit().putInt(PrefKeys.PARAM2, param2).apply()
    }

    fun getParam3(a): String {
        return prefs.getString(PrefKeys.PARAM3, null) 
            ?: DefaulsValues.PARAM3
    }

    fun saveParam3(param3: String) {
        prefs.edit().putString(PrefKeys.PARAM2, param3).apply()
    }

    companion object {
        private object PrefKeys {
            const val PARAM1 = "param1"
            const val PARAM2 = "param2"
            const val PARAM3 = "special_key_param3"
        }

        private object DefaultValues {
            const val PARAM3 = "defaultParam3"}}}Copy the code

Here we take the default SharedPreferences and provide method users to read and store the values of the parameters. We’ve also made param3 special — it uses special keys and has a non-standard default value.

Once again, we see that we’ve written duplicate code. We can certainly move duplicate logic into methods, but we’ll still be left with clunky code. In addition, what if we want to reuse this logic in other classes? Let’s take a look at how delegates simplify code.

To keep things interesting, let’s try a slightly different approach. This time we will use object expressions and create an extension function for SharedPreferences.

fun SharedPreferences.string(
    defaultValue: String = "",
    key: (KProperty< * >) - >String = KProperty<*>::name
): ReadWriteProperty<Any, String> =
    object : ReadWriteProperty<Any, String> {
        override fun getValue(
            thisRef: Any,
            property: KProperty< * >) = getString(key(property), defaultValue)

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>,
            value: String
        ) = edit().putString(key(property), value).apply()
    }
Copy the code

Here we create an extension function for SharedPreferences that returns an object from the ReadWriteProperty subclass as our delegate.

This delegate reads the String value from SharedPreferences using the value provided by the function key as the key. By default, the key is the name of the property, so we don’t have to maintain and pass any constants. Also, we can provide a custom key if we want to avoid key collisions or access the key. We can also provide a default value for the property in case the value is not found in SharedPreferences.

The delegate can also use the same key to store the new value of the property in SharedPreferences.

In order for our example to work, we also need the String? And Int increments the delegate, which is the same as before:

fun SharedPreferences.stringNullable(
    defaultValue: String? = null,
    key: (KProperty< * >) - >String = KProperty<*>::name
): ReadWriteProperty<Any, String? > =object: ReadWriteProperty<Any, String? > {override fun getValue(
            thisRef: Any,
            property: KProperty< * >) = getString(key(property), defaultValue)

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>,
            value: String?). = edit().putString(key(property), value).apply()
    }

fun SharedPreferences.int(
    defaultValue: Int = 0,
    key: (KProperty< * >) - >String = KProperty<*>::name
): ReadWriteProperty<Any, Int> =
    object : ReadWriteProperty<Any, Int> {
        override fun getValue(
            thisRef: Any,
            property: KProperty< * >) = getInt(key(property), defaultValue)

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>,
            value: Int
        ) = edit().putInt(key(property), value).apply()
    }
Copy the code

Now we can finally simplify the Settings class:

class Settings(context: Context) {

    private val prefs: SharedPreferences =
        PreferenceManager.getDefaultSharedPreferences(context)

    var param1 by prefs.stringNullable()
    var param2 by prefs.int()
    var param3 by prefs.string(
        key = { "KEY_PARAM3" },
        defaultValue = "default")}Copy the code

The code now looks much better, and if you need to add an attribute, a single line of code will suffice.

Commissioned by the View

Suppose we have a custom View with three text fields — a title, a subtitle, and a description — laid out like this:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tvSubtitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tvDescription"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
Copy the code

We want CustomView to provide methods for modifying and retrieving three fields:

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
    var title: String
        get() = tvTitle.text.toString()
        set(value) {
            tvTitle.text = value
        }
    var subtitle: String
        get() = tvSubtitle.text.toString()
        set(value) {
            tvSubtitle.text = value
        }
    var description: String
        get() = tvDescription.text.toString()
        set(value) {
            tvDescription.text = value
        }
    init {
        inflate(context, R.layout.custom_view, this)}}Copy the code

Here we use the View binding of Kotlin Android Extension to get the controls in the layout.

There is obviously some code that could easily be moved to another class and done with a delegate.

Let’s write a TextView extension function that returns a delegate to handle its text content:

fun TextView.text(a): ReadWriteProperty<Any, String> =
    object : ReadWriteProperty<Any, String> {
        override fun getValue(
            thisRef: Any,
            property: KProperty< * >): String = text.toString()

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>, value: String
        ) {
            text = value
        }
    }
Copy the code

Then use it in CustomView:

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    init {
        inflate(context, R.layout.custom_view, this)}var title by tvTitle.text()
    var subtitle by tvSubtitle.text()
    var description by tvDescription.text()
}
Copy the code

Be sure to initialize the properties after the init method renders the layout, because the control cannot be NULL.

This may not be a huge improvement over source code, but the point is to demonstrate the power of delegation. Other than that, it’s fun to write.

Of course, it’s not limited to TextView. For example, here’s a delegate for control visibility (keepBounds determines whether the control takes up space when it’s not visible) :

fun View.isVisible(keepBounds: Boolean): ReadWriteProperty<Any, Boolean> =
    object : ReadWriteProperty<Any, Boolean> {
        override fun getValue(
            thisRef: Any,
            property: KProperty< * >): Boolean = visibility == View.VISIBLE

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>,
            value: Boolean
        ) {
            visibility = when {
                value -> View.VISIBLE
                keepBounds -> View.INVISIBLE
                else -> View.GONE
            }
        }
    }
Copy the code

Here is a delegate to the ProgressBar progress, which returns floating point numbers from 0 to 1.

fun ProgressBar.progress(a): ReadWriteProperty<Any, Float> =
    object : ReadWriteProperty<Any, Float> {
        override fun getValue(
            thisRef: Any,
            property: KProperty< * >): Float = if (max == 0) 0f else progress / max.toFloat()

        override fun setValue(
            thisRef: Any,
            property: KProperty<*>, value: Float
        ) {
            progress = (value * max).toInt()
        }
    }
Copy the code

Here’s how to use a ProgressBar if there is one in CustomView:

class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    init {
        inflate(context, R.layout.custom_view, this)}var title by tvTitle.text()
    var subtitle by tvSubtitle.text()
    var description by tvDescription.text()

    var progress by progressBar.progress()
    var isProgressVisible by progressBar.isVisible(keepBounds = false)
Copy the code

As you can see, you can delegate to anything. There are no limits.

conclusion

Let’s look at some examples of using the Kotlin delegate attribute in Android development. Of course, you can use it in other ways, too. The goal of this article is to show how powerful the delegate attribute is and what we can do with it.

Hopefully by now you’ve got the idea that you want to use delegates.