How to set val property with Kotlin reflection?

3.6k Views Asked by At

I would like to return a copy of a class with a different value assigned to a val property.

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Morné", 25)
    val property = person::class.declaredMemberProperties.first { it.name == "age" }
    person.copyWithValue(property.name, 22)
}

if age was a var then I could do it like follows:

fun main() {
    val person = Person("Morné", 25)
    val property = person::class.declaredMemberProperties.first { it.name == "age" }
    if (property is KMutableProperty<*>)
        property.setter.call(person, 22)
}
2

There are 2 best solutions below

4
On BEST ANSWER

If you really want to return just a copy of the object, you can use copy, e.g.:

fun main() {
  val person = Person("Morné", 25)
  val newPerson = person.copy(age = 22)
}

Otherwise if you really must edit the age it must not be a val in the first place. Using reflection you could still adapt the value, but if the answers up to here already suffice, they are the way to go...

For a more dynamic way you can use the following (I would still target the copy-method, as then you wouldn't accidently update the current object):

val person = Person("Morné", 25)
val updates = mapOf("lastname" to "N/A", "age" to 22)

val copiedPerson = with(person::class.memberFunctions.first { it.name == "copy" }) {
  callBy(mapOf(instanceParameter!! to person)
      .plus(updates.mapNotNull { (property, newValue) ->
        parameters.firstOrNull { it.name == property }
            ?.let { it to newValue }
      })
  )
}
println(copiedPerson)

which prints:

Person(name=Morné, age=22)

You could also use something like .let { person::class.cast(it } after callBy if you want to continue with the actual type. If you only want this to work with the Person type you could also exchange person with Person and cast it directly to as Person.

0
On

As always, reflection should be used with care, as it undermines the assumptions taken by the compiler. However, if you do need to modify a val and you don't have the luxury of a data class (hence, no copy method), you will need to resort to using kotlin-java-reflection:

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Morné", 25)
    val property = person::class.memberProperties.first { it.name == "age" }
    val javaField = property.javaField!!
    javaField.isAccessible = true
    javaField.set(person, 32)
    println(person.age)
}

This prints 32.

However, this will only work on Kotlin/JVM.