Search code examples
propertiesdelegateskotlindelegation

Kotlin - How to make a property delegate by map with a custom name?


I'm trying to get my head around property delegates, and I have an interesting use case. Is it possible to have something like this:

class MyClass {
    val properties = mutableMapOf<String, Any>()
    val fontSize: Any by MapDelegate(properties, "font-size")
}

That would allow me to store fontSize using the map as a delegate, but with a custom key (i.e. "font-size").

The specific use case if for storing things like CSS property tags that can be accessed through variables (fontSize) for use in code, but can be rendered properly when iterating through the map (font-size: 18px;).


Solution

  • The documentation on the delegated properties is a good source of information on the topic. It probably is a bit longer read than the following examples:

    fun <T, TValue> T.map(properties: MutableMap<String, TValue>, key: String): ReadOnlyProperty<T, TValue> {
        return object : ReadOnlyProperty<T, TValue> {
            override fun getValue(thisRef: T, property: KProperty<*>) = properties[key]!!
        }
    }
    
    class MyClass {
        val properties = mutableMapOf<String, Any>()
        val fontSize: Any by map(properties, "font-size")
    }
    

    You can ease up things a little bit and avoid typing the CSS property name by converting Kotlin property names to CSS attributes equivalents like so:

    fun <T, TValue> map(properties: Map<String, TValue>, naming:(String)->String): ReadOnlyProperty<T, TValue?> {
        return object : ReadOnlyProperty<T, TValue?> {
            override fun getValue(thisRef: T, property: KProperty<*>) = properties[naming(property.name)]
        }
    }
    
    object CamelToHyphen : (String)->String {
        override fun invoke(camelCase: String): String {
            return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, camelCase)
        }
    }
    
    fun <T, TValue> T.cssProperties(properties: Map<String,TValue>) = map(properties, CamelToHyphen)
    
    class MyClass {
        val properties = mutableMapOf<String, Any>()
        val fontSize: Any? by cssProperties(properties)
    }
    

    The above sample uses Guava's CaseFormat.

    If you'd like to have mutable property your delegate will have to implement setter method:

    fun <T, TValue> map(properties: MutableMap<String, TValue?>, naming: (String) -> String): ReadWriteProperty<T, TValue?> {
        return object : ReadWriteProperty<T, TValue?> {
            override fun setValue(thisRef: T, property: KProperty<*>, value: TValue?) {
                properties[naming(property.name)] = value
            }
    
            override fun getValue(thisRef: T, property: KProperty<*>) = properties[naming(property.name)]
        }
    }