ViewModel 的已儲存狀態模組 Android Jetpack 的一部分。
如「儲存 UI 狀態」一文所述,ViewModel
物件可以處理設定變更,因此您不必擔心旋轉或其他情況下的狀態。不過,如需處理系統啟動的程序終止事件,建議使用 SavedStateHandle
API 做為備份。
UI 狀態通常是在 ViewModel
物件中儲存或參照,而非在活動中,因此使用 onSaveInstanceState()
或 rememberSaveable
時,必須有一些已儲存狀態模組可以為您處理的樣板。
使用這個模組時,ViewModel
物件會透過其建構函式接收 SavedStateHandle
物件。這個物件是鍵/值對應,可讓你寫入及擷取儲存狀態的物件。這些值會在系統終止程序後保留,並且仍可透過相同的物件存取。
已儲存狀態會與工作堆疊建立關聯。如果工作堆疊消失,已儲存狀態也會隨之消失。如果強制停止應用程式、從「最近使用」選單移除應用程式,或重新啟動裝置,則可能會發生這種情況。在這種情況下,工作堆疊會消失,而且您無法還原處於儲存狀態的資訊。在使用者啟動的 UI 狀態關閉情境中,已儲存的狀態不會還原。在系統啟動的情境中,則會還原。
設定
從 Fragment 1.2.0 或間接依附元件 Activity 1.1.0 開始,您可接受 SavedStateHandle
做為建構函式引數,然後加入 ViewModel
。
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
接下來,無需額外設定,你就能擷取 ViewModel
的執行個體。預設的 ViewModel
工廠會為你的 ViewModel
提供適當的 SavedStateHandle
。
Kotlin
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Java
class MainFragment extends Fragment { private SavedStateViewModel vm; public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { vm = new ViewModelProvider(this).get(SavedStateViewModel.class); ... } ... }
提供自訂的 ViewModelProvider.Factory
執行個體時,您可以擴充 AbstractSavedStateViewModelFactory
來啟用 SavedStateHandle
。
使用 SavedStateHandle
SavedStateHandle
類別是鍵/值對應,可讓你透過 set()
和 get()
方法,將資料寫入已儲存狀態並從中擷取資料。
使用 SavedStateHandle
時,查詢值會在整個程序終止期間保留,確保使用者在重新建立前後都可看到同一組經過篩選的資料,完全不需要任何活動或片段來手動儲存、還原,然後將該值傳回 ViewModel
。
透過鍵/值對應進行互動時,SavedStateHandle
還提供其他方法:
contains(String key)
- 檢查特定鍵是否有值。remove(String key)
- 移除指定鍵的值。keys()
- 傳回SavedStateHandle
中的所有鍵。
此外,您也可以使用可觀測的資料容器,從 SavedStateHandle
擷取值。支援的類型清單如下:
LiveData
使用 getLiveData()
,擷取 LiveData
可觀測項目中納入的 SavedStateHandle
值。更新鍵的值時,LiveData
會收到新的值。這個值通常是因使用者互動而設定的值,例如輸入查詢以篩選資料清單。然後就使用此更新的值轉換 LiveData
。
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveData<List<String>> = savedStateHandle.getLiveData<String>("query").switchMap { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle savedStateHandle; public LiveData<List<String>> filteredData; public SavedStateViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; LiveData<String> queryLiveData = savedStateHandle.getLiveData("query"); filteredData = Transformations.switchMap(queryLiveData, query -> { return repository.getFilteredData(query); }); } public void setQuery(String query) { savedStateHandle.set("query", query); } }
StateFlow
使用 getStateFlow()
,擷取 StateFlow
可觀測項目中納入的 SavedStateHandle
值。更新鍵值時,StateFlow
會接收新的值。在多數情況下,您可能會因為使用者互動 (例如輸入查詢以篩選資料清單) 而設定值。接著,您可以使用其他資料流運算子轉換此更新的值。
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Compose 的狀態支援功能 (實驗性質)
lifecycle-viewmodel-compose
構件提供實驗性的 saveable
API,允許 SavedStateHandle
和 Compose Saver
之間有互通性,如此一來,任何您可以使用自訂 Saver
透過 rememberSaveable
儲存的 State
,也可以使用 SavedStateHandle
儲存。
Kotlin
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 類別
如果類別尚未實作 Parcelable
或 Serializable
,而且無法修改並實作其中一個介面,則無法直接將該類別的執行個體儲存至 SavedStateHandle
。
從 Lifecycle 2.3.0-alpha03 開始,SavedStateHandle
可讓您使用 setSavedStateProvider()
方法,以便提供自己的邏輯,將物件儲存及還原為 Bundle
。SavedStateRegistry.SavedStateProvider
介面可定義單一 saveState()
方法,以傳回包含所要儲存狀態的 Bundle
。當 SavedStateHandle
準備好儲存狀態時,會呼叫 saveState()
從 SavedStateProvider
擷取 Bundle
,並儲存相關鍵的 Bundle
。
舉例來說,假設應用程式透過 ACTION_IMAGE_CAPTURE
意圖要求相機應用程式提供圖片,並傳入暫存檔案讓相機儲存圖片。TempFileViewModel
會封裝暫存檔案的建立邏輯。
Kotlin
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel() { } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } }
為避免暫存檔案在活動程序終止時遺失並方便在稍後還原,TempFileViewModel
可以使用 SavedStateHandle
保留其資料。如要允許 TempFileViewModel
儲存資料,請實作 SavedStateProvider
,並在 ViewModel
的 SavedStateHandle
將其設為提供者:
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } } }
如要在使用者回訪時還原 File
資料,請從 SavedStateHandle
擷取 temp_file
Bundle
。這與包含絕對路徑的 saveTempFile()
所提供的 Bundle
相同。因此,絕對路徑就可用來建立新的 File
執行個體。
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { Bundle tempFileBundle = savedStateHandle.get("temp_file"); if (tempFileBundle != null) { tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle); } savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } @Nullable private static File restoreTempFile(Bundle bundle) { if (bundle.containsKey("path") { return File(bundle.getString("path")); } return null; } } }
測試中的 SavedStateHandle
如要測試以 SavedStateHandle
做為依附元件的 ViewModel
,請建立新的 SavedStateHandle
執行個體,提供其所需的測試值,並將其傳送至您正在進行的 ViewModel
執行個體測試。
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
其他資源
如要進一步瞭解 ViewModel
的「已儲存狀態」模組,請參閱下列資源。
程式碼研究室
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 儲存 UI 狀態
- 使用可觀察的資料物件
- 建立含依附元件的 ViewModel