https://kotlinlang.org logo
#getting-started
Title
# getting-started
p

Peter

11/26/2023, 9:48 PM
is there a better way to do the following?
Copy code
val state = MutableStateFlow(State(email = ""))

var email: String
    get() = state.value.email
    set(value) { state.update { email = value } }
Is there some
KProperty
trick, that I could use, to delegate
email
to
state
?
Something around:
Copy code
var email by mutableStateDelegate(state::email)
d

Daniel Pitts

11/27/2023, 2:18 AM
I don't know off the top of my head, but you could probably write your own pretty easily.
Okay, it wasn't so easy, but as far as code golf goes, kind of fun:
Copy code
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.instanceParameter
import kotlin.reflect.full.isSupertypeOf
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.full.valueParameters

data class State(
    val email: String,
)

val state = MutableStateFlow(State(email = ""))

var MutableStateFlow<State>.email by property(State::email)


fun main(): Unit = runBlocking {
    launch {
        state.collect {
            println(it)
        }
    }
    launch {
        repeat(10) {
            state.email = "email$it@example.com"
            delay(1000)
        }
    }
}

inline fun <reified T : Any, V> property(property: KProperty1<T, V>): PropertyUpdater<T, V> {
    val kClass = T::class
    val type = kClass.starProjectedType
    val copy = kClass.declaredFunctions.asSequence()
        .filter { it.name == "copy" }
        .filter { it.returnType.classifier?.starProjectedType == type }
        .filter { it.valueParameters.any { param -> param.name == property.name && param.type.isSupertypeOf(property.returnType) } }
        .filter {
            it.valueParameters.all { param -> param.isOptional || param.name == property.name }
        }
        .firstOrNull() ?: error("No suitable copy method found")
    val instanceParameter = copy.instanceParameter!!
    val propertyParameter = copy.valueParameters.single { it.name == property.name }
    val copyDelegate: T.(V) -> T = {
        copy.callBy(
            mapOf(
                instanceParameter to this,
                propertyParameter to it
            )
        ) as T
    }
    return PropertyUpdater(property, copyDelegate)
}

class PropertyUpdater<T : Any, V>(private val get: T.() -> V, private val update: T.(V) -> T) {
    operator fun getValue(thisRef: MutableStateFlow<T>, property: KProperty<*>): V {
        return thisRef.value.get()
    }

    operator fun setValue(thisRef: MutableStateFlow<T>, property: KProperty<*>, value: V) {
        thisRef.update { it.update(value) }
    }
}
Even though this works, it is terrible, and if i saw this in a pull-request, I would not approve of it. The reflective code necessary to get it working is too clever, and likely not maintainable.
p

Peter

11/27/2023, 8:20 PM
Nice, thanks for a deep dive. I guess dealing with
copy()
is the biggest pain.
d

Daniel Pitts

11/28/2023, 2:02 AM
yeah, that was the trickiest part of it. Could have been done without reflection and just a little more boiler-plate.
Copy code
class StatePropertyDelegate(val get: T.()->V, val update: T.(V)->T) {...}

var MutableStateFlow<State>.email by StatePropertyDelegate({email}, {copy(email=it)})
Honestly, despite it being slightly more boiler-plate, I'd probably go with something like this, since its more direct, and ultimately more flexible.