In un'applicazione Compose, il cui stato dell'UI dipende dalla necessità o meno della logica dell'interfaccia utente o della logica di business. Questo documento illustra questi due scenari principali.
Best practice
Dovresti sollevare lo stato dell'interfaccia utente al predecessore comune più basso tra tutti gli elementi componibili che lo leggono e lo scrivono. Dovresti mantenere lo stato più vicino a dove viene consumato. Dal proprietario dello stato, esponi ai consumatori lo stato e gli eventi immutabili per modificarlo.
L'antenato comune più basso può anche trovarsi al di fuori della composizione. Ad esempio, quando istruisci lo stato in un ViewModel
perché è coinvolta la logica di business.
Questa pagina illustra questa best practice in modo dettagliato e un'avvertenza da tenere presente.
Tipi di stato e logica dell'interfaccia utente
Di seguito sono riportate le definizioni dei tipi di stato e logica dell'interfaccia utente utilizzati in questo documento.
Stato UI
Lo stato UI è la proprietà che descrive l'interfaccia utente. Esistono due tipi di stato dell'interfaccia utente:
- Lo stato UI schermo è ciò che devi visualizzare sullo schermo. Ad esempio, una classe
NewsUiState
può contenere gli articoli e altre informazioni necessarie per il rendering dell'interfaccia utente. Questo stato è solitamente collegato ad altri livelli della gerarchia perché contiene dati dell'app. - Lo stato degli elementi UI si riferisce a proprietà intrinseche agli elementi dell'interfaccia utente che
influiscono sul modo in cui vengono visualizzati. Un elemento dell'interfaccia utente può essere mostrato o nascosto e può avere un carattere, una certa dimensione o un colore del carattere. Nelle viste Android, la vista gestisce questo stato in quanto è intrinsecamente stateful, mostrando metodi per modificarne lo stato o eseguire query. Un esempio di ciò sono i metodi
get
eset
della classeTextView
per il relativo testo. In Jetpack Compose, lo stato è esterno al componibile e puoi persino sollevarlo dalle immediate vicinanze del componibile nella funzione componibile chiamante o in un contenitore di stato. Un esempio èScaffoldState
per l'elemento componibileScaffold
.
Logica
La logica in un'applicazione può essere una logica di business o di UI:
- La logica di business è l'implementazione dei requisiti di prodotto per i dati delle app. Ad esempio, aggiungere ai preferiti un articolo in un'app di lettori di notizie quando l'utente tocca il pulsante. Questa logica per salvare un preferito in un file o in un database è solitamente inserita nel dominio o nei livelli dati. Il titolare dello stato di solito delega questa logica ai livelli richiamando i metodi che espongono.
- La logica dell'UI dipende dalla modalità di visualizzazione dello stato dell'UI sullo schermo. Ad esempio, il suggerimento nella barra di ricerca corretta quando l'utente ha selezionato una categoria, lo scorrimento di un determinato elemento in un elenco o la logica di navigazione a una determinata schermata quando l'utente fa clic su un pulsante.
Logica UI
Quando la logica UI deve leggere o scrivere lo stato, devi definire l'ambito dell'interfaccia utente seguendo il suo ciclo di vita. Per ottenere questo risultato, devi sollevare lo stato al livello corretto in una funzione componibile. In alternativa, puoi farlo in una classe di titolare dello stato normale, anch'essa con l'ambito del ciclo di vita della UI.
Di seguito sono riportate entrambe le soluzioni e una spiegazione di quando utilizzarle.
Elementi componibili come proprietario dello stato
Avere la logica dell'interfaccia utente e lo stato degli elementi UI nei componibili è un buon approccio se lo stato e la logica sono semplici. Puoi lasciare lo stato interno a un componibile o un paranco come necessario.
Non è necessario sollevare lo stato
Lo stato di sollevamento non è sempre obbligatorio. Lo stato può essere mantenuto all'interno di un componibile quando nessun altro componibile ha bisogno di controllarlo. In questo snippet è presente un componibile che si espande e si comprime al tocco:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } // Apply simple UI logic ) if (showDetails) { Text(message.timestamp) } }
La variabile showDetails
è lo stato interno di questo elemento UI. È solo letto e modificato in questo componibile e la logica applicata è molto semplice.
In questo caso, sollevare lo stato non porterebbe molti benefici, quindi puoi lasciarlo all'interno. Ciò rende componibile il proprietario e l'unica
fonte attendibile dello stato espanso.
Sollevamento all'interno di componibili
Se devi condividere lo stato di un elemento UI con altri componibili e applicare la logica dell'interfaccia utente in punti diversi, puoi sollevarlo più in alto nella gerarchia dell'interfaccia utente. Inoltre, i componibili sono più riutilizzabili e facili da testare.
L'esempio seguente è un'app di chat che implementa due funzionalità:
- Il pulsante
JumpToBottom
consente di scorrere l'elenco dei messaggi fino in fondo. Il pulsante esegue la logica dell'interfaccia utente nello stato dell'elenco. - L'elenco
MessagesList
scorre fino in fondo dopo che l'utente invia nuovi messaggi. UserInput esegue la logica UI nello stato dell'elenco.
La gerarchia componibile è la seguente:
Lo stato LazyColumn
viene iscritta alla schermata della conversazione in modo che l'app possa eseguire la logica dell'interfaccia utente e leggere lo stato da tutti i componibili che lo richiedono:
Infine, i componibili sono:
Il codice è il seguente:
@Composable private fun ConversationScreen(/*...*/) { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen MessagesList(messages, lazyListState) // Reuse same state in MessageList UserInput( onMessageSent = { // Apply UI logic to lazyListState scope.launch { lazyListState.scrollToItem(0) } }, ) } @Composable private fun MessagesList( messages: List<Message>, lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value ) { LazyColumn( state = lazyListState // Pass hoisted state to LazyColumn ) { items(messages, key = { message -> message.id }) { item -> Message(/*...*/) } } val scope = rememberCoroutineScope() JumpToBottom(onClicked = { scope.launch { lazyListState.scrollToItem(0) // UI logic being applied to lazyListState } }) }
LazyListState
viene sollevato all'altezza necessaria per la logica dell'interfaccia utente da
applicare. Poiché è inizializzata in una funzione componibile, viene archiviata nella composizione, seguendo il suo ciclo di vita.
Tieni presente che lazyListState
è definito nel metodo MessagesList
, con il
valore predefinito di rememberLazyListState()
. Questo è un pattern comune in Compose.
Questo rende i componibili più riutilizzabili e flessibili. Puoi quindi usare l'elemento componibile
in diverse parti dell'app, per cui non è necessario controllarne lo stato. Questo avviene di solito durante il test o l'anteprima di un componibile. Questo è esattamente il modo in cui LazyColumn
definisce il suo stato.
Classe del titolare dello stato normale come proprietario dello stato
Quando un componibile contiene una logica di interfaccia utente complessa che coinvolge uno o più campi di stato di un elemento UI, dovrebbe delegare questa responsabilità ai contenenti di stato, ad esempio una classe titolare di stato semplice. Ciò rende la logica del componibile più testabile in isolamento e ne riduce la complessità. Questo approccio favorisce il principio di separazione delle preoccupazioni: il componibile è responsabile dell'emissione di elementi UI e il titolare dello stato contiene la logica dell'interfaccia utente e lo stato degli elementi dell'interfaccia utente.
Le classi proprietario dello stato normale offrono utili funzioni ai chiamanti della tua funzione componibile, in modo che non debbano scrivere personalmente questa logica.
Queste classi semplici vengono create e memorizzate nella Composizione. Poiché seguono il ciclo di vita del componibile, possono accettare tipi forniti dalla libreria di scrittura, come rememberNavController()
o rememberLazyListState()
.
Un esempio di ciò è la classe LazyListState
plain_state holder, implementata in Compose per controllare la complessità della UI di LazyColumn
o LazyRow
.
// LazyListState.kt @Stable class LazyListState constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0 ) : ScrollableState { /** * The holder class for the current scroll position. */ private val scrollPosition = LazyListScrollPosition( firstVisibleItemIndex, firstVisibleItemScrollOffset ) suspend fun scrollToItem(/*...*/) { /*...*/ } override suspend fun scroll() { /*...*/ } suspend fun animateScrollToItem() { /*...*/ } }
LazyListState
incapsula lo stato di LazyColumn
in cui è archiviato scrollPosition
per questo elemento UI. Inoltre, vengono esposti i metodi per modificare la posizione di scorrimento, ad esempio scorrendo fino a un determinato elemento.
Come puoi vedere, l'incremento delle responsabilità di un componibile aumenta la necessità di un proprietario di stato. Le responsabilità potrebbero trovarsi nella logica dell'interfaccia utente o solo nella quantità di stato da tenere traccia.
Un altro pattern comune è l'utilizzo di una classe di stati componibili semplice per gestire la complessità delle funzioni componibili principali nell'app. Puoi utilizzare questa classe per incapsulare lo stato a livello di app, ad esempio lo stato di navigazione e le dimensioni dello schermo. Una descrizione completa di questo processo è disponibile nella pagina logica UI e relativo stato del proprietario.
Logica di business
Se le classi dei componenti componibili e dei titolari di stati semplici sono responsabili della logica dell'interfaccia utente e dello stato dell'elemento UI, un titolare dello stato a livello di schermata si occupa delle seguenti attività:
- Fornisce l'accesso alla logica di business dell'applicazione, generalmente posizionata in altri livelli della gerarchia, come il livello aziendale e quello di dati.
- Preparazione dei dati dell'applicazione per la presentazione in una schermata particolare, che diventa lo stato dell'interfaccia utente della schermata.
ViewModels come proprietario dello stato
I vantaggi dei modelli AAC ViewModel nello sviluppo per Android li rendono adatti a fornire accesso alla logica di business e a preparare i dati dell'applicazione per la presentazione sullo schermo.
Quando istruisci lo stato dell'interfaccia utente nell'elemento ViewModel
, lo sposti al di fuori della composizione.
I modelli ViewModel non vengono archiviati come parte della composizione. Sono forniti dal
framework e hanno come ambito una ViewModelStoreOwner
che può essere un'attività, un frammento, un grafico di navigazione o la destinazione di un grafico di navigazione. Per
ulteriori informazioni sugli ambiti ViewModel
, puoi consultare la documentazione.
Quindi, ViewModel
è la fonte attendibile e il predecessore più basso per lo stato dell'UI.
Stato UI schermata
Come indicato nelle definizioni precedenti, lo stato dell'interfaccia utente delle schermate viene generato applicando regole
aziendali. Dato che ne è responsabile il proprietario dello stato a livello di schermata, ciò significa che lo stato dell'interfaccia utente della schermata è in genere attivato nello stato a livello di schermata, in questo caso un ViewModel
.
Considera il ConversationViewModel
di un'app di chat e come espone lo stato dell'UI della schermata e gli eventi per modificarlo:
class ConversationViewModel( channelId: String, messagesRepository: MessagesRepository ) : ViewModel() { val messages = messagesRepository .getLatestMessages(channelId) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) // Business logic fun sendMessage(message: Message) { /* ... */ } }
I componenti componibili consumano lo stato dell'interfaccia utente della schermata visualizzato in ViewModel
. Devi inserire l'istanza ViewModel
nei componenti componibili a livello di schermo per fornire l'accesso alla logica di business.
Di seguito è riportato un esempio di ViewModel
utilizzato in un componibile a livello di schermo.
In questo caso, l'elemento componibile ConversationScreen()
utilizza lo stato dell'UI della schermata visualizzato
in ViewModel
:
@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) /* ... */ }
foratura di proprietà
Per "analisi dettagliata delle proprietà" si intende il passaggio di dati attraverso diversi componenti secondari nidificati nella posizione in cui vengono letti.
Un esempio tipico di come in Compose può essere visualizzata la visualizzazione in dettaglio delle proprietà è l'inserimento del titolare dello stato a livello di schermo al livello più alto e il trasferimento di stato e eventi agli elementi componibili secondari. Ciò potrebbe anche generare un sovraccarico di firme di funzioni componibili.
Anche se l'esposizione degli eventi come singoli parametri lambda potrebbe sovraccaricare la firma della funzione, massimizza la visibilità delle responsabilità della funzione componibile. Puoi vedere a colpo d'occhio ciò che fa.
La visualizzazione in dettaglio delle proprietà è preferibile rispetto alla creazione di classi wrapper per incapsulare stato ed eventi in un'unica posizione, poiché ciò riduce la visibilità delle responsabilità componibili. Se non utilizzi le classi di wrapper, è più probabile inoltre che i componenti componibili vengano trasferiti solo i parametri necessari, il che rappresenta una best practice.
La stessa best practice si applica se questi eventi sono eventi di navigazione. Per saperne di più, consulta i documenti sulla navigazione.
Se hai identificato un problema di prestazioni, puoi anche scegliere di posticipare la lettura dello stato. Per ulteriori informazioni, puoi consultare la documentazione sul rendimento.
Stato degli elementi UI
Puoi sollevare lo stato dell'elemento UI al titolare dello stato a livello di schermo se c'è una logica di business che deve leggerlo o scriverlo.
Proseguendo con l'esempio di un'app di chat, l'app mostra i suggerimenti degli utenti in una
chat di gruppo quando l'utente digita @
e un suggerimento. Questi suggerimenti provengono dal livello dati e la logica per calcolare un elenco di suggerimenti degli utenti è considerata logica di business. L'elemento ha il seguente aspetto:
I ViewModel
che implementano questa funzionalità avranno il seguente aspetto:
class ConversationViewModel(/*...*/) : ViewModel() { // Hoisted state var inputMessage by mutableStateOf("") private set val suggestions: StateFlow<List<Suggestion>> = snapshotFlow { inputMessage } .filter { hasSocialHandleHint(it) } .mapLatest { getHandle(it) } .mapLatest { repository.getSuggestions(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) fun updateInput(newInput: String) { inputMessage = newInput } }
inputMessage
è una variabile che memorizza lo stato TextField
. Ogni volta che
l'utente digita un nuovo input, l'app chiama la logica di business per produrre suggestions
.
suggestions
è lo stato dell'UI della schermata e viene utilizzato dall'UI di Compose mediante la raccolta da StateFlow
.
Avvertenza
Per alcuni stati dell'elemento dell'interfaccia utente di Compose, il sollevamento di ViewModel
potrebbe richiedere considerazioni speciali. Ad esempio, alcuni stati degli elementi UI di Compose mostrano metodi per modificare lo stato. Alcune di queste potrebbero essere funzioni di sospensione
che attivano animazioni. Queste funzioni di sospensione possono generare eccezioni se le chiami da una CoroutineScope
non limitata all'ambito della composizione.
Supponiamo che i contenuti del riquadro a scomparsa dell'app siano dinamici e che tu debba recuperarli
e aggiornarli dal livello dati dopo la chiusura. Dovresti sollevare lo stato del riquadro a scomparsa su ViewModel
in modo da poter chiamare sia l'UI che la logica di business di questo elemento dal proprietario dello stato.
Tuttavia, la chiamata al metodo close()
di DrawerState
utilizzando
viewModelScope
dall'interfaccia utente di Compose provoca un'eccezione di runtime di tipo
IllegalStateException
con il messaggio "MonotonicFrameClock
non è disponibile in
CoroutineContext”
.
Per risolvere il problema, utilizza un CoroutineScope
con ambito alla composizione. Fornisce un valore MonotonicFrameClock
in CoroutineContext
necessario al funzionamento delle funzioni di sospensione.
Per correggere questo arresto anomalo, cambia il valore CoroutineContext
della coroutine in
ViewModel
con uno che abbia come ambito la composizione. Potrebbe avere il seguente aspetto:
class ConversationViewModel(/*...*/) : ViewModel() { val drawerState = DrawerState(initialValue = DrawerValue.Closed) private val _drawerContent = MutableStateFlow(DrawerContent.Empty) val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow() fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { // Use instead of the default context drawerState.close() } // Fetch drawer content and update state _drawerContent.update { content } } } } // in Compose @Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val scope = rememberCoroutineScope() ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) }) }
Scopri di più
Per saperne di più sullo stato e su Jetpack Compose, consulta le seguenti risorse aggiuntive.
Campioni
Codelab
Video
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Salvare lo stato dell'interfaccia utente in Compose
- Elenchi e griglie
- Architettura dell'UI di Compose