Concetti e implementazione di Jetpack Compose
Il ruolo dell'UI è visualizzare i dati dell'applicazione sullo schermo e fungere da punto di interazione utente principale. 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'UI deve aggiornarsi per riflettere queste modifiche. In pratica, l'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 l'UI oppure potresti dover unire due origini dati diverse per presentare informazioni pertinenti per l'utente. Indipendentemente dalla logica applicata, devi passare all'UI tutte le informazioni di cui ha bisogno per il rendering completo. Il livello UI è la pipeline che converte le modifiche dei dati dell'applicazione in un formato che l'UI può presentare e poi visualizzarlo.
Esporre lo stato dell'UI
Dopo aver definito lo stato dell'UI e aver stabilito come gestirai la produzione di questo stato, il passaggio successivo consiste nel presentare lo stato prodotto all'UI. Poiché utilizzi UDF per gestire la produzione dello stato, puoi considerare lo stato prodotto come uno stream, in altre parole, nel tempo verranno prodotte più versioni dello stato. Di conseguenza, devi esporre lo stato dell'UI in un contenitore di dati osservabile come LiveData o StateFlow. Il motivo è che l'UI può reagire a qualsiasi modifica apportata allo stato senza dover recuperare manualmente i dati direttamente dal ViewModel. Questi tipi hanno anche il vantaggio di avere sempre nella cache l'ultima versione dello stato dell'UI, il che è utile per il ripristino rapido dello stato dopo le modifiche alla configurazione.
class NewsViewModel(...) : ViewModel() {
val uiState: StateFlow<NewsUiState> = …
}
Un modo comune per creare uno stream di UiState consiste nell'esporre uno stream modificabile di backup
come stream non modificabile dal ViewModel, ad esempio esponendo un
MutableStateFlow<UiState> come StateFlow<UiState>.
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
Il ViewModel può quindi esporre metodi che modificano internamente lo stato, pubblicando gli aggiornamenti che l'UI può utilizzare. Prendiamo, ad esempio, il caso in cui sia necessario eseguire un'azione asincrona; è possibile avviare una coroutine utilizzando viewModelScope e lo stato modificabile può essere aggiornato al termine.
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)
}
}
}
}
}
Utilizzare lo stato dell'UI
Quando utilizzi i contenitori di dati osservabili nell'UI, assicurati di tenere conto del ciclo di vita dell'UI. Questo è importante perché l'UI non deve osservare lo stato dell'UI quando la visualizzazione non viene mostrata all'utente. Per saperne di più su questo argomento, consulta questo post
del blog.
Quando utilizzi LiveData, LifecycleOwner si occupa implicitamente delle problematiche relative al ciclo di vita. Quando utilizzi i flussi, è consigliabile gestirli con l'ambito della coroutine appropriato e l'API repeatOnLifecycle:
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
}
}
}
}
}
Mostrare le 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'UI.
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 }
}
}
}
}
Animazioni
Per fornire transizioni di navigazione di primo livello fluide e uniformi, potresti voler attendere il caricamento dei dati della seconda schermata prima di avviare l'animazione.
Il framework di visualizzazione Android fornisce hook per ritardare le transizioni tra le destinazioni dei frammenti con le
postponeEnterTransition()
e
startPostponedEnterTransition()
API. Queste API forniscono un modo per garantire che gli elementi dell'UI nella seconda schermata (in genere un'immagine recuperata dalla rete) siano pronti per essere visualizzati prima che l'UI animi la transizione a quella schermata.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Produzione dello stato dell'UI
- Contenitori di stato e stato dell'UI {:#mad-arch}
- Guida all'architettura dell'app