UI-Ereignisse

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.

<ph type="x-smartling-placeholder">

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> Wenn das Ereignis aus ViewModel stammt, aktualisieren Sie den UI-Status. Wenn
    das Ereignis stammte von der UI und erfordert Geschäftslogik. Dann delegieren Sie
    die Geschäftslogik in ViewModel. Ob das Ereignis aus der Benutzeroberfläche stammt und
    UI-Verhaltenslogik erfordert, und ändern Sie dann den Status des UI-Elements direkt in der
    UI.
<ph type="x-smartling-placeholder">
</ph> Abbildung 1: Entscheidungsbaum für die Verarbeitung von Ereignissen

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.

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.

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: