管理和顯示載入狀態

Paging 程式庫會追蹤分頁資料的載入要求狀態,並透過 LoadState 類別公開資料。

針對每個 LoadType 和資料來源類型 (PagingSource 或 RemoteMediator) 會提供獨立的 LoadState 信號。此事件監聽器提供的 CombinedLoadStates 物件會提供所有信號的載入狀態資訊。您可以使用此詳細資訊,向使用者顯示適用的載入指標。

載入狀態

Paging 程式庫會透過 LoadState 物件公開使用者介面中的載入狀態。根據目前的載入狀態,LoadState 物件採用下列其中一種格式:

您可以透過 LazyPagingItems 包裝函式的 loadState 屬性存取這些狀態。您可以透過兩種方式使用這個狀態:處理主要內容顯示設定 (例如全螢幕重新整理旋轉圖示),或直接將載入項目插入 LazyColumn 串流 (例如頁尾旋轉圖示)。

透過事件監聽器存取載入狀態

如要監控 UI 的載入狀態,請使用 LazyPagingItems 包裝函式提供的 loadState 屬性。這會傳回 CombinedLoadStates 物件,讓您針對重新整理、附加或前置事件的載入行為做出反應。

在以下範例中,UI 會根據重新整理 (初始) 載入的目前狀態,顯示載入旋轉圖示或錯誤訊息:

@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) 載入資料之間的差異。這可能會導致 UI 短暫顯示載入旋轉圖示,即使快取資料立即可用也一樣。

如要精確控制,例如只在本機資料庫空白且網路同步處理作業進行中時,才顯示載入旋轉圖示,請直接在可組合函式中存取 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 程式庫和載入狀態,請參閱下列資源。

說明文件

查看內容