Manage and present loading states

The Paging library tracks the state of load requests for paged data and exposes it through the LoadState class.

A separate LoadState signal is provided for each LoadType and data source type (either PagingSource or RemoteMediator). The CombinedLoadStates object provided by the listener provides information about the loading state from all of these signals. You can use this detailed information to display the appropriate loading indicators to your users.

Loading states

The Paging library exposes the loading state for use in the UI through the LoadState object. LoadState objects take one of three forms depending on the current loading state:

  • If there is no active load operation and no error, then LoadState is a LoadState.NotLoading object. This subclass also includes the endOfPaginationReached property, which indicates whether the end of pagination has been reached.
  • If there is an active load operation, then LoadState is a LoadState.Loading object.
  • If there is an error, then LoadState is a LoadState.Error object.

Access these states through the loadState property of your LazyPagingItems wrapper. You can use this state in two ways: handling the main content visibility (like a full-screen refresh spinner) or inserting loading items directly into your LazyColumn stream (like a footer spinner).

Access the loading state with a listener

To monitor the loading state in your UI, use the loadState property provided by the LazyPagingItems wrapper. This returns a CombinedLoadStates object that lets you react to loading behavior for refresh, append, or prepend events.

In the following example, the UI displays a loading spinner or an error message depending on the current state of the refresh (initial) load:

@Composable
fun UserListScreen(viewModel: UserViewModel) {
  val pagingItems = viewModel.flow.collectAsLazyPagingItems()

  Box(modifier = Modifier.fillMaxSize()) {
    // Show the list content
    LazyColumn {
      items(pagingItems.itemCount) { index ->
        UserItem(pagingItems[index])
      }
    }

    // Handle the loading state
    when (val state = pagingItems.loadState.refresh) {
      is LoadState.Loading -> {
        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
      }
      is LoadState.Error -> {
        ErrorButton(
          message = state.error.message ?: "Unknown error",
          onClick = { pagingItems.retry() },
          modifier = Modifier.align(Alignment.Center)
        )
      }
      else -> {} // No separate view needed for success/not loading
    }
  }
}

For more information on LazyPagingItems, see Large data-sets (paging).

To display loading indicators at the beginning or end of your list (acting as headers or footers), add dedicated item blocks specifically for those states within your LazyColumn scope.

You can monitor the prepend state for the header and the append state for the footer using the CombinedLoadStates object.

In the following example, the list displays a progress bar or a retry button at the bottom of the list when more data is being fetched:

@Composable
fun UserList(viewModel: UserViewModel) {
  val pagingItems = viewModel.pager.flow.collectAsLazyPagingItems()

  LazyColumn {
    // 1. Header (Prepend state)
    // Useful if you support bidirectional paging or jumping to the middle
    item {
      val prependState = pagingItems.loadState.prepend
      if (prependState is LoadState.Loading) {
        LoadingItem()
      } else if (prependState is LoadState.Error) {
        ErrorItem(
          message = prependState.error.message ?: "Error",
          onClick = { pagingItems.retry() }
        )
      }
    }

    // 2. Main Data
    items(pagingItems.itemCount) { index ->
      UserItem(pagingItems[index])
    }

    // 3. Footer (Append state)
    // Shows when the user scrolls to the bottom and more data is loading
    item {
      val appendState = pagingItems.loadState.append
      if (appendState is LoadState.Loading) {
        LoadingItem()
      } else if (appendState is LoadState.Error) {
        ErrorItem(
          message = appendState.error.message ?: "Error",
          onClick = { pagingItems.retry() }
        )
      }
    }
  }
}

@Composable
fun LoadingItem() {
  Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
    CircularProgressIndicator()
  }
}

@Composable
fun ErrorItem(message: String, onClick: () -> Unit) {
  Column(
    modifier = Modifier.fillMaxWidth().padding(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally
  ) {
    Text(text = message, color = Color.Red)
    Button(onClick = onClick) { Text("Retry") }
  }
}

Access additional loading state information

As shown in the earlier examples, calling pagingItems.loadState.refresh is convenient. However, it obscures the difference between loading from your local database (PagingSource) and your network (RemoteMediator). This can cause the UI to briefly show a loading spinner even when cached data is immediately available.

For precise control, like showing a loading spinner only when the local database is empty and a network sync is active, access the source and mediator properties directly within your composable.

val loadState = pagingItems.loadState

val isSyncing = loadState.mediator?.refresh is LoadState.Loading

val isLocalEmpty = loadState.source.refresh is LoadState.NotLoading &&
                   pagingItems.itemSnapshotList.items.isEmpty()

if (isSyncing && isLocalEmpty) {
    FullScreenLoading()
} else {
    UserList(pagingItems)

    if (isSyncing) {
        TopOverlaySpinner()
    }
}

React to load state changes

You might need to trigger one-off side effects based on load state changes, such as scrolling to the top of a list or showing a Snackbar when a refresh completes.

Use snapshotFlow inside a LaunchedEffect to observe state changes as a stream. This lets you apply standard Flow operators like filter and distinctUntilChanged to isolate specific events.

val listState = rememberLazyListState()

LaunchedEffect(pagingItems) {
  // 1. Convert the state to a Flow
  snapshotFlow { pagingItems.loadState.refresh }
    // 2. Filter for the specific event (Refresh completed successfully)
    .distinctUntilChanged()
    .filter { it is LoadState.NotLoading }
    .collect {
      // 3. Trigger the side effect
      listState.animateScrollToItem(0)
    }
}

Additional resources

For further information about the Paging library and loading states, consult the following resources.

Documentation

Views content