โมดูลสถานะที่บันทึกไว้สำหรับ 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(): ใช้ตัวเลือกนี้หากต้องการอ่านสถานะเท่านั้น เมื่อคุณอัปเดตค่าของคีย์ที่อื่นใน SavedStateHandle StateFlow จะได้รับค่าใหม่ ซึ่งเหมาะอย่างยิ่งเมื่อคุณต้องการแสดงสตรีมแบบอ่านอย่างเดียวและเปลี่ยนรูปแบบโดยใช้โอเปอเรเตอร์ Flow
  • getMutableStateFlow(): ใช้ตัวเลือกนี้หากต้องการทั้งสิทธิ์อ่านและเขียน การอัปเดต .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

ดูเนื้อหา