Paging 3 with Jetpack Compose: A Practical LazyColumn Setup

Paging 3 lets a Compose screen display a large or remote collection incrementally instead of loading every row upfront. Expose Flow<PagingData<T>> from the presentation layer, collect it with collectAsLazyPagingItems(), render its indexed items in LazyColumn, and handle loading and failure with loadState.

Android’s lazy lists guide recommends Paging for large datasets and documents Compose support through androidx.paging:paging-compose. The Paging Compose API provides itemKey and itemContentType helpers for lazy layouts.

Display paged items

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey

@Composable
fun MessageList(viewModel: MessagesViewModel) {
    val messages = viewModel.messages.collectAsLazyPagingItems()

    LazyColumn {
        items(
            count = messages.itemCount,
            key = messages.itemKey { message -> message.id }
        ) { index ->
            val message = messages[index]
            if (message != null) {
                MessageRow(message = message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

LazyPagingItems may return null when placeholders are enabled, so the row must handle that case. The official documentation warns that placeholders used with RemoteMediator must have realistic dimensions: tiny or missing placeholders can cause the mediator to keep fetching because the viewport never appears full.

Keep identity and layout types explicit

Use itemKey for a stable row ID and itemContentType when paged content has distinct reusable shapes.

import androidx.paging.compose.itemContentType

items(
    count = messages.itemCount,
    key = messages.itemKey { it.id },
    contentType = messages.itemContentType { message -> message.kind }
) { index ->
    val message = messages[index]
    if (message == null) MessagePlaceholder() else MessageRow(message)
}

Keys preserve identity; content types describe compatible row structures. Read lazy list keys and contentType in mixed lists for the distinction.

Render refresh and append states

loadState distinguishes the initial refresh from loading more content at the end. Keep retry behavior explicit and let the ViewModel own any domain policy.

import androidx.paging.LoadState

when (val refresh = messages.loadState.refresh) {
    is LoadState.Loading -> FullScreenLoading()
    is LoadState.Error -> ErrorScreen(
        message = refresh.error.message,
        onRetry = messages::retry
    )
    is LoadState.NotLoading -> Unit
}

LazyColumn {
    // paged rows
    item {
        when (val append = messages.loadState.append) {
            is LoadState.Loading -> LoadingRow()
            is LoadState.Error -> RetryRow(
                onRetry = messages::retry
            )
            is LoadState.NotLoading -> Unit
        }
    }
}

Do not show a full-screen spinner for every append. Initial loading, an empty successful result, append loading, and an append error are different user situations.

Common pitfalls

  • Creating a new Pager in the composable instead of exposing a stable flow from the screen layer.
  • Omitting itemKey for data that can refresh or reorder.
  • Assuming itemCount means every item is already loaded.
  • Ignoring null placeholders.
  • Using placeholder rows with unrealistic height when a RemoteMediator is fetching.

Test refresh, append, retry, empty results, rotation, and slow network conditions. For the surrounding UI states, see the next companion topic: loading, empty, error, and retry list states.

FAQ

Does Paging 3 work with Compose?

Yes. Compose integration is provided by androidx.paging:paging-compose for Paging 3.0 and later.

Where should collectAsLazyPagingItems() run?

In the composable that renders the paged list. The Paging documentation notes that collection runs in the composition scope.

Should every paged list use placeholders?

Not necessarily. Handle null entries when placeholders are enabled, and choose realistic placeholder dimensions when using a remote mediator.