Livello UI

Il ruolo dell'interfaccia utente è visualizzare i dati dell'applicazione sullo schermo e fungere anche da punto di interazione principale con l'utente. Ogni volta che i dati cambiano, a causa dell'interazione dell'utente (ad esempio la pressione di un pulsante) o di un input esterno (come una risposta di rete), l'interfaccia utente deve aggiornarsi per riflettere tali modifiche. Di fatto, l'interfaccia utente è una rappresentazione visiva dello stato dell'applicazione recuperata dal livello dati.

Tuttavia, i dati dell'applicazione che ottieni dal livello dati hanno solitamente un formato diverso rispetto alle informazioni che devi visualizzare. Ad esempio, potresti aver bisogno solo di una parte dei dati per l'interfaccia utente oppure potresti dover unire due origini dati diverse per presentare informazioni pertinenti per l'utente. Indipendentemente dalla logica applicata, devi passare all'interfaccia utente tutte le informazioni necessarie per il rendering completo. Il livello UI è la pipeline che converte le modifiche ai dati delle applicazioni in un modulo che può essere presentato dall'interfaccia utente e poi visualizzali.

In una tipica architettura, gli elementi UI del livello UI dipendono dai titolari di stato, che a loro volta dipendono dalle classi del livello dati o del livello di 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 gli articoli da far leggere all'utente. L'app dispone di una schermata di articoli che presenta gli articoli disponibili per la lettura e consente inoltre agli utenti che hanno eseguito l'accesso di aggiungere ai preferiti gli articoli che si distinguono dagli altri. Dato che in un determinato momento sono disponibili molti articoli, il lettore dovrebbe essere in grado di sfogliare gli articoli per categoria. Per riassumere, l'app consente agli utenti di:

  • Visualizza gli articoli disponibili per la lettura.
  • Sfogliare gli articoli per categoria.
  • Accedi e aggiungi ai preferiti alcuni articoli.
  • Accedere ad alcune funzionalità premium, se idonee.
Figura 2. Esempio di app di notizie per un case study sulla UI.

Le sezioni seguenti utilizzano questo esempio come case study per introdurre i principi del flusso di dati unidirezionale e per illustrare i problemi che questi principi consentono di risolvere nel contesto dell'architettura dell'app per il livello UI.

Architettura del livello UI

Il termine UI si riferisce a elementi dell'interfaccia utente come attività e frammenti che visualizzano i dati, indipendentemente dalle API utilizzate per eseguire questa operazione (Views o Jetpack Compose). Poiché il ruolo del livello dati è conservare, gestire e fornire l'accesso ai dati dell'app, il livello UI deve eseguire i seguenti passaggi:

  1. Consuma i dati dell'app e trasformali in dati di cui la UI può facilmente eseguire il rendering.
  2. Utilizza dati di cui è possibile eseguire il rendering dell'interfaccia utente e trasformali in elementi dell'interfaccia utente da presentare all'utente.
  3. Utilizza gli eventi di input utente provenienti da questi elementi assemblati dell'interfaccia utente e riflette 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 della guida illustra come implementare un livello UI che esegue questi passaggi. In particolare, questa guida tratta le attività e i concetti seguenti:

  • Come definire lo stato dell'interfaccia utente.
  • Flusso di dati unidirezionale (UDF) come mezzo per generare e gestire lo stato dell'interfaccia utente.
  • Come esporre lo stato dell'interfaccia utente con tipi di dati osservabili in base ai principi delle funzioni definite dall'utente.
  • Come implementare l'UI che utilizza lo stato osservabile dell'UI.

Il più importante di questi è la definizione dello stato dell'interfaccia utente.

Definisci lo stato dell'interfaccia utente

Fai riferimento al case study descritto in precedenza. In breve, l'interfaccia utente mostra un elenco di articoli con alcuni metadati. Queste informazioni che l'app presenta all'utente sono lo stato della UI.

In altre parole: se l'interfaccia utente è ciò che vede l'utente, lo stato dell'interfaccia utente è ciò che l'app ha dichiarato di vedere. Come le due facce della stessa moneta, l'interfaccia utente è la rappresentazione visiva dello stato dell'interfaccia. Eventuali modifiche allo stato della UI vengono applicate immediatamente nella UI.

La UI è il risultato dell'associazione di elementi UI sullo schermo con lo stato UI.
Figura 3. La UI è il risultato dell'associazione di elementi UI sullo schermo con lo stato UI.

Prendi in considerazione il case study; per soddisfare i requisiti dell'app di notizie, le informazioni necessarie per eseguire il rendering completo dell'interfaccia utente possono essere racchiuse 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,
    ...
)

Immutabilità

La definizione dello stato dell'interfaccia utente nell'esempio precedente è immutabile. Il vantaggio principale di ciò è che gli oggetti immutabili forniscono garanzie relative allo stato dell'applicazione in un determinato momento. In questo modo l'interfaccia utente può concentrarsi su un singolo ruolo, ovvero leggere lo stato e aggiornare gli elementi dell'interfaccia utente di conseguenza. Di conseguenza, non dovresti mai modificare lo stato dell'interfaccia utente direttamente nell'interfaccia utente, a meno che quest'ultima non sia l'unica origine dei dati. La violazione di questo principio comporta la presenza di più fonti attendibili per le stesse informazioni, causando incongruenze nei dati e lievi bug.

Ad esempio, se il flag bookmarked in un oggetto NewsItemUiState dello stato dell'interfaccia utente nel case study fosse aggiornato nella classe Activity, quel flag sarebbe in concorrenza con il livello dati come origine dello stato aggiunto ai preferiti di un articolo. Le classi di dati immutabili sono molto utili per evitare questo tipo di anti-pattern.

Convenzioni di denominazione in questa guida

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

funzionalità + UiState.

Ad esempio, lo stato di una schermata su cui sono mostrate notizie potrebbe essere denominato NewsUiState, mentre lo stato di una notizia in un elenco di notizie potrebbe essere NewsItemUiState.

Gestisci lo stato con il flusso di dati unidirezionale

Nella sezione precedente è stato stabilito che lo stato della UI è uno snapshot immutabile dei dettagli necessari per il rendering della UI. Tuttavia, la natura dinamica dei dati nelle app significa 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 completare l'app.

Queste interazioni possono trarre vantaggio da un mediatore per elaborarle, definendo la logica da applicare a ciascun evento e eseguendo le trasformazioni necessarie alle origini dati di supporto per creare lo stato dell'interfaccia utente. Queste interazioni e la loro logica possono essere ospitate all'interno della UI stessa, ma ciò può diventare rapidamente poco gestibile man mano che la UI inizia a diventare superiore a quanto suggerito dal suo nome: diventa proprietario dei dati, producer, Transformer e altro ancora. Inoltre, ciò può influire sulla verificabilità perché il codice risultante è un amalgama strettamente accoppiato senza confini distinguibili. In definitiva, l'interfaccia utente trae vantaggio da un carico di lavoro ridotto. A meno che lo stato dell'interfaccia utente non sia molto semplice, l'unica responsabilità dell'interfaccia utente dovrebbe essere utilizzare e visualizzare lo stato.

Questa sezione illustra un flusso di dati unidirezionale (UDF), un modello di architettura che consente di applicare questa separazione integro delle responsabilità.

Detentori statali

Le classi responsabili della produzione dello stato dell'interfaccia utente e contenenti la logica necessaria per questa attività sono chiamate proprietari di stato. I titolari degli stati sono disponibili in varie dimensioni a seconda dell'ambito degli elementi dell'interfaccia utente corrispondenti che gestiscono, da un singolo widget come una barra dell'app nella parte inferiore, fino a un'intera schermata o a una destinazione di navigazione.

Nel secondo caso, l'implementazione tipica è un'istanza di un ViewModel, sebbene, a seconda dei requisiti dell'applicazione, una classe semplice potrebbe essere sufficiente. L'app News del case study, ad esempio, utilizza una classe NewsViewModel come proprietario di uno stato per produrre lo stato dell'interfaccia utente per la schermata visualizzata in quella sezione.

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

I dati dell&#39;applicazione passano dal livello dati al ViewModel. Lo stato della UI passa da ViewModel agli elementi UI, mentre gli eventi dagli elementi UI di nuovo al ViewModel.
Figura 4. Diagramma del funzionamento della funzione definita dall'utente nell'architettura dell'app.

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

  • ViewModel memorizza ed espone lo stato che deve essere utilizzato dall'interfaccia utente. Lo stato dell'interfaccia utente è costituito dai dati dell'applicazione trasformati da ViewModel.
  • L'interfaccia utente invia una notifica al ViewModel degli eventi utente.
  • ViewModel gestisce le azioni dell'utente e aggiorna lo stato.
  • Lo stato aggiornato viene restituito all'interfaccia utente per il rendering.
  • Quanto riportato sopra viene ripetuto per ogni evento che causa una mutazione dello stato.

Per le destinazioni o le schermate di navigazione, ViewModel funziona con repository o classi di casi d'uso per ottenere dati e trasformarli nello stato dell'interfaccia utente, incorporando al contempo gli effetti di eventi che potrebbero causare mutazioni dello stato. Il case study citato in precedenza contiene un elenco di articoli, ciascuno con un titolo, una descrizione, una fonte, il nome dell'autore, la data di pubblicazione e l'eventuale aggiunta ai preferiti. L'interfaccia utente per ogni articolo ha il seguente aspetto:

Figura 5. UI di un articolo nell'app del case study.

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

Un evento UI si verifica quando l&#39;utente aggiunge un articolo ai preferiti. ViewModel avvisa il livello dati del cambiamento di stato. Il livello dati persiste 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 poi 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 funzione definita dall'utente.

Le seguenti sezioni analizzano più attentamente gli eventi che causano modifiche di stato e il modo in cui possono essere elaborati utilizzando la funzione definita dall'utente.

Tipi di logica

L'aggiunta ai preferiti di un articolo è un esempio di logica di business perché dà valore alla tua app. Per scoprire di più in merito, consulta la pagina relativa al livello dati. Tuttavia, è importante definire diversi tipi di logica:

  • La logica di business è l'implementazione dei requisiti di prodotto per i dati delle app. Come già accennato, un esempio è l'aggiunta ai preferiti di un articolo nell'app del case study. La logica di business in genere è inserita nel dominio o nei livelli dati, ma mai nel livello UI.
  • La logica del comportamento dell'interfaccia utente, o logica dell'interfaccia utente, indica come visualizzare le modifiche di stato sullo schermo. Alcuni esempi sono: 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 visualizzare un messaggio utente sullo schermo utilizzando un toast o uno snackbar.

La logica dell'interfaccia utente, in particolare quando riguarda tipi di UI come Context, deve trovarsi nell'interfaccia utente, non nel ViewModel. Se la UI diventa più complessa e vuoi delegare la logica dell'interfaccia utente a un'altra classe per favorire la testabilità e la separazione dei problemi, puoi creare una classe semplice come proprietario di uno stato. Le classi semplici create nell'interfaccia utente possono assumere dipendenze dell'SDK Android perché seguono il ciclo di vita dell'interfaccia utente; gli oggetti ViewModel hanno una durata maggiore.

Per ulteriori informazioni sui titolari di stato e su come si inseriscono nel contesto dell'assistenza alla UI per la creazione, consulta la guida per lo stato di Jetpack Compose.

Perché utilizzare le funzioni definite dall'utente?

UDF modella il ciclo di produzione degli stati, come mostrato nella Figura 4. Separa inoltre il luogo in cui hanno origine i cambiamenti di stato, il luogo in cui vengono trasformati e il luogo in cui vengono finalmente consumati. Questa separazione consente all'interfaccia utente di fare esattamente ciò che implica il suo nome: visualizzare le informazioni osservando i cambiamenti di stato e inoltrare l'intent dell'utente passando queste modifiche al modello ViewModel.

In altre parole, la funzione definita dall'utente consente di:

  • Coerenza dei dati. Esiste un'unica fonte attendibile per l'interfaccia utente.
  • Testabilità. La sorgente dello stato è isolata e quindi testabile indipendentemente dall'interfaccia utente.
  • Mantenabilità. La mutazione dello stato segue un modello ben definito in cui le mutazioni sono il risultato sia di eventi utente che delle origini dei dati da cui ricavano.

Esponi stato UI

Dopo aver definito lo stato dell'interfaccia utente e stabilito come gestirai la produzione, il passaggio successivo consiste nel presentare lo stato generato all'interfaccia utente. Poiché utilizzi la funzione definita dall'utente per gestire la produzione dello stato, puoi considerare lo stato prodotto come un flusso, ovvero nel corso del tempo verranno prodotte più versioni dello stato. Di conseguenza, devi esporre lo stato dell'interfaccia utente in un titolare di dati osservabile come LiveData o StateFlow. Il motivo è che l'interfaccia utente può reagire a qualsiasi modifica apportata allo stato senza dover estrarre manualmente i dati direttamente da ViewModel. Questi tipi hanno inoltre il vantaggio di avere sempre la versione più recente dello stato dell'interfaccia utente memorizzata nella cache, il che è utile per il ripristino rapido dello stato dopo le modifiche alla configurazione.

Visualizzazioni

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

    val uiState: StateFlow<NewsUiState> = …
}

Scrivi

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

    val uiState: NewsUiState = …
}

Per un'introduzione a LiveData in qualità di titolare osservabile dei dati, consulta questo codelab. Per un'introduzione simile ai flussi Kotlin, consulta Flussi Kotlin su Android.

Nei casi in cui i dati esposti all'interfaccia utente sono relativamente semplici, spesso conviene includerli in un tipo di stato dell'interfaccia utente perché trasmette la relazione tra l'emissione del proprietario dello stato e la schermata o l'elemento UI associati. Inoltre, man mano che l'elemento UI diventa più complesso, è sempre più facile aggiungere la definizione dello stato dell'interfaccia utente per includere le informazioni aggiuntive necessarie per il rendering dell'elemento.

Un modo comune per creare un flusso di UiState è esporre un flusso modificabile di supporto come flusso immutabile da ViewModel, ad esempio esponendo un MutableStateFlow<UiState> come StateFlow<UiState>.

Visualizzazioni

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Scrivi

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

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

ViewModel può quindi esporre metodi che modificano internamente lo stato, pubblicando aggiornamenti per l'utilizzo dell'interfaccia utente. Prendiamo, ad esempio, il caso in cui deve essere eseguita un'azione asincrona: una coroutina può essere avviata utilizzando viewModelScope e lo stato modificabile può essere aggiornato al completamento.

Visualizzazioni

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

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

Scrivi

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 cerca di recuperare gli articoli per una determinata categoria, quindi riflette il risultato del tentativo (riuscita o non riuscita) nello stato dell'interfaccia utente, in cui può reagire in modo appropriato. Consulta la sezione Mostrare gli errori sullo schermo per scoprire di più sulla gestione degli errori.

Considerazioni aggiuntive

Oltre alle indicazioni precedenti, considera quanto segue durante l'esposizione dello stato dell'interfaccia utente:

  • Un oggetto stato UI deve gestire stati correlati tra loro. Questo comporta meno incoerenze e rende il codice più facile da capire. Se mostri l'elenco di notizie e il numero di preferiti in due stream diversi, potresti ritrovarti in una situazione in cui uno è stato aggiornato e l'altro no. Quando usi un unico stream, entrambi gli elementi vengono mantenuti aggiornati. Inoltre, alcune logiche di business potrebbero richiedere una combinazione di origini. Ad esempio, potresti dover mostrare un pulsante ai preferiti solo se l'utente ha eseguito l'accesso e è abbonato a un servizio di notizie premium. Puoi definire una classe di stato dell'interfaccia utente come segue:

    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 Preferito è una proprietà derivata di altre due proprietà. Man mano che la logica di business diventa più complessa, avere una classe UiState singolare in cui tutte le proprietà sono immediatamente disponibili diventa sempre più importante.

  • Stati UI: stream singolo o stream multipli? Il principio guida fondamentale per scegliere tra l'esposizione dello stato dell'interfaccia utente in un singolo flusso o in più flussi è il punto elenco precedente, ovvero la relazione tra gli elementi emessi. Il più grande vantaggio di un'esposizione a flusso singolo è la comodità e la coerenza dei dati: i consumatori statali hanno sempre a disposizione le informazioni più recenti in qualsiasi momento. Tuttavia, in alcuni casi potrebbero essere appropriati flussi di stato distinti da ViewModel:

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

    • Differenza di UiState: maggiore è il numero di campi presenti in un oggetto UiState, più è probabile che il flusso venga emesso a seguito dell'aggiornamento di uno dei suoi campi. Poiché le viste non hanno un meccanismo di differenziazione per capire se le emissioni consecutive sono diverse o uguali, ogni emissione causa un aggiornamento della visualizzazione. Ciò significa che potrebbe essere necessaria la mitigazione mediante le API Flow o metodi come distinctUntilChanged() sull'LiveData.

Utilizza stato UI

Per utilizzare il flusso di oggetti UiState nell'interfaccia utente, utilizza l'operatore di terminale per il tipo di dati osservabili in uso. Ad esempio, per LiveData utilizzi il metodo observe() e per i flussi Kotlin utilizzi il metodo collect() o le sue varianti.

Quando utilizzi i titolari di dati osservabili nella UI, assicurati di prendere in considerazione il ciclo di vita dell'UI. Questo è importante perché la UI non deve osservare lo stato dell'UI quando la vista non viene mostrata all'utente. Per saperne di più su questo argomento, consulta questo post del blog. Quando si utilizza LiveData, LifecycleOwner si occupa implicitamente dei problemi del ciclo di vita. Quando utilizzi i flussi, è preferibile gestire questo problema con l'ambito coroutine appropriato e l'API repeatOnLifecycle:

Visualizzazioni

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Scrivi

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Mostra operazioni in corso

Un modo semplice per rappresentare gli stati di caricamento in una classe UiState è utilizzare 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.

Visualizzazioni

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Scrivi

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

Mostra errori sullo schermo

La visualizzazione degli errori nell'interfaccia utente è simile alla visualizzazione delle operazioni in corso perché sono entrambi facilmente rappresentati da valori booleani che ne indicano la presenza o l'assenza. Tuttavia, gli errori possono includere anche un messaggio associato da inoltrare all'utente o un'azione associata che ripete l'operazione non riuscita. Pertanto, mentre un'operazione in corso si carica o non si carica, è possibile che gli stati di errore debbano essere modellati con classi di dati che ospitano i metadati appropriati per il contesto dell'errore.

Prendiamo come esempio l'esempio della sezione 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 con i dettagli relativi all'errore.

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

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

I messaggi di errore potrebbero poi essere presentati all'utente sotto forma di elementi dell'interfaccia utente, ad esempio snackbar. Poiché è correlato al modo in cui gli eventi UI vengono prodotti e utilizzati, consulta la pagina Eventi UI per saperne di più.

Threading e contemporaneità

Qualsiasi lavoro eseguito in un ViewModel deve essere main-safe, in modo da poterlo chiamare dal thread principale. Questo perché i livelli dati e dominio sono responsabili dello spostamento del lavoro in un thread diverso.

Se un ViewModel esegue operazioni a lunga esecuzione, è anche responsabile dello spostamento di quella 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 Le coroutine Kotlin su Android.

I cambiamenti nella navigazione nelle app sono spesso dovuti a emissioni simili a quelle degli eventi. Ad esempio, dopo che una classe SignInViewModel ha eseguito un accesso, in UiState il campo isSignedIn potrebbe essere impostato su true. Attivatori come questi dovrebbero essere consumati come quelli descritti nella sezione Utilizzo dello stato dell'interfaccia utente sopra, ad eccezione del fatto che l'implementazione del consumo deve rinviare al componente di navigazione.

Cercapersone

La libreria di pagine di destinazione viene utilizzata nell'interfaccia utente con un tipo denominato PagingData. Poiché PagingData rappresenta e contiene elementi che possono cambiare nel tempo, ovvero non è di tipo immutabile, non deve essere rappresentato in uno stato UI immutabile. Devi invece esporlo da ViewModel in modo indipendente nel proprio flusso. Per un esempio specifico, consulta il codelab relativo a Paging Android.

Animazioni

Per fornire transizioni di navigazione di primo livello fluide e fluide, ti consigliamo di attendere che i dati vengano caricati sul secondo schermo prima di avviare l'animazione. Il framework di viste Android fornisce hook per ritardare le transizioni tra le destinazioni dei frammenti con le API postponeEnterTransition() e startPostponedEnterTransition(). Queste API consentono di garantire che gli elementi dell'interfaccia utente sulla seconda schermata (in genere un'immagine recuperata dalla rete) siano pronti per essere visualizzati prima che l'interfaccia utente attivi la transizione a quella schermata. Per ulteriori dettagli e specifiche sull'implementazione, guarda l'esempio di Android Motion.

Samples

I seguenti esempi di Google mostrano l'uso del livello UI. Esplorale per vedere concretamente queste indicazioni: