Zarządzaj stanami wczytywania i wyświetlaj je

Biblioteka Paging śledzi stan żądań wczytywania danych podzielonych na strony i udostępnia go za pomocą klasy LoadState.

Dla każdego LoadType i typu źródła danych (PagingSource lub RemoteMediator) jest dostarczany osobny sygnał LoadState. Obiekt CombinedLoadStates dostarczany przez detektor zawiera informacje o stanie wczytywania ze wszystkich tych sygnałów. Możesz użyć tych szczegółowych informacji, aby wyświetlać użytkownikom odpowiednie wskaźniki wczytywania.

Wczytuję stany

Biblioteka Paging udostępnia stan wczytywania do użycia w interfejsie za pomocą obiektu LoadState. Obiekty LoadState mogą przyjmować jedną z 3 form w zależności od bieżącego stanu wczytywania:

  • Jeśli nie ma aktywnej operacji wczytywania i nie wystąpił błąd, LoadState jest obiektem LoadState.NotLoading. Ta podklasa zawiera też właściwość endOfPaginationReached, która wskazuje, czy osiągnięto koniec paginacji.
  • Jeśli trwa operacja wczytywania, LoadState jest obiektem LoadState.Loading.
  • Jeśli wystąpi błąd, LoadState jest obiektem LoadState.Error.

Dostęp do tych stanów możesz uzyskać za pomocą właściwości loadState obiektu opakowującego LazyPagingItems. Możesz użyć tego stanu na 2 sposoby: obsługując widoczność treści głównej (np. wskaźnik odświeżania na pełnym ekranie) lub wstawiając elementy wczytywania bezpośrednio do strumienia LazyColumn (np. wskaźnik w stopce).

Uzyskiwanie dostępu do stanu wczytywania za pomocą detektora

Aby monitorować stan wczytywania w interfejsie, użyj właściwości loadState udostępnianej przez kod LazyPagingItems. Zwraca obiekt CombinedLoadStates, który pozwala reagować na zachowanie podczas wczytywania w przypadku zdarzeń odświeżania, dołączania lub dołączania na początku.

W poniższym przykładzie interfejs wyświetla wskaźnik postępu wczytywania lub komunikat o błędzie w zależności od bieżącego stanu odświeżania (początkowego) wczytywania:

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

Więcej informacji o funkcji LazyPagingItems znajdziesz w artykule Duże zbiory danych (stronicowanie).

Aby wyświetlać wskaźniki wczytywania na początku lub na końcu listy (jako nagłówki lub stopki), dodaj dedykowane bloki elementów dla tych stanów w zakresie LazyColumn.

Stan dołączenia nagłówka i stopki możesz monitorować za pomocą obiektu CombinedLoadStates.

W tym przykładzie lista wyświetla pasek postępu lub przycisk ponownej próby u dołu, gdy pobierane są dodatkowe dane:

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

Dostęp do dodatkowych informacji o stanie wczytywania

Jak widać na wcześniejszych przykładach, wywoływanie funkcji pagingItems.loadState.refresh jest wygodne. Zaciera jednak różnicę między wczytywaniem z lokalnej bazy danych (PagingSource) a wczytywaniem z sieci (RemoteMediator). Może to spowodować, że w interfejsie na chwilę pojawi się wskaźnik wczytywania, nawet jeśli dane z pamięci podręcznej są natychmiast dostępne.

Aby uzyskać precyzyjną kontrolę, np. wyświetlać wskaźnik ładowania tylko wtedy, gdy lokalna baza danych jest pusta, a synchronizacja sieci jest aktywna, uzyskaj dostęp do właściwości source i mediator bezpośrednio w funkcji kompozycyjnej.

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

pagingItems.loadState emituje nowy obiekt CombinedLoadStates za każdym razem, gdy prepend lub append zmienia się podczas przewijania, dlatego opakuj sprawdzanie odświeżania w derivedStateOf za pomocą remember(pagingItems). Zapobiega to ponownemu tworzeniu zakresu nadrzędnego przez aktualizacje krawędzi przewijania, które przełączają tylko wskaźnik odświeżania pełnego ekranu:

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

Reagowanie na zmiany stanu wczytywania

Może być konieczne wywołanie jednorazowych efektów ubocznych na podstawie zmian stanu wczytywania, takich jak przewijanie do góry listy lub wyświetlanie Snackbar po zakończeniu lub niepowodzeniu odświeżania.

Użyj snapshotFlow wewnątrz LaunchedEffect, aby obserwować zmiany stanu jako strumień. Dzięki temu możesz stosować standardowe operatory Flow, takie jak filter, distinctUntilChanged i distinctUntilChangedBy, aby wyodrębniać konkretne zdarzenia.

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

Podobnie możesz użyć distinctUntilChangedBy w loadState, aby wyświetlić błąd Snackbar tylko wtedy, gdy stan refresh zmieni się na LoadState.Error:

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

Dodatkowe materiały

Więcej informacji o bibliotece Paging i stanach wczytywania znajdziesz w tych materiałach.

Dokumentacja

Wyświetla treści