Mengelola dan menampilkan status pemuatan

Libary Paging melacak status permintaan pemuatan untuk data yang di-page dan menampilkannya melalui class LoadState.

Sinyal LoadState terpisah diberikan untuk setiapLoadType dan jenis sumber data (PagingSource atau RemoteMediator). Objek CombinedLoadStates yang diberikan oleh pemroses berisi informasi tentang status pemuatan dari semua sinyal ini. Anda dapat menggunakan informasi mendetail ini untuk menampilkan indikator pemuatan yang sesuai untuk pengguna Anda.

Status pemuatan

Library Paging menampilkan status pemuatan untuk digunakan di UI melalui objek LoadState. Objek LoadState mengambil salah satu dari tiga bentuk, bergantung pada status pemuatan saat ini:

  • Jika tidak ada operasi pemuatan yang aktif dan tidak ada error, LoadState adalah objek LoadState.NotLoading. Subclass ini juga menyertakan properti endOfPaginationReached, yang menunjukkan apakah akhir penomoran halaman telah tercapai.
  • Jika ada operasi pemuatan yang aktif, LoadState adalah objek LoadState.Loading.
  • Jika ada error, LoadState adalah objek LoadState.Error.

Akses status ini melalui properti loadState wrapper LazyPagingItems Anda. Anda dapat menggunakan status ini dengan dua cara: menangani visibilitas konten utama (seperti indikator lingkaran berputar refresh layar penuh) atau menyisipkan item pemuatan langsung ke aliran data LazyColumn (seperti indikator lingkaran berputar footer).

Mengakses status pemuatan dengan pemroses

Untuk memantau status pemuatan di UI, gunakan properti loadState yang disediakan oleh wrapper LazyPagingItems. Tindakan ini akan menampilkan objek CombinedLoadStates yang memungkinkan Anda bereaksi terhadap perilaku pemuatan untuk peristiwa refresh, penambahan, atau penambahan di awal.

Dalam contoh berikut, UI menampilkan indikator lingkaran berputar untuk pemuatan atau pesan error, bergantung pada status pemuatan refresh (awal) saat ini:

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

Untuk mengetahui informasi selengkapnya tentang LazyPagingItems, lihat Set data besar (paging).

Untuk menampilkan indikator pemuatan di awal atau akhir daftar (berfungsi sebagai header atau footer), tambahkan blok item khusus untuk status tersebut dalam cakupan LazyColumn Anda.

Anda dapat memantau status penambahan di awal untuk header dan status penambahan di akhir untuk footer menggunakan objek CombinedLoadStates.

Dalam contoh berikut, daftar menampilkan status progres atau tombol coba lagi di bagian bawah daftar saat lebih banyak data diambil:

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

Mengakses informasi status pemuatan tambahan

Seperti yang ditunjukkan dalam contoh sebelumnya, memanggil pagingItems.loadState.refresh sangatlah mudah. Namun, hal ini mengaburkan perbedaan antara pemuatan dari database lokal (PagingSource) dan jaringan (RemoteMediator). Hal ini dapat menyebabkan UI menampilkan indikator lingkaran berputar pemuatan secara singkat meskipun data yang di-cache langsung tersedia.

Untuk kontrol yang akurat, seperti menampilkan indikator lingkaran berputar hanya saat database lokal kosong dan sinkronisasi jaringan aktif, akses properti source dan mediator langsung dalam composable Anda.

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

Karena pagingItems.loadState memunculkan objek CombinedLoadStates baru setiap kali prepend atau append berubah selama men-scroll, gabungkan pemeriksaan refresh di derivedStateOf dengan remember(pagingItems). Hal ini mencegah update scroll-edge merekomposisi cakupan induk yang hanya mengalihkan indikator refresh layar penuh:

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

Merespons perubahan status pemuatan

Anda mungkin perlu memicu efek samping satu kali berdasarkan perubahan status pemuatan, seperti men-scroll ke bagian atas daftar atau menampilkan Snackbar saat pemuatan ulang selesai atau gagal.

Gunakan snapshotFlow di dalam LaunchedEffect untuk mengamati perubahan status sebagai aliran data. Dengan begitu, Anda dapat menerapkan operator Flow standar seperti filter, distinctUntilChanged, dan distinctUntilChangedBy untuk mengisolasi peristiwa tertentu.

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

Demikian pula, Anda dapat menggunakan distinctUntilChangedBy di loadState untuk menampilkan Snackbar error hanya saat status refresh bertransisi ke 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()
      }
    }
}

Referensi lainnya

Untuk informasi selengkapnya tentang library Paging dan status pemuatan, lihat referensi berikut.

Dokumentasi

Konten penayangan