Marc
01/31/2025, 11:30 PMabstract 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.Zach Klippenstein (he/him) [MOD]
02/01/2025, 2:37 AMContent
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.Marc
02/01/2025, 3:19 AMkey
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.Zach Klippenstein (he/him) [MOD]
02/03/2025, 7:07 PMcollectAsState
, it's this:
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.