Produktion des UI-Status

Moderne Benutzeroberflächen sind selten statisch. Der Status der Benutzeroberfläche ändert sich, wenn der Nutzer mit der Benutzeroberfläche interagiert oder wenn die App neue Daten anzeigen muss.

Dieses Dokument enthält Richtlinien für die Erstellung und Verwaltung von UI-Elementen. Bundesstaat. Am Ende sollten Sie Folgendes tun:

  • Informieren Sie sich, mit welchen APIs Sie den UI-Status erstellen sollten. Das hängt davon ab, die Art der Ursachen für den Statuswechsel bei Ihren Inhabern, gemäß den Prinzipien des unidirektionalen Datenflusses.
  • Sie sollten wissen, wie Sie den UI-Zustand erstellen können, Systemressourcen.
  • Informieren Sie sich, wie Sie den UI-Status für die Nutzung durch die UI verfügbar machen sollten.

Im Grunde ist die staatliche Produktion die schrittweise Anwendung dieser Änderungen. in den UI-Zustand. Der Status ist immer vorhanden und ändert sich infolge von Ereignissen. Die Die Unterschiede zwischen Ereignissen und Status sind in der folgenden Tabelle zusammengefasst:

Ereignisse Bundesland
Vorübergehend, unvorhersehbar und für einen begrenzten Zeitraum vorhanden. Immer vorhanden.
Die Eingaben der staatlichen Produktion. Die Ausgabe der Zustandsproduktion.
Das Produkt der Benutzeroberfläche oder anderer Quellen. Wird von der UI verarbeitet.

Eine gute Merkhilfe, die das Obige zusammenfasst, ist state is; Ereignisse stattfinden. Die Diagramm unten hilft bei der Visualisierung von Zustandsänderungen, wenn Ereignisse auf einer Zeitachse auftreten. Jedes Ereignis wird vom entsprechenden Inhaber des Bundesstaats verarbeitet, was zu einer Statusänderung:

<ph type="x-smartling-placeholder">
</ph> Ereignisse im Vergleich zu Status
Abbildung 1: Ereignisse führen zu einer Statusänderung

Ereignisse können stammen aus:

  • Nutzer: Wenn sie mit der UI der App interagieren.
  • Andere Quellen von Statusänderungen: APIs, die App-Daten aus der Benutzeroberfläche, oder Datenebenen wie Zeitüberschreitungen, Anwendungsfälle oder Repositories.

Produktionspipeline für den UI-Status

Die Statusproduktion in Android-Apps kann man sich als Verarbeitungspipeline vorstellen. bestehend aus:

  • Eingaben: Die Quellen von Statusänderungen. Mögliche Gründe: <ph type="x-smartling-placeholder">
      </ph>
    • Lokal für die UI-Ebene: Dabei kann es sich um Nutzerereignisse wie die Eingabe eines Titel für eine Aufgabe Aufgabenverwaltungs-App oder APIs, die Zugriff auf UI-Logik, die Änderungen am UI-Status vorantreibt Beispiel: Durch Aufrufen der Methode open für DrawerState in Jetpack Compose
    • Außerhalb der UI-Ebene: Dies sind Quellen aus der Domain oder Daten. Ebenen, die Änderungen am UI-Status verursachen. Zum Beispiel Nachrichten, die aus einem NewsRepository oder anderen Ereignissen geladen werden.
    • Eine Mischung aus allem oben Genannten.
  • Inhaber eines Bundesstaats: Typen, die Geschäftslogik anwenden und/oder UI-Logik für Quellen von Statusänderungen und Verarbeitung von Nutzerereignissen, um UI-Status
  • Ausgabe: Der UI-Status, den die App rendern kann, um Nutzern die die sie benötigen.
<ph type="x-smartling-placeholder">
</ph> Die Zustandsproduktionspipeline
Abbildung 2: Die Produktionspipeline des Status

Staatliche Produktions-APIs

Es gibt zwei Haupt-APIs, die in der Zustandsproduktion verwendet werden, je nachdem, in welcher Phase der in welcher Pipeline Sie sich befinden:

Pipelinephase API
Eingang Sie sollten asynchrone APIs verwenden, um Arbeiten außerhalb des UI-Threads durchzuführen, damit die UI-Verzögerung kostenlos bleibt. Zum Beispiel Coroutines oder Flows in Kotlin und RxJava oder Callbacks in der Programmiersprache Java.
Ausgabe Sie sollten APIs für beobachtbare Dateninhaber verwenden, um die UI zu entwerten und neu zu rendern, wenn sich der Status ändert. Beispiel: StateFlow, Compose State oder LiveData. Inhaber beobachtbarer Daten garantieren, dass die UI immer einen UI-Status hat, der auf dem Bildschirm angezeigt werden kann.

Davon hat die Wahl der asynchronen API für die Eingabe einen größeren Einfluss auf die Beschaffenheit der staatlichen Produktionspipeline als die Wahl der beobachtbaren API. für die Ausgabe. Das liegt daran, dass die Eingaben die Art der Verarbeitung vorgeben, auf die Pipeline angewendet werden kann.

Zusammensetzung der staatlichen Produktionspipeline

In den nächsten Abschnitten geht es um staatliche Produktionstechniken, die für verschiedene Eingaben und die übereinstimmenden Ausgabe-APIs. Jede Zustandsproduktionspipeline ist ein Kombination von Ein- und Ausgaben und sollte:

  • Lebenszyklusbewusst: Falls die Benutzeroberfläche nicht sichtbar oder aktiv ist, wird der Status-Produktionspipeline sollte nur dann Ressourcen verbrauchen, erforderlich.
  • Einfache Nutzung: Die erstellte UI sollte einfach gerendert werden können. Bundesstaat. Überlegungen zur Ausgabe der Zustandsproduktionspipeline variieren je nach View-APIs wie dem View-System oder Jetpack Compose.

Eingaben in Zustandsproduktionspipelines

Eingaben in eine Zustandsproduktionspipeline können entweder ihre Zustandsquellen angeben Ändern via:

  • One-Shot-Operationen, die beispielsweise synchron oder asynchron sein können. suspend-Funktionen aufrufen.
  • Stream-APIs, z. B. Flows
  • Alle oben genannten Optionen

In den folgenden Abschnitten wird beschrieben, wie Sie eine Zustandsproduktionspipeline zusammenstellen für jede der oben genannten Eingaben.

One-Shot-APIs als Quellen von Statusänderungen

Die MutableStateFlow API als beobachtbare, änderbare API verwenden Container of state. In Jetpack Compose-Apps können Sie auch mutableStateOf, insbesondere wenn Sie mit Text-APIs verfassen Beide APIs ermöglichen sichere atomische Aktualisierungen der gehosteten Werte unabhängig davon, ob die Aktualisierungen synchron oder asynchron sein.

Betrachten Sie beispielsweise Statusaktualisierungen in einer einfachen Würfel-App. Jede Rolle von ruft der Nutzer die synchrone Random.nextInt()-Methode und das Ergebnis wird in den UI-Status

Zustandsfluss

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,
            )
        }
    }
}

Erstellungsstatus

@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
    }
}

UI-Status bei asynchronen Aufrufen ändern

Starten Sie für Statusänderungen, die ein asynchrones Ergebnis erfordern, eine Koroutine in der entsprechende CoroutineScope. So kann die App die Arbeit verwerfen, wenn die CoroutineScope wurde abgesagt. Der Staatsinhaber schreibt dann das Ergebnis der Aufruf der angehaltenen Methode an die beobachtbare API, mit der der UI-Status angezeigt wird.

Betrachten Sie zum Beispiel die AddEditTaskViewModel in der Architekturbeispiel. Wenn die Methode saveTask() angehalten wird die Aufgabe asynchron speichert, kann die Methode update im MutableStateFlow leitet die Statusänderung an den UI-Status weiter.

Zustandsfluss

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))
                }
            }
        }
    }
}

Erstellungsstatus

@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))
            }
        }
    }
}

UI-Status von Hintergrundthreads ändern

Es empfiehlt sich, Coroutines auf dem Haupt-Dispatcher für die Produktion zu starten. des UI-Status. Das heißt, außerhalb des withContext-Blocks in den Code-Snippets. unten. Wenn Sie den UI-Status jedoch in einem anderen Hintergrund aktualisieren müssen, Kontext bietet, können Sie dies mithilfe der folgenden APIs tun:

  • Verwenden Sie die Methode withContext, um Koroutinen in einer gleichzeitigen Kontext.
  • Wenn Sie MutableStateFlow verwenden, verwenden Sie die Methode update als Üblicherweise.
  • Wenn Sie den Erstellungsstatus nutzen, können Sie mit Snapshot.withMutableSnapshot Atomare Aktualisierungen des Status im gleichzeitigen Kontext garantieren.

Nehmen wir zum Beispiel an, dass im DiceRollViewModel-Snippet unten SlowRandom.nextInt() ist eine rechenintensive suspend -Funktion, die von einer CPU-gebundenen Koroutine aufgerufen werden muss.

Zustandsfluss

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,
                    )
                }
            }
        }
    }
}

Erstellungsstatus

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
                }
            }
        }
    }
}

APIs als Quellen von Statusänderungen streamen

Für Quellen von Zustandsänderungen, die im Laufe der Zeit in Streams mehrere Werte erzeugen, Das Aggregieren der Ergebnisse aller Quellen zu einem zusammenhängenden Ganzen ist einen einfachen Ansatz für die staatliche Produktion.

Bei der Verwendung von Kotlin-Flows erreichen Sie dies mit der Methode combine . Ein Beispiel hierfür finden Sie "Jetzt für Android" Sample 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
    )
}

Durch die Verwendung des Operators stateIn zum Erstellen von StateFlows wird die Benutzeroberfläche differenzierter dargestellt. die Aktivität der staatlichen Produktionspipeline zu kontrollieren, nur aktiv sein, wenn die Benutzeroberfläche sichtbar ist.

  • Verwenden Sie SharingStarted.WhileSubscribed(), wenn die Pipeline nur aktiv sein soll. Wenn die Benutzeroberfläche beim Erfassen des Ablaufs in einem Lebenszyklus-sensitiven Prozess sichtbar ist auf die Art und Weise.
  • Verwenden Sie SharingStarted.Lazily, wenn die Pipeline aktiv sein soll, solange die kann der Nutzer zur Benutzeroberfläche zurückkehren, d. h., die Benutzeroberfläche befindet sich auf dem Backstack oder in einem anderen Tab aus dem Bildschirm zu sehen.

In Fällen, in denen das Zusammenfassen von streambasierten Statusquellen nicht angewendet wird, werden Streams APIs wie Kotlin-Flows bieten eine Vielzahl von Transformationen, wie Zusammenführen, Abflachen usw. um die Streams in den UI-Zustand zu versetzen.

One-Shot- und Stream-APIs als Quellen von Statusänderungen

Wenn die Zustandsproduktionspipeline von beiden One-Shot-Aufrufen abhängt als Quellen für Zustandsänderungen. Streams sind die definierende Einschränkung. Konvertieren Sie also One-Shot-Aufrufe in Streams, APIs verwenden oder ihre Ausgabe in Streams weiterleiten und die Verarbeitung wie beschrieben fortsetzen finden Sie oben im Abschnitt „Streams“.

Bei Abläufen bedeutet dies in der Regel das Erstellen einer oder mehrerer privater Sicherungen MutableStateFlow Instanz, um Statusänderungen zu übertragen. Sie können auch Snapshot-Abläufe erstellen.

Betrachten Sie die TaskDetailViewModel aus der architecture-sample:

Zustandsfluss

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 }
    }
}

Erstellungsstatus

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 Pipelines für die Statusproduktion

Die Auswahl der Ausgabe-API für den UI-Status und die Art der Darstellung hängt weitgehend von der API ab, die Ihre App zum Rendern der Benutzeroberfläche verwendet. In Android-Apps können entweder Views oder Jetpack Compose verwenden. Folgendes sollte dabei berücksichtigt werden:

In der folgenden Tabelle wird zusammengefasst, welche APIs für die Zustandsproduktion verwendet werden Pipeline für eine bestimmte Eingabe und einen bestimmten Nutzer:

Eingang Nutzer Ausgabe
One-Shot-APIs Aufrufe StateFlow oder LiveData
One-Shot-APIs Schreiben StateFlow oder „Schreiben“ State
Stream-APIs Aufrufe StateFlow oder LiveData
Stream-APIs Schreiben StateFlow
One-Shot- und Stream-APIs Aufrufe StateFlow oder LiveData
One-Shot- und Stream-APIs Schreiben StateFlow

Initialisierung der Produktionspipeline

Zum Initialisieren von Pipelines für die Statusproduktion müssen die Anfangsbedingungen festgelegt werden für die Ausführung der Pipeline. Dazu können erste Eingabewerte gehören. entscheidend für den Start der Pipeline ist, z. B. ein id für die Detailansicht eines Nachrichtenartikels anzeigen oder einen asynchronen Ladevorgang starten.

Sie sollten die Zustandsproduktionspipeline nach Möglichkeit verzögert initialisieren um Systemressourcen zu schonen. In der Praxis bedeutet dies oft, zu warten, bis ein Kunde des Kunden . Flow APIs ermöglichen dies mithilfe der started-Argument im stateIn . Sollte dies nicht anwendbar sein, Definieren eines idempotenten initialize()-Funktion zum expliziten Starten der Zustandsproduktionspipeline Dies wird 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
        }
    }
}

Produktproben

Die folgenden Google-Beispiele zeigen die Produktion von Staat in der UI-Ebene. Sehen Sie sich diese Tipps in der Praxis an: