Moderne UIs sind selten statisch. Der Status der UI ändert sich, wenn der Nutzer mit der UI interagiert oder wenn die App neue Daten anzeigen muss.
In diesem Dokument werden Richtlinien für die Erstellung und Verwaltung des UI-Status beschrieben. Am Ende sollten Sie Folgendes wissen:
- Welche APIs Sie verwenden sollten, um den UI-Status zu erstellen. Das hängt von der Art der Quellen für Statusänderungen ab, die in Ihren Status-Holdern verfügbar sind, wobei die Prinzipien des unidirektionalen Datenflusses gelten.
- Wie Sie die Erstellung des UI-Status so gestalten sollten, dass Sie die Systemressourcen im Blick behalten.
- Wie Sie den UI-Status für die Nutzung durch die UI bereitstellen sollten.
Grundsätzlich ist die Statuserstellung die schrittweise Anwendung dieser Änderungen auf den UI-Status. Der Status ist immer vorhanden und ändert sich aufgrund von Ereignissen. Die Unterschiede zwischen Ereignissen und Status sind in der folgenden Tabelle zusammengefasst:
| Ereignisse | Status |
|---|---|
| Vorübergehend, unvorhersehbar und nur für einen begrenzten Zeitraum vorhanden. | Immer vorhanden. |
| Die Eingaben der Statuserstellung. | Die Ausgabe der Statuserstellung. |
| Das Produkt der UI oder anderer Quellen. | Wird von der UI verwendet. |
Eine gute Merkhilfe, die das oben Genannte zusammenfasst, lautet: Status ist; Ereignisse passieren. Das folgende Diagramm veranschaulicht Änderungen am Status, wenn Ereignisse in einer Zeitachse auftreten. Jedes Ereignis wird vom entsprechenden Status-Holder verarbeitet und führt zu einer Statusänderung:
Ereignisse können aus folgenden Quellen stammen:
- Nutzer: Wenn sie mit der UI der App interagieren.
- Andere Quellen für Statusänderungen: APIs, die App-Daten aus der UI-, Domain- oder Datenschicht präsentieren, z. B. Ereignisse für das Snackbar-Timeout, Anwendungsfälle oder Repositories.
Die Produktionspipeline für den UI-Status
Die Statuserstellung in Android-Apps kann als Verarbeitungspipeline betrachtet werden, die Folgendes umfasst:
- Eingaben: Die Quellen für Statusänderungen. Dazu gehören:
- Lokal in der UI-Schicht: Das können Nutzerereignisse sein, z. B. wenn ein Nutzer in einer Aufgabenverwaltungs-App einen
Titel für eine Aufgabe eingibt, oder APIs, die
Zugriff auf die UI-Logik bieten, die Änderungen am UI-Status bewirkt. Beispiel:
Aufrufen der
openMethode fürDrawerStatein Jetpack Compose. - Extern in der UI-Schicht: Das sind Quellen aus der Domain- oder Datenschicht, die Änderungen am UI-Status verursachen. Beispiel: Nachrichten, die aus einem
NewsRepositorygeladen wurden, oder andere Ereignisse. - Eine Mischung aus allen oben genannten Optionen.
- Lokal in der UI-Schicht: Das können Nutzerereignisse sein, z. B. wenn ein Nutzer in einer Aufgabenverwaltungs-App einen
Titel für eine Aufgabe eingibt, oder APIs, die
Zugriff auf die UI-Logik bieten, die Änderungen am UI-Status bewirkt. Beispiel:
Aufrufen der
- Status-Holder: Typen, die Geschäftslogik und/oder UI-Logik auf Quellen für Statusänderungen anwenden und Nutzerereignisse verarbeiten, um den UI-Status zu erstellen.
- Ausgabe: Der UI-Status, den die App rendern kann, um Nutzern die benötigten Informationen zu liefern.
APIs für die Statuserstellung
Je nach Phase der Pipeline werden zwei Haupt-APIs für die Statuserstellung verwendet:
| Pipelinephase | API |
|---|---|
| Eingabe | Sie sollten asynchrone APIs verwenden, um Aufgaben außerhalb des UI-Threads auszuführen, damit die UI nicht ruckelt. Beispiele: Coroutines oder Flows in Kotlin und RxJava oder Callbacks in der Programmiersprache Java. |
| Ausgabe | Sie sollten APIs für beobachtbare Daten-Holder verwenden, um die UI bei Statusänderungen zu invalidieren und neu zu rendern. Beispiele: StateFlow, Compose State oder LiveData. Beobachtbare Daten-Holder sorgen dafür, dass die UI immer einen UI-Status hat, der auf dem Bildschirm angezeigt werden kann. |
Die Wahl der asynchronen API für die Eingabe hat einen größeren Einfluss auf die Art der Produktionspipeline für den Status als die Wahl der beobachtbaren API für die Ausgabe. Das liegt daran, dass die Eingaben die Art der Verarbeitung bestimmen, die auf die Pipeline angewendet werden kann.
Zusammenstellung der Produktionspipeline für den Status
In den nächsten Abschnitten werden Techniken zur Statuserstellung beschrieben, die für verschiedene Eingaben am besten geeignet sind, sowie die entsprechenden Ausgabe-APIs. Jede Produktionspipeline für den Status ist eine Kombination aus Eingaben und Ausgaben und sollte folgende Eigenschaften haben:
- Lebenszyklusbewusst: Wenn die UI nicht sichtbar oder aktiv ist, sollte die Produktionspipeline für den Status keine Ressourcen verbrauchen, es sei denn, dies ist ausdrücklich erforderlich.
- Einfach zu verwenden: Die UI sollte den erstellten UI Status einfach rendern können. Die Überlegungen zur Ausgabe der Produktionspipeline für den Status variieren je nach View-API, z. B. dem View-System oder Jetpack Compose.
Eingaben in Produktionspipelines für den Status
Eingaben in einer Produktionspipeline für den Status können ihre Quellen für Statusänderungen über Folgendes bereitstellen:
- Einmalige Vorgänge, die synchron oder asynchron sein können, z. B. Aufrufe von
suspend-Funktionen. - Stream-APIs, z. B.
Flows. - Alle oben genannten Optionen.
In den folgenden Abschnitten wird beschrieben, wie Sie für jede der oben genannten Eingaben eine Produktionspipeline für den Status zusammenstellen können.
Einmalige APIs als Quellen für Statusänderungen
Verwenden Sie die MutableStateFlow-API als beobachtbaren, veränderlichen
Container für den Status. In Jetpack Compose-Apps können Sie auch
mutableStateOf verwenden, insbesondere bei der Arbeit mit
Compose-Text-APIs. Beide APIs bieten Methoden, die sichere atomare Aktualisierungen der von ihnen gehosteten Werte ermöglichen, unabhängig davon, ob die Aktualisierungen synchron oder asynchron sind.
Betrachten Sie beispielsweise Statusaktualisierungen in einer einfachen Würfel-App. Jeder Würfelwurf von
den Würfeln des Nutzers ruft die synchrone
Random.nextInt() Methode auf und das Ergebnis wird in den
UI-Status geschrieben.
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Compose State
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
Ändern des UI-Status über asynchrone Aufrufe
Bei Statusänderungen, die ein asynchrones Ergebnis erfordern, starten Sie eine Coroutine im entsprechenden CoroutineScope. So kann die App die Arbeit verwerfen, wenn CoroutineScope abgebrochen wird. Der State Holder schreibt dann das Ergebnis des Methodenaufrufs der suspend-Methode in die beobachtbare API, die zum Bereitstellen des UI-Status verwendet wird.
Betrachten Sie beispielsweise die AddEditTaskViewModel im
Architekturbeispiel. Wenn die suspend-Methode saveTask()
eine Aufgabe asynchron speichert, überträgt die update Methode für den
MutableStateFlow die Statusänderung an den UI-Status.
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Compose State
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
Ändern des UI-Status über Hintergrundthreads
Es ist besser, Coroutines im Haupt-Dispatcher für die Erstellung des UI-Status zu starten. Das heißt, außerhalb des withContext-Blocks in den Code-Snippets unten. Wenn Sie den UI-Status jedoch in einem anderen Hintergrundkontext aktualisieren müssen, können Sie die folgenden APIs verwenden:
- Verwenden Sie die
withContextMethode, um Coroutines in einem anderen parallelen Kontext auszuführen. - Bei Verwendung von
MutableStateFlowverwenden Sie dieupdateMethode wie gewohnt. - Bei Verwendung von Compose State verwenden Sie die
Snapshot.withMutableSnapshotum atomare Aktualisierungen des Status im parallelen Kontext zu garantieren.
Angenommen, in dem Code-Snippet DiceRollViewModel unten ist SlowRandom.nextInt() eine rechenintensive suspend-Funktion, die von einer CPU-gebundenen Coroutine aufgerufen werden muss.
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
Compose State
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
Stream-APIs als Quellen für Statusänderungen
Bei Quellen für Statusänderungen, die im Laufe der Zeit mehrere Werte in Streams erzeugen, ist das Aggregieren der Ausgaben aller Quellen zu einem zusammenhängenden Ganzen ein einfacher Ansatz für die Statuserstellung.
Wenn Sie Kotlin Flows verwenden, können Sie dies mit der combine Funktion erreichen. Ein Beispiel dafür finden Sie im Beispiel „Now in Android“ in InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
Die Verwendung des Operators stateIn zum Erstellen von StateFlows gibt der UI eine genauere Kontrolle über die Aktivität der Produktionspipeline für den Status, da sie möglicherweise nur aktiv sein muss, wenn die UI sichtbar ist.
- Verwenden Sie
SharingStarted.WhileSubscribed(), wenn die Pipeline nur aktiv sein soll, wenn die UI sichtbar ist, während der Flow lebenszyklusbewusst erfasst wird. - Verwenden Sie
SharingStarted.Lazily, wenn die Pipeline so lange aktiv sein soll, wie der Nutzer zur UI zurückkehren kann, d. h. wenn sich die UI im Backstack oder auf einem anderen Tab außerhalb des Bildschirms befindet.
In Fällen, in denen die Aggregation von stream-basierten Statusquellen nicht anwendbar ist, bieten Stream APIs wie Kotlin Flows eine Vielzahl von Transformationen wie Zusammenführen, Reduzieren usw., um die Streams in den UI-Status zu verarbeiten.
Einmalige und Stream-APIs als Quellen für Statusänderungen
Wenn die Produktionspipeline für den Status sowohl von einmaligen Aufrufen als auch von Streams als Quellen für Statusänderungen abhängt, sind Streams die definierende Einschränkung. Wandeln Sie daher die einmaligen Aufrufe in Stream-APIs um oder leiten Sie ihre Ausgabe in Streams weiter und setzen Sie die Verarbeitung wie oben im Abschnitt zu Streams beschrieben fort.
Bei Flows bedeutet das in der Regel, dass eine oder mehrere private unterstützende MutableStateFlow-Instanzen erstellt werden, um Statusänderungen zu übertragen. Sie können auch
Snapshot-Flows aus dem Compose-Status erstellen.
Betrachten Sie das TaskDetailViewModel aus dem
architecture-samples Repository unten:
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Compose State
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
Ausgabetypen in Produktionspipelines für den Status
Die Wahl der Ausgabe-API für den UI-Status und die Art der Darstellung hängen weitgehend von der API ab, die Ihre App zum Rendern der UI verwendet. In Android-Apps können Sie Views oder Jetpack Compose verwenden. Dabei sind folgende Punkte zu beachten:
- Status lebenszyklusbewusst lesen.
- Ob der Status in einem oder mehreren Feldern des Status-Holders bereitgestellt werden soll.
In der folgenden Tabelle ist zusammengefasst, welche APIs für Ihre Produktionspipeline für den Status für eine bestimmte Eingabe und einen bestimmten Consumer verwendet werden sollten:
| Eingabe | Consumer | Ausgabe |
|---|---|---|
| Einmalige APIs | Views | StateFlow oder LiveData |
| Einmalige APIs | Compose | StateFlow oder Compose State |
| Stream-APIs | Views | StateFlow oder LiveData |
| Stream-APIs | Compose | StateFlow |
| Einmalige und Stream-APIs | Views | StateFlow oder LiveData |
| Einmalige und Stream-APIs | Compose | StateFlow |
Initialisierung der Produktionspipeline für den Status
Bei der Initialisierung von Produktionspipelines für den Status werden die Anfangsbedingungen für die Ausführung der Pipeline festgelegt. Dazu kann es erforderlich sein, anfängliche Eingabewerte anzugeben, die für den Start der Pipeline entscheidend sind, z. B. eine id für die Detailansicht eines Nachrichtenartikels, oder einen asynchronen Ladevorgang zu starten.
Sie sollten die Produktionspipeline für den Status nach Möglichkeit verzögert initialisieren, um Systemressourcen zu sparen.
In der Praxis bedeutet das oft, dass Sie warten, bis ein Consumer der Ausgabe vorhanden ist. Flow APIs ermöglichen dies mit dem
started Argument in der stateIn
Methode. In den Fällen, in denen dies nicht anwendbar ist,
definieren Sie eine idempotente
initialize() Funktion, um die Produktionspipeline für den Status explizit zu starten,
wie im folgenden Code-Snippet gezeigt:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Beispiele
In den folgenden Google-Beispielen wird die Erstellung des Status in der UI-Schicht veranschaulicht. Sehen Sie sich die Beispiele an, um die Anleitung in der Praxis zu sehen:
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- UI-Schicht
- Offline-First-App erstellen
- Status-Holder und UI-Status {:#mad-arch}