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

Kyant

04/02/2024, 2:02 PM
snapshotFlow
can't detect the state changes within a frame, for example:
Copy code
SideEffect {
    // pos = 0 now
    pos = player.currentPosition
    doSomething()
    pos = 0
}
I am tracking the position change of my player in Compose. If seek to next song,
pos
(notice it may be 0 currently) will be set to
player.currentPosition
(refresh the position), then it will be set as 0 immediately (for detecting position change and update notification internally). The problem is: if
pos
is 0 before seek to next song (= user haven't seek to another position), after the seeking the
pos
is still 0, although it has an immediate value
player.currentPosition
, and the
snapshotFlow
can't detect the changes of
pos
.
j

jw

04/02/2024, 2:08 PM
I think if you wrap each mutation in
Snapshot.withMutableSnapshot
it should be observed
k

Kyant

04/02/2024, 2:11 PM
But the
set
is done in another project that doesn't use Compose. I am using interface and interface delegation to inject states to it.
j

jw

04/02/2024, 2:14 PM
seems like you can put it in the implementation which writes to the state object then
k

Kyant

04/02/2024, 2:16 PM
Can you explain more?
Copy code
object AudioPlayerState : PlayerState {
    override var currentPosition: Long by mutableLongConfigStateOf(0L)
}
Copy code
class MediaLibraryService : BaseMediaLibraryService(
    playerState = AudioPlayerState,
)
j

jw

04/02/2024, 2:17 PM
Copy code
private val realState = mutableLongConfigStateOf(0L)
override var currentPosition: Long
  get() = realState.value
  set(value) { Snapshot.withMutableSnapshot { realState.value = value } }
k

Kyant

04/02/2024, 2:20 PM
I tried it, but it only randomly work
I think I can add a
positionIncrement
, but it's not elegant
s

Stylianos Gakis

04/02/2024, 2:39 PM
Isn’t the real reason behind this that even if one makes sure that they use
Snapshot.withMutableSnapshot
that in-between doing those two, if there is not a frame clock tick, there will not be a new composition, therefore the snapshotFlow will also not get that new emission?
It works with a
registerApplyObserver
internally to read for state changes, but does that get called before there is a frame clock tick too? The docs for it say “Register an apply listener that is called back when snapshots are applied to the global state.“. I don’t think I know enough to confidently answer this though.
If
registerApplyObserver
is supposed to read those regardless of a tick then ignore what I am saying.
I stand corrected
j

jw

04/02/2024, 2:40 PM
snapshots (and state, to some degree) exist "above" compose so they're not tied to the frame clock in any way
thank you color 1
or "below", depending on which direction you like to look
😂 1
s

Stylianos Gakis

04/02/2024, 2:44 PM
Okay, so this is what was happening without the
withMutableSnapshot
, but adding it should make the
snapshotFlow
really get all the emissions, since they will be individually committed to the global snapshot.
👌 1
k

Kyant

04/02/2024, 2:44 PM
After changing from picture 1 to picture 2 and 3 is working
j

jw

04/02/2024, 2:45 PM
Your collectLatest will drop values, no? That should be the source of missing emissions
k

Kyant

04/02/2024, 2:46 PM
Oh, I am using a coroutine in it, it will automatically be stopped and renew one when using collectLatest
a

Alex Vanyo

04/02/2024, 3:51 PM
I don’t believe there’s a guarantee that
snapshotFlow
will emit every incremental state update - it can internally have a
collectLatest
-type behavior for state updates
A simple repro:
Copy code
@Preview
@Composable
fun Repro() {
    var state by remember { mutableStateOf(0) }
    val receivedStates = remember { mutableStateListOf<Int>() }

    LaunchedEffect(Unit) {
        snapshotFlow { state }
            .onEach {
                receivedStates.add(it)
            }
            .collect()
    }

    Column {
        Text("receivedStates: ${receivedStates.toList()}")
        Button(
            onClick = {
                Snapshot.withMutableSnapshot {
                    state++
                }
                Snapshot.withMutableSnapshot {
                    state++
                }
            }
        ) {
            Text("+2")
        }
    }
}
This ends up only addding even numbers to the
receivedStates
list
z

Zach Klippenstein (he/him) [MOD]

04/02/2024, 7:48 PM
Not only does it not guarantee it, the fact that it coalesces updates is an intentional feature of not just snapshotFlow but the entire snapshot state system.
j

jw

04/02/2024, 7:50 PM
Is that a side effect of the apply call effectively having to wait until it's scheduled on the dispatcher?
Like, would an unconfined dispatcher doing apply mean you'd see everything?
z

Zach Klippenstein (he/him) [MOD]

04/02/2024, 7:54 PM
In that case a state changed multiple times in the same snapshot, or by nested snapshots, would still only notify for the final state before the snapshot is applied. Changes from other threads could get in there too.
snapshotFlow specifically tries to coalesce as many changes as possible by draining its change notification channel before rerunning the block, which increases the opportunity for changes from other threads to get in there.
👍 1
Looking at the initial code snippet though, that looks real smelly. Why are you passing the position to doSomething() via a global variable? It looks like you’re trying to introduce a second source of truth
y

yschimke

04/02/2024, 8:14 PM
If you are dealing with something like media3 Player, it's more complicated than that. Your operations may not be immediately applied, you should wait for it to update. There are also ways to avoid polling position on every frame, but no universal blessed way AFAIK.
Cc @jolo
o

Oliver.O

04/03/2024, 7:00 PM
Regarding the conflating behavior of
snapshotFlow
and the
Snapshot
system in general, I've always understood it to behave that way. What was less clear, and really became clearer after re-reading the latest version of the guide's section on Mutating the UI state from background threads, is that
MutableState
was never really thread-safe, which I had assumed it to be because of the snapshot system. I could verify that this can make Compose sporadically miss an update, which I could only observe with intense parallelism. I'd find it very valuable to explicitly hint at using
Snapshot.withMutableSnapshot
whenever writers can be on a non-UI thread in the docs of
mutableStateOf
,
MutableState
, and the like.
s

Stylianos Gakis

04/03/2024, 7:28 PM
Isn’t the issue in the linked thread docs(Sorry, I meant about the link) not that an update would be missed, but that of the three states updated, maybe 2 of them would be updated and shown on one frame, and the other one might be on the same frame. But to make sure that all three change in one go together you need to wrap it in
Snapshot.withMutableSnapshot
?
o

Oliver.O

04/03/2024, 8:35 PM
As I'd summarize it, this thread discusses two aspects: 1. Multiple changes getting conflated (frames are not really the issue here). As Zach mentioned above, even two changes, each within its own
Snapshot.withMutableSnapshot
block, will get conflated. 2. Jack and Alex dropped
Snapshot.withMutableSnapshot
, and Zach referred to state changes from other threads. Reading about aspect 2 made me check my scenario, leading to the observation I posted. If everything is confined to the main dispatcher as the above examples suggest,
withMutableSnapshot
is not needed (and wouldn't help to get more updates). But as soon as other threads may change mutable state, is is essential to use
withMutableSnapshot
in oder to avoid subtle bugs. This could be documented more prominently. So not the original issue, but related to the follow-up discussion.
Regarding the docs: The example shows multiple updates made atomic, which is a different issue. At the end of the section, however, there is this:
Warning: Updating Compose state from a non UI thread without using
Snapshot.withMutableSnapshot{ }
may cause inconsistencies in the state produced.
This is generic wording, which I understand to include a single
MutableState
update from a non-UI thread. I was observing Compose very sporadically missing a single update to a
MutableState<String>
in a single-writer, non-UI thread scenario under extreme load. The problem went away when surrounding the update with
Snapshot.withMutableSnapshot
. Is that the expected behavior?
z

Zach Klippenstein (he/him) [MOD]

04/04/2024, 3:27 PM
@Chuck Jazdzewski [G]
57 Views