UI-Ereignisse sind Aktionen, die auf der UI-Ebene verarbeitet werden sollen, entweder von der UI oder von ViewModel. Der häufigste Ereignistyp sind Nutzerereignisse. Der Nutzer generiert Nutzerereignisse durch Interaktion mit der App, z. B. durch Tippen auf den Bildschirm oder durch Generieren von Gesten. Die UI 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 ein Nutzer zum Aktualisieren einiger Daten auf eine Schaltfläche klickt. Normalerweise stellt ViewModel Funktionen zur Verfügung, die die UI aufrufen kann. Nutzerereignisse können auch UI-Verhaltenslogik enthalten, die in der UI direkt verarbeitet werden kann, z. B. das Wechseln zu einem anderen Bildschirm oder das Anzeigen eines Snackbar
.
Während die Geschäftslogik für ein und dieselbe App auf verschiedenen mobilen Plattformen oder Formfaktoren gleich bleibt, ist die UI-Verhaltenslogik ein Implementierungsdetail, das in diesen Fällen unterschiedlich sein kann. Auf der Seite der UI-Ebene werden diese Logiktypen so definiert:
- Die Geschäftslogik bezieht sich darauf, was mit Statusänderungen zu tun ist, z. B. das Ausführen einer Zahlung oder das Speichern von Nutzereinstellungen. In der Regel übernehmen die Domain- und Datenschichten diese Logik. In diesem Leitfaden wird die Klasse Architecture Components ViewModel als Meinungslösung für Klassen verwendet, die Geschäftslogik verarbeiten.
- Die UI-Verhaltenslogik oder UI-Logik bezieht sich darauf, wie Statusänderungen angezeigt werden, z. B. die Navigationslogik oder wie Nachrichten für den Nutzer angezeigt werden. Die Benutzeroberfläche verarbeitet diese Logik.
Entscheidungsbaum für UI-Ereignisse
Das folgende Diagramm zeigt einen Entscheidungsbaum, um den besten Ansatz für den Umgang mit einem bestimmten Anwendungsfall zu finden. Im weiteren Verlauf dieses Leitfadens werden diese Ansätze ausführlich erläutert.
![Wenn das Ereignis aus „ViewModel“ stammt, aktualisieren Sie den UI-Status. Wenn das Ereignis von der UI stammt und Geschäftslogik erfordert, delegieren Sie die Geschäftslogik an ViewModel. Wenn das Ereignis aus der UI stammt und eine UI-Verhaltenslogik erfordert, ändern Sie den Status des UI-Elements direkt in der UI.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-uievents-tree.png?authuser=6&hl=de)
Nutzerereignisse verarbeiten
Nutzerereignisse können direkt über die UI verarbeitet werden, wenn sie sich auf die Änderung des Status eines UI-Elements beziehen, z. B. des Zustands eines Expandable-Elements. Wenn für das Ereignis eine Geschäftslogik erforderlich ist, z. B. die Aktualisierung der Daten auf dem Bildschirm, sollte es von ViewModel verarbeitet werden.
Das folgende Beispiel zeigt, wie verschiedene Schaltflächen verwendet werden, um ein UI-Element zu maximieren (UI-Logik) und die Daten auf dem Bildschirm zu aktualisieren (Geschäftslogik):
Aufrufe
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()
}
}
}
Schreiben
@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {
// State of whether more details should be shown
var expanded by remember { mutableStateOf(false) }
Column {
Text("Some text")
if (expanded) {
Text("More details")
}
Button(
// The expand details event is processed by the UI that
// modifies this composable's internal state.
onClick = { expanded = !expanded }
) {
val expandText = if (expanded) "Collapse" else "Expand"
Text("$expandText details")
}
// The refresh event is processed by the ViewModel that is in charge
// of the UI's business logic.
Button(onClick = { viewModel.refreshNews() }) {
Text("Refresh data")
}
}
}
Nutzerereignisse in RecyclerViews
Wenn die Aktion weiter unten in der UI-Baumstruktur generiert wird, z. B. in einem RecyclerView
-Element oder einem benutzerdefinierten View
, sollte ViewModel
weiterhin die Aktion sein, die Nutzerereignisse verarbeitet.
Angenommen, alle Nachrichtenartikel von NewsActivity
enthalten eine Lesezeichenschaltfläche. ViewModel
muss die ID des als Lesezeichen gespeicherten Nachrichtenelements kennen. Wenn der Nutzer ein Nachrichtenelement als Lesezeichen speichert, ruft der RecyclerView
-Adapter die verfügbare addBookmark(newsId)
-Funktion aus der ViewModel
nicht auf, für die eine Abhängigkeit von ViewModel
erforderlich wäre. Stattdessen stellt ViewModel
ein Statusobjekt mit dem Namen NewsItemUiState
zur Verfügung, 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)
}
)
}
}
Auf diese Weise funktioniert der RecyclerView
-Adapter nur mit den Daten, die er benötigt: die Liste der NewsItemUiState
-Objekte. Der Adapter hat keinen Zugriff auf die gesamte ViewModel. Dadurch ist es unwahrscheinlicher, dass die von ViewModel bereitgestellte Funktion missbraucht wird. Wenn Sie zulassen, dass nur die Aktivitätsklasse mit dem ViewModel arbeitet, trennen Sie die Verantwortlichkeiten. Dadurch wird sichergestellt, 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 basierend auf der Aktion benannt, die sie verarbeiten, z. B. addBookmark(id)
oder logIn(username, password)
.
ViewModel-Ereignisse verarbeiten
UI-Aktionen, die aus ViewModel-Ereignissen – also ViewModel-Ereignissen – stammen, sollten immer zu einer Aktualisierung des UI-Status führen. Dies entspricht den Prinzipien des unidirektionalen Datenflusses. Sie sorgt dafür, dass Ereignisse nach Konfigurationsänderungen reproduziert werden und dass UI-Aktionen nicht verloren gehen. Optional können Sie Ereignisse auch nach dem Prozessende reproduzieren, wenn Sie das gespeicherte Statusmodul verwenden.
Das Zuordnen von UI-Aktionen zum UI-Status ist nicht immer einfach, führt aber zu einer einfacheren Logik. Ihr Denkprozess sollte nicht damit enden, wie die UI zu einem bestimmten Bildschirm navigieren soll. Sie müssen weiter überdenken und überlegen, wie Sie diesen Nutzerfluss in Ihrem UI-Status darstellen können. Mit anderen Worten: Überlegen Sie nicht, welche Aktionen die UI ausführen muss, sondern überlegen Sie, wie sich diese Aktionen auf den Status der UI auswirken.
Stellen Sie sich beispielsweise vor, Sie gehen zum Startbildschirm, wenn der Nutzer auf dem Anmeldebildschirm angemeldet ist. Sie könnten dies im UI-Status so modellieren:
data class LoginUiState(
val isLoading: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
Diese UI reagiert auf Änderungen am Status isUserLoggedIn
und ruft bei Bedarf das richtige Ziel auf:
Aufrufe
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.
}
...
}
}
}
}
}
Schreiben
class LoginViewModel : ViewModel() {
var uiState by mutableStateOf(LoginUiState())
private set
/* ... */
}
@Composable
fun LoginScreen(
viewModel: LoginViewModel = viewModel(),
onUserLogIn: () -> Unit
) {
val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
// Whenever the uiState changes, check if the user is logged in.
LaunchedEffect(viewModel.uiState) {
if (viewModel.uiState.isUserLoggedIn) {
currentOnUserLogIn()
}
}
// Rest of the UI for the login screen.
}
Die Nutzung von Ereignissen kann Statusaktualisierungen auslösen
Die Verarbeitung bestimmter ViewModel-Ereignisse in der UI kann zu anderen UI-Statusaktualisierungen führen. Wenn beispielsweise vorübergehende Meldungen auf dem Bildschirm angezeigt werden, um den Nutzer über etwas zu informieren, muss die UI die ViewModel-Benutzeroberfläche benachrichtigen, damit eine weitere Statusaktualisierung ausgelöst wird, wenn die Nachricht auf dem Bildschirm angezeigt wurde. Das Ereignis, das eintritt, wenn der Nutzer die Nachricht abruft (durch Schließen oder nach einer Zeitüberschreitung) kann als „Nutzereingabe“ behandelt werden. Dies sollte dem ViewModel bewusst sein. In dieser Situation 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 aktualisiert den UI-Status wie folgt, wenn die Geschäftslogik dem Nutzer eine neue vorübergehende Nachricht anzeigen muss:
Aufrufe
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)
}
}
}
Schreiben
class LatestNewsViewModel(/* ... */) : ViewModel() {
var uiState by mutableStateOf(LatestNewsUiState())
private set
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
uiState = uiState.copy(userMessage = "No Internet connection")
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
uiState = uiState.copy(userMessage = null)
}
}
Das ViewModel muss nicht wissen, wie die UI die Nachricht auf dem Bildschirm anzeigt. Es weiß nur, dass eine Nutzernachricht angezeigt werden muss. Sobald die vorübergehende Meldung angezeigt wurde, muss die UI die ViewModel darüber informieren. Daraufhin wird das Attribut userMessage
in einer weiteren UI-Statusaktualisierung gelöscht:
Aufrufe
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()
}
...
}
}
}
}
}
Schreiben
@Composable
fun LatestNewsScreen(
snackbarHostState: SnackbarHostState,
viewModel: LatestNewsViewModel = viewModel(),
) {
// Rest of the UI content.
// If there are user messages to show on the screen,
// show it and notify the ViewModel.
viewModel.uiState.userMessage?.let { userMessage ->
LaunchedEffect(userMessage) {
snackbarHostState.showSnackbar(userMessage)
// Once the message is displayed and dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
}
}
Auch wenn es sich um eine vorübergehende Nachricht handelt, ist der Status der Benutzeroberfläche eine realistische Darstellung dessen, was zu jedem Zeitpunkt auf dem Bildschirm angezeigt wird. Entweder wird die Nutzernachricht angezeigt oder nicht.
Navigationsereignisse
Im Abschnitt Verbrauchsereignisse können Statusaktualisierungen auslösen wird beschrieben, wie Sie den UI-Status verwenden, um Nutzernachrichten auf dem Bildschirm anzuzeigen. Navigationsereignisse sind auch ein häufiger Ereignistyp in Android-Apps.
Wenn das Ereignis in der UI ausgelöst wird, weil der Nutzer auf eine Schaltfläche getippt hat, übernimmt die UI dies, indem der Navigations-Controller aufgerufen oder das Ereignis entsprechend der aufrufenden zusammensetzbaren Funktion verfügbar gemacht wird.
Aufrufe
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
}
}
}
Schreiben
@Composable
fun LoginScreen(
onHelp: () -> Unit, // Caller navigates to the right screen
viewModel: LoginViewModel = viewModel()
) {
// Rest of the UI
Button(onClick = onHelp) {
Text("Get help")
}
}
Wenn für die Dateneingabe vor dem Navigieren eine Validierung der Geschäftslogik erforderlich ist, muss ViewModel diesen Status für die UI bereitstellen. Die UI würde auf diese Statusänderung reagieren und entsprechend navigieren. Dieser Anwendungsfall wird im Abschnitt Umgang mit ViewModel-Ereignissen beschrieben. Hier ist ein ähnlicher Code:
Aufrufe
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.
}
...
}
}
}
}
}
Schreiben
@Composable
fun LoginScreen(
onUserLogIn: () -> Unit, // Caller navigates to the right screen
viewModel: LoginViewModel = viewModel()
) {
Button(
onClick = {
// ViewModel validation is triggered
viewModel.login()
}
) {
Text("Log in")
}
// Rest of the UI
val lifecycle = LocalLifecycleOwner.current.lifecycle
val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
LaunchedEffect(viewModel, lifecycle) {
// Whenever the uiState changes, check if the user is logged in and
// call the `onUserLogin` event when `lifecycle` is at least STARTED
snapshotFlow { viewModel.uiState }
.filter { it.isUserLoggedIn }
.flowWithLifecycle(lifecycle)
.collect {
currentOnUserLogIn()
}
}
}
Im obigen Beispiel funktioniert die Anwendung wie erwartet, da das aktuelle Ziel, Log-in, nicht im Back-Stack gehalten wird. Nutzende können nicht mehr dorthin zurückgehen, wenn sie „Zurück“ drücken. Sollte dies jedoch der Fall sein, erfordert die Lösung eine zusätzliche Logik.
Navigationsereignisse, wenn das Ziel im Back Stack liegt
Wenn ein ViewModel einen Status festlegt, der ein Navigationsereignis von Bildschirm A zu Bildschirm B erzeugt, und Bildschirm A im Backstapel der Navigation bleibt, benötigen Sie möglicherweise zusätzliche Logik, um nicht automatisch zu B zu gelangen. Für die Implementierung ist ein zusätzlicher Status erforderlich, der angibt, ob über die UI der andere Bildschirm aufgerufen werden soll. Normalerweise wird dieser Status in der UI übernommen, da die Navigationslogik ein Problem der UI und nicht von ViewModel ist. Betrachten wir zur Veranschaulichung den folgenden Anwendungsfall.
Angenommen, Sie befinden sich im Registrierungsvorgang Ihrer App. Wenn der Nutzer im Überprüfungsbildschirm für das Geburtsdatum ein Datum eingibt, wird es durch ViewModel bestätigt, sobald der Nutzer auf die Schaltfläche "Weiter" tippt. Das ViewModel delegiert die Validierungslogik an die Datenschicht. Wenn das Datum gültig ist, wechselt der Nutzer zum nächsten Bildschirm. Als zusätzliche Funktion können Nutzer bei Bedarf zwischen den verschiedenen Registrierungsbildschirmen hin- und herwechseln. Daher werden alle Ziele im Registrierungsablauf im selben Back-Stack gehalten. Angesichts dieser Anforderungen könnten Sie diesen Bildschirm so implementieren:
Aufrufe
// 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)
}
}
Schreiben
class DobValidationViewModel(/* ... */) : ViewModel() {
var uiState by mutableStateOf(DobValidationUiState())
private set
}
@Composable
fun DobValidationScreen(
onNavigateToNextScreen: () -> Unit, // Caller navigates to the right screen
viewModel: DobValidationViewModel = viewModel()
) {
// TextField that updates the ViewModel when a date of birth is selected
var validationInProgress by rememberSaveable { mutableStateOf(false) }
Button(
onClick = {
viewModel.validateInput()
validationInProgress = true
}
) {
Text("Continue")
}
// Rest of the UI
/*
* The following code implements the requirement of advancing automatically
* to the next screen when a valid date of birth has been introduced
* and the user wanted to continue with the registration process.
*/
if (validationInProgress) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val currentNavigateToNextScreen by rememberUpdatedState(onNavigateToNextScreen)
LaunchedEffect(viewModel, lifecycle) {
// If the date of birth is valid and the validation is in progress,
// navigate to the next screen when `lifecycle` is at least STARTED,
// which is the default Lifecycle.State for the `flowWithLifecycle` operator.
snapshotFlow { viewModel.uiState }
.filter { it.isDobValid }
.flowWithLifecycle(lifecycle)
.collect {
validationInProgress = false
currentNavigateToNextScreen()
}
}
}
}
Die Validierung des Geburtsdatums ist die Geschäftslogik, für die ViewModel verantwortlich ist. In den meisten Fällen delegiert ViewModel diese Logik an die Datenschicht. Die Logik, um den Nutzer zum nächsten Bildschirm zu leiten, ist die UI-Logik, da diese Anforderungen je nach UI-Konfiguration variieren können. Es ist beispielsweise nicht sinnvoll, automatisch zu einem anderen Bildschirm auf einem Tablet zu wechseln, wenn mehrere Registrierungsschritte gleichzeitig angezeigt werden. Die Variable validationInProgress
im Code oben implementiert diese Funktion und steuert, ob die UI automatisch aufgerufen werden soll, wenn das Geburtsdatum gültig ist und der Nutzer mit dem folgenden Registrierungsschritt fortfahren möchte.
Andere Anwendungsfälle
Wenn Sie der Meinung sind, dass der Anwendungsfall Ihres UI-Ereignisses nicht durch UI-Statusaktualisierungen gelöst werden kann, müssen Sie möglicherweise den Datenfluss in Ihrer App noch einmal überdenken. Berücksichtigen Sie dabei die folgenden Prinzipien:
- Jede Klasse sollte tun, wofür sie verantwortlich sind, nicht mehr. Die Benutzeroberfläche ist für bildschirmspezifische Verhaltenslogik zuständig, z. B. Navigationsaufrufe, Klickereignisse und das Abrufen von Berechtigungsanfragen. Das ViewModel enthält Geschäftslogik und wandelt die Ergebnisse von niedrigeren Ebenen der Hierarchie in den Status der Benutzeroberfläche um.
- Denken Sie darüber nach, woher das Ereignis stammt. Folgen Sie dem Entscheidungsbaum am Anfang dieser Anleitung und legen Sie fest, wofür jede Klasse zuständig ist. Wenn das Ereignis beispielsweise aus der UI stammt und zu einem Navigationsereignis führt, muss dieses Ereignis in der UI verarbeitet werden. Ein Teil der Logik kann an ViewModel delegiert werden. Die Verarbeitung des Ereignisses kann jedoch nicht vollständig an ViewModel delegiert werden.
- Wenn Sie mehrere Nutzer haben und befürchten, dass das Ereignis mehrmals genutzt wird, müssen Sie möglicherweise Ihre Anwendungsarchitektur überdenken. Wenn mehrere Nutzer gleichzeitig mehrere Nutzer haben, kann dies dazu führen, dass der Vertrag genau einmal zugestellt sehr schwer zu garantieren ist und die Komplexität und das subtile Verhalten explodieren. Wenn Sie dieses Problem haben, sollten Sie diese Bedenken in Ihrer UI-Struktur nach oben schieben. Möglicherweise benötigen Sie eine andere Entität weiter oben in der Hierarchie.
- Überlegen Sie, wann der Staat konsumiert werden muss. In bestimmten Situationen kann es sinnvoll sein, den Status nicht weiter zu nutzen, wenn die Anwendung im Hintergrund läuft, z. B. durch Anzeige eines
Toast
. In diesen Fällen kann es sinnvoll sein, den Status zu übernehmen, wenn die UI im Vordergrund ausgeführt wird.
Produktproben
In den folgenden Google-Beispielen werden die UI-Ereignisse auf der UI-Ebene veranschaulicht. Sehen Sie sich diese Tipps in der Praxis an:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- UI-Ebene
- Staatsinhaber und UI-Status {:#mad-arch}
- Leitfaden zur App-Architektur