Moderne Benutzeroberflächen sind selten statisch. Der Status der Benutzeroberfläche ändert sich, wenn der Nutzer mit ihr interagiert oder wenn die App neue Daten anzeigen muss.
In diesem Dokument werden Richtlinien für die Erstellung und Verwaltung des UI-Status beschrieben. Es soll Ihnen helfen, Folgendes zu verstehen:
- Welche APIs zum Erstellen des UI-Status verwendet werden sollten. Das hängt von der Art der Quellen für Statusänderungen ab, die in Ihren State Holdern verfügbar sind, und folgt den Prinzipien des unidirektionalen Datenflusses.
- Wie Sie die Erstellung des UI-Status so gestalten, dass Systemressourcen geschont werden.
- Wie Sie den UI-Status für die Verwendung durch die Benutzeroberfläche bereitstellen.
Grundsätzlich ist die Statuserstellung die inkrementelle 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 Benutzeroberfläche oder anderer Quellen. | Wird von der Benutzeroberfläche verwendet. |
Eine gute Merkhilfe, die das oben Genannte zusammenfasst, ist Status ist; Ereignisse passieren. Das folgende Diagramm veranschaulicht Änderungen am Status, wenn Ereignisse auf einer Zeitachse auftreten. Jedes Ereignis wird vom entsprechenden State Holder verarbeitet und führt zu einer Statusänderung:
Ereignisse können aus folgenden Quellen stammen:
- Nutzer: Wenn sie mit der Benutzeroberfläche der App interagieren.
- Andere Quellen für Statusänderungen: APIs, die App-Daten aus UI-, Domain- oder Datenschichten präsentieren, z. B. Snackbar-Timeout-Ereignisse, 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. Sie können Folgendes sein:
- Lokal für die 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 steuert, z. B.
der Aufruf der Methode
openfürDrawerStatein Jetpack Compose. - Extern für die UI-Schicht: Das sind Quellen aus der Domain- oder Datenschicht, die Änderungen am UI-Status verursachen, z. B. Nachrichten, die aus einem
NewsRepositorygeladen wurden, oder andere Ereignisse. - Eine Mischung aus den oben genannten Optionen.
- Lokal für die 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 steuert, z. B.
der Aufruf der Methode
- State Holder: Typen, die Geschäftslogik und 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 | Verwenden Sie asynchrone APIs wie Coroutines und Flows, um Aufgaben außerhalb des UI-Threads auszuführen und so zu verhindern, dass die Benutzeroberfläche ruckelt. |
| Ausgabe | Verwenden Sie APIs für beobachtbare Daten-Holder wie Compose State oder StateFlow, um die Benutzeroberfläche bei Statusänderungen zu invalidieren und neu zu rendern. Beobachtbare Daten-Holder sorgen dafür, dass die Benutzeroberfläche immer einen UI-Zustand 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 behandelt, 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 muss Folgendes sein:
- Lebenszyklusbewusst: Wenn die Benutzeroberfläche nicht sichtbar oder aktiv ist, darf die Produktionspipeline für den Status keine Ressourcen verbrauchen, es sei denn, dies ist ausdrücklich erforderlich.
- Einfach zu verwenden: Die Benutzeroberfläche muss den erstellten UI Status einfach rendern können. In Jetpack Compose ist die Statusnutzung von zentraler Bedeutung für die Benutzeroberfläche, da zusammensetzbare Funktionen basierend auf Statusänderungen aktualisiert werden können.
Eingaben in Produktionspipelines für den Status
Eingaben in einer Produktionspipeline für den Status stellen ihre Quellen für Statusänderungen über Folgendes bereit:
- 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
Verwalten Sie den Status mit beobachtbaren Daten-Holdern. Verwenden Sie die mutableStateOf-API,
insbesondere bei der Arbeit mit Compose-Text-APIs. Für eine komplexere Status
verwaltung oder bei der Integration mit anderen Architekturkomponenten verwenden Sie die
MutableStateFlow-API. 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 Methode Random.nextInt auf,
und das Ergebnis wird in den UI-Status geschrieben.
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
}
}
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,
)
}
}
}
Ändern des UI-Status durch 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 der 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 MutableStateFlow die
Statusänderung an den UI-Status.
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))
}
}
}
}
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))
}
}
}
}
}
Ändern des UI-Status durch Hintergrundthreads
Es ist besser, Coroutines im Haupt-Dispatcher für die Erstellung des UI-Status zu starten, d. h. außerhalb des withContext-Blocks in den folgenden Code-Snippets.
Wenn Sie den UI-Status jedoch in einem anderen Hintergrundkontext aktualisieren müssen, können Sie Folgendes tun:
- Verwenden Sie die
withContextMethode, um Coroutines in einem anderen parallelen Kontext auszuführen. - Wenn Sie
MutableStateFlowverwenden, verwenden Sie dieupdateMethode wie gewohnt. - Wenn Sie Compose State verwenden, verwenden Sie die
Snapshot.withMutableSnapshotMethode, um atomare Aktualisierungen des Status im parallelen Kontext zu garantieren.
Angenommen, in dem folgenden DiceRollViewModel-Snippet ist SlowRandom.nextInt eine rechenintensive suspend-Funktion, die von einer CPU-gebundenen Coroutine aufgerufen werden muss.
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
}
}
}
}
}
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,
)
}
}
}
}
}
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 die Aggregation 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 der
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 Benutzeroberfläche eine genauere Kontrolle über die Aktivität der Produktionspipeline für den Status, da sie möglicherweise nur aktiv sein muss, wenn die Benutzeroberfläche sichtbar ist.
- Verwenden Sie
SharingStarted.WhileSubscribed, wenn die Pipeline nur aktiv sein muss, wenn die Benutzeroberfläche sichtbar ist, während der Flow auf lebenszyklusbewusste Weise erfasst wird. - Verwenden Sie
SharingStarted.Lazily, wenn die Pipeline aktiv sein muss, solange der Nutzer zur Benutzeroberfläche zurückkehren kann, d. h. wenn sich die Benutzeroberfläche im Backstack oder in 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. Konvertieren Sie daher die einmaligen Aufrufe in Stream-APIs 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 Sie eine oder mehrere private unterstützende MutableStateFlow-Instanzen erstellen, um Statusänderungen weiterzugeben. Sie können auch Snapshot-Flows
aus dem Compose-Statuserstellen.
Betrachten Sie das TaskDetailViewModel aus dem architecture-samples
Repository. Der UI-Status hängt von einem Stream für die aktuelle Aufgabe (_task) und einer einmaligen Quelle (_isTaskDeleted) ab, die aktualisiert wird, wenn die Aufgabe gelöscht wird. Dieses Flag ist erforderlich, um zu unterscheiden, ob eine Aufgabe aufgrund einer falschen ID nicht in der Datenbank gefunden wurde oder ob sie nicht gefunden wurde, weil der Nutzer sie gerade gelöscht hat:
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, taskAsync ->
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
}
}
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, taskAsync ->
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 }
}
}
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 Benutzeroberfläche verwendet, z. B. Compose. Jetpack Compose ist das empfohlene moderne Toolkit für die Erstellung nativer Benutzeroberflächen. Dabei sind folgende Punkte zu berücksichtigen:
- Status auf lebenszyklusbewusste Weise lesen.
- Ob der Status in einem oder mehreren Feldern des State Holders bereitgestellt werden soll.
In der folgenden Tabelle ist zusammengefasst, welche APIs Sie für Ihre Produktionspipeline für den Status verwenden sollten, wenn Sie Jetpack Compose verwenden:
| Eingabe | Ausgabe |
|---|---|
| Einmalige APIs | StateFlow oder Compose State |
| Stream-APIs | StateFlow |
| Einmalige und Stream-APIs | 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 müssen möglicherweise anfängliche Eingabewerte angegeben werden, die für den Start der Pipeline entscheidend sind, z. B. eine id für die Detailansicht eines Nachrichtenartikels oder der Start eines asynchronen Ladevorgangs.
Initialisieren Sie die Produktionspipeline für den Status nach Möglichkeit verzögert, 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 Argument started
in der Methode stateIn. In 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 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 diese Anleitung in der Praxis zu sehen:
Zusätzliche Ressourcen
Weitere Informationen zum UI-Status finden Sie in den folgenden zusätzlichen Ressourcen:
Dokumentation
Inhalte ansehen
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- UI-Schicht
- Offline-First-App erstellen
- State Holder und UI-Status {:#mad-arch}