If you’ve ever tried to combine a Flow<PagingData<T>> with another Flow, you’ve probably discovered that PagingData doesn’t behave like a regular list of items.
That’s by design. PagingData is meant to be consumed by Paging’s presenters, like PagingDataAdapter or LazyPagingItems. Those presenters know how to listen for page events, trigger more loads as the user scrolls, and update the UI as new items arrive.
But sometimes your screen needs the currently loaded items as state. Maybe you want to combine them with selected IDs, optimistic likes, or locally edited titles. This has been a common source of confusion for developers trying to combine PagingData with another Flow, expose paging from an MVI-style screen (one where the UI renders a single state object), or update loaded items without forcing a full refresh.
Paging 3.5.0 adds a new API for this: asItemSnapshotListFlow.
The new shape: loaded items as state
At its simplest, the new API looks like this:
1 2 | |
Each emission gives you the currently loaded items in snapshot.items, along with counts of any placeholders before and after them. You can read and transform that list anywhere you collect the flow.
One heads-up before we go further: reading from a snapshot doesn’t tell Paging to load more, and append() and prepend() are the only way to do it with this API. If you render these items yourself, you’ll need to call them. We’ll get to that in Loading more data.
Combining loaded items with other state
Suppose you have a feed of posts. The posts come from Paging, but the screen also tracks a few other things: which posts the user has liked, and which authors they’ve muted.
Here’s the obvious first attempt:
1 2 3 4 5 6 | |
It compiles, and then it crashes:
1 2 3 | |
When likeOverrides changes, combine hands the same PagingData to your UI again, and Paging won’t let it be collected twice. Adding cachedIn before the combine fixes it, and for simple per-item transforms like this one, that’s a perfectly good answer.
It gets awkward when you need to treat the loaded items as data. Looking one up by ID or filtering by state that lives in another Flow both mean working inside PagingData’s transform operators. asItemSnapshotListFlow gets you out of that:
1 2 3 4 5 6 7 8 9 10 11 12 | |
That’s an ordinary Flow of lists. Paging still loads incrementally, but the code doing the combining only sees what’s loaded right now.
It’s a good fit for UI-only state: selection, muted rows, expanded or collapsed sections, local filters. None of that belongs in your database or network model. It’s just how this screen reads the loaded items.
Optimistic likes and local display edits
When a user taps a like button, you want the UI to update right away, even while the network request is still running. The same goes for editing a display title locally before saving.
Historically, Paging pushed you toward updating the backing data source and invalidating the PagingSource. That’s still the right answer for durable data. But for a temporary visual change, going through a full paging refresh is a lot of ceremony for a heart icon.
With asItemSnapshotListFlow, you can keep local overlays separate and apply them to the loaded snapshot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
These overlays don’t replace your source of truth. If the request succeeds, you still need to update your backing data source, and once the refreshed item comes back with the new value you can drop the override so it can’t mask a later change from the server. If the request fails, drop it right away, as setLiked does above.
The payoff is that your UI doesn’t wait on Paging to show a local change.
Loading more data
As mentioned up top, append() and prepend() are how you load more when you work from snapshots. Here’s the shape in a ViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Then call them from your UI when the user nears an edge of the list. The 3.5.0-alpha01 release notes show a Compose example that puts a LaunchedEffect calling append() in an item at the end of the LazyColumn.
The 3.5.0 release notes also list Pager.refresh() and Pager.retry() for recovering from load errors when you work from snapshots.
Wrap-up
For a straightforward paged list, I’d still reach for collectAsLazyPagingItems() or PagingDataAdapter, since they handle scroll-driven loading for you.
I’d use asItemSnapshotListFlow when the screen needs local edits, filters from another Flow, or lookups across the loaded items. You get a list you can work with using the Flow operators you already know.
It does come with a couple of tradeoffs. Every emission gives you the whole loaded list, so any overlay change re-maps all of it, which is worth keeping in mind for very large lists. And you’re responsible for loading more pages when you render snapshots yourself.