Is it intentional behaviour that if you have ```ab...
# compose
m
Is it intentional behaviour that if you have
Copy code
abstract class Base<T>(val flow: StateFlow<T>, val transform: (T) -> T) {
    @Composable
    fun Content() {
        val flowValue by flow.collectAsState()
        Text(transform(flowValue).toString())
    }
}

object Derived1 : Base<String>(MutableStateFlow("asdf"), { it.uppercase() })
object Derived2 : Base<Int>(MutableStateFlow(1), { it * 2 })

fun main() = singleWindowApplication { 
    var currentImplementation by remember { mutableStateOf<Base<*>>(Derived1) }

    Switch(
        checked = currentImplementation is Derived1,
        onCheckedChange = { currentImplementation = if (it) Derived1 else Derived2 }
    )
    
    currentImplementation.Content()
}
flipping the switch causes the app to crash with a
ClassCastException
because
flowValue
persists the old value of the flow until recomposition happens? If yes, is there a good way to avoid this issue? Providing an initial value to
collectAsState()
does not seem to do anything.
z
Wrap your
Content
method's body in
key(this) { … }
. Often when you have a composable method on a class, the class itself is an important key. You could also pass
flow
to
key
, but
this
is a catch-all.
m
Alright, that seems to work, thanks! However, how does this fix the problem exactly? I don't quite understand what the
key
function actually does, even after reading the documentation. What is also not quite clear to me is why the fix only works if I put the key function as the outermost call inside the
Content
function. If I do something like
Content
->
Box
->
key
-> logic instead of
Content
->
key
->
Box
-> logic, it doesn't work.
z
If you flatten the abstractions in
collectAsState
, it's this:
Copy code
fun <T> collectAsState(flow: StateFlow<T>): State<T> {
    val result = remember { mutableStateOf(flow.value) }
    LaunchedEffect(flow) {
        flow.collect {
            result.value = it
        } 
    }
    return result
}
Note that the
MutableState
instance is remembered without a key., but the
LaunchedEffect
is keyed on the
Flow
. This means, when the instance of
Flow
being passed in changes over the lifetime of the composable, that: • The state object will not be recreated. On the first recomposition with the new
Flow
instance, the state object will still have the value from the previous instance. • The effect will be restarted after the first recomposition with the new
Flow
instance. However, effects don't start until after composition is finished, so while the new flow is collected as soon as possible, composables won't see the new
result.value
until the next recomposition on the next frame. When you switch from
Derived1
to
Derived2
, as far as Compose is concerned, you're calling the same
Content
function just with a different
this
(and thus different
flow
and
transform
properties). Compose's "positional memoization" means that as long as you're calling the function in the same place, it holds on to any `remember`ed state in that function. In this case, that means the
remember
call inside
collectAsFlow
will keep returning the same
MutableState
instance even when you change the instance of
Base
. So in the recomposition where you do so, you're already calling the new
transform
function but the
flowValue
hasn't got updated from the new
flow
property yet.
key
fixes this is because it forces Compose to recreate all the state inside its content function when the key changes, even if it would not otherwise do so. So
key(this)
means that when the instance of
Base
changes, all the state in
Content
, including that
MutableState
remembered by
collectAsState
, is created from scratch in that recomposition. When the new
MutableState
is created it is also initialized with the initial value from the new
flow
, and thus gives your
transform
function what it expects.
❤️ 2
plus1 1
📝 1
👍 1