UI ที่ทันสมัยมักจะไม่คงที่ สถานะของ UI จะเปลี่ยนแปลงเมื่อผู้ใช้โต้ตอบกับ UI หรือเมื่อแอปต้องแสดงข้อมูลใหม่
เอกสารนี้กำหนดแนวทางสำหรับการสร้างและการจัดการสถานะ UI เมื่ออ่านเอกสารนี้จบแล้ว คุณควรจะทำสิ่งต่อไปนี้ได้
- ทราบว่าควรใช้ API ใดในการสร้างสถานะ UI ซึ่งขึ้นอยู่กับ ลักษณะของแหล่งที่มาของการเปลี่ยนแปลงสถานะที่มีอยู่ในตัวเก็บสถานะ โดยเป็นไปตามหลักการของโฟลว์ข้อมูลแบบทิศทางเดียว
- ทราบว่าควรจำกัดขอบเขตการสร้างสถานะ UI อย่างไรเพื่อให้ตระหนักถึงทรัพยากรของระบบ
- ทราบว่าควรเปิดเผยสถานะ UI ให้ UI ใช้ได้อย่างไร
โดยพื้นฐานแล้ว การสร้างสถานะคือการใช้การเปลี่ยนแปลงเหล่านี้กับสถานะ UI แบบเพิ่มทีละน้อย สถานะมีอยู่เสมอและเปลี่ยนแปลงไปตามเหตุการณ์ ตารางด้านล่างจะสรุปความแตกต่างระหว่างเหตุการณ์และสถานะ
| เหตุการณ์ | สถานะ |
|---|---|
| ชั่วคราว คาดเดาไม่ได้ และมีอยู่เป็นระยะเวลาจำกัด | มีอยู่เสมอ |
| อินพุตของการสร้างสถานะ | เอาต์พุตของการสร้างสถานะ |
| ผลลัพธ์ของ UI หรือแหล่งที่มาอื่นๆ | UI ใช้ |
คำช่วยจำที่ดีที่สรุปแนวคิดข้างต้นคือ สถานะเป็น เหตุการณ์เกิดขึ้น แผนภาพด้านล่างช่วยให้เห็นภาพการเปลี่ยนแปลงสถานะเมื่อเหตุการณ์เกิดขึ้นในไทม์ไลน์ ตัวเก็บสถานะที่เหมาะสมจะประมวลผลเหตุการณ์แต่ละรายการและทำให้เกิดการเปลี่ยนแปลงสถานะ
เหตุการณ์อาจมาจากแหล่งที่มาต่อไปนี้
- ผู้ใช้: เมื่อโต้ตอบกับ UI ของแอป
- แหล่งที่มาอื่นๆ ของการเปลี่ยนแปลงสถานะ: API ที่แสดงข้อมูลแอปจากเลเยอร์ UI, โดเมน หรือข้อมูล เช่น เหตุการณ์หมดเวลาของ Snackbar, Use Case หรือ Repository ตามลำดับ
ไปป์ไลน์การสร้างสถานะ UI
คุณสามารถนึกถึงการสร้างสถานะในแอป Android ว่าเป็นไปป์ไลน์การประมวลผลที่ประกอบด้วยสิ่งต่อไปนี้
- อินพุต: แหล่งที่มาของการเปลี่ยนแปลงสถานะ ซึ่งอาจเป็นสิ่งต่อไปนี้
- เฉพาะในเลเยอร์ UI: อาจเป็นเหตุการณ์ของผู้ใช้ เช่น ผู้ใช้ป้อนชื่อสำหรับ "สิ่งที่ต้องทำ" ในแอปการจัดการงาน หรือ API ที่ให้สิทธิ์เข้าถึง ตรรกะ UI ที่ขับเคลื่อนการเปลี่ยนแปลงในสถานะ UI เช่น
การเรียกใช้เมธอด
openในDrawerStateใน Jetpack Compose - ภายนอกเลเยอร์ UI: แหล่งที่มาจากเลเยอร์โดเมนหรือชั้นข้อมูลที่ทำให้เกิดการเปลี่ยนแปลงสถานะ UI เช่น ข่าวที่โหลดเสร็จจาก
NewsRepositoryหรือเหตุการณ์อื่นๆ - การผสมผสานของทั้งหมดข้างต้น
- เฉพาะในเลเยอร์ UI: อาจเป็นเหตุการณ์ของผู้ใช้ เช่น ผู้ใช้ป้อนชื่อสำหรับ "สิ่งที่ต้องทำ" ในแอปการจัดการงาน หรือ API ที่ให้สิทธิ์เข้าถึง ตรรกะ UI ที่ขับเคลื่อนการเปลี่ยนแปลงในสถานะ UI เช่น
การเรียกใช้เมธอด
- ตัวเก็บสถานะ: ประเภทที่ใช้ตรรกะทางธุรกิจและ/หรือ ตรรกะ UIกับแหล่งที่มาของการเปลี่ยนแปลงสถานะและประมวลผลเหตุการณ์ของผู้ใช้เพื่อสร้าง สถานะ UI
- เอาต์พุต: สถานะ UI ที่แอปแสดงผลได้เพื่อให้ข้อมูลที่ผู้ใช้ต้องการ
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 ลองสำรวจตัวอย่างเหล่านี้เพื่อดูคำแนะนำนี้ในทางปฏิบัติ
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- เลเยอร์ UI
- สร้างแอปที่ทำงานแบบออฟไลน์เป็นหลัก
- ตัวเก็บสถานะและสถานะ UI {:#mad-arch}