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.