Carica e visualizza i dati impaginati

La libreria Paging offre potenti funzionalità per il caricamento e la visualizzazione di dati paginati da un set di dati più grande. Questa guida mostra come utilizzare la libreria Paging per configurare un flusso di dati impaginati da un'origine dati di rete e visualizzarli in un elenco pigro.

Definire un'origine dati

Il primo passaggio consiste nel definire un'implementazione PagingSource per identificare l'origine dati. La classe API PagingSource include il metodo load, che esegui l'override per indicare come recuperare i dati impaginati dall'origine dati corrispondente.

Utilizza direttamente la classe PagingSource per utilizzare le coroutine Kotlin per il caricamento asincrono.

Seleziona i tipi di chiave e valore

PagingSource<Key, Value> ha due parametri di tipo: Key e Value. La chiave definisce l'identificatore utilizzato per caricare i dati, mentre il valore è il tipo di dati stesso. Ad esempio, se carichi pagine di oggetti User dalla rete passando i numeri di pagina Int a Retrofit, seleziona Int come tipo Key e User come tipo Value.

Definisci PagingSource

L'esempio seguente implementa un PagingSource che carica le pagine degli elementi in base al numero di pagina. Il tipo Key è Int e il tipo Value è User.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Una tipica implementazione di PagingSource passa i parametri forniti nel suo costruttore al metodo load per caricare i dati appropriati per una query. Nell'esempio riportato sopra, questi parametri sono:

  • backend: un'istanza del servizio di backend che fornisce i dati
  • query: la query di ricerca da inviare al servizio indicato da backend

L'oggetto LoadParams contiene informazioni sull'operazione di caricamento da eseguire. Sono inclusi la chiave da caricare e il numero di elementi da caricare.

L'oggetto LoadResult contiene il risultato dell'operazione di caricamento. LoadResult è una classe sigillata che assume una delle tre forme, a seconda che la chiamata load sia andata a buon fine:

  • Se il caricamento ha esito positivo, restituisci un oggetto LoadResult.Page.
  • Se il caricamento non va a buon fine, restituisci un oggetto LoadResult.Error.
  • Se PagingSource non è più valido e deve essere sostituito da una nuova istanza (ad esempio, a causa di una modifica dei dati sottostanti), restituisci un oggetto LoadResult.Invalid.

La figura seguente illustra come la funzione load in questo esempio riceve la chiave per ogni caricamento e fornisce la chiave per il caricamento successivo.

A ogni chiamata di caricamento, ExamplePagingSource riceve la chiave corrente
    e restituisce la chiave successiva da caricare.
Figura 1. Diagramma che mostra come load utilizza e aggiorna la chiave.

L'implementazione di PagingSource deve implementare anche un metodo getRefreshKey che accetta un oggetto PagingState come parametro. Restituisce la chiave da passare al metodo load quando i dati vengono aggiornati o invalidati dopo il caricamento iniziale. La libreria Paging chiama questo metodo automaticamente nei successivi aggiornamenti dei dati.

Gestisci gli errori

Le richieste di caricamento dei dati possono non riuscire per diversi motivi, soprattutto quando il caricamento avviene tramite una rete. Segnala gli errori riscontrati durante il caricamento restituendo un oggetto LoadResult.Error dal metodo load.

Ad esempio, puoi rilevare e segnalare gli errori di caricamento in ExamplePagingSource dell'esempio precedente aggiungendo quanto segue al metodo load:

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

Per ulteriori informazioni sulla gestione degli errori di Retrofit, consulta gli esempi nel riferimento API PagingSource.

PagingSource raccoglie e distribuisce gli oggetti LoadResult.Error all'interfaccia utente in modo che tu possa agire di conseguenza. Per saperne di più sull'esposizione dello stato di caricamento nell'interfaccia utente, consulta Gestire e presentare gli stati di caricamento.

Configurare un flusso di PagingData

A questo punto, hai bisogno di un flusso di dati paginati dall'implementazione di PagingSource. Configura lo stream di dati in ViewModel. La classe Pager fornisce metodi che espongono un flusso reattivo di oggetti PagingData da un PagingSource. La libreria Paging espone il flusso di dati come Flow.

Quando crei un'istanza Pager per configurare lo stream reattivo, devi fornire all'istanza un oggetto di configurazione PagingConfig e una funzione che indichi a Pager come ottenere un'istanza dell'implementazione di PagingSource, come mostrato nell'esempio seguente.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

L'operatore cachedIn rende condivisibile lo stream di dati e memorizza nella cache i dati caricati con il CoroutineScope fornito. Senza cachedIn, non è possibile recuperare PagingData. Questo esempio utilizza viewModelScope fornito dall'artefatto del ciclo di vita lifecycle-viewmodel-ktx.

L'oggetto Pager chiama il metodo load dall'oggetto PagingSource, fornendogli l'oggetto LoadParams e ricevendo in cambio l'oggetto LoadResult.

Raccogliere e visualizzare i dati nell'interfaccia utente

Per connettere lo stream paginato all'UI, ottieni il flusso da ViewModel e passalo al composable della lista.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Utilizza collectAsLazyPagingItems per convertire il flusso PagingData in LazyPagingItems. Quindi, utilizza l'API items all'interno di un LazyColumn per disporre ogni elemento.

Assicurati di fornire un identificatore univoco e stabile per ogni articolo utilizzando itemKey. Il seguente esempio utilizza it.id (che fa riferimento alla proprietà User.id) perché rimane stabile per l'istanza User durante gli aggiornamenti dei dati.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

La libreria Paging utilizza null per i segnaposto durante il caricamento di una pagina, quindi se hai attivato i segnaposto, devi gestire i valori null nel blocco di contenuti.

Ora l'elenco mostra i dati paginati e la libreria Paging carica pagine aggiuntive man mano che l'utente scorre la pagina.

Risorse aggiuntive

Per saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:

Documentazione

Visualizza contenuti