ViewModel 的已儲存狀態模組   Android Jetpack 的一部分。

如「儲存 UI 狀態」一文所述,ViewModel 物件可以處理設定變更,因此您不必擔心旋轉或其他情況下的狀態。不過,如需處理系統啟動的程序終止事件,建議使用 SavedStateHandle API 做為備份。

UI 狀態通常是在 ViewModel 物件中儲存或參照,因此在 Compose 中使用 rememberSaveable 時,必須有已儲存狀態模組可以為你處理的一些範本。

使用這個模組時,ViewModel 物件會透過其建構函式接收 SavedStateHandle 物件。這個物件是鍵/值對應,可讓你寫入及擷取儲存狀態的物件。這些值會在系統終止程序後保留,並且仍可透過相同的物件存取。

已儲存狀態會與工作堆疊建立關聯。如果工作堆疊消失,已儲存狀態也會隨之消失。如果強制停止應用程式、從「最近使用」選單移除應用程式,或重新啟動裝置,則可能會發生這種情況。在這種情況下,工作堆疊會消失,而且您無法還原處於儲存狀態的資訊。在使用者啟動的 UI 狀態關閉情境中,已儲存的狀態不會還原。在系統啟動的情境中,則會還原。

設定

如要使用 SavedStateHandle,請將其做為建構函式引數傳遞至 ViewModel

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

接下來,無需額外設定,你就能在可組合函式中擷取 ViewModel 的執行個體。預設的 ViewModel 工廠會為你的 ViewModel 提供適當的 SavedStateHandle

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

提供自訂的 ViewModelProvider.Factory 執行個體時,您可以使用 CreationExtrasviewModelFactory DSL 啟用 SavedStateHandle

使用 SavedStateHandle

SavedStateHandle 類別是鍵/值對應,可讓您透過 set()get() 方法,將資料寫入已儲存狀態並從中擷取資料。

使用 SavedStateHandle 時,查詢值會在整個程序終止期間保留,確保使用者在重新建立前後都可看到同一組經過篩選的資料,完全不需要任何活動或片段來手動儲存、還原,然後將該值傳回 ViewModel

透過鍵/值對應進行互動時,SavedStateHandle 還提供其他方法:

此外,您也可以使用可觀測的資料容器,從 SavedStateHandle 擷取值。支援的類型清單如下:

StateFlow

您可以從包裝在 StateFlow 可觀測項目中的 SavedStateHandle 擷取值。視是否需要直接變更值而定,您可以選擇唯讀或可變動的串流:

  • getStateFlow():如果您只需要讀取狀態,請使用這個方法。在 SavedStateHandle 的其他位置更新鍵值時,StateFlow 會接收新的值。如果您想公開唯讀串流,並使用 Flow 運算子轉換串流,這就是理想做法。
  • getMutableStateFlow():如果您需要讀取和寫入存取權,請使用這個方法。更新傳回的 MutableStateFlow 中的 .value,系統會自動更新基礎 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 狀態,可以搭配 KotlinX 序列化使用 saved 屬性委派。這個委派可讓您將自訂 @Serializable 資料類別直接保存到 SavedStateHandle。這樣一來,ViewModel 的狀態就會在程序終止後保留下來,Compose UI 也能在重建時順利還原狀態。

如要使用這項功能,請使用 @Serializable 註解資料類別,並在 ViewModel 中使用 saved 委派:

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

如果您的狀態依賴 Compose 的 Saver API,而非 KotlinX 序列化,則 lifecycle-viewmodel-compose 構件會提供 saveable 委派。這項功能可讓 SavedStateHandle 和 Compose 的 Saver 互通,因此您可以使用自訂 Saver 透過 rememberSaveable 儲存任何 State,也可以使用 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 一起。

直接支援的類型

根據預設,針對與 Bundle 相同的資料類型,你可以在 SavedStateHandle 呼叫 set()get(),如下所示:

類型/類別支援 陣列支援
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 類別

如果類別尚未實作 ParcelableSerializable,而且無法修改並實作其中一個介面,則無法直接將該類別的執行個體儲存至 SavedStateHandle

Lifecycle 2.3.0-alpha03 開始,SavedStateHandle 可讓您使用 setSavedStateProvider() 方法,提供自己的邏輯,將物件儲存及還原為 BundleSavedStateRegistry.SavedStateProvider 介面可定義單一 saveState() 方法,以傳回包含所要儲存狀態的 Bundle。當 SavedStateHandle 準備好儲存狀態時,會呼叫 saveState()SavedStateProvider 擷取 Bundle,並儲存相關鍵的 Bundle

舉例來說,假設應用程式透過 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 並在 ViewModelSavedStateHandle 將其設為提供者:

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 資料,請從 SavedStateHandle 擷取 temp_file Bundle。這與包含絕對路徑的 saveTempFile() 所提供的 Bundle 相同。因此,絕對路徑就可用來建立新的 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

如要測試以 SavedStateHandle 做為依附元件的 ViewModel,請建立新的 SavedStateHandle 執行個體,提供其所需的測試值,並將其傳送至您正在進行的 ViewModel 執行個體測試。

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

其他資源

如要進一步瞭解 ViewModel 的「已儲存狀態」模組,請參閱下列資源。

程式碼研究室

Views content