Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin data class: how to read the value of property if I don't know its name at compile time?

How can I read the value of property in a Kotlin data class instance if the property name is only known at runtime?

like image 638
Elifarley Avatar asked Feb 20 '16 15:02

Elifarley


People also ask

What does :: mean in Kotlin?

:: is used for Reflection in kotlin. Class Reference val myClass = MyClass::class.

What is KClass in Kotlin?

The KClass type is Kotlin's counterpart to Java's java. lang. Class type. It's used to hold references to Kotlin classes; you'll see what it lets you do with those classes in the “Reflection” section later in this chapter. The type parameter of KClass specifies which Kotlin classes can be referred to by this reference.

What is the difference between field and property in Kotlin?

Properties vs fieldsField is just a class member variable that hold a value. It can be read-only or mutable and marked with any access modifier such as public or private . Property is more complex element that contain a private field and accessors. By accessors I mean getter and setter.


4 Answers

Here is a function to read a property from an instance of a class given the property name (throws exception if property not found, but you can change that behaviour):

import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties

@Suppress("UNCHECKED_CAST")
fun <R> readInstanceProperty(instance: Any, propertyName: String): R {
    val property = instance::class.members
                     // don't cast here to <Any, R>, it would succeed silently 
                     .first { it.name == propertyName } as KProperty1<Any, *> 
    // force a invalid cast exception if incorrect type here
    return property.get(instance) as R  
}

build.gradle.kts

dependencies {
    implementation(kotlin("reflect"))
}

Using

// some data class
data class MyData(val name: String, val age: Int)
val sample = MyData("Fred", 33)

// and reading property "name" from an instance...
val name: String = readInstanceProperty(sample, "name")

// and reading property "age" placing the type on the function call...
val age = readInstanceProperty<Int>(sample, "age")

println(name) // Fred
println(age)  // 33
like image 62
9 revs, 2 users 87% Avatar answered Oct 22 '22 14:10

9 revs, 2 users 87%


You can do it through reflection, and it's all the same for data classes and normal ones.

The first option is just to use Java reflection:

val name = obj.javaClass
              .getMethod("getName") // to get property called `name`
              .invoke(obj)

You can even make an extension function:

inline fun <reified T : Any> Any.getThroughReflection(propertyName: String): T? {
    val getterName = "get" + propertyName.capitalize()
    return try {
        javaClass.getMethod(getterName).invoke(this) as? T
    } catch (e: NoSuchMethodException) {
        null
    }
}

It calls the public getter. To get a value of a private property you can modify this code using getDeclaredMethod and setAccessible. This will also work for Java objects with the corresponding getters (but it misses the convention of is and has getters for boolean).

And usage:

data class Person(val name: String, val employed: Boolean)

val p = Person("Jane", true)
val name = p.getThroughReflection<String>("name")
val employed = p.getThroughReflection<Boolean>("employed")

println("$name - $employed") // Jane - true


The second option involves using kotlin-reflect library which you should add to your project separately, here's its documentation. It will let you get actual Kotlin property value, ignoring Java getters.

You can use javaClass.kotlin to get actual Kotlin class token, and then you get a property from it:

val name = p.javaClass.kotlin.memberProperties.first { it.name == "name" }.get(p)

This solution will work only for Kotlin classes, not for Java ones, but it is much more reliable if you need to work with Kotlin classes: it doesn't depend on the underlying implementation.

like image 30
hotkey Avatar answered Oct 22 '22 13:10

hotkey


The answers above didn't work for me so I've created an extension function for this:

@Throws(IllegalAccessException::class, ClassCastException::class)
inline fun <reified T> Any.getField(fieldName: String): T? {
    this::class.memberProperties.forEach { kCallable ->
        if (fieldName == kCallable.name) {
            return kCallable.getter.call(this) as T?
        }
    }
    return null
}

This is an example call:

val valueNeeded: String? = yourObject.getField<String>("exampleFieldName")

Also include this in your app's build.gradle:

implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
like image 15
Adam Kis Avatar answered Oct 22 '22 13:10

Adam Kis


I wonder if it is possible to define the type of the field programmatically. You can get the type easily with:

kCallable.returnType

However you still have to assign the generic type explicitely:

getField<String>

instead of

getField<kCallable.returnType>

Edit: I ended up using the following:

when (prop.call(object)) {
    is ObservableList<*> -> {}
    is Property<*> -> {}
}
like image 1
Burtan Avatar answered Oct 22 '22 14:10

Burtan