การสร้างสถานะ UI

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

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

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

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

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

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

เหตุการณ์เทียบกับสถานะ
รูปที่ 1: เหตุการณ์ทำให้สถานะเปลี่ยนแปลง

เหตุการณ์อาจมาจากแหล่งที่มาต่อไปนี้

  • ผู้ใช้: เมื่อโต้ตอบกับ UI ของแอป
  • แหล่งที่มาอื่นๆ ของการเปลี่ยนแปลงสถานะ: API ที่แสดงข้อมูลแอปจากเลเยอร์ UI, โดเมน หรือข้อมูล เช่น เหตุการณ์หมดเวลาของ Snackbar, Use Case หรือ Repository ตามลำดับ

ไปป์ไลน์การสร้างสถานะ UI

คุณสามารถนึกถึงการสร้างสถานะในแอป Android ว่าเป็นไปป์ไลน์การประมวลผลที่ประกอบด้วยสิ่งต่อไปนี้

  • อินพุต: แหล่งที่มาของการเปลี่ยนแปลงสถานะ ซึ่งอาจเป็นสิ่งต่อไปนี้
    • เฉพาะในเลเยอร์ UI: อาจเป็นเหตุการณ์ของผู้ใช้ เช่น ผู้ใช้ป้อนชื่อสำหรับ "สิ่งที่ต้องทำ" ในแอปการจัดการงาน หรือ API ที่ให้สิทธิ์เข้าถึง ตรรกะ UI ที่ขับเคลื่อนการเปลี่ยนแปลงในสถานะ UI เช่น การเรียกใช้เมธอด open ใน DrawerState ใน Jetpack Compose
    • ภายนอกเลเยอร์ UI: แหล่งที่มาจากเลเยอร์โดเมนหรือชั้นข้อมูลที่ทำให้เกิดการเปลี่ยนแปลงสถานะ UI เช่น ข่าวที่โหลดเสร็จจาก NewsRepository หรือเหตุการณ์อื่นๆ
    • การผสมผสานของทั้งหมดข้างต้น
  • ตัวเก็บสถานะ: ประเภทที่ใช้ตรรกะทางธุรกิจและ/หรือ ตรรกะ UIกับแหล่งที่มาของการเปลี่ยนแปลงสถานะและประมวลผลเหตุการณ์ของผู้ใช้เพื่อสร้าง สถานะ UI
  • เอาต์พุต: สถานะ UI ที่แอปแสดงผลได้เพื่อให้ข้อมูลที่ผู้ใช้ต้องการ
ไปป์ไลน์การผลิตของรัฐ
รูปที่ 2: ไปป์ไลน์การสร้างสถานะ

API การสร้างสถานะ

มี API หลัก 2 รายการที่ใช้ในการสร้างสถานะ ซึ่งขึ้นอยู่กับขั้นตอนของไปป์ไลน์ที่คุณอยู่

ขั้นตอนไปป์ไลน์ API
อินพุต คุณควรใช้ API แบบไม่พร้อมกันเพื่อทำงานนอกเธรด UI เพื่อให้ UI ทำงานได้อย่างราบรื่น เช่น Coroutines หรือ Flows ใน Kotlin และ RxJava หรือ Callback ในภาษาโปรแกรม Java
เอาต์พุต คุณควรใช้ API ตัวเก็บข้อมูลที่สังเกตได้เพื่อล้างข้อมูลและแสดงผล UI อีกครั้งเมื่อสถานะเปลี่ยนแปลง เช่น StateFlow, Compose State หรือ LiveData ตัวเก็บข้อมูลที่สังเกตได้จะรับประกันว่า UI จะมีสถานะ UI ให้แสดงบนหน้าจอเสมอ

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

การประกอบไปป์ไลน์การสร้างสถานะ

ส่วนถัดไปจะครอบคลุมเทคนิคการสร้างสถานะที่เหมาะกับอินพุตต่างๆ และ API เอาต์พุตที่ตรงกัน ไปป์ไลน์การสร้างสถานะแต่ละรายการเป็นการผสมผสานระหว่างอินพุตและเอาต์พุต และควรมีลักษณะดังนี้

  • ตระหนักถึงวงจรการทำงาน: ในกรณีที่ UI ไม่ปรากฏหรือไม่ได้ใช้งาน ไปป์ไลน์การสร้างสถานะไม่ควรใช้ทรัพยากรใดๆ เว้นแต่จะมีการกำหนดไว้อย่างชัดเจน
  • ใช้งานง่าย: UI ควรแสดงผลสถานะ UI ที่สร้างขึ้นได้อย่างง่ายดาย ข้อควรพิจารณาสำหรับเอาต์พุตของไปป์ไลน์การสร้างสถานะจะแตกต่างกันไปตาม API ของ View ต่างๆ เช่น ระบบ View หรือ Jetpack Compose

อินพุตในไปป์ไลน์การสร้างสถานะ

อินพุตในไปป์ไลน์การสร้างสถานะอาจให้แหล่งที่มาของการเปลี่ยนแปลงสถานะผ่านสิ่งต่อไปนี้

  • การดำเนินการแบบครั้งเดียวที่อาจเป็นแบบพร้อมกันหรือไม่พร้อมกันก็ได้ เช่น การเรียกใช้ฟังก์ชัน suspend
  • API สตรีม เช่น Flows
  • ทั้งหมดข้างต้น

ส่วนต่อไปนี้จะอธิบายวิธีประกอบไปป์ไลน์การสร้างสถานะสำหรับอินพุตแต่ละรายการข้างต้น

API แบบครั้งเดียวเป็นแหล่งที่มาของการเปลี่ยนแปลงสถานะ

ใช้ API MutableStateFlow เป็นคอนเทนเนอร์ของสถานะที่สังเกตได้และเปลี่ยนแปลงได้ ในแอป Jetpack Compose คุณยังพิจารณาใช้ mutableStateOf ได้ด้วย โดยเฉพาะอย่างยิ่งเมื่อทำงานกับ Compose Text API API ทั้ง 2 รายการมีเมธอดที่อนุญาตให้อัปเดตค่าที่เก็บไว้อย่างปลอดภัยและเป็นอะตอมมิก ไม่ว่าการอัปเดตจะเป็นแบบพร้อมกันหรือไม่พร้อมกันก็ตาม

ตัวอย่างเช่น ลองพิจารณาการอัปเดตสถานะในแอปทอยลูกเต๋าแบบง่าย ผู้ใช้ทอยลูกเต๋าแต่ละครั้งจะเรียกใช้เมธอด แบบพร้อมกัน Random.nextInt() และระบบจะเขียนผลลัพธ์ลงในสถานะ UI

StateFlow

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Called from the UI
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
            firstDieValue = Random.nextInt(from = 1, until = 7),
            secondDieValue = Random.nextInt(from = 1, until = 7),
            numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Compose State

@Stable
interface DiceUiState {
    val firstDieValue: Int?
    val secondDieValue: Int?
    val numberOfRolls: Int?
}

private class MutableDiceUiState: DiceUiState {
    override var firstDieValue: Int? by mutableStateOf(null)
    override var secondDieValue: Int? by mutableStateOf(null)
    override var numberOfRolls: Int by mutableStateOf(0)
}

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

    // Called from the UI
    fun rollDice() {
        _uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.numberOfRolls = _uiState.numberOfRolls + 1
    }
}

การเปลี่ยนแปลงสถานะ UI จากการเรียกใช้แบบไม่พร้อมกัน

สำหรับการเปลี่ยนแปลงสถานะที่ต้องใช้ผลลัพธ์แบบไม่พร้อมกัน ให้เรียกใช้ Coroutine ใน CoroutineScope ที่เหมาะสม ซึ่งจะช่วยให้แอปยกเลิกงานได้เมื่อ CoroutineScope ถูกยกเลิก จากนั้นตัวเก็บสถานะจะเขียนผลลัพธ์ของการเรียกใช้เมธอด suspend ลงใน API ที่สังเกตได้ซึ่งใช้เพื่อเปิดเผยสถานะ UI

ตัวอย่างเช่น ลองพิจารณา AddEditTaskViewModel ใน ตัวอย่างสถาปัตยกรรม เมื่อเมธอด saveTask() แบบ suspend บันทึกงานแบบไม่พร้อมกัน เมธอด update ใน MutableStateFlow จะเผยแพร่การเปลี่ยนแปลงสถานะไปยังสถานะ UI

StateFlow

data class AddEditTaskUiState(
    val title: String = "",
    val description: String = "",
    val isTaskCompleted: Boolean = false,
    val isLoading: Boolean = false,
    val userMessage: String? = null,
    val isTaskSaved: Boolean = false
)

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableStateFlow(AddEditTaskUiState())
   val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.update {
                    it.copy(isTaskSaved = true)
                }
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.update {
                    it.copy(userMessage = getErrorMessage(exception))
                }
            }
        }
    }
}

Compose State

@Stable
interface AddEditTaskUiState {
    val title: String
    val description: String
    val isTaskCompleted: Boolean
    val isLoading: Boolean
    val userMessage: String?
    val isTaskSaved: Boolean
}

private class MutableAddEditTaskUiState : AddEditTaskUiState() {
    override var title: String by mutableStateOf("")
    override var description: String by mutableStateOf("")
    override var isTaskCompleted: Boolean by mutableStateOf(false)
    override var isLoading: Boolean by mutableStateOf(false)
    override var userMessage: String? by mutableStateOf<String?>(null)
    override var isTaskSaved: Boolean by mutableStateOf(false)
}

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableAddEditTaskUiState()
   val uiState: AddEditTaskUiState = _uiState

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.isTaskSaved = true
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.userMessage = getErrorMessage(exception))
            }
        }
    }
}

การเปลี่ยนแปลงสถานะ UI จากเธรดเบื้องหลัง

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

  • ใช้เมธอด withContext เพื่อเรียกใช้ Coroutine ในบริบทพร้อมกันอื่น
  • เมื่อใช้ MutableStateFlow ให้ใช้เมธอด update ตาม ปกติ
  • เมื่อใช้ Compose State ให้ใช้ Snapshot.withMutableSnapshot เพื่อ รับประกันการอัปเดตสถานะแบบอะตอมมิกในบริบทพร้อมกัน

ตัวอย่างเช่น สมมติว่าในข้อมูลโค้ด DiceRollViewModel ด้านล่าง SlowRandom.nextInt() เป็นฟังก์ชัน suspend ที่ต้องใช้การคำนวณสูงและต้องเรียกใช้จาก Coroutine ที่ผูกกับ CPU

StateFlow

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            
            withContext(defaultDispatcher) {
                _uiState.update { currentState ->
                    currentState.copy(
                        firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        numberOfRolls = currentState.numberOfRolls + 1,
                    )
                }
            }
        }
    }
}

Compose State

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            
            withContext(defaultDispatcher) {
                Snapshot.withMutableSnapshot {
                    _uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.numberOfRolls = _uiState.numberOfRolls + 1
                }
            }
        }
    }
}

API สตรีมเป็นแหล่งที่มาของการเปลี่ยนแปลงสถานะ

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

เมื่อใช้ Kotlin Flows คุณสามารถทำได้ด้วย combine ฟังก์ชัน ตัวอย่างของฟังก์ชันนี้ดูได้ใน "Now in Android" sample ใน InterestsViewModel

class InterestsViewModel(
    authorsRepository: AuthorsRepository,
    topicsRepository: TopicsRepository
) : ViewModel() {

    val uiState = combine(
        authorsRepository.getAuthorsStream(),
        topicsRepository.getTopicsStream(),
    ) { availableAuthors, availableTopics ->
        InterestsUiState.Interests(
            authors = availableAuthors,
            topics = availableTopics
        )
    }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = InterestsUiState.Loading
    )
}

การใช้โอเปอเรเตอร์ stateIn เพื่อสร้าง StateFlows ช่วยให้ UI ควบคุมกิจกรรมของไปป์ไลน์การสร้างสถานะได้ละเอียดยิ่งขึ้น เนื่องจากอาจต้องใช้งานเฉพาะเมื่อ UI ปรากฏเท่านั้น

  • ใช้ SharingStarted.WhileSubscribed() หากไปป์ไลน์ควรใช้งานเฉพาะเมื่อ UI ปรากฏขณะรวบรวมโฟลว์ในลักษณะที่ตระหนักถึงวงจรการทำงาน
  • ใช้ SharingStarted.Lazily หากไปป์ไลน์ควรใช้งานตราบใดที่ผู้ใช้อาจกลับมาที่ UI นั่นคือ UI อยู่ใน Backstack หรือในแท็บอื่นนอกหน้าจอ

ในกรณีที่ไม่สามารถใช้การรวมแหล่งที่มาของสถานะตามสตรีมได้ API สตรีม เช่น Kotlin Flows มีการแปลงที่หลากหลาย เช่น การผสาน, การทำให้แบน และอื่นๆ เพื่อ ช่วยในการประมวลผลสตรีมเป็นสถานะ UI

API แบบครั้งเดียวและ API สตรีมเป็นแหล่งที่มาของการเปลี่ยนแปลงสถานะ

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

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

ลองพิจารณา TaskDetailViewModel จาก ที่เก็บ architecture-samples ด้านล่าง

StateFlow

class TaskDetailViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _isTaskDeleted = MutableStateFlow(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        _isTaskDeleted,
        _task
    ) { isTaskDeleted, task ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted.update { true }
    }
}

Compose State

class TaskDetailViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private var _isTaskDeleted by mutableStateOf(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        snapshotFlow { _isTaskDeleted },
        _task
    ) { isTaskDeleted, task ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted = true
    }
}

ประเภทเอาต์พุตในไปป์ไลน์การสร้างสถานะ

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

ตารางต่อไปนี้จะสรุป API ที่ควรใช้สำหรับไปป์ไลน์การสร้างสถานะสำหรับอินพุตและผู้ใช้ที่กำหนด

อินพุต ผู้ใช้ เอาต์พุต
API แบบครั้งเดียว Views StateFlow หรือ LiveData
API แบบครั้งเดียว Compose StateFlow หรือ State ของ Compose
API สตรีม Views StateFlow หรือ LiveData
API สตรีม Compose StateFlow
API แบบครั้งเดียวและ API สตรีม Views StateFlow หรือ LiveData
API แบบครั้งเดียวและ API สตรีม Compose StateFlow

การเริ่มต้นไปป์ไลน์การสร้างสถานะ

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

คุณควรเริ่มต้นไปป์ไลน์การสร้างสถานะแบบเลื่อนออกไปเมื่อเป็นไปได้เพื่อประหยัดทรัพยากรของระบบ ในทางปฏิบัติแล้ว มักหมายถึงการรอจนกว่าจะมีผู้ใช้เอาต์พุต API Flow อนุญาตให้ทำเช่นนี้ได้ด้วยอาร์กิวเมนต์ started ในเมธอด stateIn ในกรณีที่ไม่สามารถใช้ได้ ให้กำหนดฟังก์ชัน Idempotent initialize() เพื่อเริ่มไปป์ไลน์การสร้างสถานะอย่างชัดเจน ตามที่แสดงในข้อมูลโค้ดต่อไปนี้

class MyViewModel : ViewModel() {

    private var initializeCalled = false

    // This function is idempotent provided it is only called from the UI thread.
    @MainThread
    fun initialize() {
        if(initializeCalled) return
        initializeCalled = true

        viewModelScope.launch {
            // seed the state production pipeline
        }
    }
}

ตัวอย่าง

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