I have a kotlin object defined as such:
data class UserUpdateRequest(val map: Map<String, Any?>) {
@get:Email
val email: String? by map
val firstName: String? by map
val lastName: String? by map
}
So that works just fine, so the problem I'm having is that the properties are nullable, and when I access one, say by doing instance.email
it throws a NoSuchElementException
if said property is not set in the map.
Instead, it'd be more convenient if it returned null
, since it's optional/nullable. Is there any way to achieve this without writing my own delegate?
You can basically use the .withDefault { ... }
extension that wraps a Map
for delegation, so that it executes the lambda to calculate a value on absent key:
data class UserUpdateRequest(val map: Map<String, Any?>) {
private val defaultMap = map.withDefault { null }
@get:Email
val email: String? by defaultMap
val firstName: String? by defaultMap
val lastName: String? by defaultMap
}
Note that simple defaultMap.get(key)
and defaultMap[key]
queries are not handled by this wrapper, it only affects the defaulMap.getValue(key)
calls (which also happen to be used by the delegation implementation).
Here is a delegate implementation, if you want to have more control over what the delegate does and understand how it works.
data class UserUpdateRequest(val map: Map<String, Any?>) {
var email: String? by valueFromMap(map = map)
var firstName: String? by valueFromMap(map = map)
var lastName: String? by valueFromMap(map = map)
var age: Int? by valueFromMap(map = map)
}
val myInitialMap = mutableMapOf<String,Any?>()
myInitialMap["email"] = "[email protected]"
myInitialMap["age"] = 34
val request = UserUpdateRequest(myInitialMap)
request.lastName = "Genericname"
println("email: ${request.email}, firstNme: ${request.firstName}, lastName: ${request.lastName}, age: ${request.age}")
request.lastName = null
println("lastName after setting to null: ${request.lastName}")
This will print:
email: [email protected], firstName: null, lastName: Genericname, age: 34
lastName after setting to null: null
So the variables are set to the initialMap's values if they exist and the value's type matches the variable type. Else the variable is set to null.
fun <R>valueFromMap(key: (KProperty<*>) -> String = KProperty<*>::name,
map: Map<String, Any?>): ReadWriteProperty<Any, R?>{
val myMap = map.toMutableMap()
return object : ReadWriteProperty<Any, R?> {
override fun getValue(thisRef: Any, property: KProperty<*>): R? {
return (myMap.getOrDefault(key(property), null) as? R)?: return null
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: R?) {
if(value == null){
myMap.remove(key(property))
} else {
myMap[key(property)] = value
}
}
}
}
So valueFromMap(map = map) is a function that returns a ReadWriteProperty for a val/var(KProperty) that uses the KPproperty's name as key to look for a value in the map, passed as argument.
So when we call:
request.lastName = "Genericname"
In the above example, actually we call .
initialMap["lastName"] = "Genericname"
If the variable name and the corresponding key differ you could even pass your key as argument.
With this approach you're able to handle the value access on your own in the getValue and setValue functions.
Hope this helps anybody.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With