Status Update
Comments
an...@google.com <an...@google.com> #2
Hello, can you please attach a sample that reproduces your error?
du...@google.com <du...@google.com> #3
I’ve attached two screen recordings demonstrating the mediator's behavior during the first and second launches after installation. Additionally,
mv...@google.com <mv...@google.com> #4
Based on first_launch_after_installation.mp4
, your paging state is indeed showing null
for nextKey, so it makes sense that Paging concluded there are no more items to load.
So I think the question is why the PagingState
contains only an empty page and null
values. My guess is either
- RemoteMediator REFRESH actually returned empty data - what is the returned
allNewsItems
value on initial REFRESH? - RemoteMediator REFRESH successfully loaded data and inserted it into database, but Paging hasn't processed that yet by the time Append is triggered, so PagingState is empty.
The second case doesn't seem likely though.
If you attach an executable app, I can look into this further.
Otherwise, you may gain more insight through Paging logs with adb shell setprop log.tag.Paging VERBOSE
. You can also try implementing RemoteKeys
in this
du...@google.com <du...@google.com> #5
Hey! I ran into this same issue. It is in fact the second case you mentioned and is documented
I've published a repo
mv...@google.com <mv...@google.com> #6
how would you normally deal with other mutable state?
Yeah, for the complex examples you mention, the developer could be flexible and expose multiple streams if that's what they need.
Could you help me understand how exposing PagingData vs Flow<PagingData> in UiState makes it more testable though?
Not sure if it's more testable, but IMO, it makes the test more deterministic, readable and easier to setup for a beginner. You could argue the same with pretty much everything :) (e.g. in your UiState data class you could have a userId: Flow<String>
and pass flowOf("userId")
in tests! But the semantics of that field being a Flow<String>
or simply String
are different. IIUC, PagingData.from
is immutable, right? You cannot change the content of the data once that's called.
The problem is that Paging is inherently stateful as PageEvent is an incremental update
Yeah... this makes sense. But that's implementation details, right? Taking a step back, you said that LazyListScope.items(LazyPagingItems<T>)
is needed instead of something like LazyListScope.items(PagingData<T>)
because people could incorrectly collect
from the paging flow? What if someone wanted to collect those items from the viewModelScope
instead of the composable scope? Not sure if this use case makes a lot of sense, but... should we allow that?
du...@google.com <du...@google.com> #7
Taking a step back, you said that LazyListScope.items(LazyPagingItems<T>) is needed instead of something like LazyListScope.items(PagingData<T>) because people could incorrectly collect from the paging flow?
I actually meant that on invalidation you get a new instance of PagingData<T>
, so you would be surprised that db updates don't get picked up by paging if we exposed that outside the static list constructors.
What if someone wanted to collect those items from the viewModelScope instead of the composable scope? Not sure if this use case makes a lot of sense, but... should we allow that?
This is already possible in the base Paging API as you normally hook up to Flow<PagingData<T>>
directly, but that's a good question for paging-compose since the entry point is LazyPagingItems
and it would need to be called in a @Composable
. I guess part of the problem is even if you launch from viewModeScope there, it would still have originated from composable scope.
That said, I'm not sure it really makes sense to do this because the only thing you gain is to continue listening to scroll position to continue loading, which won't update unless composable scope is active I think. The typical pattern is to call .cachedIn(viewModelScope)
, so when you resume or composable is re-created, you don't need to begin loading from scratch. .cachedIn
will condense the page event stream so you'll immediately get to pick up from where you left off.
du...@google.com <du...@google.com> #8
For more context, someone filed a bug that make be partially solved by providing a different scope for paging collection:
mv...@google.com <mv...@google.com> #9
Hi Dustin! I think this is a different issue. It's not about the scope to use to cache the data, it's about being able to expose the raw PagingData<T>
into a uiState
stream than the View can consume instead of a Flow<PagingData<T>>
du...@google.com <du...@google.com> #10
Ah yeah that's why I didn't dedupe this bug - I was mostly replying to your previous comment:
What if someone wanted to collect those items from the viewModelScope instead of the composable scope? Not sure if this use case makes a lot of sense, but... should we allow that?
In terms of PagingData
vs Flow<PagingData>
Does my explanation for enforcing handling of invalidation make sense though?
If you could fan-fiction an API what would it look like?
mv...@google.com <mv...@google.com> #11
I'm not aware of the implications of not invalidating properly, but I trust this is a tough problem to solve so I leave this up to you :)
Can that invalidation be done in any other way? Maybe inside a PagingData<T>.asFlow()
extension function? At some point, the flow with the invalidation logic needs to be created, can that be a public API that devs could use on demand?
is...@xero.com <is...@xero.com> #12
I have similar issues with the collectAsLazyPagingItems API. I use "Single State" ViewModels, which expose a single, immutable data class that is supposed to represent the entire screen. This works very well with Compose. Exposing a Flow from the ViewModel's state, and adding that element of mutability into the state feels wrong to me. It also makes it difficult to centralise behaviour into the ViewModel, because the ViewModel can't (easily) observe the error or loading states that LazyPagingItems returns, without the Composable function calling back into the ViewModel whenever an error/loading state is reached. I want the UI to be as simple as possible, and to only render exactly the data that it is given, and I want the ViewModel to be the source of truth for this data.
Essentially, I would like some kind of (mostly immutable) view on what is happening with the PagingData, and to update my ViewModel's state every time the paging data is changed (including being able to add the loading/error states of the paging data into the ViewModel's state).
If you could fan-fiction an API what would it look like?
I've played around with this a little bit, by copy-pasting the entire LazyPagingItems class into my codebase and changing the APIs. I have had most success from adding a CoroutineScope to the LazyPagingItems constructor, and doing the Flow collection that "collectAsLazyPagingItems" does from inside the LazyPagingItems constructor using the input CoroutineScope. This allows me to create an instance of LazyPagingItems from inside the ViewModel, and it appears to work reasonably well for what I want to do, but I don't want to maintain my own side-by-side version of LazyPagingItems.
Description
The only way to use Paging in Compose is with the
collectAsLazyPagingItems()
method that is an extension function onFlow<PagingData<T>>
.Problem: This limitation forces the ViewModel to expose that type as a stream. This
Flow<PagingData<T>>
cannot be used as part of the typicalUiState
of a screen since it'd break immutability.Expectation: It'd be nice if the Compose Paging integration allows an alternative for using
PagingData<T>
as an input ofLazyListScope.items
or related methods. In this way, the ViewModel only needs to expose a single stream of data with theuiState
that containtsPagingData<T>
inside.