Ladestatus verwalten und einblenden

Die Paging-Bibliothek verfolgt den Status von Ladeanfragen für paginierte Daten und stellt ihn über die Klasse LoadState bereit.

Für jeden LoadType und Datenquellentyp (entweder PagingSource oder RemoteMediator) wird ein separates LoadState-Signal bereitgestellt. Das vom Listener bereitgestellte CombinedLoadStates-Objekt enthält Informationen zum Ladestatus aller dieser Signale. Anhand dieser detaillierten Informationen können Sie Ihren Nutzern die entsprechenden Ladeanzeigen präsentieren.

Bundesstaaten werden geladen

Die Paging-Bibliothek stellt den Ladestatus für die Verwendung in der Benutzeroberfläche über das LoadState-Objekt bereit. LoadState-Objekte können je nach aktuellem Ladestatus eine von drei Formen annehmen:

  • Wenn kein aktiver Ladevorgang und kein Fehler vorliegt, ist LoadState ein LoadState.NotLoading-Objekt. Diese Unterklasse enthält auch das Attribut endOfPaginationReached, das angibt, ob das Ende der Paginierung erreicht wurde.
  • Wenn ein aktiver Ladevorgang vorhanden ist, ist LoadState ein LoadState.Loading-Objekt.
  • Wenn ein Fehler auftritt, ist LoadState ein LoadState.Error-Objekt.

Auf diese Status können Sie über das Attribut loadState des LazyPagingItems-Wrappers zugreifen. Du kannst diesen Status auf zwei Arten verwenden: Du kannst die Sichtbarkeit des Hauptinhalts steuern (z. B. mit einem Vollbild-Ladesymbol) oder Ladeelemente direkt in deinen LazyColumn-Stream einfügen (z. B. mit einem Ladesymbol in der Fußzeile).

Mit einem Listener auf den Ladestatus zugreifen

Wenn Sie den Ladestatus in Ihrer Benutzeroberfläche überwachen möchten, verwenden Sie das Attribut loadState, das vom Wrapper LazyPagingItems bereitgestellt wird. Dadurch wird ein CombinedLoadStates-Objekt zurückgegeben, mit dem Sie auf das Ladeverhalten für Aktualisierungs-, Anfüge- oder Voranstellungsereignisse reagieren können.

Im folgenden Beispiel wird in der Benutzeroberfläche je nach aktuellem Status des Aktualisierungsvorgangs (anfänglicher Ladevorgang) ein rotierendes Ladesymbol oder eine Fehlermeldung angezeigt:

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

Weitere Informationen zu LazyPagingItems finden Sie unter Große Datasets (Paging).

Wenn Sie Ladeanzeigen am Anfang oder Ende Ihrer Liste (als Kopf- oder Fußzeilen) anzeigen möchten, fügen Sie im LazyColumn-Bereich spezielle Elementblöcke für diese Status hinzu.

Mit dem Objekt CombinedLoadStates können Sie den Status des Voranstellens für den Header und den Status des Anfügens für den Footer überwachen.

Im folgenden Beispiel wird am Ende der Liste eine Fortschrittsanzeige oder eine Schaltfläche zum Wiederholen angezeigt, wenn weitere Daten abgerufen werden:

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

Auf zusätzliche Informationen zum Ladezustand zugreifen

Wie in den vorherigen Beispielen gezeigt, ist der Aufruf von pagingItems.loadState.refresh praktisch. Der Unterschied zwischen dem Laden aus der lokalen Datenbank (PagingSource) und dem Laden aus dem Netzwerk (RemoteMediator) wird jedoch verschleiert. Dies kann dazu führen, dass auf der Benutzeroberfläche kurz ein Ladesymbol angezeigt wird, obwohl im Cache gespeicherte Daten sofort verfügbar sind.

Wenn Sie eine präzise Steuerung wünschen, z. B. um ein Ladesymbol nur dann anzuzeigen, wenn die lokale Datenbank leer ist und eine Netzwerksynchronisierung aktiv ist, greifen Sie direkt in Ihrer zusammensetzbaren Funktion auf die Attribute source und mediator zu.

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

Da pagingItems.loadState jedes Mal ein neues CombinedLoadStates-Objekt ausgibt, wenn sich prepend oder append beim Scrollen ändert, verpacken Sie die Aktualisierungsprüfung in derivedStateOf mit remember(pagingItems). Dadurch wird verhindert, dass Aktualisierungen am Scrollrand einen übergeordneten Bereich neu zusammensetzen, der nur eine Aktualisierungsanzeige im Vollbildmodus umschaltet:

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

Auf Änderungen des Ladestatus reagieren

Möglicherweise müssen Sie einmalige Nebenwirkungen basierend auf Änderungen des Ladestatus auslösen, z. B. zum Anfang einer Liste scrollen oder eine Snackbar anzeigen, wenn eine Aktualisierung abgeschlossen ist oder fehlschlägt.

Verwenden Sie snapshotFlow in einem LaunchedEffect, um Statusänderungen als Stream zu beobachten. So können Sie Standard-Flow-Operatoren wie filter, distinctUntilChanged und distinctUntilChangedBy anwenden, um bestimmte Ereignisse zu isolieren.

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

Ebenso können Sie distinctUntilChangedBy für loadState verwenden, um den Fehler Snackbar nur dann anzuzeigen, wenn der Status refresh zu LoadState.Error wechselt:

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

Zusätzliche Ressourcen

Weitere Informationen zur Paging-Bibliothek und zu Ladestatus finden Sie in den folgenden Ressourcen.

Dokumentation

Inhalte ansehen