Livello UI

Il ruolo della UI è visualizzare i dati dell'applicazione sullo schermo. L'interfaccia utente funge anche da punto di interazione principale per gli utenti. Ogni volta che i dati cambiano, a causa dell'interazione dell'utente (ad esempio la pressione di un pulsante) o di un input esterno (ad esempio una risposta di rete), l'interfaccia utente si aggiorna per riflettere queste modifiche. In sostanza, la UI è una rappresentazione visiva dello stato dell'applicazione recuperato dal livello dati.

Tuttavia, i dati dell'applicazione che ottieni dal livello dati sono in genere in un formato diverso rispetto alle informazioni che devi visualizzare. Ad esempio, potresti aver bisogno solo di una parte dei dati per la UI o potresti dover unire due origini dati diverse per presentare informazioni pertinenti per l'utente. Indipendentemente dalla logica che applichi, devi trasmettere all'interfaccia utente tutte le informazioni necessarie per il rendering completo. Il livello UI è la pipeline che converte le modifiche ai dati dell'applicazione in un formato che la UI può presentare e poi visualizzare.

In un'architettura tipica, gli elementi UI del livello UI dipendono dai
    gestori di stato, che a loro volta dipendono dalle classi del livello dati o del
    livello del dominio facoltativo.
Figura 1. Il ruolo del livello UI nell'architettura dell'app.

Un case study di base

Prendi in considerazione un'app che recupera articoli di notizie da leggere per un utente. L'app ha una schermata degli articoli che presenta gli articoli disponibili per la lettura e consente agli utenti che hanno eseguito l'accesso di aggiungere ai preferiti gli articoli che si distinguono particolarmente. Dato che in qualsiasi momento potrebbero essere disponibili molti articoli, il lettore deve essere in grado di sfogliarli per categoria. In sintesi, l'app consente agli utenti di:

  • Visualizza gli articoli disponibili per la lettura.
  • Sfoglia gli articoli per categoria.
  • Accedere e aggiungere ai preferiti determinati articoli.
  • Accedere ad alcune funzionalità premium, se idonee.
Un'app di notizie di esempio che mostra le anteprime degli articoli, uno dei quali è stato aggiunto ai preferiti.
Figura 2. Un'app di notizie di esempio per un case study sulla UI.

Le sezioni seguenti utilizzano questo esempio come caso di studio per introdurre i principi del flusso di dati unidirezionale, oltre a illustrare i problemi che questi principi aiutano a risolvere nel contesto dell'architettura dell'app per il livello UI.

Architettura dello strato UI

Il termine UI si riferisce a elementi dell'interfaccia utente come contenitori e funzioni componibili che visualizzano i dati. Per la creazione di UI Android, Jetpack Compose è il toolkit consigliato. Poiché il ruolo del livello dati è quello di contenere, gestire e fornire l'accesso ai dati dell'app, il livello UI deve eseguire i seguenti passaggi:

  1. Utilizza i dati delle app e trasformali in dati che la UI può visualizzare facilmente.
  2. Utilizza i dati visualizzabili nell'interfaccia utente e trasformali in elementi UI per la presentazione all'utente.
  3. Utilizza gli eventi di input dell'utente provenienti da questi elementi dell'interfaccia utente assemblati e rifletti i loro effetti nei dati dell'interfaccia utente, se necessario.
  4. Ripeti i passaggi da 1 a 3 per tutto il tempo necessario.

Il resto di questa guida mostra come implementare un livello UI che esegue questi passaggi. In particolare, questa guida tratta i seguenti concetti e attività:

  • Come definire lo stato dell'interfaccia utente
  • Flusso di dati unidirezionale (UDF) come mezzo per produrre e gestire lo stato dell'interfaccia utente
  • Come esporre lo stato della UI con tipi di dati osservabili in base ai principi UDF
  • Come implementare un'interfaccia utente che utilizza lo stato dell'interfaccia utente osservabile

Il più fondamentale è la definizione dello stato della UI.

Definisci lo stato dell'interfaccia utente

Nel case study descritto in precedenza, l'interfaccia utente mostra un elenco di articoli insieme ad alcuni metadati per ciascun articolo. Queste informazioni che l'app presenta all'utente sono lo stato dell'interfaccia utente.

In altre parole, se la UI è ciò che vede l'utente, lo stato della UI è ciò che l'app dice che dovrebbe vedere. Come le due facce della stessa medaglia, la UI è la rappresentazione visiva dello stato della UI. Eventuali modifiche allo stato della UI vengono immediatamente riflesse nella UI.

L'interfaccia utente è il risultato dell'associazione degli elementi UI sullo schermo con lo stato dell'interfaccia utente.
Figura 3. L'interfaccia utente è il risultato dell'associazione degli elementi UI sullo schermo con lo stato dell'interfaccia utente.

Considera il case study: per soddisfare i requisiti dell'app News, le informazioni necessarie per il rendering completo della UI possono essere incapsulate in una classe di dati NewsUiState definita come segue:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Per saperne di più sullo stato della UI, consulta Stato e Jetpack Compose.

Immutabilità

La definizione dello stato dell'interfaccia utente nell'esempio precedente è immutabile. Il vantaggio principale è che gli oggetti immutabili forniscono garanzie sullo stato dell'applicazione in un determinato momento. In questo modo, la UI può concentrarsi sul suo ruolo principale: leggere lo stato e aggiornare di conseguenza i suoi elementi. Non modificare mai lo stato della UI direttamente nella UI, a meno che la UI stessa non sia l'unica origine dei suoi dati. La violazione di questo principio comporta la creazione di più fonti di verità per la stessa informazione, il che porta a incoerenze nei dati e bug sottili.

Ad esempio, considera il case study precedente. Se il flag bookmarked in un oggetto NewsItemUiState dello stato dell'interfaccia utente viene aggiornato nella classe Activity, questo flag compete con il data layer come origine dello stato dei preferiti di un articolo. Le classi di dati immutabili sono molto utili per prevenire questo tipo di incoerenza.

Convenzioni di denominazione in questa guida

In questa guida, le classi di stato dell'interfaccia utente vengono denominate in base alla funzionalità della schermata o della parte di schermata che descrivono. La convenzione è la seguente:

funzionalità + UiState.

Ad esempio, lo stato di una schermata che mostra notizie potrebbe essere chiamato NewsUiState, mentre lo stato di una notizia in un elenco di notizie potrebbe essere un NewsItemUiState.

Gestire lo stato con il flusso di dati unidirezionale

La sezione precedente ha stabilito che lo stato dell'interfaccia utente è uno snapshot immutabile dei dettagli necessari per il rendering dell'interfaccia utente. Tuttavia, la natura dinamica dei dati nelle app implica che lo stato può cambiare nel tempo. Ciò potrebbe essere dovuto all'interazione dell'utente o ad altri eventi che modificano i dati sottostanti utilizzati per popolare l'app.

Queste interazioni possono trarre vantaggio da un mediatore per elaborarle, definendo la logica da applicare a ogni evento e trasformando le origini dati di backend per creare lo stato dell'interfaccia utente. Sebbene queste interazioni e la loro logica possano essere contenute nell'interfaccia utente stessa, ciò può diventare rapidamente ingombrante man mano che l'interfaccia utente si assume troppe responsabilità. Inoltre, ciò può influire sulla testabilità perché il codice risultante è strettamente accoppiato. A meno che lo stato dell'interfaccia utente non sia molto semplice, assicurati che l'unica responsabilità dell'interfaccia utente sia consumare e visualizzare lo stato dell'interfaccia utente.

Questa sezione descrive il flusso di dati unidirezionale (UDF), un pattern di architettura che contribuisce a imporre questa sana separazione delle responsabilità.

Contenitori di stato

I titolari dello stato sono le classi responsabili della produzione dello stato dell'interfaccia utente e della logica necessaria per produrlo. I contenitori di stato hanno dimensioni diverse a seconda dell'ambito degli elementi UI corrispondenti che gestiscono, da un singolo widget come una barra delle app inferiore a un'intera schermata o a una destinazione di navigazione.

In quest'ultimo caso, l'implementazione tipica è un'istanza di un ViewModel, anche se a seconda dei requisiti dell'applicazione, una semplice classe potrebbe essere sufficiente. L'app News dell'esempio pratico, ad esempio, utilizza una classe NewsViewModel come contenitore di stato per produrre lo stato dell'UI per la schermata visualizzata in quella sezione.

Esistono molti modi per modellare la codipendenza tra la UI e il relativo produttore di stato. Tuttavia, poiché l'interazione tra la UI e la relativa classe ViewModel può essere intesa in gran parte come input di eventi e il relativo output di stato, la relazione può essere rappresentata come mostrato nel seguente diagramma:

I dati dell&#39;applicazione vengono trasferiti dal livello dati al ViewModel. Lo stato dell&#39;interfaccia utente
    fluisce dal ViewModel agli elementi UI e gli eventi fluiscono dagli elementi UI
    al ViewModel.
Figura 4. Diagramma del funzionamento della UDF nell'architettura dell'app.

Il pattern in cui lo stato scorre verso il basso e gli eventi scorrono verso l'alto è chiamato flusso di dati unidirezionale (UDF). Le implicazioni di questo pattern per l'architettura dell'app sono le seguenti:

  • ViewModel contiene ed espone lo stato da utilizzare nell'interfaccia utente. Lo stato della UI è costituito dai dati dell'applicazione trasformati dalla ViewModel.
  • La UI invia una notifica al ViewModel degli eventi utente.
  • Il ViewModel gestisce le azioni utente e aggiorna lo stato.
  • Lo stato aggiornato viene restituito all'interfaccia utente per il rendering.
  • Quanto sopra viene ripetuto per qualsiasi evento che causi una mutazione dello stato.

Per le destinazioni o le schermate di navigazione, ViewModel funziona con i repository o le classi di casi d'uso per ottenere i dati e trasformarli nello stato dell'interfaccia utente, incorporando gli effetti degli eventi che potrebbero causare mutazioni dello stato. Il case study menzionato in precedenza contiene un elenco di articoli, ognuno con titolo, descrizione, fonte, nome dell'autore, data di pubblicazione e indicazione se è stato aggiunto ai preferiti. L'interfaccia utente di ogni elemento dell'articolo ha il seguente aspetto:

Un singolo articolo dell&#39;app Case study. La UI mostra una miniatura, il titolo dell&#39;articolo, l&#39;autore, il tempo di lettura stimato dell&#39;articolo e un&#39;icona segnalibro.
Figura 5. UI di un articolo nell'app Case Study.

Un utente che richiede di aggiungere un articolo ai preferiti è un esempio di evento che può causare mutazioni di stato. In qualità di produttore di stato, è responsabilità del ViewModel definire tutta la logica necessaria per compilare tutti i campi nello stato dell'interfaccia utente ed elaborare gli eventi necessari per il rendering completo dell'interfaccia utente.

Un evento UI si verifica quando l&#39;utente aggiunge un articolo ai preferiti. Il ViewModel
    notifica al livello dati la modifica dello stato. Il livello dati mantiene la
    modifica dei dati e aggiorna i dati dell&#39;applicazione. I nuovi dati dell&#39;app con l&#39;articolo
    aggiunto ai preferiti vengono passati al ViewModel, che produce il
    nuovo stato dell&#39;interfaccia utente e lo passa agli elementi dell&#39;interfaccia utente per la visualizzazione.
Figura 6. Diagramma che illustra il ciclo di eventi e dati nella UDF.

Le sezioni seguenti esaminano più da vicino gli eventi che causano modifiche dello stato e come possono essere elaborati utilizzando le UDF.

Tipi di logica

L'aggiunta di un articolo ai preferiti è un esempio di logica di business perché aggiunge valore alla tua app. Per saperne di più, consulta la pagina livello dati. Tuttavia, esistono diversi tipi di logica che è importante definire:

  • La logica di business è l'implementazione dei requisiti di prodotto per i dati delle app. Come già accennato, un esempio è l'aggiunta di un articolo ai preferiti nell'app di studio di caso. La logica di business viene in genere inserita nei livelli di dominio o di dati, ma mai nel livello UI.
  • La logica di comportamento della UI o logica della UI indica come visualizzare le modifiche di stato sullo schermo. Ad esempio, ottenere il testo corretto da mostrare sullo schermo utilizzando Android Resources, passare a una schermata specifica quando l'utente fa clic su un pulsante o mostrare un messaggio utente sullo schermo utilizzando un toast o uno snackbar.

Mantieni la logica dell'UI nell'UI, non nel ViewModel, in particolare quando coinvolge tipi di UI come Context. Se la complessità dell'interfaccia utente aumenta e vuoi delegare la logica dell'interfaccia utente a un'altra classe per favorire la testabilità e la separazione delle responsabilità, puoi creare una classe semplice come contenitore di stato. Le classi semplici create nell'interfaccia utente possono utilizzare le dipendenze dell'SDK Android perché seguono il ciclo di vita dell'interfaccia utente; gli oggetti ViewModel hanno una durata più lunga.

Per saperne di più sugli state holder e sul loro ruolo nella creazione di UI, consulta la guida a Jetpack Compose State.

Perché utilizzare le UDF?

La funzione definita dall'utente modella il ciclo di produzione dello stato come mostrato nella Figura 4. Separa inoltre il luogo in cui hanno origine le modifiche dello stato, il luogo in cui vengono trasformate e il luogo in cui vengono infine utilizzate. Questa separazione consente alla UI di fare esattamente ciò che implica il suo nome: visualizzare le informazioni osservando le modifiche dello stato e trasmettere l'intento dell'utente passando queste modifiche al ViewModel.

In altre parole, le UDF consentono quanto segue:

  • Coerenza dei dati. Esiste un'unica fonte attendibile per la UI.
  • Testabilità. L'origine dello stato è isolata e quindi testabile indipendentemente dalla UI.
  • Manutenibilità. La mutazione dello stato segue un pattern ben definito in cui le mutazioni sono il risultato sia degli eventi utente sia delle origini dei dati da cui vengono estratti.

Esporre lo stato dell'UI

Dopo aver definito lo stato dell'interfaccia utente e determinato come gestire la produzione di questo stato, il passaggio successivo consiste nel presentare lo stato prodotto all'interfaccia utente.

Quando utilizzi la funzione definita dall'utente per gestire la produzione dello stato, puoi considerare lo stato prodotto come un flusso, ovvero nel tempo vengono prodotte più versioni dello stato. Esporre lo stato dell'UI in un contenitore di dati osservabile come StateFlow. In questo modo, l'interfaccia utente reagisce a qualsiasi modifica apportata allo stato senza dover estrarre manualmente i dati direttamente dalla ViewModel. In questo modo, inoltre, si ha il vantaggio di avere sempre memorizzata nella cache l'ultima versione dello stato della UI, il che è utile per il ripristino rapido dello stato dopo le modifiche alla configurazione.

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = 
}

Per un'introduzione ai flussi Kotlin, consulta Flussi Kotlin su Android. Per scoprire come utilizzare StateFlow come contenitore di dati osservabile, consulta il codelab Stato ed effetti collaterali avanzati in Jetpack Compose.

Nei casi in cui i dati esposti all'interfaccia utente sono relativamente semplici, spesso vale la pena racchiuderli in un tipo di stato dell'interfaccia utente perché trasmette la relazione tra l'emissione del contenitore di stato e la schermata o l'elemento UI associato. Man mano che l'elemento UI diventa più complesso, è semplice aggiungerlo alla definizione dello stato dell'UI, in modo da poter ospitare le informazioni aggiuntive necessarie per il rendering dell'elemento UI.

Un modo comune per creare un flusso di UiState consiste nell'esporre una proprietà mutableStateOf con un private set, mantenendo lo stato modificabile all'interno di ViewModel ma di sola lettura per la UI.

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

Il ViewModel può quindi esporre metodi che modificano internamente lo stato, pubblicando aggiornamenti che la UI può utilizzare. Prendiamo ad esempio il caso in cui devi eseguire un'azione asincrona. Puoi avviare una coroutine utilizzando viewModelScope e poi aggiornare lo stato modificabile al termine.

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

Nell'esempio precedente, la classe NewsViewModel tenta di recuperare gli articoli per una determinata categoria e poi riflette il risultato del tentativo, che sia riuscito o fallito, nello stato dell'interfaccia utente, dove l'interfaccia utente può reagire in modo appropriato. Per saperne di più sulla gestione degli errori, consulta la sezione Mostrare gli errori sullo schermo.

Considerazioni aggiuntive

Oltre alle indicazioni precedenti, tieni presente quanto segue quando esponi lo stato della UI:

  • Utilizza un singolo oggetto stato UI per gestire gli stati correlati tra loro. Ciò comporta meno incoerenze e rende il codice più facile da comprendere. Se esponi l'elenco di notizie e il numero di preferiti in due flussi diversi, potresti trovarti in una situazione in cui uno è stato aggiornato e l'altro no. Quando utilizzi un singolo stream, entrambi gli elementi vengono aggiornati. Inoltre, alcune logiche di business potrebbero richiedere una combinazione di origini. Ad esempio, potresti dover mostrare un pulsante per i preferiti solo se l'utente ha eseguito l'accesso e se è abbonato a un servizio di notizie premium. Puoi definire una classe di stato della UI nel seguente modo:

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    In questa dichiarazione, la visibilità del pulsante Segnalibro è una proprietà derivata di altre due proprietà. Man mano che la logica di business diventa più complessa, avere una singola classe UiState in cui tutte le proprietà sono immediatamente disponibili diventa sempre più importante.

  • Stati dell'interfaccia utente: singolo stream o più stream? Il principio guida fondamentale per scegliere se esporre lo stato dell'interfaccia utente in un singolo flusso o in più flussi è la relazione tra gli elementi emessi. I maggiori vantaggi di un'esposizione a un singolo stream sono la praticità e la coerenza dei dati: i consumatori di stato dispongono sempre delle informazioni più recenti disponibili in qualsiasi momento. Tuttavia, esistono casi in cui potrebbero essere appropriati stream separati di stato dal ViewModel:

    • Tipi di dati non correlati: alcuni stati necessari per il rendering della UI potrebbero essere completamente indipendenti l'uno dall'altro. In questi casi, i costi di raggruppamento di questi stati diversi potrebbero superare i vantaggi, soprattutto se uno di questi stati viene aggiornato più frequentemente dell'altro.

    • Differenze UiState: più campi sono presenti in un oggetto UiState, più è probabile che lo stream venga emesso in seguito all'aggiornamento di uno dei suoi campi. Poiché gli elementi dell'interfaccia utente non dispongono di un meccanismo di differenziazione per comprendere se le emissioni consecutive sono diverse o uguali, ogni emissione causa un aggiornamento dell'elemento dell'interfaccia utente. Ciò significa che potrebbe essere necessario eseguire la mitigazione utilizzando i metodi API Flow come distinctUntilChanged().

Per saperne di più sul rendering e sullo stato della UI, consulta Ciclo di vita dei componenti componibili.

Utilizzare lo stato dell'interfaccia utente

Per utilizzare il flusso di oggetti UiState nell'interfaccia utente, utilizza l'operatore terminale per il tipo di dati osservabile che stai utilizzando. Ad esempio, per i flussi Kotlin utilizza il metodo collect() o le sue varianti.

Quando utilizzi i contenitori di dati osservabili nella UI, assicurati di considerare il ciclo di vita della UI. Non fare in modo che la UI osservi lo stato della UI quando il composable non viene visualizzato dall'utente. Per saperne di più su questo argomento, consulta questo post del blog. Quando utilizzi i flussi, è consigliabile gestire i problemi del ciclo di vita con l'ambito della coroutine appropriato e l'API collectAsStateWithLifecycle:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Mostra le operazioni in corso

Un modo semplice per rappresentare gli stati di caricamento in una classe UiState è con un campo booleano:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

Il valore di questo flag rappresenta la presenza o l'assenza di una barra di avanzamento nell'interfaccia utente.

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Mostrare gli errori sullo schermo

La visualizzazione degli errori nella UI è simile alla visualizzazione delle operazioni in corso perché entrambe sono facilmente rappresentate da valori booleani che indicano la loro presenza o assenza. Tuttavia, gli errori potrebbero includere anche un messaggio associato da inoltrare all'utente o un'azione associata che riprova l'operazione non riuscita. Pertanto, mentre un'operazione in corso è in fase di caricamento o non è in fase di caricamento, gli stati di errore potrebbero dover essere modellati con classi di dati che ospitano i metadati appropriati per il contesto dell'errore.

Considera l'esempio precedente che mostrava una barra di avanzamento durante il recupero degli articoli. Se questa operazione genera un errore, ti consigliamo di mostrare all'utente uno o più messaggi che spieghino in dettaglio cosa è andato storto.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

Potresti quindi presentare i messaggi di errore all'utente sotto forma di elementi dell'interfaccia utente come le snackbar. Per saperne di più su come vengono prodotti e utilizzati gli eventi UI, consulta Eventi UI.

Threading e concorrenza

Assicurati che tutto il lavoro eseguito in un ViewModel sia main-safe, ovvero sicuro da chiamare dal thread principale. I livelli di dati e dominio sono responsabili dello spostamento del lavoro in un thread diverso.

Se un ViewModel esegue operazioni a lunga esecuzione, è anche responsabile di spostare questa logica in un thread in background. Le coroutine Kotlin sono un ottimo modo per gestire le operazioni simultanee e i componenti dell'architettura Jetpack forniscono supporto integrato. Per scoprire di più sull'utilizzo delle coroutine nelle app per Android, consulta Coroutine Kotlin su Android.

Le modifiche alla navigazione nell'app sono spesso determinate da emissioni simili a eventi. Ad esempio, dopo che una classe SignInViewModel esegue l'accesso, UiState potrebbe avere un campo isSignedIn impostato su true. Utilizza trigger come questi proprio come quelli trattati nella sezione precedente Utilizzo dello stato della UI, ma rimanda l'implementazione dell'utilizzo al componente Navigation.

Per ulteriori informazioni sulla navigazione nell'interfaccia utente, vedi Navigazione 3.

Paging

La libreria Paging viene utilizzata nell'interfaccia utente con un tipo chiamato PagingData. Poiché PagingData rappresenta e contiene elementi che possono cambiare nel tempo, in altre parole, non è un tipo immutabile, non rappresentarlo in uno stato dell'interfaccia utente immutabile. Esporlo invece dalla ViewModel in modo indipendente nel proprio stream.

Il seguente esempio mostra l'API Compose della libreria Paging:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

Animazioni

Per fornire transizioni fluide nella navigazione di primo livello, potresti voler attendere che la seconda schermata carichi i dati prima di avviare l'animazione.

Per ulteriori informazioni sulle transizioni di navigazione, vedi Navigazione 3 e Transizioni degli elementi condivisi in Compose.

Risorse aggiuntive

Visualizza contenuti

Esempi

I seguenti esempi di Google mostrano l'utilizzo del livello UI. Esplorali per vedere queste indicazioni in pratica: