State<T> และ MutableState<T> ของ Jetpack Compose เป็นตัวยึดค่าอเนกประสงค์ที่ระบบสแนปชอตของ Compose รองรับ แม้ว่าโดยทั่วไปแล้วจะใช้เพื่อขับเคลื่อนการจัดองค์ประกอบ UI ใหม่ แต่ระบบสแนปชอตเองไม่ได้เชื่อมโยงกับการแสดงผล UI
ไลบรารี AppState ซึ่งประกอบด้วย Transform และ StateStore ช่วยให้คุณเก็บ ดึงข้อมูล สังเกต และคง State ของ Compose ไว้ใน ViewModel, ผู้จัดการ, ที่เก็บ และบริการที่ทำงานอยู่เบื้องหลังได้
เหตุใดจึงควรใช้สถานะ Compose นอกเหนือจาก UI
เมื่อคุณอ่าน Compose State ภายในบล็อกที่สังเกตสถานะ เช่น UI Composable, transform หรือ derivedStateOf รันไทม์ของ Compose จะให้ประโยชน์หลัก 2 ประการในทุกเลเยอร์ของสถาปัตยกรรม ดังนี้
- การติดตามทรัพยากร Dependency แบบอัตโนมัติและแบบไดนามิก: การอ่าน
.valueของสถานะ (หรือการใช้การมอบสิทธิ์พร็อพเพอร์ตี้by) จะลงทะเบียนสถานะนั้นเป็นทรัพยากร Dependency ของบล็อกโดยอัตโนมัติ เนื่องจากการติดตามเป็นแบบไดนามิก ระบบจึงกำหนดทรัพยากร Dependency ขณะรันไทม์ตามเส้นทางโค้ดที่ดำเนินการ หากไม่มีการใช้ Branch แบบมีเงื่อนไข (เช่น หลังจากการตรวจสอบifหรือreturnก่อนกำหนด) ระบบจะไม่อ่านสถานะภายใน Branch นั้น และการเปลี่ยนแปลงสถานะจะไม่ทริกเกอร์การคำนวณใหม่โดยไม่จำเป็น - ความสอดคล้องและความละเอียด: คุณสามารถประกาศพร็อพเพอร์ตี้
MutableStateที่ละเอียดและแยกกันได้ และรวมการเขียนพร็อพเพอร์ตี้หลายรายการไว้ในSnapshot.withMutableSnapshotการอัปเดตระดับกลางจะแยกออกจากสแนปชอตที่เปลี่ยนแปลงได้และนำไปใช้แบบอะตอมเมื่อบล็อกเสร็จสมบูรณ์ ดังนั้นผู้สังเกตการณ์ที่อ่านภายในสแนปชอตจะไม่เห็นสถานะระดับกลางที่ไม่สอดคล้องกัน และจะคำนวณใหม่เฉพาะเมื่อพร็อพเพอร์ตี้ที่อ่านมีการเปลี่ยนแปลงเท่านั้น คุณไม่จำเป็นต้องเลือกระหว่างStateFlow<UiState>แบบโมโนลิธิก รายการเดียว (ซึ่งจะทำให้ตัวรวบรวมทุกตัวใช้งานไม่ได้เมื่อมีการเปลี่ยนแปลงฟิลด์ ใดๆ) กับพร็อพเพอร์ตี้StateFlowที่แยกต่างหาก (ซึ่งไม่สามารถอัปเดต พร้อมกันในธุรกรรมแบบอะตอมรายการเดียว) ซึ่งต่างจากStateFlow
เลือก State API ที่เหมาะสมในสถาปัตยกรรมของคุณ
เลือกวิธีเก็บและคงสถานะ State ของ Compose โดยอิงตามระยะเวลาที่สถานะหนึ่งๆ ควรคงอยู่และคอมโพเนนต์ที่ต้องเข้าถึงสถานะนั้น
| หมวดหมู่รัฐ | อายุการใช้งานและขอบเขต | ตัวอย่าง | API ของ Compose State ที่แนะนำ |
|---|---|---|---|
| สถานะ Live | จัดเก็บไว้ในหน่วยความจำเสมอ โดยจะค้นหาใหม่เมื่อเริ่มต้นระบบและไม่คงอยู่หลังจากการสิ้นสุดการประมวลผล | สถานะการเชื่อมต่อเครือข่าย สถานะการเชื่อมต่อบลูทูธ สถานะ UI ชั่วคราว เช่น การเลือกรายการ |
|
| สถานะ UI ชั่วคราว | คงอยู่แม้จะมีการเปลี่ยนแปลงการกำหนดค่าและการสิ้นสุดการประมวลผลที่ระบบเริ่มต้น ไม่ได้จัดเก็บไว้อย่างไม่มีกำหนด โดยกำหนดขอบเขตไว้ที่หน้าจอหรือกิจกรรมเดียว | องค์ประกอบ UI ที่ขยายหรือยุบ ตำแหน่งการเลื่อน คำค้นหาในหน้าจอ หรือปุ่มเปิด/ปิดตัวกรอง |
|
| สถานะเซสชันชั่วคราว | คงอยู่แม้กระบวนการเบื้องหลังจะสิ้นสุดลง แต่ไม่ได้จัดเก็บไว้อย่างไม่มีกำหนด โดยจะแชร์ในกิจกรรม บริการ หรือวิดเจ็ตต่างๆ | ตัวจับเวลาโฟกัสที่ทำงานอยู่ รถเข็นชำระเงินแบบหลายขั้นตอน หรือฉบับร่างแบบฟอร์มที่กำลังดำเนินการอยู่ |
|
| สถานะถาวร | เก็บไว้ในดิสก์แบบไม่มีกำหนด | ค่ากำหนดและการตั้งค่าของผู้ใช้ (DataStore), ข้อมูลแอปที่มีโครงสร้าง และคลังสื่อ (Room) | รวบรวมโฟลว์ DataStore หรือ Room ภายใน transform โดยใช้ collectAsState |
ใช้สถานะ Compose ใน ViewModel
ภายใน ViewModel คุณสามารถประกาศอินพุตหน้าจออิสระเป็นพร็อพเพอร์ตี้ MutableState แบบละเอียด, คงอินพุตเหล่านั้นไว้ตลอดการสิ้นสุดการประมวลผลด้วย SavedStateHandle.saveable, อัปเดตพร็อพเพอร์ตี้หลายรายการแบบอะตอมด้วย Snapshot.withMutableSnapshot และรวมพร็อพเพอร์ตี้เหล่านั้นกับสตรีม Flow ของที่เก็บภายใน transform ได้ ดังนี้
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
รูปแบบสำคัญในViewModelนี้
- ใช้
SavedStateHandle.saveableสำหรับสถานะที่บันทึกไว้แบบรีแอ็กทีฟ: การมอบสิทธิ์searchQueryให้กับSavedStateHandle.saveableด้วยmutableStateOf(จากandroidx.lifecycle:lifecycle-viewmodel-compose) จะคงค่าไว้ตลอดการสิ้นสุดการประมวลผลเป็นMutableStateดังนั้นtransformจะติดตามการอ่านโดยอัตโนมัติ หลีกเลี่ยงSavedStateHandle.savedเปล่าๆ ซึ่งจะแสดงผลพร็อพเพอร์ตี้ Kotlin ธรรมดาแทนที่จะเป็นStateของ Compose และจะไม่ทริกเกอร์การจัดองค์ประกอบใหม่เมื่อมีการเปลี่ยนแปลง - การเขียนหลายพร็อพเพอร์ตี้แบบอะตอมด้วย
Snapshot.withMutableSnapshot: การอัปเดตsearchQueryและselectedTaskIdsภายในSnapshot.withMutableSnapshotจะแยกการเขียนและคอมมิตการเปลี่ยนแปลงทั้ง 2 รายการในธุรกรรมสแนปชอตแบบอะตอมรายการเดียว ดังนั้นผู้สังเกตการณ์ (รวมถึงtransform) จะเห็นการอัปเดตทั้ง 2 รายการพร้อมกันในรอบการจัดองค์ประกอบใหม่รอบเดียว แทนที่จะสังเกตเห็นsearchQueryใหม่ที่จับคู่กับselectedTaskIdsที่ล้าสมัย - ใช้ Flow ซ้ำในการจัดองค์ประกอบใหม่ด้วย
remember(key): การรวมtasksRepository.getTasks(searchQuery)ไว้ในrememberโดยใช้searchQueryเป็นคีย์จะช่วยให้มั่นใจได้ว่าเมื่อselectedTaskIdsมีการเปลี่ยนแปลงและจัดองค์ประกอบบล็อกtransformใหม่ Compose จะใช้Flowของฐานข้อมูลที่มีอยู่ซ้ำแทนที่จะเริ่มการค้นหาใหม่
แชร์สถานะระบบแบบเรียลไทม์ในแอปของคุณ
สำหรับสถานะระบบแบบเรียลไทม์ที่แชร์ใน ViewModel, กิจกรรม หรือบริการหลายรายการ เช่น สถานะการเชื่อมต่อเครือข่ายหรือบลูทูธ คุณไม่ควรเก็บสถานะที่ล้าสมัยไว้ในดิสก์เมื่อการประมวลผลสิ้นสุดลง แต่ให้ค้นหาสถานะปัจจุบันของระบบเมื่อเริ่มต้นระบบ เก็บไว้ในพร็อพเพอร์ตี้ mutableStateOf ส่วนตัวภายใน Singleton ที่กำหนดขอบเขตแอปพลิเคชัน และแสดง State<T> แบบอ่านอย่างเดียวแทน
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState รองรับการอ่านและการอัปเดตที่สอดคล้องกันจากชุดข้อความใดก็ได้ ดังนั้น NetworkCallback จึงสามารถอัปเดต _isOnline.value ได้โดยตรงจากชุดข้อความ Callback ในเบื้องหลัง หาก Callback ในเบื้องหลังอัปเดตพร็อพเพอร์ตี้ MutableState หลายรายการพร้อมกัน ให้รวมการเขียนไว้ใน Snapshot.withMutableSnapshot เพื่อให้ระบบนำไปใช้แบบอะตอมมิก
คอมโพเนนต์ใดก็ตามในแอปสามารถอ่าน networkMonitor.isOnline.value ได้ ไม่ว่าจะเป็นการตรวจสอบแบบครั้งเดียว ภายใน UI Composable หรือภายในบล็อกการสังเกตสถานะ เช่น listener, derivedStateOf หรือ transform เช่น TaskListViewModel สามารถอ่าน networkMonitor.isOnline.value ภายในบล็อก transform ได้ดังนี้
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
เนื่องจาก transform ติดตาม networkMonitor.isOnline.value ควบคู่ไปกับ searchQuery, selectedTaskIds และที่เก็บ Flow การเปลี่ยนแปลงการเชื่อมต่อจะคำนวณ TaskListState.Success ใหม่โดยอัตโนมัติโดยไม่ต้องค้นหาฐานข้อมูลอีกครั้ง