אירועים בממשק המשתמש

אירועים בממשק המשתמש הם פעולות שצריך לטפל בהן בשכבת ממשק המשתמש, על ידי ממשק המשתמש או על ידי ViewModel. הסוג הנפוץ ביותר של אירועים הוא אירועים שקשורים למשתמשים. המשתמש יוצר אירועים על ידי אינטראקציה עם האפליקציה – למשל, על ידי הקשה על המסך או על ידי יצירת תנועות. לאחר מכן, ממשק המשתמש צורך את האירועים האלה באמצעות קריאות חוזרות (callbacks) כמו onClick() listeners.

בדרך כלל, ViewModel אחראי לטיפול בלוגיקה העסקית של אירוע משתמש מסוים – לדוגמה, משתמש לוחץ על לחצן כדי לרענן נתונים מסוימים. בדרך כלל, ה-ViewModel מטפל בזה על ידי חשיפת פונקציות שממשק המשתמש יכול לקרוא להן. יכול להיות שאירועים של משתמשים כוללים גם לוגיקה של התנהגות בממשק המשתמש, שהממשק יכול לטפל בה ישירות – למשל, מעבר למסך אחר או הצגה של Snackbar.

הלוגיקה העסקית נשארת זהה לאותה אפליקציה בפלטפורמות שונות לניידים או בגורמי צורה שונים, אבל הלוגיקה של התנהגות ממשק המשתמש היא פרט הטמעה שעשוי להיות שונה בין המקרים האלה. בדף של שכבת ממשק המשתמש מוגדרים סוגי הלוגיקה האלה:

  • הלוגיקה העסקית מתייחסת למה צריך לעשות עם שינויים במצב – לדוגמה, ביצוע תשלום או שמירת העדפות משתמש. הלוגיקה הזו בדרך כלל מטופלת על ידי הדומיין ושכבות הנתונים. במדריך הזה, המחלקה Architecture Components ViewModel משמשת כפתרון מומלץ למחלקות שמטפלות בלוגיקה עסקית.
  • לוגיקת התנהגות ממשק המשתמש או לוגיקת ממשק המשתמש מתייחסת לאופן ההצגה של שינויים במצב – לדוגמה, לוגיקת הניווט או אופן הצגת ההודעות למשתמש. ממשק המשתמש מטפל בלוגיקה הזו.

עץ החלטות של אירועים בממשק המשתמש

בתרשים הבא מוצג עץ החלטות שיעזור לכם למצוא את הגישה הכי טובה לטיפול בתרחיש שימוש מסוים באירוע. בהמשך המדריך מוסבר בפירוט על הגישות האלה.

אם האירוע נוצר ב-ViewModel, צריך לעדכן את מצב ממשק המשתמש. אם
    האירוע נוצר בממשק המשתמש ודורש לוגיקה עסקית, צריך להעביר את הלוגיקה העסקית ל-ViewModel. אם האירוע נוצר בממשק המשתמש ונדרשת לוגיקת התנהגות של ממשק המשתמש, צריך לשנות את מצב רכיב ממשק המשתמש ישירות בממשק המשתמש.
איור 1. עץ החלטות לטיפול באירועים.

טיפול באירועי משתמשים

ממשק המשתמש יכול לטפל ישירות באירועים של משתמשים אם האירועים האלה קשורים לשינוי המצב של רכיב בממשק המשתמש – לדוגמה, המצב של פריט שניתן להרחבה. אם האירוע דורש ביצוע של לוגיקה עסקית, כמו רענון הנתונים במסך, צריך לעבד אותו באמצעות ViewModel.

בדוגמה הבאה אפשר לראות איך משתמשים בכפתורים שונים כדי להרחיב רכיב בממשק המשתמש (לוגיקת ממשק משתמש) ולרענן את הנתונים במסך (לוגיקה עסקית):

צפיות

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

אימייל חדש

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

אירועי משתמשים ב-RecyclerViews

אם הפעולה נוצרת בהמשך העץ של ממשק המשתמש, כמו בRecyclerView item או בView מותאם אישית, עדיין ViewModel צריך לטפל באירועים של המשתמש.

לדוגמה, נניח שכל פריטי החדשות מ-NewsActivity מכילים לחצן לסימון במועדפים. התג ViewModel צריך לדעת את המזהה של פריט החדשות שנוסף לסימנייה. כשמשתמש מוסיף סימנייה לפריט חדשות, המתאם RecyclerView לא קורא לפונקציה addBookmark(newsId) שמוצגת מ-ViewModel, מה שמחייב תלות ב-ViewModel. במקום זאת, הרכיב ViewModel חושף אובייקט מצב שנקרא NewsItemUiState, שמכיל את ההטמעה לטיפול באירוע:

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

כך, מתאם RecyclerView פועל רק עם הנתונים שהוא צריך: רשימת אובייקטים NewsItemUiState. למתאם אין גישה ל-ViewModel כולו, ולכן הסיכוי לשימוש לרעה בפונקציונליות שנחשפת על ידי ה-ViewModel נמוך יותר. כשמאפשרים רק למחלקת הפעילות לעבוד עם ViewModel, מפרידים בין האחריות. כך מוודאים שאובייקטים ספציפיים לממשק המשתמש, כמו תצוגות או מתאמי RecyclerView, לא יוצרים אינטראקציה ישירה עם ViewModel.

מוסכמות מתן שם של פונקציות של אירועים ברמת המשתמש

במדריך הזה, הפונקציות של ViewModel שמטפלות באירועים של משתמשים נקראות בשם שמתחיל בפועל שמתאר את הפעולה שהן מבצעות – לדוגמה: addBookmark(id) או logIn(username, password).

טיפול באירועים של ViewModel

פעולות בממשק המשתמש שמקורן ב-ViewModel – אירועי ViewModel – צריכות תמיד להוביל לעדכון של מצב ממשק המשתמש. הפעולה הזו תואמת לעקרונות של זרימת נתונים חד-כיוונית. הוא מאפשר לשחזר אירועים אחרי שינויים בהגדרות, ומבטיח שפעולות בממשק המשתמש לא יאבדו. אופציונלית, אפשר גם לשחזר אירועים אחרי שהתהליך מסתיים אם משתמשים במודול של מצב שמור.

מיפוי פעולות בממשק המשתמש למצב ממשק המשתמש הוא לא תמיד תהליך פשוט, אבל הוא מוביל ללוגיקה פשוטה יותר. תהליך החשיבה שלכם לא צריך להסתיים בקביעה איך להעביר את המשתמש למסך מסוים בממשק המשתמש, למשל. צריך לחשוב מעבר לכך ולשקול איך לייצג את מסלול המשתמש במצב ממשק המשתמש. במילים אחרות: אל תחשבו על הפעולות שממשק המשתמש צריך לבצע, אלא על האופן שבו הפעולות האלה משפיעות על מצב ממשק המשתמש.

לדוגמה, נניח שהמשתמש מנווט למסך הבית כשהוא מחובר במסך הכניסה. אפשר להגדיר את המודל הזה במצב ממשק המשתמש באופן הבא:

data class LoginUiState(
    val isLoading: Boolean = false,
    val errorMessage: String? = null,
    val isUserLoggedIn: Boolean = false
)

ממשק המשתמש הזה מגיב לשינויים במצב isUserLoggedIn ועובר ליעד הנכון לפי הצורך:

צפיות

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

אימייל חדש

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

צריכת אירועים יכולה להפעיל עדכוני סטטוס

צריכה של אירועים מסוימים של ViewModel בממשק המשתמש עשויה להוביל לעדכונים אחרים של מצב ממשק המשתמש. לדוגמה, כשמציגים הודעות זמניות במסך כדי להודיע למשתמש שמשהו קרה, ממשק המשתמש צריך להודיע ל-ViewModel להפעיל עדכון נוסף של המצב אחרי שההודעה הוצגה במסך. האירוע שמתרחש כשהמשתמש צורך את ההודעה (על ידי סגירה או אחרי זמן קצוב לתפוגה) יכול להיחשב כ'קלט של משתמשים', ולכן ה-ViewModel צריך להיות מודע לכך. במקרה כזה, אפשר לתאר את מצב ממשק המשתמש כך:

// 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 יעודכן במצב ממשק המשתמש באופן הבא, כשנדרש להציג למשתמש הודעה זמנית חדשה בגלל הלוגיקה העסקית:

צפיות

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

אימייל חדש

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 לא צריך לדעת איך ממשק המשתמש מציג את ההודעה במסך, הוא רק יודע שיש הודעה למשתמש שצריך להציג. אחרי שההודעה הזמנית מוצגת, ממשק המשתמש צריך להודיע על כך ל-ViewModel, מה שגורם לעדכון נוסף של מצב ממשק המשתמש כדי לנקות את המאפיין userMessage:

צפיות

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

אימייל חדש

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

למרות שההודעה היא זמנית, מצב ממשק המשתמש הוא ייצוג נאמן של מה שמוצג במסך בכל נקודת זמן. ההודעה למשתמש מוצגת או לא מוצגת.

בקטע אירועים שמופעלים יכולים לעדכן את המצב מוסבר איך משתמשים במצב ממשק המשתמש כדי להציג הודעות למשתמשים במסך. אירועי ניווט הם גם סוג נפוץ של אירועים באפליקציית Android.

אם האירוע מופעל בממשק המשתמש כי המשתמש הקיש על לחצן, ממשק המשתמש מטפל בזה על ידי קריאה לבקר הניווט או חשיפת האירוע לרכיב הניתן להרכבה של המתקשר, לפי הצורך.

צפיות

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

אימייל חדש

@Composable
fun LoginScreen(
    onHelp: () -> Unit, // Caller navigates to the right screen
    viewModel: LoginViewModel = viewModel()
) {
    // Rest of the UI

    Button(onClick = onHelp) {
        Text("Get help")
    }
}

אם נדרש אימות של לוגיקה עסקית מסוימת לפני המעבר, ה-ViewModel צריך לחשוף את המצב הזה לממשק המשתמש. ממשק המשתמש יגיב לשינוי המצב הזה וינווט בהתאם. התרחיש הזה מוסבר בקטע בנושא טיפול באירועים של ViewModel. הנה קוד דומה:

צפיות

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

אימייל חדש

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

בדוגמה שלמעלה, האפליקציה פועלת כמצופה כי היעד הנוכחי, Login, לא יישמר במקבץ פעילויות קודמות (back stack). אם המשתמשים ילחצו על לחצן החזרה, הם לא יוכלו לחזור אליה. עם זאת, במקרים שבהם זה עלול לקרות, הפתרון ידרוש לוגיקה נוספת.

כש-ViewModel מגדיר מצב מסוים שיוצר אירוע ניווט ממסך א' למסך ב', ומסך א' נשמר במקבץ פעילויות קודמות (back stack) של הניווט, יכול להיות שתצטרכו להוסיף לוגיקה כדי שלא תהיה התקדמות אוטומטית למסך ב'. כדי להטמיע את זה, צריך מצב נוסף שמציין אם ממשק המשתמש צריך להתייחס לניווט למסך השני. בדרך כלל, המצב הזה נשמר בממשק המשתמש כי לוגיקת הניווט היא חלק מממשק המשתמש ולא מ-ViewModel. כדי להמחיש את זה, נתייחס לתרחיש השימוש הבא.

נניח שאתם נמצאים בתהליך ההרשמה באפליקציה. במסך האימות של תאריך הלידה, כשהמשתמש מזין תאריך, ה-ViewModel מאמת את התאריך כשהמשתמש מקיש על הלחצן 'המשך'. ה-ViewModel מעביר את לוגיקת האימות לשכבת הנתונים. אם התאריך תקין, המשתמש עובר למסך הבא. בנוסף, המשתמשים יכולים לחזור קדימה ואחורה בין מסכי ההרשמה השונים אם הם רוצים לשנות נתונים מסוימים. לכן, כל היעדים בתהליך ההרשמה נשמרים באותו מקבץ פעילויות קודמות (back stack). בהתאם לדרישות האלה, אפשר להטמיע את המסך הזה באופן הבא:

צפיות

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

אימייל חדש

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

אימות תאריך הלידה הוא לוגיקה עסקית ש-ViewModel אחראי לה. ברוב המקרים, ה-ViewModel יעביר את הלוגיקה הזו לשכבת הנתונים. הלוגיקה להעברת המשתמש למסך הבא היא לוגיקת ממשק משתמש, כי הדרישות האלה עשויות להשתנות בהתאם להגדרת ממשק המשתמש. לדוגמה, אם מציגים כמה שלבי הרשמה בו-זמנית בטאבלט, יכול להיות שלא תרצו שהמשתמש יעבור אוטומטית למסך אחר. המשתנה validationInProgress בקוד שלמעלה מטמיע את הפונקציונליות הזו ומטפל בשאלה אם ממשק המשתמש צריך לנווט באופן אוטומטי בכל פעם שתאריך הלידה תקין והמשתמש רוצה להמשיך לשלב הבא בתהליך ההרשמה.

תרחישים אחרים לדוגמה

אם לדעתכם אי אפשר לפתור את תרחיש השימוש באירועים בממשק המשתמש באמצעות עדכונים של מצב ממשק המשתמש, יכול להיות שתצטרכו לשנות את האופן שבו הנתונים זורמים באפליקציה. כדאי להביא בחשבון את העקרונות הבאים:

  • כל כיתה צריכה לעשות את מה שהיא אחראית לו, ולא יותר. ממשק המשתמש אחראי ללוגיקת ההתנהגות הספציפית למסך, כמו קריאות לניווט, אירועי קליקים וקבלת בקשות הרשאה. ה-ViewModel מכיל לוגיקה עסקית וממיר את התוצאות משכבות נמוכות יותר בהיררכיה למצב של ממשק המשתמש.
  • תחשבו מאיפה האירוע מגיע. פועלים לפי עץ ההחלטות שמוצג בתחילת המדריך הזה, ודואגים שכל מחלקה תטפל במה שהיא אחראית לו. לדוגמה, אם האירוע נוצר בממשק המשתמש והוא מוביל לאירוע ניווט, צריך לטפל באירוע הזה בממשק המשתמש. חלק מהלוגיקה עשוי להיות מוקצה ל-ViewModel, אבל אי אפשר להקצות את הטיפול באירוע כולו ל-ViewModel.
  • אם יש לכם כמה צרכנים ואתם חוששים שהאירוע ייצרך כמה פעמים, כדאי לשקול מחדש את ארכיטקטורת האפליקציה. אם יש כמה צרכנים בו-זמניים, קשה מאוד להבטיח את החוזה delivered exactly once, ולכן רמת המורכבות וההתנהגות העדינה עולה באופן משמעותי. אם נתקלתם בבעיה הזו, כדאי להעביר את הבעיות האלה למעלה בעץ ממשק המשתמש. יכול להיות שתצטרכו ישות אחרת בהיקף גבוה יותר בהיררכיה.
  • חשבו מתי צריך להשתמש במצב. במצבים מסוימים, יכול להיות שלא תרצו להמשיך להשתמש במצב הצריכה כשהאפליקציה פועלת ברקע – לדוגמה, כשמוצגת מודעה מסוג Toast. במקרים כאלה, כדאי לצרוך את המצב כשהממשק נמצא בחזית.

דוגמאות

בדוגמאות הבאות של Google מוצגים אירועים בממשק המשתמש בשכבת ממשק המשתמש. כדאי לעיין בהם כדי לראות איך ההנחיות האלה באות לידי ביטוי בפועל: