Is there a way it can collapse handler.posts so cl...
# coroutines
u
Is there a way it can collapse handler.posts so close together somehow? cc @Adam Powell
a
no, the semantics of specific posted handler messages are opaque to the handler mechanism itself. You're describing a higher level policy that handlers shouldn't know about.
u
I see. Just to clarify, it doesnt make any sense to update views 1ms or whatever apart (i.e. shorter than vsync), and therefore
stateFlow
should be debounced to vsync value at app level? (or is this debounces somehow at a lower layer)
a
most view properties boil down to internal property sets and a call to invalidate or requestLayout as appropriate, the latter two operations are very cheap if a pending draw or layout is already scheduled.
so some debouncing is happening at a higher layer already
TextView#setText has some cost associated with it but not one you're likely to see a lot of in most cases.
Or to put it another way, have you measured a problem with this approach so far? 🙂
u
I havent measured as its not a perf problem but consistency, but if ui state is actually rendered only on vsync then it should be okay, althought I set it twice without debounce
whole issue basically is if written to room it will emit later than suspend function which writes it, returns
tldr; writing to db is sideffect, and that messes up the whole flow
Copy code
viewModel.scope {
   room.itemFlow
    .collect {
        itemLiveData.set = it
    }
}
viewModel.scope {
   room.updateItem
   updateCompleteLiveData.set = true
}
here updateCompleteLiveData will be true, but the itemLiveData will still contain item where item.activated == false for a brief moment
a
I'm pretty sure you can make room updates use suspend calls so that you know the update is complete before you move on
u
sure, but the room query flow will emit whenever it will emit, regardless if update is suspending or not
you might notice that in the logs of the original question
a
so if I'm reading this right, you have a LiveData reflecting the activated state, but it's set by a process that is independent of the actual database change being reflected?
u
not exactly, its just an example, imagine the items live data contains items (room query stream), and activation live data contain# events as such to show toast "activation successful" after
Copy code
suspend fun activate(itemId) {
   api.activate(itemId)
   db.setActivated(itemId)
}
...
viewModelScope.launch {
   activate(itemId)
   activationLiveData.set = ActivationState.Success
}
...
viewModelScope.launch {
   db.itemById(...).asFlow().collect {
     itemLiveData.set = it
   }
}
something like this, whereas Fragment reacts to activationLiveData by showing toasts for example, and to itemLiveData refeshing the recycler view or somethimg
issue is: there is point in time when itemLiveData contains item which item.activated == false, while activationLiveData already contains success
a
ok, it still seems a bit like there's some sort of a multiple sources of truth issue at play, or a question of what data is being modeled. If the activation state is something stored in the database, the LiveData for the activation state of a single item could just as well be
roomTableState.mapNotNull { it.firstOrNull { it.itemId == itemId }?.isActivated }
or whatnot, right?
er, pretend I tacked a,
.asLiveData()
on the end of that 🙂
u
well exactly, its because its 2 sources of truth. the activation live data contains the state of the whole "routine" and its in memory only
however imagine there will be Loading and error states to it as well viewModelScope.launch { activationLiveData.set = Loading activate() activationLiveData.set = Success }
I could model this as a column in the db as you said, but, then I have the burden of clearing the transient states on app start, as they are now persistent, and im not sure if this is so great for every such use case as it is very common,.. or is it?
a
well, it's more about deciding what observed events/stream items need to have what kind of serialization applied
using the db as the source of truth is one way, another would be to route other reads of db changes through some sort of repository object that muxes together your other in-memory signals
and then zip/combineLatest as appropriate, or what have you
if this idea of, "activated" isn't present in the db itself, then it doesn't much matter if other parts of the system see implementation detail db changes along the way as long as any decisions they might make at any snapshot are valid/consistent
and if those db changes are intended to be implementation details that don't get leaked, you need some sort of filter/serialization construct in front of other things that read it
u
hmm, in this case I was thinking only certain ui cares about the in flight activation state as to show progressbar / toasts, othere dont
a
does the content of the stream from your log matter then?
u
of what stream? I simplfied it here for this convo. Actually I have just 1 livedata/flow and a single State which contains all viewmodel state, and copy construct new instances
a
you mentioned that the order of observing the db updates from room vs. direct LiveData writes made during different db manipulating operations was bothering you
is it a problem, or is it just a quirk of the data flow?
u
well, the code above produces 2 State changes, 1ms apart, of which the first is basically bogus, since it contains activation=success, and the item.activated is still false
after the 1ms or whenever io scheduler desires it will emit new db item with activated=true reflected and then its all good
a
so can the activated liveData be a mapping from the flow of db updates instead, so that there's only ever a single source of truth?
u
and it botheres me since ui will react to the 1st emit and will try for example crossfade recyclerview with progressbar, and then after 1ms the new item will come and I will refresh thr adapter basically during the animation
a
You mentioned there were a few other properties not reflected as db columns but this doesn't seem like one of those
u
no, if the request state is a column, then everything is fine and my question is invalid
a
oh I see, you're reflecting this as pending vs. committed
u
however it feels like a overkill, I never seen people persisting the request state of every table in database, also I will need multiple columns per action, imagine actions as activate, rename, doawhatever.., all would need to be a separate column
a
yeah I wouldn't do that.
u
however it do would solve my dual truth problem
a
it does sound like there's some property of, "only the db reflecting activated = true can change the state from whatever => activated" that would be useful to leverage
u
If I were to combineLatest the 2 streams, would you just debounce it by x, and call it a day?
a
probably. Even if you got this stream perfectly synchronized you'll still have weird cases of one or two frames between these events and still get some flicker in the UI
IO latency can go way up on some devices under load or on low-end devices with slow flash parts
u
true, basically what I need is a way to attribute the db query emit to who caused it
since now it anonymous. If I knew that, then I could combineLatest safely
a
that tends to be quite difficult for a lot of IO systems, not just things backed by sqlite change observers
u
yea -.-
I could just pull the query not push but then whole modern approach is a bust
a
but if it were me, I'd probably do like you're saying and just debounce it for now. imo the problem is further downstream with the way that quick, successive changes to the UI have unwanted side effects on the views being used to implement/animate it, which is something we're looking to address with #compose in particular
there have also been a number of other changes to android animation systems to better handle interruption/new targets of the animation
u
I think ui is just where it shows, the actual issue is not representing invalid state, there still will be transiently invalid state, compose will just make it look good
😂 1
a
could be, in which case my question from above comes back. If you're representing invalid state between different flow/LD emits, then you'll want to bring those into the same stream and only ever emit valid states that way, without letting anything else side-channel observe your db changes
u
yes
but how can I tell which is valid at the subscriber level? its not always clear, and also the debounce-and-pray can be broken based on business of io as you said
a
individual subscribers have to be able to assume anything they see is valid; you'll want to enforce consistency as far upstream as possible. That seems like it's some sort of combine/merge point between your in-flight operations and the db updates themselves
which means you can't permit anything but this code that does this consistency enforcement to hand out a
Flow
of your raw observed db changes
you might take a look at how things like https://github.com/dropbox/Store address cases like this
u
im pretty sure ive seen a case where subscriber couldnt tell, let me think, sec..
I remember, it was pagination in chat, were user could delete a message
so I couldnt just assume if next emit contains more messages = pagination happened
in this case is not invalid "as much" but there still exists a point in time, where old list is shown without a progressbar at the end (since paginationState==Finished, and db items are still old since io didnt emit yet), which doesnt make sense; and also its a pointless adapter notify call/list diffing just to be interuppted intederminate few ms later -- same issue as we were talking about with the activation thing. And here I cannot determine if pagination happened (at least easily naively, since user can send new messages during the chat history pagination is in flight), and can only debounce-and-pray, so is really "best effort" best reactive programming can do here? Since the pagination library of you guys, internally its basically imperative / blocking listeners if I remember correctly