เหตุการณ์ UI

เหตุการณ์ UI คือการดำเนินการที่ควรจัดการในเลเยอร์ UI ไม่ว่าจะเป็นโดย UI หรือ ViewModel เหตุการณ์ประเภทที่พบบ่อยที่สุดคือ เหตุการณ์ของผู้ใช้ ผู้ใช้สร้างเหตุการณ์ของผู้ใช้โดยการโต้ตอบกับแอป เช่น การแตะหน้าจอหรือการสร้างท่าทางสัมผัส จากนั้น UI จะใช้เหตุการณ์เหล่านี้โดยใช้ การเรียกกลับ เช่น แลมบ์ดาที่กำหนดไว้ใน Composables ต่างๆ

โดยปกติแล้ว ViewModel จะมีหน้าที่จัดการตรรกะทางธุรกิจของเหตุการณ์ของผู้ใช้หนึ่งๆ เช่น ผู้ใช้คลิกปุ่มเพื่อรีเฟรชข้อมูลบางอย่าง โดยปกติแล้ว ViewModel จะจัดการเรื่องนี้ด้วยการแสดงฟังก์ชันที่ UI เรียกได้ เหตุการณ์ของผู้ใช้อาจมีตรรกะลักษณะการทำงานของ UI ที่ UI จัดการได้ โดยตรง เช่น การไปยังหน้าจออื่นหรือการแสดง Snackbar

แม้ว่า ตรรกะทางธุรกิจ จะยังคงเหมือนเดิมสำหรับแอปเดียวกันในแพลตฟอร์มอุปกรณ์เคลื่อนที่หรือฟอร์มแฟกเตอร์ต่างๆ แต่ ตรรกะลักษณะการทำงานของ UI เป็นรายละเอียดการติดตั้งใช้งานที่อาจแตกต่างกันไปในแต่ละกรณี หน้าเลเยอร์ UI จะกำหนดตรรกะประเภทต่างๆ ดัง นี้

  • ตรรกะทางธุรกิจ หมายถึง สิ่งที่ต้องทำ กับการเปลี่ยนแปลงสถานะ เช่น การชำระเงินหรือการจัดเก็บค่ากำหนดของผู้ใช้ โดยปกติแล้วเลเยอร์โดเมนและเลเยอร์ข้อมูลจะเป็นผู้จัดการตรรกะนี้ ตลอดทั้งคู่มือนี้ เราจะใช้คลาส Architecture Components ViewModel ของคอมโพเนนต์สถาปัตยกรรมเป็นโซลูชันที่แนะนำสำหรับคลาสที่จัดการตรรกะทางธุรกิจ
  • ตรรกะลักษณะการทำงานของ UI หรือ ตรรกะ UI หมายถึง วิธีแสดง การเปลี่ยนแปลงสถานะ เช่น ตรรกะการนำทางหรือวิธีแสดงข้อความแก่ผู้ใช้ โดย UI จะเป็นผู้จัดการตรรกะนี้

แผนผังการตัดสินใจเกี่ยวกับเหตุการณ์ UI

แผนภาพต่อไปนี้แสดงแผนผังการตัดสินใจเพื่อค้นหาวิธีที่ดีที่สุดในการจัดการ Use Case ของเหตุการณ์หนึ่งๆ ส่วนที่เหลือของคู่มือนี้จะอธิบายแนวทางเหล่านี้โดยละเอียด

หากเหตุการณ์เกิดขึ้นใน ViewModel ให้อัปเดตสถานะ UI หาก
    เหตุการณ์เกิดขึ้นใน UI และต้องใช้ตรรกะทางธุรกิจ ให้มอบหมาย
    ตรรกะทางธุรกิจให้กับ ViewModel หากเหตุการณ์เกิดขึ้นใน UI และ
    ต้องใช้ตรรกะลักษณะการทำงานของ UI ให้แก้ไขสถานะองค์ประกอบ UI โดยตรงใน
    UI
รูปที่ 1 แผนผังการตัดสินใจสำหรับการจัดการเหตุการณ์

จัดการเหตุการณ์ของผู้ใช้

UI สามารถจัดการเหตุการณ์ของผู้ใช้ได้โดยตรงหากเหตุการณ์เหล่านั้นเกี่ยวข้องกับการแก้ไขสถานะขององค์ประกอบ UI เช่น สถานะของรายการที่ขยายได้ หากเหตุการณ์ต้องใช้ตรรกะทางธุรกิจ เช่น การรีเฟรชข้อมูลบนหน้าจอ ViewModel ควรเป็นผู้ประมวลผล

ตัวอย่างต่อไปนี้แสดงวิธีใช้ปุ่มต่างๆ เพื่อขยายองค์ประกอบ UI (ตรรกะ UI) และรีเฟรชข้อมูลบนหน้าจอ (ตรรกะทางธุรกิจ)

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

เหตุการณ์ของผู้ใช้ในรายการแบบ Lazy

หากการดำเนินการเกิดขึ้นในส่วนล่างของแผนผัง UI เช่น ในรายการ LazyColumn ViewModel ควรเป็นผู้จัดการเหตุการณ์ของผู้ใช้

ตัวอย่างเช่น พิจารณารายการรายการที่คลิกได้ อย่าส่งอินสแตนซ์ ViewModel ลงใน Composables ของรายการ (MyList) เนื่องจากวิธีนี้จะผูกคอมโพเนนต์ UI กับรายละเอียดการติดตั้งใช้งานอย่างแน่นหนา

แต่ให้แสดงเหตุการณ์เป็นพารามิเตอร์ฟังก์ชันแลมบ์ดาใน Composables วิธีนี้จะช่วยให้รายการทริกเกอร์เหตุการณ์ได้โดยไม่ต้องทราบว่าใครเป็นผู้จัดการหรือจัดการอย่างไร

data class MyItem(val id: Int)

@Composable
fun MyList(
    items: List<String>,
    onItemClick: (MyItem) -> Unit
) {
    Card {
        LazyColumn {
            itemsIndexed(items) { index, string ->
                ListItem(
                    modifier = Modifier.clickable {
                        onItemClick(MyItem(index))
                    },
                    headlineContent = {
                        Text(text = string)
                    }
                )
            }
        }
    }
}

ในแนวทางนี้ Composables MyList จะทำงานกับข้อมูลที่แสดงและเหตุการณ์ที่แสดงเท่านั้น โดยจะไม่มีสิทธิ์เข้าถึง ViewModel เหตุการณ์จะถูกยกขึ้นและส่งไปยัง ViewModel ใน Composables ก่อนหน้า

ดูข้อมูลเพิ่มเติมเกี่ยวกับการจัดการเหตุการณ์ได้ที่ เหตุการณ์ใน Compose

แบบแผนการตั้งชื่อสำหรับฟังก์ชันเหตุการณ์ของผู้ใช้และตัวจัดการเหตุการณ์

ในคู่มือนี้ ฟังก์ชัน ViewModel ที่จัดการเหตุการณ์ของผู้ใช้จะตั้งชื่อด้วยคำกริยาตามการดำเนินการที่ฟังก์ชันนั้นจัดการ เช่น validateInput() หรือ login()

ตัวจัดการเหตุการณ์ใน Compose เป็นไปตามแบบแผนการตั้งชื่อมาตรฐานเพื่อให้การไหลของข้อมูลชัดเจน ดังนี้

  • ชื่อพารามิเตอร์: on + Verb + Target (เช่น onExpandClicked หรือ onValueChange)
  • นิพจน์แลมบ์ดา: เมื่อเรียก Composables แลมบ์ดามักจะเป็นเพียงการติดตั้งใช้งานเหตุการณ์นั้น

จัดการเหตุการณ์ ViewModel

การดำเนินการ UI ที่มาจาก ViewModel หรือเหตุการณ์ ViewModel ควร ส่งผลให้มีการอัปเดตสถานะ UI เสมอ ซึ่งเป็นไปตามหลักการของการไหลของข้อมูลแบบทิศทางเดียว วิธีนี้จะทำให้เหตุการณ์สามารถทำซ้ำได้หลังจากการเปลี่ยนแปลงการกำหนดค่า และรับประกันว่าการดำเนินการ UI จะไม่สูญหาย นอกจากนี้ คุณยังทำให้เหตุการณ์สามารถทำซ้ำได้หลังจากการสิ้นสุดการประมวลผลหากใช้โมดูลสถานะที่บันทึกไว้

การแมปการดำเนินการ UI กับสถานะ UI อาจไม่ใช่กระบวนการที่ง่ายเสมอไป แต่จะนำไปสู่ตรรกะที่ง่ายขึ้น กระบวนการคิดของคุณไม่ควรจบลงด้วยการกำหนดวิธีทำให้ UI ไปยังหน้าจอหนึ่งๆ เช่น คุณต้องคิดให้ไกลกว่านั้นและพิจารณาว่าจะแสดงโฟลว์ของผู้ใช้ดังกล่าวในสถานะ UI ได้อย่างไร กล่าวอีกนัยหนึ่งคือ อย่าคิดว่า UI ต้องดำเนินการใดบ้าง แต่ให้คิดว่า การดำเนินการเหล่านั้นส่งผลต่อสถานะ UI อย่างไร

ตัวอย่างเช่น พิจารณากรณีของหน้าจอเข้าสู่ระบบ คุณสามารถสร้างโมเดลสถานะ UI ของหน้าจอนี้ได้ดังนี้

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

หน้าจอเข้าสู่ระบบจะตอบสนองต่อการเปลี่ยนแปลงสถานะ UI

class LoginViewModel : ViewModel() {

    var uiState by mutableStateOf(LoginUiState())

    fun tryLogin(username: String, password: String) {
        viewModelScope.launch {
            // Emit a new state indicating that login is in progress
            uiState = uiState.copy(isLoginInProgress = true)

            uiState = if (login(username, password)) {
                // Emit a new state indicating that login was successful
                uiState.copy(isLoginInProgress = false, isUserLoggedIn = true)
            } else {
                // Emit a new state with the error message
                LoginUiState(isLoginInProgress = false, errorMessage = "Login failed")
            }
        }
    }

    private suspend fun login(username: String, password: String): Boolean {
        delay(1000)
        return (username == "Hello" && password == "World!")
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel, onSuccessfulLogin: () -> Unit) {

    val uiState = viewModel.uiState

    LaunchedEffect(uiState) {
        if (uiState.isUserLoggedIn) {
            onSuccessfulLogin()
        }
    }

    if (uiState.isLoginInProgress) {
        CircularProgressIndicator()
    } else {
        LoginForm(
            onLoginAttempt = { username, password ->
                viewModel.tryLogin(username, password)
            },
            errorMessage = uiState.errorMessage
        )
    }
}

การใช้เหตุการณ์อาจทริกเกอร์การอัปเดตสถานะ

การใช้เหตุการณ์ ViewModel บางอย่างใน UI อาจส่งผลให้มีการอัปเดตสถานะ UI อื่นๆ ตัวอย่างเช่น เมื่อแสดงข้อความชั่วคราวบนหน้าจอเพื่อแจ้งให้ผู้ใช้ทราบว่ามีบางอย่างเกิดขึ้น UI ต้องแจ้งให้ ViewModel ทริกเกอร์การอัปเดตสถานะอื่นเมื่อข้อความแสดงบนหน้าจอ เหตุการณ์ที่เกิดขึ้นเมื่อผู้ใช้ใช้ข้อความแล้ว (โดยการปิดข้อความหรือหลังจากหมดเวลา) สามารถถือเป็น "ข้อมูลจากผู้ใช้" และ ViewModel ควรทราบถึงเหตุการณ์ดังกล่าว ในสถานการณ์นี้ คุณสามารถสร้างโมเดลสถานะ UI ได้ดังนี้

// 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 จะอัปเดตสถานะ UI ดังนี้เมื่อตรรกะทางธุรกิจกำหนดให้แสดงข้อความชั่วคราวใหม่แก่ผู้ใช้

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 ไม่จำเป็นต้องทราบว่า UI แสดงข้อความบนหน้าจออย่างไร เพียงแต่ทราบว่ามีข้อความสำหรับผู้ใช้ที่ต้องแสดง เมื่อแสดงข้อความชั่วคราวแล้ว UI ต้องแจ้งให้ ViewModel ทราบ ซึ่งจะทำให้มีการอัปเดตสถานะ UI อีกครั้งเพื่อล้างพร็อพเพอร์ตี้ userMessage

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

แม้ว่าข้อความจะเป็นข้อความชั่วคราว แต่สถานะ UI ก็แสดงสิ่งที่แสดงบนหน้าจออย่างถูกต้องในทุกช่วงเวลา ไม่ว่าข้อความสำหรับผู้ใช้จะแสดงหรือไม่ก็ตาม

ส่วนการใช้เหตุการณ์อาจทริกเกอร์การอัปเดตสถานะ จะอธิบายรายละเอียดเกี่ยวกับวิธีใช้สถานะ UI เพื่อแสดงข้อความสำหรับผู้ใช้บน หน้าจอ เหตุการณ์การนำทางยังเป็นเหตุการณ์ประเภทที่พบบ่อยในแอป Android

หากเหตุการณ์ทริกเกอร์ใน UI เนื่องจากผู้ใช้แตะปุ่ม UI จะจัดการเหตุการณ์นั้นโดยการแสดงเหตุการณ์ต่อ Composables ที่เรียก

@Composable
fun LoginScreen(
    onHelp: () -> Unit, // Caller navigates to the help screen
    viewModel: LoginViewModel = viewModel()
) {
    // Rest of the UI
    Button(
        onClick = dropUnlessResumed { onHelp() }
    ) {
        Text("Get help")
    }
}

dropUnlessResumed เป็นส่วนหนึ่งของไลบรารี Lifecycle และช่วยให้คุณเรียกใช้ฟังก์ชัน onHelp ได้เฉพาะเมื่อ Lifecycle อยู่ในสถานะ RESUMED เป็นอย่างน้อย

หากอินพุตข้อมูลต้องมีการตรวจสอบตรรกะทางธุรกิจก่อนการนำทาง ViewModel จะต้องแสดงสถานะนั้นต่อ UI UI จะตอบสนองต่อการเปลี่ยนแปลงสถานะนั้นและนำทางตามความเหมาะสม ส่วนการจัดการเหตุการณ์ ViewModel จะครอบคลุม Use Case นี้ ตัวอย่างโค้ดที่คล้ายกันมีดังนี้

@Composable
fun LoginScreen(
    onUserLogIn: () -> Unit, // Caller navigates to the right screen
    viewModel: LoginViewModel = viewModel()
) {
    Button(
        onClick = {
            // ViewModel validation is triggered
            viewModel.tryLogin()
        }
    ) {
        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()
            }
    }
}

ในตัวอย่างข้างต้น แอปจะทำงานตามที่คาดไว้เนื่องจากระบบจะไม่เก็บปลายทางปัจจุบัน (เข้าสู่ระบบ) ไว้ใน Back Stack ผู้ใช้จะกลับไปยังปลายทางดังกล่าวไม่ได้หากกดปุ่มย้อนกลับ อย่างไรก็ตาม ในกรณีที่อาจเกิดขึ้นได้ โซลูชันจะต้องมีตรรกะเพิ่มเติม

เมื่อ ViewModel ตั้งค่าสถานะบางอย่างที่สร้างเหตุการณ์การนำทางจากหน้าจอ A ไปยังหน้าจอ B และระบบเก็บหน้าจอ A ไว้ใน Back Stack การนำทาง คุณอาจต้องใช้ตรรกะเพิ่มเติมเพื่อไม่ให้ระบบไปยังหน้าจอ B โดยอัตโนมัติ หากต้องการติดตั้งใช้งานฟังก์ชันนี้ คุณต้องมีสถานะเพิ่มเติมเพื่อระบุว่า UI ควรนำทางไปยังหน้าจออื่นหรือไม่ โดยปกติแล้ว สถานะดังกล่าวจะอยู่ใน UI เนื่องจากตรรกะการนำทางเป็นหน้าที่ของ UI ไม่ใช่ ViewModel ตัวอย่างเช่น พิจารณา Use Case ต่อไปนี้

สมมติว่าคุณอยู่ในโฟลว์การลงทะเบียนของแอป ในหน้าจอการตรวจสอบ วันเกิด เมื่อผู้ใช้ป้อนวันที่ ViewModel จะตรวจสอบวันที่เมื่อผู้ใช้แตะปุ่ม "ดำเนินการต่อ" ViewModel จะมอบหมายตรรกะการตรวจสอบไปยังเลเยอร์ข้อมูล หากวันที่ถูกต้อง ผู้ใช้จะไปยังหน้าจอถัดไป นอกจากนี้ ผู้ใช้ยังสามารถย้อนกลับไปมาระหว่างหน้าจอการลงทะเบียนต่างๆ ได้ในกรณีที่ต้องการเปลี่ยนข้อมูลบางอย่าง ดังนั้น ระบบจะเก็บปลายทางทั้งหมดในโฟลว์การลงทะเบียนไว้ใน Back Stack เดียวกัน เมื่อพิจารณาข้อกำหนดเหล่านี้ คุณสามารถติดตั้งใช้งานหน้าจอนี้ได้ดังนี้

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 จะมอบหมายตรรกะดังกล่าวไปยังเลเยอร์ข้อมูล ตรรกะในการนำผู้ใช้ไปยังหน้าจอถัดไปคือ ตรรกะ UI เนื่องจากข้อกำหนดเหล่านี้อาจเปลี่ยนแปลงไปตามการกำหนดค่า UI ตัวอย่างเช่น คุณอาจไม่ต้องการไปยังหน้าจออื่นโดยอัตโนมัติในแท็บเล็ตหากแสดงขั้นตอนการลงทะเบียนหลายขั้นตอนพร้อมกัน ตัวแปร validationInProgress ในโค้ดด้านบนจะติดตั้งใช้งานฟังก์ชันนี้และจัดการว่า UI ควรนำทางโดยอัตโนมัติหรือไม่เมื่อวันเกิดถูกต้องและผู้ใช้ต้องการไปยังขั้นตอนการลงทะเบียนถัดไป

Use Case อื่นๆ

หากคิดว่า Use Case ของเหตุการณ์ UI ไม่สามารถแก้ไขได้ด้วยการอัปเดตสถานะ UI คุณอาจต้องพิจารณาใหม่ว่าข้อมูลไหลเวียนในแอปอย่างไร โดยพิจารณาหลักการต่อไปนี้

  • แต่ละคลาสควรทำหน้าที่ที่ตนรับผิดชอบเท่านั้น UI มีหน้าที่รับผิดชอบตรรกะลักษณะการทำงานที่เฉพาะเจาะจงกับหน้าจอ เช่น การเรียกการนำทาง เหตุการณ์การคลิก และการรับคำขอสิทธิ์ ViewModel มีตรรกะทางธุรกิจและแปลงผลลัพธ์จากเลเยอร์ล่างของลำดับชั้นเป็นสถานะ UI
  • พิจารณาว่าเหตุการณ์มาจากที่ใด ทำตามแผนผังการตัดสินใจที่แสดงไว้ที่ตอนต้นของคู่มือนี้ และให้แต่ละ คลาสจัดการสิ่งที่ตนรับผิดชอบ ตัวอย่างเช่น หากเหตุการณ์มาจาก UI และส่งผลให้เกิดเหตุการณ์การนำทาง เหตุการณ์นั้นจะต้องได้รับการจัดการใน UI คุณอาจมอบหมายตรรกะบางอย่างไปยัง ViewModel แต่ไม่สามารถมอบหมายการจัดการเหตุการณ์ทั้งหมดไปยัง ViewModel ได้
  • หากคุณมีผู้ใช้หลายรายและกังวลว่าเหตุการณ์จะถูก ใช้หลายครั้ง คุณอาจต้องพิจารณาสถาปัตยกรรมแอปใหม่ การมีผู้ใช้หลายรายพร้อมกันจะทำให้สัญญา ส่งมอบเพียงครั้งเดียว เป็นเรื่องยากมากที่จะรับประกันได้ ดังนั้นความซับซ้อนและลักษณะการทำงานที่ละเอียดอ่อนจึงเพิ่มขึ้นอย่างมาก หากคุณพบปัญหานี้ ให้พิจารณาผลักดันความกังวลเหล่านั้นขึ้นไปในแผนผัง UI คุณอาจต้องใช้เอนทิตีอื่นที่มีขอบเขตสูงขึ้นในลำดับชั้น
  • พิจารณาว่าเมื่อใดที่ต้องใช้สถานะ ในบางสถานการณ์ คุณอาจไม่ต้องการใช้สถานะต่อไปเมื่อแอปทำงานอยู่เบื้องหลัง เช่น การแสดง Toast ในกรณีดังกล่าว ให้พิจารณาใช้สถานะเมื่อ UI ทำงานอยู่เบื้องหน้า

ตัวอย่าง

ตัวอย่างต่อไปนี้ของ Google แสดงเหตุการณ์ UI ในเลเยอร์ UI ลองสำรวจตัวอย่างเหล่านี้เพื่อดูคำแนะนำนี้ในทางปฏิบัติ

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับเหตุการณ์ UI ได้จากแหล่งข้อมูลเพิ่มเติมต่อไปนี้

Codelab

เอกสารประกอบ

เนื้อหา Views