管理和显示加载状态

Paging 库会跟踪分页数据的加载请求的状态,并通过 LoadState 类公开该状态。

每个 LoadType 和数据源类型(PagingSource 或 RemoteMediator)都会获得一个单独的 LoadState 信号。监听器提供的 CombinedLoadStates 对象则会提供来自所有这些信号的加载状态的信息。您可以利用此详细信息向用户显示相应的加载指示器。

加载状态

Paging 库通过 LoadState 对象公开要在界面中使用的加载状态。LoadState 对象根据当前的加载状态采用以下三种形式之一:

通过 LazyPagingItems 封装容器的 loadState 属性访问这些状态。您可以通过两种方式使用此状态:处理主要内容的可见性(例如全屏刷新旋转图标),或将加载项直接插入 LazyColumn 流(例如页脚旋转图标)。

使用监听器获取加载状态

如需在界面中监控加载状态,请使用 LazyPagingItems 封装容器提供的 loadState 属性。这会返回一个 CombinedLoadStates 对象,让您可以对刷新、附加或前置事件的加载行为做出响应。

在以下示例中,界面会根据刷新(初始)加载的当前状态显示加载旋转图标或出错提示:

@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
    }
  }
}

如需详细了解 LazyPagingItems,请参阅大型数据集(分页)。

如需在列表的开头或结尾显示加载指示器(作为页眉或页脚),请在 LazyColumn 作用域内专门为这些状态添加专用项块。

您可以使用 CombinedLoadStates 对象监控页眉的前置状态和页脚的附加状态。

在以下示例中,当提取更多数据时,列表会在底部显示进度条或重试按钮:

@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") }
  }
}

获取更多加载状态信息

如前面的示例所示,调用 pagingItems.loadState.refresh 非常方便。不过,它会掩盖从本地数据库 (PagingSource) 加载与从网络 (RemoteMediator) 加载之间的差异。这可能会导致界面短暂显示加载旋转图标,即使缓存的数据立即可用也是如此。

如需进行精确控制(例如,仅在本地数据库为空且网络同步处于活跃状态时显示“正在加载”旋转图标),请直接在可组合项中访问 source 和 mediator 属性。

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()
    }
}

由于每当 prepend 或 append 在滚动期间发生变化时,pagingItems.loadState 都会发出一个新的 CombinedLoadStates 对象,因此请使用 remember(pagingItems) 将 derivedStateOf 中的刷新检查封装起来。这样可以防止滚动边缘更新重组仅切换全屏刷新指示器的父级作用域:

val showFullScreenLoading by remember(pagingItems) {
  derivedStateOf {
    // Avoid recomposing on prepend or append updates while scrolling, and
    // avoid covering existing items during a background refresh.
    pagingItems.loadState.source.refresh is LoadState.Loading &&
      pagingItems.itemCount == 0
  }
}

if (showFullScreenLoading) {
  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
  ) {
    CircularProgressIndicator()
  }
}

根据加载状态的变化采取相应操作

您可能需要根据加载状态变化触发一次性副作用,例如在刷新完成或失败时滚动到列表顶部或显示 Snackbar。

在 LaunchedEffect 中使用 snapshotFlow,以流的形式观察状态变化。这样,您就可以应用标准 Flow 运算符(例如 filter、distinctUntilChanged 和 distinctUntilChangedBy)来隔离特定事件。

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)
    }
}

同样,您可以在 loadState 上使用 distinctUntilChangedBy,以便仅在 refresh 状态转换为 LoadState.Error 时显示错误 Snackbar:

val snackbarHostState = remember { SnackbarHostState() }

// Key on pagingItems rather than loadState so state updates do not restart the
// coroutine and reset distinctUntilChangedBy.
LaunchedEffect(pagingItems) {
  snapshotFlow { pagingItems.loadState }
    // Ignore prepend and append updates so scrolling does not re-trigger the
    // refresh error Snackbar.
    .distinctUntilChangedBy { it.refresh }
    .filter { it.refresh is LoadState.Error }
    .collect { loadState ->
      val error = (loadState.refresh as LoadState.Error).error
      val result = snackbarHostState.showSnackbar(
        message = "Data failed to load: ${error.localizedMessage ?: "Unknown error"}",
        actionLabel = "Retry"
      )
      if (result == SnackbarResult.ActionPerformed) {
        pagingItems.retry()
      }
    }
}

其他资源

如需详细了解 Paging 库和加载状态,请参阅以下资源。

文档

查看内容

  • 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
  • Paging 库概览