Jetpack Compose and collectAsLazyPagingItems
Efficiently Managing Large Data Sets in Jetpack Compose with LazyColumn

Introduction
In modern Android development, handling large datasets efficiently is crucial for delivering a smooth user experience. Jetpack Compose, Android’s modern UI toolkit, offers powerful tools for working with lists, including LazyColumn
for rendering large, scrollable lists. When dealing with paginated data, Jetpack Compose’s collectAsLazyPagingItems
function simplifies and optimizes the process, ensuring performance is maintained even with massive datasets. In this article, we’ll explore how LazyColumn
and collectAsLazyPagingItems
work together to streamline pagination in your Compose-based apps, and how you can customize their behavior to suit your needs.
What is LazyColumn
?
LazyColumn
is a key component in Jetpack Compose for displaying lists of items. Unlike a traditional Column
, LazyColumn
only renders the visible items on the screen, making it much more efficient for large lists. This lazy loading mechanism ensures that memory usage remains low and performance stays smooth, regardless of the list size.
LazyColumn {
items(listOfItems) { item ->
Text(text = item.title)
}
}
Introducing the Paging Library
The Android Paging library is designed to handle large datasets by loading data incrementally as the user scrolls through the list. This is especially useful when dealing with remote data sources, where fetching all data at once is impractical. Paging divides the data into pages, loading each page as needed, which helps in reducing memory usage and network load.
collectAsLazyPagingItems
: A Seamless Integration
collectAsLazyPagingItems
is an extension function provided by the Paging Compose library, which allows you to easily integrate paginated data into a LazyColumn
. This function bridges the gap between the PagingData
stream and Compose’s LazyColumn
, enabling the latter to consume paginated data directly.
Here’s a basic example:
@Composable
fun MyPagingScreen(viewModel: MyViewModel = viewModel()) {
val lazyPagingItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
LazyColumn {
items(lazyPagingItems) { item ->
item?.let {
MyListItem(it)
}
}
}
}
How It Works
- Collecting PagingData:
collectAsLazyPagingItems
is used within a composable function to collect thePagingData
emitted by your ViewModel. This function converts thePagingData
flow into aLazyPagingItems
object that theLazyColumn
can directly consume. - Rendering Items: Inside
LazyColumn
, you can use theitems
composable to render each item in the paginated list. TheLazyColumn
automatically handles the efficient display and recycling of views as the user scrolls. - Handling Load States:
LazyPagingItems
also provides convenient access to load states likeLoading
,Error
, andEmpty
, allowing you to display appropriate UI elements based on the current state of data loading.
@Composable
fun MyPagingScreen(viewModel: MyViewModel = viewModel()) {
val lazyPagingItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyPagingItems.loadState.refresh) {
is LoadState.Loading -> {
CircularProgressIndicator()
}
is LoadState.Error -> {
Text("An error occurred")
}
else -> {
LazyColumn {
items(lazyPagingItems) { item ->
item?.let {
MyListItem(it)
}
}
}
}
}
}
Performance Optimizations
The combination of LazyColumn
and collectAsLazyPagingItems
offers significant performance benefits:
- Memory Efficiency:
LazyColumn
only renders the items currently visible on the screen, andcollectAsLazyPagingItems
ensures that only the necessary data pages are loaded into memory, minimizing resource usage. - Smooth Scrolling: Since
LazyColumn
works lazily andPaging
loads data in chunks, the UI remains responsive even when dealing with thousands of items. - Automatic Data Management:
collectAsLazyPagingItems
abstracts away the complexity of managing data loading, retrying on failures, and handling edge cases like empty states. This results in cleaner, more maintainable code.
Customizing the Behavior
While the default implementation of LazyColumn
and collectAsLazyPagingItems
works well for most use cases, there are several ways you can customize their behavior to better fit your app’s needs.
1. Customizing Item Layout
You can customize how each item is displayed within the LazyColumn
. Instead of just displaying text, you can create complex layouts with images, buttons, or any other UI elements.
@Composable
fun MyListItem(data: MyData) {
Row(modifier = Modifier.padding(8.dp)) {
Image(painter = rememberImagePainter(data.imageUrl), contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = data.title, style = MaterialTheme.typography.h6)
Text(text = data.description, style = MaterialTheme.typography.body2)
}
}
}
2. Handling Load States with Custom UI
You can customize the UI for different loading states such as Loading
, Error
, and Empty
.
@Composable
fun MyPagingScreen(viewModel: MyViewModel = viewModel()) {
val lazyPagingItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyPagingItems.loadState.refresh) {
is LoadState.Loading -> {
CircularProgressIndicator(modifier = Modifier.fillMaxSize())
}
is LoadState.Error -> {
Text(
text = "An error occurred. Please try again.",
modifier = Modifier.fillMaxSize(),
textAlign = TextAlign.Center
)
}
else -> {
LazyColumn {
items(lazyPagingItems) { item ->
item?.let {
MyListItem(it)
}
}
lazyPagingItems.apply {
when {
loadState.append is LoadState.Loading -> {
item { CircularProgressIndicator() }
}
loadState.append is LoadState.Error -> {
item {
Text(
text = "Failed to load more items.",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
}
}
}
3. Adding a Footer or Header
You can add a custom header or footer to the LazyColumn
. This is useful if you want to show a loading spinner at the bottom of the list when more data is being fetched.
@Composable
fun MyPagingScreen(viewModel: MyViewModel = viewModel()) {
val lazyPagingItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
LazyColumn {
item {
Text("List Header", style = MaterialTheme.typography.h5, modifier = Modifier.padding(8.dp))
}
items(lazyPagingItems) { item ->
item?.let {
MyListItem(it)
}
}
lazyPagingItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item {
CircularProgressIndicator(modifier = Modifier.padding(8.dp))
}
}
is LoadState.Error -> {
item {
Text(
text = "Failed to load more items.",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(8.dp)
)
}
}
else -> {}
}
}
item {
Text("List Footer", style = MaterialTheme.typography.h5, modifier = Modifier.padding(8.dp))
}
}
}
4. Pre-fetching Data
You can control when data is pre-fetched by tweaking the PagingConfig
parameters in the ViewModel. For example, you might want to start fetching the next page before the user reaches the end of the current list to ensure a seamless scrolling experience.
val pagingConfig = PagingConfig(
pageSize = 20,
prefetchDistance = 5, // Pre-fetch the next page when 5 items away from the end
initialLoadSize = 40 // Initial load size
)
val pagingDataFlow: Flow<PagingData<MyData>> = Pager(pagingConfig) {
MyPagingSource()
}.flow.cachedIn(viewModelScope)
5. Custom Paging Source Logic
You can customize how data is fetched in your PagingSource
. For instance, you might add caching, retries, or custom error handling.
class MyPagingSource : PagingSource<Int, MyData>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> {
return try {
val currentPage = params.key ?: 1
val data = fetchDataFromSource(page = currentPage)
if (data.isEmpty()) {
LoadResult.Error(NoDataException("No data available"))
} else {
LoadResult.Page(
data = data,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (data.isEmpty()) null else currentPage + 1
)
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, MyData>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
6. Differentiating Items in LazyColumn
If you have different types of items, you can differentiate them within the LazyColumn
by using the itemsIndexed
or items
function with a custom key or type.
LazyColumn {
items(lazyPagingItems, key = { item -> item.id }) { item ->
when (item) {
is MyDataTypeA -> MyTypeAItem(item)
is MyDataTypeB -> MyTypeBItem(item)
}
}
}
Conclusion
collectAsLazyPagingItems
in combination with LazyColumn
provides a robust and customizable way to handle large, paginated data sets in Jetpack Compose. The flexibility offered by Jetpack Compose allows you to customize everything from item layouts to load state handling, enabling you to craft a user experience tailored to your app's specific needs.
Whether you need to add custom headers, footers, or loading states, or if you need to tweak the pagination behavior itself, these tools provide the necessary building blocks. This level of customization ensures that you can deliver a performant, polished app, even when dealing with extensive data. Start using them in your projects today, and you’ll quickly see the benefits in both your app’s performance and your development workflow.

Dobri Kostadinov
Android Consultant | Trainer
Email me | Follow me on LinkedIn | Follow me on Medium | Buy me a coffee