1

I have an object:

class User {
  var id: String? = null
  var name: String? = null
}

and list of pairs:

val fieldsToChange = listOf<Pair<String, String>>(Pair("name", "foo"), Pair("id", "bar"))

I would like to iterate trough list of pairs and set appropriate values for given properties using reflection.

pixel
  • 24,905
  • 36
  • 149
  • 251
  • Are you sure you need to use reflection for that? You could use a [map as a delegate](https://kotlinlang.org/docs/reference/delegated-properties.html#storing-properties-in-a-map) instead. – jsamol Jul 18 '19 at 10:32
  • @JuliaSamól in my case I have input file that contains object field and required value. For me using reflection seemed rational in this particular use-case. I wonder if map using map as delegate would be possible in such case... – pixel Jul 18 '19 at 15:09
  • In fact this "map delegate" uses reflection under the hood, but I think it's more readable than any custom solution. Anyway, unless for some reason you have to change already existing `User` object, yes, you can use a map in such a way. – jsamol Jul 18 '19 at 15:32

2 Answers2

3

Given class instance obj we can extract properties with names using obj::class.memberProperties.

We can construct a mapping from property name to property:

val nameToProperty = obj::class.memberProperties.associateBy(KProperty<*>::name)

Then we can iterate over fieldsToChange and retrieve property and set it:

fieldsToChange.forEach { (propertyName, propertyValue) ->
   nameToProperty[propertyName]
       .takeIf { it is KMutableProperty<*> } // take only "settable" (var) properties
       ?.let { it as KMutableProperty<*> } // cast it to mutable property so we can access setter
       ?.let { it.setter.call(obj, propertyValue) } // call the setter
}

Additionally, we can make this generic:

fun setFields(obj: Any, fieldsToChange: List<Pair<String, Any?>>) {
    val nameToProperty = obj::class.memberProperties.associateBy(KProperty<*>::name)
    fieldsToChange.forEach { (propertyName, propertyValue) ->
        nameToProperty[propertyName]
                .takeIf { it is KMutableProperty<*> }
                ?.let { it as KMutableProperty<*> }
                ?.let { it.setter.call(obj, propertyValue) }
    }
}

val user = User()
setFields(user, fieldsToChange)

assert(user.name == "foo")
assert(user.id == "bar")

Possible improvement would be to optimize nameToProperty to contain only MutableProperties already casted to KMutableProperty

KarolisL
  • 349
  • 1
  • 10
  • IMO, the difficulty of doing this just highlights that Kotlin is statically-typed language.  Bypassing all that, while possible, is rarely a good approach: it's likely to perform worse, spot fewer errors at compile time, and be harder to maintain, as well as needing more code than traditional OO (as this clever answer demonstrates).  Sometimes it's necessary for e.g. parsing JSON, but there are libraries for that. – gidds Jul 18 '19 at 14:54
2

You can use a map as a delegate for that:

class User(map: Map<String, String>) {
    val id: String by map
    val name: String by map
}

val fieldsToChange = listOf(Pair("name", "foo"), Pair("id", "bar"))
val map = fieldsToChange.map { it.first to it.second }.toMap()

val user = User(map)
jsamol
  • 3,042
  • 2
  • 16
  • 27