Architettura della UI di Compose

In Compose, la UI è immutabile: non è possibile aggiornarla dopo che è stata disegnata. Quello che puoi controllare è lo stato della tua UI. Ogni volta che lo stato della UI cambia, Compose ricrea le parti dell'albero della UI che sono cambiate. I componibili possono accettare lo stato ed esporre eventi, ad esempio un TextField accetta un valore ed espone un callback onValueChange che richiede al gestore callback di modificare il valore.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Poiché i componibili accettano lo stato ed espongono gli eventi, il pattern del flusso di dati unidirezionale si adatta bene a Jetpack Compose. Questa guida spiega come implementare il pattern del flusso di dati unidirezionale in Compose, come implementare gli eventi e i contenitori di stato e come utilizzare i ViewModel in Compose.

Flusso di dati unidirezionale

Un flusso di dati unidirezionale (UDF) è un pattern di progettazione in cui lo stato scorre verso il basso e gli eventi verso l'alto. Seguendo il flusso di dati unidirezionale, puoi disaccoppiare i componibili che mostrano lo stato nella UI dalle parti dell'app che archiviano e modificano lo stato.

Il ciclo di aggiornamento della UI per un'app che utilizza il flusso di dati unidirezionale è il seguente:

  1. Evento: una parte della UI genera un evento e lo passa verso l'alto, ad esempio un clic su un pulsante passato al ViewModel per la gestione; oppure un evento viene passato da altri livelli dell'app, ad esempio per indicare che la sessione utente è scaduta.
  2. Aggiornamento dello stato: un gestore di eventi potrebbe modificare lo stato.
  3. Mostra lo stato: il contenitore di stato trasmette lo stato e la UI lo mostra.
Gli eventi passano dalla UI a un contenitore di stato, mentre lo stato passa dal contenitore di stato alla UI.
Figura 1. Flusso di dati unidirezionale.

Seguire questo pattern quando utilizzi Jetpack Compose offre diversi vantaggi:

  • Testabilità: il disaccoppiamento dello stato dalla UI che lo visualizza semplifica il test di entrambi in modo isolato.
  • Incapsulamento dello stato: poiché lo stato può essere aggiornato solo in un punto e esiste una sola fonte di attendibilità per lo stato di un composable, è meno probabile che si creino bug dovuti a stati incoerenti.
  • Coerenza della UI: tutti gli aggiornamenti dello stato vengono immediatamente riflessi nella UI tramite l'utilizzo di contenitori di stato osservabili, come StateFlow o LiveData.

Flusso di dati unidirezionale in Jetpack Compose

I componibili funzionano in base allo stato e agli eventi. Ad esempio, un TextField viene aggiornato solo quando viene aggiornato il relativo parametro value ed espone un callback onValueChange, un evento che richiede la modifica del valore con uno nuovo. Compose definisce l'oggetto State come contenitore di valori e le modifiche al valore dello stato attivano una ricomposizione. Puoi mantenere lo stato in un remember { mutableStateOf(value) } o in un rememberSaveable { mutableStateOf(value) } a seconda di quanto tempo devi ricordare il valore.

Il tipo di valore del componibile TextField è String, quindi può provenire da qualsiasi luogo: da un valore impostato come hardcoded, da un ViewModel o passato dal componibile principale. Non è necessario conservarlo in un oggetto State, ma devi aggiornare il valore quando viene chiamato onValueChange.

  • mutableStateOf(value) crea un MutableState, che è un tipo osservabile in Compose. Qualsiasi modifica al suo valore pianifica la ricomposizione di qualsiasi funzione componibile che legge quel valore.
  • remember archivia gli oggetti nella composizione e li dimentica quando il componibile che ha chiamato remember viene rimosso dalla composizione.
  • rememberSaveable conserva lo stato tra le modifiche alla configurazione salvandolo in un Bundle.

Definisci i parametri componibili

Quando definisci i parametri di stato di un composable, tieni presente quanto segue:

  • Quanto è riutilizzabile o flessibile il composable?
  • In che modo i parametri di stato influiscono sulle prestazioni di questo composable?

Per promuovere il disaccoppiamento e il riutilizzo, ogni componibile deve contenere la quantità minima di informazioni possibile. Ad esempio, quando crei un composable per contenere l'intestazione di un articolo, preferisci passare solo le informazioni che devono essere visualizzate, anziché l'intero articolo:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

A volte, l'utilizzo di singoli parametri migliora anche le prestazioni. Ad esempio, se News contiene più informazioni rispetto a title e subtitle, ogni volta che una nuova istanza di News viene passata a Header(news), il composable verrà ricomposto, anche se title e subtitle non sono cambiati.

Valuta attentamente il numero di parametri che passi. Una funzione con troppi parametri riduce l'ergonomia della funzione, quindi in questo caso è preferibile raggrupparli in una classe.

Eventi in Scrivi

Ogni input dell'app deve essere rappresentato come un evento: tocchi, modifiche al testo e persino timer o altri aggiornamenti. Poiché questi eventi modificano lo stato della UI, ViewModel deve gestirli e aggiornare lo stato della UI.

Il livello UI non deve mai cambiare stato al di fuori di un gestore di eventi, perché ciò può introdurre incoerenze e bug nell'applicazione.

Preferisci passare valori immutabili per le espressioni lambda di stato e gestore di eventi. Questo approccio presenta i seguenti vantaggi:

  • Migliori la riusabilità.
  • Verifichi che la UI non modifichi direttamente il valore dello stato.
  • Eviti i problemi di concorrenza perché ti assicuri che lo stato non venga modificato da un altro thread.
  • Spesso, riduci la complessità del codice.

Ad esempio, un componibile che accetta un String e una funzione lambda come parametri può essere chiamato da molti contesti ed è altamente riutilizzabile. Supponiamo che la barra delle app in alto nella tua app mostri sempre del testo e abbia un pulsante Indietro. Puoi definire un componibile MyAppTopAppBar più generico che riceve il testo e il gestore del pulsante Indietro come parametri:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModel, stati ed eventi: un esempio

Utilizzando ViewModel e mutableStateOf, puoi anche introdurre il flusso di dati unidirezionale nella tua app se una delle seguenti condizioni è vera:

  • Lo stato della UI viene esposto utilizzando contenitori di stato osservabili, come StateFlow o LiveData.
  • ViewModel gestisce gli eventi provenienti dalla UI o da altri livelli dell'app e aggiorna il contenitore di stato in base agli eventi.

Ad esempio, quando implementi una schermata di accesso, toccando un pulsante Accedi, l'app dovrebbe mostrare un indicatore di avanzamento e una chiamata di rete. Se l'accesso è andato a buon fine, l'app passa a un'altra schermata; in caso di errore, l'app mostra una Snackbar. Ecco come modellare lo stato dello schermo e l'evento:

La schermata ha quattro stati:

  • Disconnesso: quando l'utente non ha ancora eseguito l'accesso.
  • In corso: quando l'app tenta di far accedere l'utente eseguendo una chiamata di rete.
  • Errore: quando si è verificato un errore durante l'accesso.
  • Accesso eseguito: quando l'utente ha eseguito l'accesso.

Puoi modellare questi stati come una classe sealed. ViewModel espone lo stato come State, imposta lo stato iniziale e lo aggiorna in base alle esigenze. ViewModel gestisce anche l'evento di accesso esponendo un metodo onSignIn().

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

Oltre all'API mutableStateOf, Compose fornisce estensioni per LiveData, Flow e Observable per la registrazione come listener e la rappresentazione del valore come stato.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

Scopri di più

Per scoprire di più sull'architettura in Jetpack Compose, consulta le seguenti risorse:

Esempi