Alexander Maryanovsky
05/27/2022, 6:20 AMclass Data(
val index: Int,
val dataFlow: StateFlow<String>
)
private val LocalData = compositionLocalOf<Data>{ error("Missing local data") }
@Composable
fun DataView(){
val data = LocalData.current
val text by data.dataFlow.collectAsState()
println("Displaying $text for index ${data.index}")
Text(text)
}
fun main(){
singleWindowApplication(
state = WindowState(
width = 800.dp,
height = 800.dp,
)
) {
Column {
var data: Data by remember { mutableStateOf(Data(1, MutableStateFlow("Hello"))) }
CompositionLocalProvider(LocalData provides data){
DataView()
}
Button(
onClick = {
data = Data(2, MutableStateFlow("World"))
}
){
Text("Click me")
}
}
}
}
Displaying Hello for index 1
Displaying Hello for index 2
Displaying World for index 2
when the button is clicked.Data
object in existance that has index 2 and a dataFlow
whose value is “Hello”class Data(
val index: Int,
val dataFlow: StateFlow<String>
)
@Composable
fun DataView(data: Data){
val text by data.dataFlow.collectAsState()
println("Displaying $text for index ${data.index}")
Text(text)
}
fun main(){
singleWindowApplication(
state = WindowState(
width = 800.dp,
height = 800.dp,
)
) {
Column {
var data by remember { mutableStateOf(Data(1, MutableStateFlow("Hello"))) }
DataView(data)
Button(
onClick = {
data = Data(2, MutableStateFlow("World"))
}
){
Text("Click me")
}
}
}
}
produceState
@Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
result
whose (old) value
can be read, and only afterwards LaunchedEffect
will update it.@Composable
fun <T> StateFlow<T>.fixedCollectAsState(
context: CoroutineContext = EmptyCoroutineContext
): State<T> = fixedCollectAsState(value, context)
@Composable
fun <T : R, R> Flow<T>.fixedCollectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = fixedProduceStateForCollect(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
@OptIn(ExperimentalTypeInference::class)
@Composable
fun <T> fixedProduceStateForCollect(
initialValue: T,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend MutableState<T>.() -> Unit
): State<T> {
val result = remember(key1, key2) { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2){
result.producer()
}
return result
}
collectAsState
uses in my app with fixedCollectAsState
and that fixed another problem.
I have a root object with many properties exposed as StateFlow
and many UI elements taking the root object as an argument and each collects and displays a different property. When the root object changes, what would happen is I would see the UI update in parts. For a split second, you could see some of the UI elements displaying the properties of the old object and some displaying the properties of the new object. I thought this was just how Compose worked with Flows.
But after replacing collectAsState
with fixedCollectAsState
, this issue is completely gone. The UI updates all at once to the new object!Albert Chang
05/27/2022, 9:14 AMproduceState()
is designed. A workaround is to use val text by key(data) { data.dataFlow.collectAsState() }
if you can’t change Data
class to use mutable states.Alexander Maryanovsky
05/27/2022, 10:00 AMAlbert Chang
05/27/2022, 10:08 AMAlexander Maryanovsky
05/27/2022, 10:11 AMAlbert Chang
05/27/2022, 10:14 AMAlexander Maryanovsky
05/27/2022, 10:20 AMMutableStateFlow(n).collectAsState().value
can be not equal to n
?Albert Chang
05/27/2022, 10:33 AMcollectAsState()
doesn't look right to me in the first place probably because I'm used to the compose world as it's actually a common cause of error.Alexander Maryanovsky
05/27/2022, 10:38 AM@Composable
fun DataView(n: Int){
val value = MutableStateFlow(n).collectAsState().value
if (value != n)
throw IllegalStateException()
Text(n.toString())
}
fun main(){
singleWindowApplication(
state = WindowState(
width = 800.dp,
height = 800.dp,
)
) {
Column {
var value by remember { mutableStateOf(0) }
DataView(value)
Button(
onClick = {
value = 1
}
){
Text("Click me")
}
}
}
}
IllegalStateException
when you press the button.Albert Chang
05/27/2022, 10:41 AMcollectAsState()
is designed. Doesn't change my argument.Alexander Maryanovsky
05/27/2022, 10:42 AMAlbert Chang
05/27/2022, 10:45 AMAlexander Maryanovsky
05/27/2022, 10:47 AMAlbert Chang
05/27/2022, 11:02 AMAlexander Maryanovsky
05/27/2022, 11:13 AMAlbert Chang
05/27/2022, 11:23 AMfor (index in (0 until people.size)) {
key(people[index]) {
val person = people[index].collectAsState().value
PersonUi(person)
if (person != null){
val car = person.car.collectAsState().value
Text(" ${car.model}")
}
}
}
Alexander Maryanovsky
05/27/2022, 1:22 PM@Composable
fun <T : R, R> Flow<T>.fixedCollectAsState(
initialValue: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R>{
val result = remember { mutableStateOf(initialValue) }
key(this){
result.value = initialValue
}
LaunchedEffect(this, context){
if (context == EmptyCoroutineContext) {
collect { result.value = it }
} else withContext(context) {
collect { result.value = it }
}
}
return result
}
This keeps returning the same State
and avoids the problem of updating the value too late on a switch.
But it causes an extra recomposition for some reason. So my original example would print
Displaying Hello for index 1
Displaying World for index 2
Displaying World for index 2
Albert Chang
05/27/2022, 1:30 PMkey(this) { … }
part in your code doesn’t make any sense. Just use key(flow) { flow.collectAsState() }
.Alexander Maryanovsky
05/27/2022, 1:34 PMAlbert Chang
05/27/2022, 1:36 PMresult.value = initialValue
will run when any of the parameters change, including initialValue
and context
.Alexander Maryanovsky
05/27/2022, 1:39 PMthis
changes?Albert Chang
05/27/2022, 1:41 PMkey
and remember
here. They are different.Alexander Maryanovsky
05/27/2022, 1:45 PMremember
but without the actual remembering@Composable
inline fun <T> key(
@Suppress("UNUSED_PARAMETER")
vararg keys: Any?,
block: @Composable () -> T
) = block()
Why is it called when something that isn’t its key changes? Because it’s inline
?Albert Chang
05/27/2022, 1:49 PMremember
, key
execute the body every time, just like without the key
call. It just dismiss all the internal states when any of the keys passed to key
changes.key(this) {
result.value = initialValue
}
is mostly the same as
result.value = initialValue
, just that when this
changes, the former will run again.Alexander Maryanovsky
05/27/2022, 1:53 PMAlbert Chang
05/27/2022, 1:55 PMkey
(key
is designed to do this).Alexander Maryanovsky
05/27/2022, 2:16 PMAlbert Chang
05/27/2022, 2:51 PMAlexander Maryanovsky
05/27/2022, 3:35 PMLeland Richardson [G]
05/27/2022, 5:08 PMblock
, your code never actually calls it, and when the compiler sees calls to it it transforms it into a different call. I believe the documentation used to say that, but at some point it may have gotten removed. It is more or less transformed into a call to startMovableGroup()
with the appropriate dataKey
passed in.
We could/should update the actual source code to:
1. mention the fact that it is an intrinsic
2. maybe make the source code (which never actually gets used) do something resembling what the intrinsic does. or just throw an error saying “this is an intrinsic and shouldnt ever get run”.natario1
05/27/2022, 7:24 PMAlexander Maryanovsky
06/01/2022, 7:29 PMkey
does and what it’s for.
I have UI that displays something like a spreadsheet, and in each cell there is an AnimatedContent
such that when the value of the cell changes to +1 or -1, there’s an animation of the number going up/down. This is so when you press +/- on a cell, it does a nice animation.
Now, what happened is that by pure chance, I had two spreadsheets, one with 4
in a cell and the other with 5
in the same cell. So when I would switch between the two spreadsheets, the animation would run.
Solved by wrapping the entire rendering of the spreadsheet in key(spreadsheet){}
which clears the gap buffer and removes the state remembered by AnimatedContent
, preventing the animation from running on different spreadsheets.