Konzepte und Jetpack Compose-Implementierung
UI-Ereignisse sind Aktionen, die in der UI-Schicht verarbeitet werden sollten, entweder von der UI oder vom ViewModel. Die häufigste Art von Ereignissen sind Nutzerereignisse. Der Nutzer löst Nutzerereignisse aus, indem er mit der App interagiert, z. B. durch Tippen auf den Bildschirm oder durch Gesten. Die Benutzeroberfläche verarbeitet diese Ereignisse dann mithilfe von Callbacks wie onClick()-Listenern.
Das ViewModel ist normalerweise für die Verarbeitung der Geschäftslogik eines bestimmten Nutzerereignisses verantwortlich, z. B. wenn der Nutzer auf eine Schaltfläche klickt, um einige Daten zu aktualisieren. Normalerweise übernimmt das ViewModel diese Aufgabe, indem es Funktionen bereitstellt, die von der UI aufgerufen werden können. Nutzerereignisse können auch eine Logik für das UI-Verhalten haben, die das UI direkt verarbeiten kann, z. B. das Navigieren zu einem anderen Bildschirm oder das Anzeigen eines Snackbar.
Die Geschäftslogik bleibt für dieselbe App auf verschiedenen mobilen Plattformen oder Formfaktoren gleich, die Logik für das UI-Verhalten ist jedoch ein Implementierungsdetail, das sich in diesen Fällen unterscheiden kann. Auf der Seite UI-Ebene werden diese Arten von Logik so definiert:
- Geschäftslogik bezieht sich darauf, was bei Zustandsänderungen zu tun ist, z. B. eine Zahlung vornehmen oder Nutzereinstellungen speichern. Diese Logik wird in der Regel von der Domain- und der Datenschicht verarbeitet. In diesem Leitfaden wird die Klasse Architecture Components ViewModel als empfohlene Lösung für Klassen verwendet, die Geschäftslogik verarbeiten.
- UI-Verhaltenslogik oder UI-Logik bezieht sich darauf, wie Zustandsänderungen angezeigt werden, z. B. Navigationslogik oder wie Nachrichten für den Nutzer angezeigt werden. Die Benutzeroberfläche übernimmt diese Logik.
Nutzerereignisse verarbeiten
Die Benutzeroberfläche kann Nutzerereignisse direkt verarbeiten, wenn sie sich auf die Änderung des Status eines UI-Elements beziehen, z. B. des Status eines minimierbaren Elements. Wenn für das Ereignis Geschäftslogik ausgeführt werden muss, z. B. das Aktualisieren der Daten auf dem Bildschirm, sollte es vom ViewModel verarbeitet werden.
Im folgenden Beispiel wird gezeigt, wie verschiedene Schaltflächen verwendet werden, um ein UI-Element (UI-Logik) zu maximieren und die Daten auf dem Bildschirm zu aktualisieren (Geschäftslogik):
class LatestNewsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLatestNewsBinding
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
// The expand details event is processed by the UI that
// modifies a View's internal state.
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// The refresh event is processed by the ViewModel that is in charge
// of the business logic.
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
}
}
Nutzerereignisse in RecyclerViews
Wenn die Aktion weiter unten im UI-Baum erfolgt, z. B. in einem RecyclerView-Element oder einem benutzerdefinierten View, sollte das ViewModel weiterhin Nutzerereignisse verarbeiten.
Angenommen, alle Nachrichtenartikel von NewsActivity enthalten eine Schaltfläche zum Setzen von Lesezeichen. Die ViewModel muss die ID des mit einem Lesezeichen versehenen Nachrichtenartikels kennen. Wenn der Nutzer einen Artikel mit einem Lesezeichen versieht, ruft der RecyclerView-Adapter die bereitgestellte addBookmark(newsId)-Funktion nicht über ViewModel auf, was eine Abhängigkeit von ViewModel erfordern würde. Stattdessen stellt ViewModel ein Statusobjekt namens NewsItemUiState bereit, das die Implementierung für die Verarbeitung des Ereignisses enthält:
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
val publicationDate: String,
val onBookmark: () -> Unit
)
class LatestNewsViewModel(
private val formatDateUseCase: FormatDateUseCase,
private val repository: NewsRepository
)
val newsListUiItems = repository.latestNews.map { news ->
NewsItemUiState(
title = news.title,
body = news.body,
bookmarked = news.bookmarked,
publicationDate = formatDateUseCase(news.publicationDate),
// Business logic is passed as a lambda function that the
// UI calls on click events.
onBookmark = {
repository.addBookmark(news.id)
}
)
}
}
So funktioniert der RecyclerView-Adapter nur mit den Daten, die er benötigt: der Liste der NewsItemUiState-Objekte. Der Adapter hat keinen Zugriff auf das gesamte ViewModel, wodurch die Wahrscheinlichkeit eines Missbrauchs der vom ViewModel bereitgestellten Funktionen geringer ist. Wenn Sie nur der Aktivitätsklasse erlauben, mit dem ViewModel zu arbeiten, trennen Sie die Verantwortlichkeiten. So wird dafür gesorgt, dass UI-spezifische Objekte wie Ansichten oder RecyclerView-Adapter nicht direkt mit dem ViewModel interagieren.
Namenskonventionen für Nutzerereignisfunktionen
In diesem Leitfaden werden die ViewModel-Funktionen, die Nutzerereignisse verarbeiten, mit einem Verb benannt, das auf der Aktion basiert, die sie verarbeiten, z. B. addBookmark(id) oder logIn(username, password).
ViewModel-Ereignisse verarbeiten
UI-Aktionen, die vom ViewModel ausgehen – ViewModel-Ereignisse – sollten immer zu einer Aktualisierung des UI-Status führen. Dies entspricht den Prinzipien des unidirektionalen Datenflusses. So können Ereignisse nach Konfigurationsänderungen reproduziert werden und UI-Aktionen gehen nicht verloren. Optional können Sie Ereignisse auch nach dem Beenden des Prozesses reproduzierbar machen, wenn Sie das Modul für den gespeicherten Status verwenden.
Die Zuordnung von UI-Aktionen zum UI-Status ist nicht immer einfach, führt aber zu einer einfacheren Logik. Ihr Denkprozess sollte nicht damit enden, dass Sie festlegen, wie die Benutzeroberfläche zu einem bestimmten Bildschirm navigiert. Sie müssen weiterdenken und überlegen, wie Sie diesen Nutzerfluss in Ihrem UI-Status darstellen. Mit anderen Worten: Denken Sie nicht darüber nach, welche Aktionen die Benutzeroberfläche ausführen muss, sondern darüber, wie sich diese Aktionen auf den UI-Status auswirken.
Nehmen wir an, der Nutzer wird zum Startbildschirm weitergeleitet, wenn er auf dem Anmeldebildschirm angemeldet ist. Sie könnten dies im UI-Zustand so modellieren:
data class LoginUiState(
val isLoading: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
Die Benutzeroberfläche reagiert auf Änderungen des isUserLoggedIn-Status und navigiert bei Bedarf zum richtigen Ziel:
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
/* ... */
}
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
Durch den Empfang von Ereignissen können Statusaktualisierungen ausgelöst werden.
Wenn bestimmte ViewModel-Ereignisse in der Benutzeroberfläche verarbeitet werden, kann dies zu anderen Aktualisierungen des UI-Status führen. Wenn beispielsweise kurzzeitig Meldungen auf dem Bildschirm angezeigt werden, um den Nutzer darüber zu informieren, dass etwas passiert ist, muss die Benutzeroberfläche das ViewModel benachrichtigen, damit eine weitere Statusaktualisierung ausgelöst wird, wenn die Meldung auf dem Bildschirm angezeigt wurde. Das Ereignis, das eintritt, wenn der Nutzer die Nachricht gesehen hat (durch Schließen oder nach einem Zeitlimit), kann als „Nutzereingabe“ behandelt werden. Das ViewModel sollte sich dessen bewusst sein. In diesem Fall kann der UI-Status so modelliert werden:
// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
val news: List<News> = emptyList(),
val isLoading: Boolean = false,
val userMessage: String? = null
)
Das ViewModel würde den UI-Status so aktualisieren, wenn die Geschäftslogik erfordert, dass dem Nutzer eine neue temporäre Nachricht angezeigt wird:
class LatestNewsViewModel(/* ... */) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = "No Internet connection")
}
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
}
Das ViewModel muss nicht wissen, wie die Meldung auf dem Bildschirm angezeigt wird. Es weiß nur, dass eine Nutzermeldung angezeigt werden muss. Nachdem die temporäre Meldung angezeigt wurde, muss die UI das ViewModel darüber informieren. Dadurch wird eine weitere Aktualisierung des UI-Zustands ausgelöst, um die userMessage-Eigenschaft zu löschen:
class LatestNewsActivity : AppCompatActivity() {
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
uiState.userMessage?.let {
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
...
}
}
}
}
}
Auch wenn die Meldung vorübergehend ist, spiegelt der UI-Zustand zu jedem Zeitpunkt genau das wider, was auf dem Bildschirm angezeigt wird. Entweder wird die Nutzernachricht angezeigt oder nicht.
Navigationsereignisse
Im Abschnitt Durch das Verarbeiten von Ereignissen können Statusaktualisierungen ausgelöst werden wird beschrieben, wie Sie den UI-Status verwenden, um Nutzern Nachrichten auf dem Bildschirm anzuzeigen. Navigationsereignisse sind ebenfalls ein häufiger Ereignistyp in einer Android-App.
Wenn das Ereignis in der Benutzeroberfläche ausgelöst wird, weil der Nutzer auf eine Schaltfläche getippt hat, wird der Navigationscontroller von der Benutzeroberfläche aufgerufen.
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.helpButton.setOnClickListener {
navController.navigate(...) // Open help screen
}
}
}
Wenn für die eingegebenen Daten eine Validierung der Geschäftslogik erforderlich ist, bevor navigiert werden kann, muss das ViewModel diesen Status für die Benutzeroberfläche verfügbar machen. Die Benutzeroberfläche würde auf diese Statusänderung reagieren und entsprechend navigieren. Dieser Anwendungsfall wird im Abschnitt „ViewModel-Ereignisse verarbeiten“ behandelt. Hier ist ein ähnlicher Code:
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
Im obigen Beispiel funktioniert die App wie erwartet, da das aktuelle Ziel „Login“ nicht im Back Stack gespeichert wird. Nutzer können nicht mehr darauf zurückgreifen, wenn sie die Zurück-Taste drücken. In Fällen, in denen das passieren könnte, wäre für die Lösung jedoch zusätzliche Logik erforderlich.
Navigationsereignisse, wenn das Ziel im Backstack verbleibt
Wenn ein ViewModel einen Zustand festlegt, der ein Navigationsereignis von Bildschirm A zu Bildschirm B erzeugt, und Bildschirm A im Navigations-Backstack verbleibt, benötigen Sie möglicherweise zusätzliche Logik, damit nicht automatisch zu B gewechselt wird. Dazu ist ein zusätzlicher Zustand erforderlich, der angibt, ob die Benutzeroberfläche zum anderen Bildschirm wechseln soll. Normalerweise wird dieser Status in der Benutzeroberfläche gespeichert, da die Navigationslogik zur Benutzeroberfläche gehört, nicht zum ViewModel. Das folgende Beispiel veranschaulicht diesen Aspekt.
Angenommen, Sie befinden sich im Registrierungsablauf Ihrer App. Auf dem Validierungsbildschirm Geburtsdatum wird das vom Nutzer eingegebene Datum vom ViewModel validiert, wenn der Nutzer auf die Schaltfläche „Weiter“ tippt. Das ViewModel delegiert die Validierungslogik an die Datenschicht. Wenn das Datum gültig ist, wird dem Nutzer der nächste Bildschirm angezeigt. Als zusätzliche Funktion können Nutzer zwischen den verschiedenen Registrierungsbildschirmen hin- und herwechseln, falls sie einige Daten ändern möchten. Daher befinden sich alle Ziele im Registrierungsablauf im selben Backstack. Angesichts dieser Anforderungen könnte der Bildschirm so aussehen:
// Key that identifies the `validationInProgress` state in the Bundle
private const val DOB_VALIDATION_KEY = "dobValidationKey"
class DobValidationFragment : Fragment() {
private var validationInProgress: Boolean = false
private val viewModel: DobValidationViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = // ...
validationInProgress = savedInstanceState?.getBoolean(DOB_VALIDATION_KEY) ?: false
binding.continueButton.setOnClickListener {
viewModel.validateDob()
validationInProgress = true
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect { uiState ->
// Update other parts of the UI ...
// If the input is valid and the user wants
// to navigate, navigate to the next screen
// and reset `validationInProgress` flag
if (uiState.isDobValid && validationInProgress) {
validationInProgress = false
navController.navigate(...) // Navigate to next screen
}
}
}
return binding
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(DOB_VALIDATION_KEY, validationInProgress)
}
}
Die Validierung des Geburtsdatums ist eine Geschäftslogik, für die das ViewModel zuständig ist. In den meisten Fällen delegiert das ViewModel diese Logik an die Datenschicht. Die Logik, mit der der Nutzer zum nächsten Bildschirm weitergeleitet wird, ist UI-Logik, da sich diese Anforderungen je nach UI-Konfiguration ändern können. Wenn Sie beispielsweise mehrere Registrierungsschritte gleichzeitig auf einem Tablet anzeigen, möchten Sie möglicherweise nicht, dass automatisch zum nächsten Bildschirm gewechselt wird. Die Variable validationInProgress im obigen Code implementiert diese Funktion und legt fest, ob die Benutzeroberfläche automatisch navigieren soll, wenn das Geburtsdatum gültig ist und der Nutzer mit dem nächsten Registrierungsschritt fortfahren möchte.
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- UI-Ebene
- State-Holder und UI-Status {:#mad-arch}
- Leitfaden zur App-Architektur