Gerenciar e exibir estados de carregamento

A biblioteca Paging monitora o estado das solicitações de carregamento dos dados paginados e os expõe usando a classe LoadState.

Um sinal LoadState separado é fornecido para cada LoadType e cada tipo de fonte de dados (PagingSource ou RemoteMediator). O objeto CombinedLoadStates fornecido pelo listener fornece informações sobre o estado de carregamento de todos esses sinais. Use essas informações detalhadas para exibir os indicadores de carregamento adequados aos usuários.

Estados de carregamento

A biblioteca Paging usa o objeto LoadState para expor o estado de carregamento que será usado na IU. Os objetos LoadState podem assumir uma de três formas, dependendo do estado de carregamento atual:

  • Se não houver operação de carregamento ativa nem erros, LoadState será um objeto LoadState.NotLoading. Essa subclasse também inclui a propriedade endOfPaginationReached, que indica se o fim da paginação foi atingido.
  • Se houver uma operação de carregamento ativa, LoadState será um objeto LoadState.Loading.
  • Se houver algum erro, LoadState será um objeto LoadState.Error.

Acesse esses estados pela propriedade loadState do wrapper LazyPagingItems. É possível usar esse estado de duas maneiras: processando a visibilidade do conteúdo principal (como um ícone de carregamento em tela cheia) ou inserindo itens de carregamento diretamente no stream LazyColumn (como um ícone de carregamento no rodapé).

Acessar o estado de carregamento usando um listener

Para monitorar o estado de carregamento na interface, use a propriedade loadState fornecida pelo wrapper LazyPagingItems. Isso retorna um objeto CombinedLoadStates que permite reagir ao comportamento de carregamento para eventos de atualização, adição ao final ou adição ao início.

No exemplo a seguir, a IU mostra um ícone de carregamento ou uma mensagem de erro, dependendo do estado atual do carregamento de atualização (inicial):

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

Para mais informações sobre LazyPagingItems, consulte Conjuntos de dados grandes (paginação).

Para mostrar indicadores de carregamento no início ou no fim da lista (atuando como cabeçalhos ou rodapés), adicione blocos de itens dedicados especificamente para esses estados no escopo LazyColumn.

É possível monitorar o estado de adição ao início para o cabeçalho e o estado de adição ao final para o rodapé usando o objeto CombinedLoadStates.

No exemplo a seguir, a lista mostra uma barra de progresso ou um botão "Repetir" na parte de baixo da lista quando mais dados estão sendo buscados:

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

Acessar informações adicionais sobre o estado de carregamento

Como mostrado nos exemplos anteriores, chamar pagingItems.loadState.refresh é conveniente. No entanto, ele oculta a diferença entre o carregamento do banco de dados local (PagingSource) e da rede (RemoteMediator). Isso pode fazer com que a IU mostre brevemente um ícone de carregamento, mesmo quando os dados em cache estão disponíveis imediatamente.

Para um controle preciso, como mostrar um ícone de carregamento somente quando o banco de dados local estiver vazio e uma sincronização de rede estiver ativa, acesse as propriedades source e mediator diretamente no elemento de composição.

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

Como pagingItems.loadState emite um novo objeto CombinedLoadStates sempre que prepend ou append muda durante a rolagem, envolva a verificação de atualização em derivedStateOf com remember(pagingItems). Isso impede que as atualizações de borda de rolagem recomponham um escopo pai que apenas alterna um indicador de atualização em tela cheia:

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

Reagir a mudanças de estado de carregamento

Talvez seja necessário acionar efeitos colaterais únicos com base em mudanças no estado de carregamento, como rolar até o topo de uma lista ou mostrar um Snackbar quando uma atualização for concluída ou falhar.

Use snapshotFlow dentro de um LaunchedEffect para observar as mudanças de estado como um stream. Isso permite aplicar operadores Flow padrão, como filter, distinctUntilChanged e distinctUntilChangedBy, para isolar eventos específicos.

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

Da mesma forma, é possível usar distinctUntilChangedBy em loadState para mostrar um erro Snackbar somente quando o estado refresh mudar para 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()
      }
    }
}

Outros recursos

Para mais informações sobre a biblioteca Paging e os estados de carregamento, consulte os recursos abaixo.

Documentação

Visualiza conteúdo