Produktion des UI-Status

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 im Vergleich zum Status
Abbildung 1: Ereignisse führen zu Statusänderungen

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 open für DrawerState in 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 NewsRepository geladen wurden, oder andere Ereignisse.
    • Eine Mischung aus den oben genannten Optionen.
  • 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.
Produktionspipeline für den Status
Abbildung 2: Die Produktionspipeline für den Status

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 withContext Methode, um Coroutines in einem anderen parallelen Kontext auszuführen.
  • Wenn Sie MutableStateFlow verwenden, verwenden Sie die update Methode wie gewohnt.
  • Wenn Sie Compose State verwenden, verwenden Sie die Snapshot.withMutableSnapshot Methode, 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:

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