Hey, I have to develop a Paged LazyList in Compose...
# compose-android
n
Hey, I have to develop a Paged LazyList in Compose Android. Is there any good, recent and detailed tutorial about Paging3 in Compose? The 2 "official" codelabs are deprecated (source [1] & [2]). ๐Ÿคฆ Back in the days of XML I did a paged RecyclerView by myself using Flows, it was really easy compared to Paging3. But this time I have to use Paging3 so any help, tutorial, recommendation will be appreciated. ๐Ÿ™‚
a
> Note: If your app uses Compose for its UI, use the
androidx.paging:paging-compose
artifact to integrate Paging with your UI layer instead. To learn more, see the API documentation for
collectAsLazyPagingItems()
. from https://developer.android.com/topic/libraries/architecture/paging/v3-overview#ui
There is also a sample in the API documentation for `collectAsLazyPagingItems()`: https://developer.android.com/reference/kotlin/androidx/paging/compose/package-summary#collectaslazypagingitems
i
r
Qq: why do you "have to" use Paging 3? In my opinion, it's hard for paging 3 to justify itself over just manually doing it.
n
The team used Paging3 in another (yet simpler) screen. So I wanted to use it too. But there's so much stuff that I have to do that I can't do with Paging3 so I guess once again I'll do it myself with flows and a LazyList scroll listener.
๐Ÿ‘ 1
i
But there's so much stuff that I have to do that I can't do with Paging3
Can you give some examples?
n
1/ I need to display a "header" in the LazyColumn with the total count of items (which is given by my backend). My backend response can be summarized like that:
Copy code
{
  "items": [
    ...
  ],
  "totalCount": 140
}
So in my
PagingSource.load()
function, for the first page, I have to create an extra "item" on top and wrap other "real items" on the first page.
Copy code
LoadResult.Page(
    data = if (page == 0) {
        buildList {
            add(
                Wrapper.Header(totalCount = response.totalCount)
            )

            addAll(response.items.map { Wrapper.Content(it) })
        }
    } else {
        response.items.map { Wrapper.Content(it) }
    },
    ...
)
That's basically doing UI stuff in the data layer. No separation of concerns there. ๐Ÿ˜ž 2/ We have an MVI Architecture so it breaks every principle around it. Can't access the data that is inside the flow, can't have a single source of truth out of the ViewModel, can't refresh or reload the data from the ViewModel, we have to trust the view, etc. 3/ Tracking events is hard. There's no way to know the view requested more data outside of the PagingSource, and exposing those state is doing half of the job I'd have to do to do paging myself anyway so... And that's only the first issues I encountered in 24 hours so I guess there will be more.
๐Ÿ‘ 1
โ˜๏ธ 1
โž• 2
r
It feels to me like an over-engineered abstraction trying to be generic enough for everyone, but it can still fail to capture the many nuances one might have when doing pagination in their own specific use-case. I also had to live with it in existing projects but in my own, I just do the implementation myself. And any adjustment necessary can be done directly without much fanfare (or fear of breaking things).
n
I was so pleased how easy it was to implement it. In less than an hour, a newly-created playground project with fake API was working just fine. But trying to use it in a real project is just so disappointing. I guess for most people it should be OK ? But if you deviate just a bit from the lib's usecase, you're in for trouble.
a
Regarding 1): You don't have to wrap a pseudo-element in your data just to display a header in a
LazyColumn
. You can simply insert a header-item directly in the Compose-code:
Copy code
LazyColumn {
    item {
        Header(totalCount)
    }
    items(...) {
    ...
    }
}
Or if you want the
Header
to always be visible, you can put the
Header
into a non-scrollable
Column
with
fillMaxSize()
and the
LazyColumn
as the second
Composable
in the
Column
.
n
How would you get the
totalCount
value in Compose that is coming from the backend endpoint? I would need to put that information in the LoadResult.Page.data field, wouldn't I?
i
The
LoadResult.Page
lets you set the
itemsBefore
and
itemsAfter
, so you already have the totalCount of all items just by looking at the
lazyPagingItems.itemCount
(assuming you have placeholders turned on for Pager, which you absolutely should)
n
The backend gives me the total loadable content size (which I need to display), while
lazyPagingItems.itemCount
gives me the total loaded content size
i
No, that is incorrect. If you use placeholders, then
lazyPagingItems.itemCount
is the total loadable content size
n
How would Paging3 even know this information, it's coming from my backend?
i
The
LoadResult.Page
lets you set the
itemsBefore
and
itemsAfter
If you are loading page 0, you return 20 items, you set
itemsBefore
to
0
and
itemsAfter
to
totalCount - 20
, now Paging knows how many total items you have
๐Ÿ’ก 1
n
Yes that's what I was getting to, but now my tracking is incorrect because I was using
lazyPagingItems.itemCount
to send to the tracking team how many items were seen / loaded...
i
If you wanted to check how many items are currently loaded, you'd want to use
lazyPagingItems.itemSnapshotList.items.size
since that is defined as:
The presented data, excluding placeholders.
But how many items are currently loaded is completely different from how many items have been seen, since Paging is going to be unloading previous items if you scroll far enough - that's all set by the
PagingConfig
you pass to your
Pager
(specifically,
maxSize
and
jumpThreshold
)
Not to mention that 'seen' also doesn't take into account that LazyColumn is going to be prefetching items before users scroll to them (not to mention if your page size is as high as it should, you should also be loading items before users scroll to them), so 'seen' isn't quite the right word even for looking at what data is already loaded
If you actually want to know what is actually seen by the user, you need to look at the scroll state and visible items at that layer
v
I thought the totalCount is basically a count of all items across all pages. If this is true, then you're right you have to create some models in your domain and wrap your data (I'm doing the same thing)
i
I thought the totalCount is basically a count of all items across all pages. If this is true, then you're right you have to create some models in your domain and wrap your data (I'm doing the same thing)
No, you don't, as per the above - Paging lets you track the total items already
v
It's kinda a different thing. If we have 5 pages with pageSize 10, but the actual data is for first 4 pages is 10 items and for the last is 1 item, then totalCount should be 41, not 50. Paging can't know this without getting all of the pages.
i
You do have to fill in
itemsAfter
correctly. You can't lie
v
Well, for this case โ€“ yes. You can set this param. But what if we want to add some banners on first page, every n page or something else? Then we're back to our domain layer with additional models and wrapping initial items.
n
Yes "seen" was basically "has appeared on screen" + up to loadSize items, and tracking was OK with that. So for the header, if I provide
LoadResult.Page.itemsAfter
with the totalCount (from backend) - actual server response size - actual list size, I can use
itemCount
for the total loadable item count? And for the tracking (I'm using compose), using a
LaunchedEffect
keyed with the
itemSnapshotList.items.size
will get me the best approximation of how much content has been seen (up to loadSize items) ?
โž• 1
i
Paging also has APIs for adding list separators such as banners every N elements: https://developer.android.com/topic/libraries/architecture/paging/v3-transform#separators
So for the header, if I provide
LoadResult.Page.itemsAfter
with the totalCount (from backend) - actual server response size - actual list size, I can use
itemCount
for the total loadable item count?
Yep, as long as you have placeholders enabled in your Pager and fill in
itemsBefore
and
itemsAfter
, Paging will report the correct
itemCount
for your entire dataset
And for the tracking (I'm using compose), using a
LaunchedEffect
keyed with the
itemSnapshotList.items.size
will get me the best approximation of how much content has been seen (up to loadSize items) ?
Yep, that would contain the total number of currently loaded items
n
Ok, thank you for your time
v
I'm aware of separators and transformations. But there're some problems with this approach for me: 1. We don't have access to the current page in PagingData or insertSeparators function, so we can't handle case "every n page" 2. What if we have asynchronous data. Suppose we need to go to different API route to get banners (I know, not the best architecture in the first place). We can easily do this in our suspend method of our datasource, but it seems weird to do this in ViewModel, inside insertSeparators function So, basically it it's ok for simple separators, hence the insertSeparators function name, but for more complex things it's easier to do this on lower level
i
Yeah, do what feels natural to you. You can certainly add a
pageNumber
field to your data classes (which would allow you to do
if (before.pageNumber != after.pageNumber && after.pageNumber % EVERY_PAGE_COUNT == 0)
, it isn't like you are somehow prohibited from doing that
๐Ÿ‘ 1
v
Of course that's one of the ways. I've already worked around those "limitations", which are probably by design. Anyway thank you for the discussion!
๐Ÿ‘ 1