โมดูลสถานะที่บันทึกไว้สำหรับ ViewModel เป็นส่วนหนึ่งของ Android Jetpack
ตามที่กล่าวไว้ในการบันทึกสถานะ UI ออบเจ็กต์ ViewModel สามารถจัดการการเปลี่ยนแปลงการกำหนดค่าได้ คุณจึงไม่ต้องกังวลเกี่ยวกับสถานะในการหมุนหรือกรณีอื่นๆ อย่างไรก็ตาม หากคุณต้องการจัดการการสิ้นสุดการประมวลผลที่ระบบเริ่มต้น คุณอาจต้องใช้ SavedStateHandle API เป็นข้อมูลสำรอง
โดยปกติแล้ว สถานะ UI จะจัดเก็บหรืออ้างอิงในออบเจ็กต์ ViewModel ดังนั้นการใช้ rememberSaveable ใน Compose จึงต้องใช้ต้นแบบบางอย่างที่โมดูลสถานะที่บันทึกไว้สามารถจัดการให้คุณได้
เมื่อใช้โมดูลนี้ ออบเจ็กต์ ViewModel จะได้รับออบเจ็กต์ SavedStateHandle ผ่านคอนสตรัคเตอร์ ออบเจ็กต์นี้เป็นแมปคีย์-ค่าที่ช่วยให้คุณเขียนและดึงข้อมูลออบเจ็กต์ไปยังและจากสถานะที่บันทึกไว้ได้ ค่าเหล่านี้จะยังคงอยู่หลังจากที่ระบบหยุดกระบวนการ และยังคงใช้งานได้ผ่านออบเจ็กต์เดียวกัน
สถานะที่บันทึกไว้จะเชื่อมโยงกับสแต็กงานของคุณ หากสแต็กงานหายไป สถานะที่บันทึกไว้ก็จะหายไปด้วย ซึ่งอาจเกิดขึ้นเมื่อบังคับให้แอปหยุดทำงาน นำแอปออกจากเมนูล่าสุด หรือรีบูตอุปกรณ์ ในกรณีเช่นนี้ สแต็กงานจะหายไปและคุณจะกู้คืนข้อมูลในสถานะที่บันทึกไว้ไม่ได้ ในสถานการณ์การปิดสถานะ UI ที่ผู้ใช้เริ่ม ระบบจะไม่กู้คืนสถานะที่บันทึกไว้ ในสถานการณ์ที่ระบบเป็นผู้เริ่ม คำตอบคือใช่
ตั้งค่า
หากต้องการใช้ SavedStateHandle ให้ยอมรับเป็นอาร์กิวเมนต์คอนสตรัคเตอร์สำหรับ ViewModel
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
จากนั้นคุณจะเรียกข้อมูลอินสแตนซ์ของ ViewModel ภายใน Composable ได้โดยไม่ต้องกำหนดค่าเพิ่มเติม โรงงาน ViewModel เริ่มต้นจะจัดหา SavedStateHandle ที่เหมาะสมให้กับ ViewModel ของคุณ
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
เมื่อระบุอินสแตนซ์ ViewModelProvider.Factory ที่กำหนดเอง คุณสามารถเปิดใช้ SavedStateHandle ได้โดยใช้ CreationExtras และ DSL viewModelFactory
การทำงานกับ SavedStateHandle
คลาส SavedStateHandle เป็นแผนที่คีย์-ค่าที่ช่วยให้คุณเขียนและดึงข้อมูลไปยังและจากสถานะที่บันทึกไว้ผ่านเมธอด set() และ get()
เมื่อใช้ SavedStateHandle ระบบจะเก็บค่าการค้นหาไว้ตลอดการสิ้นสุดการประมวลผล
เพื่อให้มั่นใจว่าผู้ใช้จะเห็นชุดข้อมูลที่กรองแล้วชุดเดียวกันทั้งก่อนและหลัง
การสร้างใหม่ โดยที่กิจกรรมหรือ Fragment ไม่จำเป็นต้องบันทึก กู้คืน
และส่งต่อค่านั้นกลับไปยัง ViewModel ด้วยตนเอง
SavedStateHandle ยังมีเมธอดอื่นๆ ที่คุณอาจคาดหวังเมื่อโต้ตอบกับแมปคีย์-ค่า ดังนี้
contains(String key)- ตรวจสอบว่ามีค่าสำหรับคีย์ที่ระบุหรือไม่remove(String key)- นำค่าของคีย์ที่ระบุออกkeys()- แสดงผลคีย์ทั้งหมดที่อยู่ในSavedStateHandle
นอกจากนี้ คุณยังดึงค่าจาก SavedStateHandle โดยใช้ตัวยึดข้อมูลที่ได้รับอนุญาตให้สังเกตพฤติกรรมผู้ใช้ได้ รายการประเภทที่รองรับมีดังนี้
StateFlow
คุณสามารถดึงค่าจาก SavedStateHandle ที่รวมอยู่ใน StateFlow ที่สังเกตได้ คุณสามารถเลือกสตรีมแบบอ่านอย่างเดียวหรือสตรีมที่เปลี่ยนแปลงได้ โดยขึ้นอยู่กับว่าคุณต้องการเปลี่ยนแปลงค่าโดยตรงหรือไม่
getStateFlow(): ใช้ตัวเลือกนี้หากต้องการอ่านสถานะเท่านั้น เมื่อคุณอัปเดตค่าของคีย์ที่อื่นในSavedStateHandleStateFlow จะได้รับค่าใหม่ ซึ่งเหมาะอย่างยิ่งเมื่อคุณต้องการแสดงสตรีมแบบอ่านอย่างเดียวและเปลี่ยนรูปแบบโดยใช้โอเปอเรเตอร์ FlowgetMutableStateFlow(): ใช้ตัวเลือกนี้หากต้องการทั้งสิทธิ์อ่านและเขียน การอัปเดต.valueของMutableStateFlowที่แสดงผลจะอัปเดตSavedStateHandleที่เกี่ยวข้องโดยอัตโนมัติ ซึ่งช่วยให้คุณไม่ต้องตั้งค่าคีย์ด้วยตนเอง
โดยส่วนใหญ่แล้ว คุณจะอัปเดตค่าเหล่านี้เนื่องจากการโต้ตอบของผู้ใช้ เช่น การป้อนคำค้นหาเพื่อกรองรายการข้อมูล
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { // Use getMutableStateFlow to read and write the query directly private val _query = savedStateHandle.getMutableStateFlow("query", "") val query: StateFlow= _query.asStateFlow() // Use getStateFlow if you only need a read-only stream to react to changes val filteredData: StateFlow<List > = query.flatMapLatest { repository.getFilteredData(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun setQuery(newQuery: String) { // Updating the MutableStateFlow automatically updates the SavedStateHandle _query.value = newQuery } }
การรองรับการเรียงอันดับ KotlinX
สำหรับสถานะ UI ที่ซับซ้อน คุณสามารถใช้ผู้รับมอบสิทธิ์พร็อพเพอร์ตี้ saved ควบคู่ไปกับการจัดรูปแบบอนุกรม KotlinX ได้ ผู้รับมอบสิทธิ์นี้ช่วยให้คุณเก็บคลาสข้อมูล @Serializable ที่กำหนดเองไว้ใน SavedStateHandle ได้โดยตรง ซึ่งจะช่วยรักษาสถานะของ ViewModel ไว้ตลอดการสิ้นสุดการประมวลผล เพื่อให้ Compose UI สามารถกู้คืนสถานะได้อย่างราบรื่นเมื่อสร้างใหม่
หากต้องการใช้ ให้ใส่คำอธิบายประกอบคลาสข้อมูลด้วย @Serializable และใช้ผู้รับมอบสิทธิ์ saved ใน ViewModel ดังนี้
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel // Ensure you have the savedstate-ktx dependency import androidx.savedstate.serialization.saved import kotlinx.serialization.Serializable @Serializable data class UserFilterState( val searchQuery: String, val minAge: Int, val includeInactive: Boolean ) class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { // The state is automatically serialized to a Bundle on process death, // and deserialized upon recreation. var filterState by savedStateHandle.saved { UserFilterState(searchQuery = "", minAge = 18, includeInactive = false) } fun updateQuery(newQuery: String) { // Mutating the property automatically updates the underlying SavedStateHandle filterState = filterState.copy(searchQuery = newQuery) } }
การรองรับสถานะการเขียน
หากสถานะของคุณใช้ API Saver ของ Compose แทน KotlinX Serialization อาร์ติแฟกต์ lifecycle-viewmodel-compose จะมีตัวแทน saveable ซึ่งช่วยให้ SavedStateHandle และ Saver ของ Compose ทำงานร่วมกันได้ เพื่อให้ State ที่คุณบันทึกผ่าน rememberSaveable ด้วย Saver ที่กำหนดเองสามารถบันทึกด้วย SavedStateHandle ได้เช่นกัน
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
ประเภทที่รองรับ
ข้อมูลที่เก็บไว้ใน SavedStateHandle จะได้รับการบันทึกและกู้คืนเป็น Bundle พร้อมกับ savedInstanceState ที่เหลือสำหรับแอปของคุณ
ประเภทที่รองรับโดยตรง
โดยค่าเริ่มต้น คุณสามารถเรียกใช้ set() และ get() ใน SavedStateHandle สำหรับประเภทข้อมูลเดียวกันกับ Bundle ดังที่แสดงด้านล่าง
| การรองรับประเภท/คลาส | การรองรับอาร์เรย์ |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
หากคลาสไม่ได้ขยายคลาสใดคลาสหนึ่งในรายการด้านบน ให้พิจารณาสร้างคลาสที่แยกวิเคราะห์ได้โดยเพิ่มคำอธิบายประกอบ @Parcelize Kotlin หรือใช้ Parcelable โดยตรง
การบันทึกคลาสที่ไม่ใช่ Parcelable
หากคลาสไม่ได้ใช้ Parcelable หรือ Serializable และไม่สามารถแก้ไขเพื่อใช้อินเทอร์เฟซใดอินเทอร์เฟซหนึ่งดังกล่าวได้ ก็จะไม่สามารถบันทึกอินสแตนซ์ของคลาสนั้นลงใน SavedStateHandle ได้โดยตรง
ตั้งแต่ Lifecycle 2.3.0-alpha03 เป็นต้นไป SavedStateHandle จะช่วยให้คุณบันทึกออบเจ็กต์ใดก็ได้โดยระบุตรรกะของคุณเองสำหรับการบันทึกและกู้คืนออบเจ็กต์เป็น Bundle โดยใช้เมธอด setSavedStateProvider()
SavedStateRegistry.SavedStateProvider เป็นอินเทอร์เฟซที่กำหนดเมธอด saveState() รายการเดียวซึ่งแสดงผล Bundle ที่มีสถานะที่คุณต้องการบันทึก เมื่อ SavedStateHandle พร้อมที่จะบันทึกสถานะ ระบบจะเรียกใช้ saveState() เพื่อดึงข้อมูล Bundle จาก SavedStateProvider และบันทึก Bundle สำหรับคีย์ที่เชื่อมโยง
ลองดูตัวอย่างแอปที่ขอรูปภาพจากแอปกล้องผ่าน Intent ACTION_IMAGE_CAPTURE โดยส่งไฟล์ชั่วคราวสำหรับตำแหน่งที่กล้องควรจัดเก็บรูปภาพ TempFileViewModel จะสรุปตรรกะสำหรับการสร้างไฟล์ชั่วคราวนั้น
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
เพื่อให้แน่ใจว่าไฟล์ชั่วคราวจะไม่สูญหายหากกระบวนการของกิจกรรมถูกหยุดและกู้คืนในภายหลัง TempFileViewModel สามารถใช้ SavedStateHandle เพื่อคงข้อมูลไว้ได้ หากต้องการอนุญาตให้ TempFileViewModel บันทึกข้อมูล ให้ใช้ SavedStateProvider และตั้งค่าเป็นผู้ให้บริการใน SavedStateHandle ของ ViewModel ดังนี้
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
หากต้องการกู้คืนข้อมูล File เมื่อผู้ใช้กลับมา ให้ดึงข้อมูล temp_file
Bundle จาก SavedStateHandle ซึ่งเป็น Bundle เดียวกันกับที่ saveTempFile() ระบุไว้ซึ่งมี Absolute Path จากนั้นจะใช้ Absolute Path เพื่อสร้างอินสแตนซ์ File ใหม่ได้
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
SavedStateHandle ในการทดสอบ
หากต้องการทดสอบ ViewModel ที่ใช้ SavedStateHandle เป็นทรัพยากร Dependency ให้สร้างอินสแตนซ์ใหม่ของ SavedStateHandle ที่มีค่าทดสอบที่จำเป็น และส่งไปยังอินสแตนซ์ ViewModel ที่คุณกำลังทดสอบ
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับโมดูลสถานะที่บันทึกไว้สำหรับ ViewModel ได้ที่แหล่งข้อมูลต่อไปนี้
Codelab
ดูเนื้อหา
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- บันทึกสถานะ UI
- ทำงานกับออบเจ็กต์ข้อมูลที่ได้รับอนุญาตให้สังเกตพฤติกรรมผู้ใช้ได้
- สร้าง ViewModel ที่มีการขึ้นต่อกัน