Quando lavori con dati paginati, spesso devi trasformare lo stream di dati durante il caricamento. Ad esempio, potresti dover filtrare un elenco di elementi o convertire gli elementi in un tipo diverso prima di presentarli nell'UI. Un altro caso d'uso comune per la trasformazione dello stream di dati è l'aggiunta di separatori di elenco.
In generale, l'applicazione delle trasformazioni direttamente allo stream di dati consente di mantenere separati i costrutti del repository e i costrutti dell'UI.
Questa pagina presuppone che tu conosca l'utilizzo di base della libreria Paging.
Applicare le trasformazioni di base
Poiché PagingData è
incapsulato in uno stream reattivo, puoi applicare operazioni di trasformazione ai
dati in modo incrementale tra il caricamento e la presentazione dei dati.
Per applicare le trasformazioni a ogni PagingData oggetto nello stream,
inserisci le trasformazioni all'interno di un'
map()
operazione sullo stream:
pager.flow // Type is Flow<PagingData<User>>. // Map the outer stream so that the transformations are applied to // each new generation of PagingData. .map { pagingData -> // Transformations in this block are applied to the items // in the paged data. }
Convertire i dati
L'operazione più semplice su uno stream di dati è la conversione in un tipo diverso. Una volta che hai accesso all'oggetto PagingData, puoi eseguire un'operazione map() su ogni singolo elemento nell'elenco paginato all'interno dell'oggetto PagingData.
Un caso d'uso comune è mappare un oggetto del livello di rete o di database su un oggetto utilizzato specificamente nel livello dell'UI. L'esempio seguente mostra come applicare questo tipo di operazione di mapping:
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.map { user -> UiModel(user) } }
Un'altra conversione di dati comune consiste nel prendere un input dell'utente, ad esempio una stringa di query, e convertirlo nell'output della richiesta da visualizzare. Per configurare questa operazione, devi ascoltare e acquisire l'input della query dell'utente, eseguire la richiesta e inviare il risultato della query all'UI.
Puoi ascoltare l'input della query utilizzando un'API stream. Mantieni il riferimento allo stream nel tuo ViewModel. Il livello dell'UI non deve avere accesso diretto; definisci invece una funzione per notificare a ViewModel la query dell'utente.
private val queryFlow = MutableStateFlow("") fun onQueryChanged(query: String) { queryFlow.value = query }
Quando il valore della query cambia nello stream di dati, puoi eseguire operazioni per convertire il valore della query nel tipo di dati desiderato e restituire il risultato al livello dell'UI. La funzione di conversione specifica dipende dal linguaggio e dal framework utilizzati, ma tutti forniscono funzionalità simili.
val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query -> // The database query returns a Flow which is output through // querySearchResults userDatabase.searchBy(query) }
L'utilizzo di operazioni come flatMapLatest o switchMap garantisce che all'UI vengano restituiti solo i risultati più recenti. Se l'utente modifica l'input della query prima del completamento dell'operazione del database, queste operazioni ignorano i risultati della vecchia query e avviano immediatamente la nuova ricerca.
Filtrare i dati
Un'altra operazione comune è il filtraggio. Puoi filtrare i dati in base ai criteri dell'utente oppure rimuovere i dati dall'UI se devono essere nascosti in base ad altri criteri.
Devi inserire queste operazioni di filtro all'interno della chiamata map() perché il filtro si applica all'oggetto PagingData. Una volta che i dati vengono filtrati da PagingData, la nuova istanza PagingData viene passata al livello dell'UI per la visualizzazione.
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } }
Aggiungere separatori di elenco
La libreria Paging supporta i separatori di elenco dinamici. Puoi migliorare la leggibilità dell'elenco inserendo i separatori direttamente nello stream di dati come elementi componibili nel layout. Di conseguenza, i separatori sono elementi componibili completi, che consentono di ottenere semantica di interattività, stile e accessibilità complete.
L'inserimento dei separatori nell'elenco paginato prevede tre passaggi:
- Converti il modello dell'UI per adattarlo agli elementi separatori. Un modo per farlo è racchiudere l'elemento dati e il separatore in una singola classe sealed. In questo modo, l'UI può gestire più tipi di elementi nello stesso elenco.
- Trasforma lo stream di dati per aggiungere dinamicamente i separatori tra il caricamento e la presentazione dei dati.
- Aggiorna l'UI per gestire gli elementi separatori.
Convertire il modello dell'UI
La libreria Paging inserisce i separatori di elenco nell'UI come elementi di elenco effettivi, ma gli elementi separatori devono essere distinguibili dagli elementi dati nell'elenco per garantire che entrambi i tipi di elementi componibili vengano visualizzati in modo distinto. La soluzione consiste nel creare una classe sealed Kotlin con sottoclassi per rappresentare i dati e i separatori. In alternativa, puoi creare una classe base estesa dalla classe dell'elemento dell'elenco e dalla classe del separatore.
Supponiamo che tu voglia aggiungere separatori a un elenco paginato di elementi User. Il seguente snippet mostra come creare una classe base in cui le istanze possono essere un UserModel o un SeparatorModel:
sealed class UiModel { class UserModel(val id: String, val label: String) : UiModel() { constructor(user: User) : this(user.id, user.label) } class SeparatorModel(val description: String) : UiModel() }
Trasformare lo stream di dati
Devi applicare le trasformazioni allo stream di dati dopo averlo caricato e prima di presentarlo. Le trasformazioni devono:
- Convertire gli elementi dell'elenco caricati in modo che riflettano il nuovo tipo di articolo base.
- Utilizzare il metodo
PagingData.insertSeparators()per aggiungere i separatori.
Per saperne di più sulle operazioni di trasformazione, consulta Applicare le trasformazioni di base.
L'esempio seguente mostra le operazioni di trasformazione per aggiornare lo
PagingData<User> stream a uno PagingData<UiModel> stream con i separatori
aggiunti:
pager.flow.map { pagingData: PagingData<User> -> // Map outer stream, so you can perform transformations on // each paging generation. pagingData .map { user -> // Convert items in stream to UiModel.UserModel. UiModel.UserModel(user) } .insertSeparators<UiModel.UserModel, UiModel> { before, after -> when { before == null -> UiModel.SeparatorModel("HEADER") after == null -> UiModel.SeparatorModel("FOOTER") shouldSeparate(before, after) -> UiModel.SeparatorModel( "BETWEEN ITEMS $before AND $after" ) // Return null to avoid adding a separator between two items. else -> null } } }
Gestire i separatori nell'UI
L'ultimo passaggio consiste nel modificare l'UI per adattarla al tipo di elemento separatore.
In un layout lazy, puoi gestire più tipi di elementi controllando il tipo di ogni UiModel emesso. Quando scorri i dati paginati, utilizza un'istruzione when per chiamare l'elemento componibile appropriato. In questo modo puoi fornire un'UI distinta per gli elementi dati e i separatori.
@Composable fun UserList(pagingItems: LazyPagingItems) { LazyColumn { items( count = pagingItems.itemCount, key = { index -> val item = pagingItems.peek(index) when (item) { is UiModel.UserModel -> item.user.id is UiModel.SeparatorModel -> item.description else -> index } } ) { index -> when (val item = pagingItems[index]) { is UiModel.UserModel -> UserItemComposable(item.user) is UiModel.SeparatorModel -> SeparatorComposable(item.description) null -> PlaceholderComposable() } } } }
Evitare il lavoro duplicato
Un problema fondamentale da evitare è che l'app esegua operazioni non necessarie. Il recupero dei dati è un'operazione costosa e anche le trasformazioni dei dati possono richiedere molto tempo. Una volta caricati e preparati per la visualizzazione nell'UI, i dati devono essere salvati in caso di modifica della configurazione e di ricreazione dell'UI.
L'operazione cachedIn() memorizza nella cache i risultati di tutte le trasformazioni che si verificano prima. In genere, questo operatore viene applicato in ViewModel prima di esporre Flow agli elementi componibili.
Per gestire correttamente la cache, passa un CoroutineScope a cachedIn(), come mostrato nell'esempio seguente che utilizza viewModelScope.
pager.flow // Type is Flow<PagingData<User>>. .map { pagingData -> pagingData.filter { user -> !user.hiddenFromUi } .map { user -> UiModel.UserModel(user) } } .cachedIn(viewModelScope)
Per saperne di più sull'utilizzo di cachedIn() con uno stream di PagingData, consulta
Configurare uno stream di
PagingData.
Risorse aggiuntive
Per saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:
Documentazione
Visualizzare i contenuti
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Caricare e visualizzare i dati paginati
- Testare l'implementazione di Paging
- Gestire e presentare gli stati di caricamento