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 LoadState classe.

Um sinal LoadState separado é fornecido para cada LoadType e cada tipo de fonte de dados (seja PagingSource ou RemoteMediator). O CombinedLoadStates objeto 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 LoadState.Error objeto.

Acesse esses estados pela loadState propriedade do LazyPagingItems wrapper. Você pode usar esse estado de duas maneiras: processando a visibilidade do conteúdo principal (como um indicador de atualização de tela cheia) ou inserindo itens de carregamento diretamente no fluxo LazyColumn (como um indicador de rodapé).

Acessar o estado de carregamento usando um listener

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

No exemplo a seguir, a interface mostra um indicador 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.

Você pode monitorar o estado de inclusão para o cabeçalho e o estado de anexação para o rodapé usando o CombinedLoadStates objeto.

No exemplo a seguir, a lista mostra uma barra de progresso ou um botão "Tentar novamente" 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, isso oculta a diferença entre o carregamento do banco de dados local (PagingSource) e da rede (RemoteMediator). Isso pode fazer com que a interface mostre brevemente um indicador de carregamento, mesmo quando os dados armazenados em cache estão imediatamente disponíveis.

Para um controle preciso, como mostrar um indicador de carregamento apenas 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 combinável.

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

Reagir a mudanças de estado de carregamento

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

Use snapshotFlow dentro de um LaunchedEffect para observar as mudanças de estado como um fluxo. Isso permite aplicar operadores padrão Flow como filter e distinctUntilChanged 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)
    }
}

Outros recursos

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

Documentação

Conteúdo de visualizações