โมดูลสถานะที่บันทึกไว้สำหรับ 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 class คือแผนที่คีย์-ค่าที่ช่วยให้คุณเขียนและ เรียกข้อมูลไปยังและจากสถานะที่บันทึกไว้ผ่านเมธอด set() และ get()

การใช้ SavedStateHandle จะช่วยให้ค่าการค้นหายังคงอยู่เมื่อการประมวลผลสิ้นสุดลง เพื่อให้มั่นใจว่าผู้ใช้จะเห็นชุดข้อมูลที่กรองแล้วชุดเดียวกันทั้งก่อนและหลัง การสร้างใหม่โดยไม่ต้องให้กิจกรรมหรือ Fragment บันทึก กู้คืน และส่งต่อค่านั้นกลับไปยัง ViewModel ด้วยตนเอง

SavedStateHandle ยังมีเมธอดอื่นๆ ที่คุณอาจคาดหวังเมื่อโต้ตอบ กับแมปคีย์-ค่าด้วย

  • contains(String key) - ตรวจสอบว่ามีค่าสำหรับคีย์ที่ระบุหรือไม่
  • remove(String key) - นำค่าสำหรับคีย์ที่ระบุออก
  • keys() - แสดงคีย์ทั้งหมดที่อยู่ใน SavedStateHandle

นอกจากนี้ คุณยังดึงค่าจาก SavedStateHandle ได้โดยใช้ที่เก็บข้อมูลที่ได้รับอนุญาตให้สังเกตพฤติกรรมผู้ใช้ได้ รายการประเภทที่รองรับมีดังนี้

StateFlow

คุณเรียกค่าจาก SavedStateHandle ที่อยู่ใน StateFlow observable ได้ คุณสามารถเลือกสตรีมแบบอ่านอย่างเดียวหรือสตรีมที่เปลี่ยนแปลงได้ ทั้งนี้ขึ้นอยู่กับว่าคุณต้องเปลี่ยนค่าโดยตรงหรือไม่

  • 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 Serialization

สำหรับสถานะ UI ที่ซับซ้อน คุณสามารถใช้ตัวแทนพร็อพเพอร์ตี้ saved ร่วมกับ KotlinX Serialization ได้ การมอบสิทธิ์นี้ช่วยให้คุณบันทึกคลาสข้อมูล @Serializable ที่กำหนดเองลงใน SavedStateHandle ได้โดยตรง ซึ่งจะรักษาสถานะของ ViewModel ไว้เมื่อการประมวลผลสิ้นสุดลง เพื่อให้ UI ของ Compose สามารถกู้คืนสถานะได้อย่างราบรื่นเมื่อมีการสร้างใหม่

หากต้องการใช้ ให้ใส่คำอธิบายประกอบคลาสข้อมูลด้วย @Serializable และใช้ saved delegate ใน 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)
    }
}

Compose State support

หากรัฐของคุณใช้ 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 หรือ 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() ระบุไว้ซึ่งมีเส้นทางที่แน่นอน จากนั้นจะใช้เส้นทางแบบสัมบูรณ์เพื่อสร้างอินสแตนซ์ 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

ดูเนื้อหา