UI-Ereignisse sind Aktionen, die auf der UI-Ebene ausgeführt werden sollten, entweder von der UI.
oder ViewModel. Der häufigste Ereignistyp sind Nutzerereignisse. Der Nutzer
Nutzerereignisse durch Interaktionen mit der App generiert, z. B. durch Tippen auf
Bildschirm oder durch Gesten. Die UI verarbeitet diese Ereignisse dann mithilfe von
Callbacks wie onClick()
-Listener hinzugefügt.
Das ViewModel ist in der Regel für die Verarbeitung der Geschäftslogik eines
z. B. wenn der Nutzer auf eine Schaltfläche klickt,
Daten. Normalerweise verarbeitet ViewModel dies, indem es Funktionen zur Verfügung stellt, die von der Benutzeroberfläche
aufrufen. Nutzerereignisse können auch UI-Verhaltenslogik haben, die von der UI verarbeitet werden kann.
z. B. wenn Sie zu einem anderen Bildschirm wechseln oder
Snackbar
Während die Geschäftslogik für dieselbe App auf verschiedenen Mobilgeräten gleich bleibt. Plattformen oder Formfaktoren enthält, ist die UI-Verhaltenslogik ein Implementierungsdetail die in diesen Fällen abweichen können. Die UI-Ebene Seite definiert diese Arten von Logik als folgt:
- Geschäftslogik bezieht sich darauf, was mit Statusänderungen zu tun ist, z. B. eine Zahlung leisten oder Nutzereinstellungen speichern. Domain- und Datenebenen diese Logik verarbeiten. In diesem Leitfaden sind die Architekturkomponenten ViewModel-Klasse verwendet als spezielle Lösung für Klassen, die Geschäftslogik verarbeiten.
- UI-Verhaltenslogik oder UI-Logik bezieht sich auf die Anzeige des Status. z. B. die Navigationslogik oder die Art und Weise, wie den Nutzern Nachrichten angezeigt werden. Die Die UI verarbeitet diese Logik.
UI-Ereignis-Entscheidungsbaum
Das folgende Diagramm zeigt einen Entscheidungsbaum, um den besten Ansatz die Verarbeitung eines bestimmten Anwendungsfalls für ein bestimmtes Ereignis. Im weiteren Verlauf dieses Leitfadens werden diese im Detail betrachten.
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">Nutzerereignisse verarbeiten
Die Benutzeroberfläche kann Nutzerereignisse direkt verarbeiten, wenn diese Ereignisse sich auf die Änderung des Status eines UI-Elements, zum Beispiel der Status eines maximierbaren Elements. Wenn der Termin die Ausführung von Geschäftslogik erfordert, z. B. die Aktualisierung der Daten auf dem Bildschirm, sollte sie von ViewModel verarbeitet werden.
Das folgende Beispiel zeigt, wie verschiedene Schaltflächen zum Maximieren einer Benutzeroberfläche verwendet werden. (UI-Logik) und aktualisieren Sie die Daten auf dem Bildschirm (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 Struktur der Benutzeroberfläche erzeugt wird, z. B. bei einer RecyclerView
Artikel oder eine benutzerdefinierte View
hat, sollte weiterhin der ViewModel
der Nutzer sein, der den Nutzer verwaltet.
Ereignisse.
Angenommen, alle Nachrichtenartikel von NewsActivity
enthalten ein Lesezeichen
Schaltfläche. ViewModel
muss die ID der als Lesezeichen gespeicherten Nachricht kennen. Wann?
wenn der Nutzer ein Nachrichtenelement mit einem Lesezeichen versieht, ruft der RecyclerView
-Adapter die
addBookmark(newsId)
-Funktion aus dem ViewModel
offengelegt,
eine Abhängigkeit von ViewModel
. Stattdessen stellt ViewModel
ein Statusobjekt bereit.
mit dem Namen NewsItemUiState
, der die Implementierung zur Verarbeitung des
Ereignis:
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: den
Liste mit NewsItemUiState
-Objekten. Der Adapter hat keinen Zugriff auf das gesamte
ViewModel hinzugefügt, sodass es weniger wahrscheinlich ist, dass die von der
ViewModel verfügbar ist. Wenn Sie nur die Activity-Klasse mit ViewModel verwenden,
trennen Sie Verantwortlichkeiten. Dadurch wird sichergestellt, dass UI-spezifische Objekte wie Ansichten
oder RecyclerView
-Adapter nicht direkt mit ViewModel interagieren.
Namenskonventionen für Nutzerereignisfunktionen
In diesem Leitfaden werden die ViewModel-Funktionen, die Nutzerereignisse verarbeiten, mit einem
Verb basierend auf der Handlung basiert, die es verarbeitet. Beispiel: addBookmark(id)
oder
logIn(username, password)
.
ViewModel-Ereignisse verarbeiten
Benutzeroberflächenaktionen, die auf ViewModel-Ereignissen beruhen (ViewModel-Ereignisse), sollten immer wird der UI-Status aktualisiert. Dieses entspricht den Prinzipien für unidirektionale Daten Ablauf. Ereignisse reproduzierbar, nachdem Konfigurationsänderungen und sorgt dafür, dass UI-Aktionen nicht verloren gehen. Optional: können Sie Ereignisse nach dem Beenden des Prozesses reproduzierbar machen. Dazu verwenden Sie die Funktion gespeicherte Ereignisse Statusmodul.
Die Zuordnung von UI-Aktionen zum Status der Benutzeroberfläche ist nicht immer einfach, einfacher Logik zu schaffen. Ihr Denkprozess sollte nicht damit enden, zu bestimmen, wie Sie z. B. dazu, zu einem bestimmten Bildschirm zu navigieren. Sie müssen sich und überlegen Sie, wie Sie diesen User Flow in Ihrem UI-Status darstellen. In Mit anderen Worten: Denken Sie nicht darüber nach, welche Aktionen die Benutzeroberfläche ausführen muss. überlegen Sie, wie sich diese Aktionen auf den Status der Benutzeroberfläche auswirken.
Stellen Sie sich beispielsweise vor, dass Sie zum Startbildschirm navigieren, wenn die Nutzenden angemeldet sind. 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 des isUserLoggedIn
-Status und ruft die
bei Bedarf das richtige Ziel:
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 Verarbeitung von Ereignissen kann Statusaktualisierungen auslösen
Die Verarbeitung bestimmter ViewModel-Ereignisse in der Benutzeroberfläche kann zu einem anderen UI-Status führen Aktualisierungen. Wenn z. B. vorübergehende Meldungen auf dem Bildschirm angezeigt werden, wissen Nutzer, dass etwas passiert ist, muss ViewModel über die Benutzeroberfläche Ein weiteres Statusupdate wird ausgelöst, wenn die Nachricht auf dem Bildschirm angezeigt wurde. Die Ereignis, das eintritt, wenn der Nutzer die Nachricht gelesen hat, indem er sie oder nach Ablauf eines Zeitlimits) können als „Nutzereingabe“ und daher die Dies sollte bei ViewModel berücksichtigt werden. In diesem Fall kann der UI-Status wie folgt modelliert:
// 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
)
ViewModel würde den Benutzeroberflächenstatus wie folgt aktualisieren, wenn die Geschäftslogik erfordert, dass dem Nutzer eine neue vorübergehende Nachricht angezeigt wird:
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)
}
}
ViewModel muss nicht wissen, wie die Benutzeroberfläche die Nachricht auf der
Bildschirm; weiß es nur, dass eine
Botschaft für den Nutzer eingeblendet werden muss. Einmal
die vorübergehende Meldung angezeigt wurde, muss ViewModel von der Benutzeroberfläche
Dadurch wird das Attribut userMessage
bei einer weiteren Aktualisierung des UI-Status 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 die Nachricht temporär ist, ist der UI-Status ein eine naturgetreue Darstellung dessen, was auf dem Bildschirm zu einem bestimmten Zeitpunkt. Entweder wird die Nachricht für den Nutzer angezeigt oder nicht.
Navigationsereignisse
Das Attribut Ereignisse können Statusaktualisierungen auslösen wird beschrieben, wie Sie mithilfe des UI-Status Nutzermitteilungen auf der Bildschirm. Navigationsereignisse sind auch 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, dies erledigt, indem er den Navigations-Controller aufruft oder das Ereignis an die zusammensetzbare Funktion des Aufrufers an.
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 die Dateneingabe vor der Navigation eine Validierung der Geschäftslogik erfordert, ViewModel müsste diesen Status der Benutzeroberfläche zur Verfügung stellen. Die UI reagiert zu dieser Statusänderung und navigieren Sie entsprechend. ViewModel-Ereignisse verarbeiten Abschnitt für diesen Anwendungsfall. 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, wird nicht im Back-Stack beibehalten. Nutzer können nicht zu ihr zurückkehren, wenn sie drücken Sie „Zurück“. In Fällen, in denen dies jedoch der Fall sein könnte, würde die Lösung erfordern zusätzliche Logik.
Navigationsereignisse, wenn sich das Ziel im Back-Stack befindet
Wenn ein ViewModel einen Status festlegt, der ein Navigationsereignis vom Bildschirm auslöst A zu Bildschirm B und Bildschirm A im Back Stack der Navigation zusätzliche Logik, damit nicht automatisch zu B übergegangen wird. Um dies zu implementieren, ist ein zusätzlicher Status erforderlich, der angibt, ob die Benutzeroberfläche sollten Sie zum anderen Bildschirm wechseln. Normalerweise befindet sich dieser Status der Benutzeroberfläche, da die Navigationslogik von der Benutzeroberfläche abhängt, nicht von der ViewModel. Zur Veranschaulichung betrachten wir den folgenden Anwendungsfall.
Angenommen, Sie befinden sich im Registrierungsablauf Ihrer App. Am Datum Geburtsdatum angezeigt wird, wird das Datum validiert, sobald der Nutzer ein Datum eingibt. ViewModel aktiviert, wenn der Nutzer auf die Schaltfläche „Continue“ Schaltfläche. ViewModel die Validierungslogik an die Datenschicht delegiert. Wenn das Datum gültig ist, geht der Nutzer zum nächsten Bildschirm. Als zusätzliche Funktion können Nutzende zwischen den verschiedenen Anmeldebildschirmen wechseln, falls sie einige Daten. Daher werden alle Ziele im Registrierungsablauf an Back-Stack zurück. Angesichts dieser Anforderungen könnten Sie diesen Bildschirm wie folgt:
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, die das ViewModel
verantwortlich ist. Meistens delegiert ViewModel diese Logik an
mit der Datenschicht. Die Logik zum Navigieren des Nutzers zum nächsten Bildschirm
ist UI-Logik, da sich diese Anforderungen je nach UI ändern können.
Konfiguration. Zum Beispiel möchten Sie vielleicht nicht automatisch
Bildschirm eines Tablets angezeigt wird, wenn mehrere Registrierungsschritte
. Mit der Variable validationInProgress
im obigen Code wird
und legt fest, ob auf der Benutzeroberfläche
automatisch, wenn das Geburtsdatum gültig ist und der Nutzer
um zum nächsten Registrierungsschritt zu gelangen.
Weitere Anwendungsfälle
Wenn Sie der Meinung sind, dass Ihr Anwendungsfall für UI-Ereignisse nicht durch UI-Statusupdates gelöst werden kann, haben Sie müssen Sie möglicherweise den Datenfluss in Ihrer App überdenken. Hier einige Tipps: Prinzipien:
- Jeder Kurs sollte das tun, wofür er verantwortlich ist – nicht mehr. Die Benutzeroberfläche befindet sich in Kosten der bildschirmspezifischen Verhaltenslogik, wie z. B. Navigationsaufrufen, Klickklicks, und das Einholen von Berechtigungsanfragen. ViewModel enthält Unternehmen Logik und wandelt die Ergebnisse der unteren Ebenen der Hierarchie in die Benutzeroberfläche um. Bundesstaat.
- Überlegen Sie, wo das Ereignis seinen Ursprung hat. Entscheidungsfindung befolgen , die am Anfang dieses Leitfadens vorgestellt wird. für das, wofür sie verantwortlich sind. Wenn beispielsweise das Ereignis stammt und zu einem Navigationsereignis führt, muss in der UI gehandhabt werden. Ein Teil der Logik kann an ViewModel, Die Verarbeitung des Ereignisses kann jedoch nicht vollständig an das ViewModel delegiert werden.
- Wenn Sie mehrere Kunden haben und befürchten, dass die Veranstaltung die mehrmals genutzt werden, müssen Sie möglicherweise Ihre App-Architektur überdenken. Bei mehreren gleichzeitigen Nutzern erfolgt die Auslieferung genau einmal. ist es extrem schwierig, einen Vertrag zu garantieren, Komplexität und subtiles Verhalten explodieren. Wenn Sie dieses Problem haben, sollten Sie diese Bedenken in Ihrem UI-Baum nach oben schieben. benötigen Sie möglicherweise Entitäten weiter oben in der Hierarchie.
- Überlegen Sie, wann der Staat konsumiert werden muss. In bestimmten Situationen
sollten Sie den Zustand nicht weiter nutzen,
wenn sich die App im
Hintergrund, z. B.
Toast
. In diesen Fällen sollten Sie Status, wenn die UI im Vordergrund ausgeführt wird.
Produktproben
In den folgenden Google-Beispielen werden die UI-Ereignisse in der UI-Ebene. Sehen Sie sich diese Tipps in der Praxis an:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- UI-Ebene
- Statusinhaber und UI-Status {:#mad-arch}
- Leitfaden zur Anwendungsarchitektur