https://kotlinlang.org logo
#compose
Title
# compose
a

Alexander Maryanovsky

10/24/2023, 6:19 PM
So I have an "computational engine" which, based on a bunch of inputs, computes some outputs which it exposes as `StateFlow`s, so that they can be easily consumed by the Compose UI. Now, the UI sometimes needs to transform/combine the outputs before displaying the result. <continues in thread>
To run transformations on `StateFlow`s, I wrote a utility which bundles a flow and the current value, and then transforming means running a function on the current value and also on the flow. Roughly like this:
Copy code
class Property<out T>(
    val flow: Flow<T>
    val currentValue: () -> T
) {

    fun stateFlow(coroutineScope: CoroutineScope): StateFlow<T> = flow.stateIn(
            scope = coroutineScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = currentValue()
        )
}


fun <T, R> Property<T>.map(transform: (T) -> R): Property<R>{
    return Property(
        flow = flow.map(transform),
        currentValue = { transform(currentValue()) }
    )
}
and this mostly works
My problem is that collecting the state flow causes the transform to run twice. Once immediately to compute
initialValue
and again when the source flow emits a value.
Which I don't like, because some transformations can be a bit long running. Not so long that I'd want to put them on a background thread, but long enough that I'd prefer to not run them twice.
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 7:26 PM
flow.dropFirst().map(transform)
?
also, did you mean to post this in #coroutines?
j

jw

10/24/2023, 7:27 PM
that still runs the transform, it just ignores the result
i was waiting for a question or something
a

Alexander Maryanovsky

10/24/2023, 7:31 PM
Sorry if it doesn't belong here. I guess it is a question about coroutines, but I'm just running into this issue all the time in the context of Compose, because
StateFlow
is how non-compose state is converted to compose state.
The question is how to do this without running the transform twice. I'll try dropFirst.
That broke something; trying to understand what. Jake, why do you say it still runs the transform? The drop is before the map...
I think this breaks because if I have two transformations in a row, I'll actually drop a value I need.
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 7:50 PM
what do you mean by “two transformations in a row”? Like if the upstream emits twice in a row, “quickly”?
a

Alexander Maryanovsky

10/24/2023, 7:50 PM
No, just if I have property.map {...}.map{...}
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 8:05 PM
And you’re calling
transform(transform(currentValue()))
too?
a

Alexander Maryanovsky

10/24/2023, 8:06 PM
Yes, the initial value is correct
Let me write a short reproducer
Copy code
class Prop<out T> (
    val flow: Flow<T>,
    val initialValue: () -> T
) {

    constructor(stateFlow: StateFlow<T>):
            this(stateFlow, stateFlow::value)

    fun stateIn(scope: CoroutineScope): StateFlow<T> {
        return flow.stateIn(
            scope = scope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = initialValue()
        )
    }

}

fun <T, R> Prop<T>.map(transform: (T) -> R): Prop<R> {
    return Prop(
        flow = this.flow.drop(1).map(transform),
        initialValue = {
            transform(this.initialValue())
        }
    )
}

fun transform1(value: Int): Int {
    println("Transform1 $value")
    return value*2
}

fun transform2(value: Int): Int {
    println("Transform2 $value")
    return value+1
}

val stateFlow = MutableStateFlow(1)
val prop = Prop(stateFlow).map(::transform1).map(::transform2)

fun main() = singleWindowApplication {
    val value by prop.stateIn(rememberCoroutineScope()).collectAsState()

    Text("$value")

    LaunchedEffect(Unit) {
        delay(2000)
        stateFlow.value = 2
    }
}
Not very short, but it confirms it
This prints
Copy code
Transform1 1
Transform2 2
Transform1 2
transform2 doesn't run because it drops the first emission of the output of transform1
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 8:19 PM
Oh yea, because your
.map
extension is going to drop one every time. For this to work, you can only drop the actual first element after creating the state flow.
maybe this would work:
Copy code
constructor(stateFlow: StateFlow<T>):
            this(stateFlow.drop(1), stateFlow::value)
if that is always going to be the entry point
then drop the drops from your
map
extension
a

Alexander Maryanovsky

10/24/2023, 8:21 PM
Hmm, then I'm getting
Copy code
Transform1 1
Transform2 2
Transform1 2
Transform2 4
Transform1 2
Transform2 4
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 8:22 PM
oh sorry you’d need a second, internal-only constructor then that doesn’t call map that you can call from
map
my point is you need to make sure
drop
is only being called once per chain
a

Alexander Maryanovsky

10/24/2023, 8:28 PM
It seems that it is. I'm not sure why the 2nd round is called. With
Copy code
val prop = Prop(stateFlow).map(::transform1)
I'm getting
Copy code
Transform1 1
Transform1 2
Transform1 2
Ah, it's because of the recomposition, I'm calling
stateIn
again.
If I remember it, it's only called once
Still not quite working in the actual program, but I’ll figure it out from here. Thanks! Kind of embarrassing I didn’t think of something this simple myself.
z

Zach Klippenstein (he/him) [MOD]

10/24/2023, 8:56 PM
remembering your flow stuff is key, for sure
a

Alexander Maryanovsky

10/25/2023, 7:04 AM
Hmm, this breaks if I also use combine:
Copy code
fun <T1, T2, R> combine(
    prop1: Prop<T1>,
    prop2: Prop<T2>,
    transform: (T1, T2) -> R
): Prop<R> {
    return Prop(
        flow = combine(prop1.flow, prop2.flow, transform),
        initialValue = { transform(prop1.initialValue(), prop2.initialValue()) }
    )
}
Not sure why but this breaks updates to the output when an input changes.
Copy code
val stateFlow1 = MutableStateFlow(1)
val stateFlow2 = MutableStateFlow(2)
val prop = combine(Prop(stateFlow1), Prop(stateFlow2)) { v1, v2 ->
    println("Computing sum")
    v1 + v2
}
doesn't get called again when I do
stateFlow1.value = 2