您可能經常需要存儲較小或簡單的數(shù)據(jù)集。為此,您過去可能使用過 SharedPreferences,但此 API 也存在一系列缺點。Jetpack DataStore 庫旨在解決這些問題,從而創(chuàng)建一個簡單、安全性更高的異步 API 來存儲數(shù)據(jù)。它提供 2 種不同的實現(xiàn):
Preferences DataStore
Proto DataStore
| 功能 | SharedPreferences | PreferencesDataStore | ProtoDataStore |
|---|---|---|---|
| 異步 API | ?(僅用于通過監(jiān)聽器讀取已更改的值) | ?(通過 Flow) |
?(通過 Flow) |
| 同步 API | ?(但在界面線程中調用不安全) | ? | ? |
| 可在界面線程上安全調用 | ?* | ?(這項工作已在后臺移至 Dispatchers.IO) |
?(這項工作已在后臺移至 Dispatchers.IO) |
| 可以提示錯誤 | ? | ? | ? |
| 避免運行時異常 | ?** | ? | ? |
| 包含具有強一致性保證的事務性 API | ? | ? | ? |
| 處理數(shù)據(jù)遷移 | ? | ?(遷移自 SharedPreferences) | ?(遷移自 SharedPreferences) |
| 類型安全 | ? | ? | ? 使用協(xié)議緩沖區(qū) |
- SharedPreferences 有一個看上去可以在界面線程中安全調用的同步 API,但是該 API 實際上執(zhí)行磁盤 I/O 操作。此外,
apply()會阻塞fsync()上的界面線程。每次服務開始或停止以及 activity 在應用中的任何地方啟動或停止時,系統(tǒng)都會觸發(fā)待處理的fsync()調用。界面線程在apply()調度的待處理fsync()調用上會被阻塞,這通常會導致 ANR。
** SharedPreferences 將解析錯誤作為運行時異常拋出。
Preferences DataStore 與 Proto DataStore
雖然 Preferences DataStore 和 Proto DataStore 都允許保存數(shù)據(jù),但它們保存數(shù)據(jù)的方式不同:
與 SharedPreferences 一樣,Preference DataStore 可以根據(jù)鍵訪問數(shù)據(jù),而無需事先定義架構。
Proto DataStore 使用協(xié)議緩沖區(qū)定義架構。使用 Protobuf 可存留強類型數(shù)據(jù)。與 XML 和其他類似的數(shù)據(jù)格式相比,它們速度更快、規(guī)格更小、使用更簡單,并且更清楚明了。雖然使用 Proto DataStore 需要學習新的序列化機制,但我們認為 Proto DataStore 有著強大的優(yōu)勢,值得去學習。
Room 與 DataStore
如果您需要實現(xiàn)部分更新、引用完整性或大型/復雜數(shù)據(jù)集,您應考慮使用 Room,而不是 DataStore。DataStore 非常適合較小或簡單的數(shù)據(jù)集,而不支持部分更新或引用完整性。
[ Preferences DataStore 概覽]
Preference DataStore API 類似于 SharedPreferences,但與后者相比存在一些顯著差異:
- 以事務方式處理數(shù)據(jù)更新
- 公開表示當前數(shù)據(jù)狀態(tài)的 Flow
- 不提供存留數(shù)據(jù)的方法(
apply()、commit()) - 不返回對其內部狀態(tài)的可變引用
- 通過類型化鍵提供類似于
Map和MutableMap的 API
接下來我們看看如何將其添加到項目中,并將 SharedPreferences 遷移到 DataStore。
添加依賴項
更新 build.gradle 文件以添加以下 Preference DataStore 依賴項:
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha06"
[在 Preferences DataStore 中存留數(shù)據(jù)]
盡管顯示已完成標志和排序順序標志都是用戶偏好設置,但它們現(xiàn)在用兩種不同的對象表示。因此,我們的一個目標是在 UserPreferences 類中整合這兩個標志,并使用 DataStore 將其存儲在 UserPreferencesRepository 中。目前,顯示已完成標志保存在內存的 TasksViewModel 中。
首先,在 UserPreferencesRepository 中創(chuàng)建 UserPreferences 數(shù)據(jù)類。目前,它應該只有一個字段:showCompleted。稍后我們將添加排序順序。
data class UserPreferences(val showCompleted: Boolean)
創(chuàng)建 DataStore
我們使用 context.createDataStoreFactory() 方法在 UserPreferencesRepository 中創(chuàng)建 DataStore<Preferences> 私有字段。必需的參數(shù)是 Preferences DataStore 的名稱。
private val dataStore: DataStore<Preferences> =
context.createDataStore(name = "user")
從 Preferences DataStore 讀取數(shù)據(jù)
Preferences DataStore 公開 Flow<Preferences> 中存儲的數(shù)據(jù),每當偏好設置發(fā)生變化時,F(xiàn)low<Preferences> 就會發(fā)出該數(shù)據(jù)。我們不希望公開整個 Preferences 對象,而是要公開 UserPreferences 對象。為此,我們必須映射 Flow<Preferences>,根據(jù)鍵獲取感興趣的布爾值,并構造一個 UserPreferences 對象。
因此,我們首先需要定義 show completed 鍵,這是一個 booleanPreferencesKey 值,我們將其聲明為私有 PreferencesKeys 對象中的成員。
private object PreferencesKeys {
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}
我們將公開一個基于 dataStore.data: Flow<Preferences> 構造的 userPreferencesFlow: Flow<UserPreferences>,然后將其映射,以檢索正確的偏好設置:
從 Preferences DataStore 讀取數(shù)據(jù)
Preferences DataStore 公開 Flow<Preferences> 中存儲的數(shù)據(jù),每當偏好設置發(fā)生變化時,F(xiàn)low<Preferences> 就會發(fā)出該數(shù)據(jù)。我們不希望公開整個 Preferences 對象,而是要公開 UserPreferences 對象。為此,我們必須映射 Flow<Preferences>,根據(jù)鍵獲取感興趣的布爾值,并構造一個 UserPreferences 對象。
因此,我們首先需要定義 show completed 鍵,這是一個 booleanPreferencesKey 值,我們將其聲明為私有 PreferencesKeys 對象中的成員。
private object PreferencesKeys {
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}
我們將公開一個基于 dataStore.data: Flow<Preferences> 構造的 userPreferencesFlow: Flow<UserPreferences>,然后將其映射,以檢索正確的偏好設置:
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.map { preferences ->
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
UserPreferences(showCompleted)
}
處理讀取數(shù)據(jù)時的異常
當 DataStore 從文件讀取數(shù)據(jù)時,如果讀取數(shù)據(jù)期間出現(xiàn)錯誤,系統(tǒng)會拋出 IOExceptions。我們可以通過以下方式處理這些事務:在 map() 之前使用 catch() Flow 運算符,并且在拋出的異常是 IOException 時發(fā)出 emptyPreferences()。如果出現(xiàn)其他類型的異常,最好重新拋出該異常。
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
UserPreferences(showCompleted)
}
將數(shù)據(jù)寫入 Preferences DataStore
如需寫入數(shù)據(jù),DataStore 提供掛起 DataStore.edit(transform: suspend (MutablePreferences) -> Unit) 函數(shù),該函數(shù)接受 transform 塊,讓我們能夠以事務方式更新 DataStore 中的狀態(tài)。
傳遞給轉換塊的 MutablePreferences 將保持以前所有運行編輯的最新狀態(tài)。在 transform 完成后且 edit 完成之前,對 transform 塊中 MutablePreferences 的所有更改都將應用于磁盤。在 MutablePreferences 中設置一個值會使所有其他偏好設置保持不變。
注意:請勿嘗試修改轉換塊之外的 MutablePreferences。
現(xiàn)在我們來創(chuàng)建一個掛起函數(shù),以便我們能夠更新 UserPreferences 的 showCompleted 屬性,此函數(shù)稱為 updateShowCompleted(),用于調用 dataStore.edit() 并設置新值:
suspend fun updateShowCompleted(showCompleted: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
}
}
如果在讀取或寫入磁盤時發(fā)生錯誤,edit() 可能會拋出 IOException。如果轉換塊中出現(xiàn)任何其他錯誤,edit() 將拋出異常。
此時,應用應該編譯,但是在 UserPreferencesRepository 中創(chuàng)建的功能并未使用。
[從 SharedPreferences 遷移到 Preferences DataStore]
排序順序保存在 SharedPreferences 中。讓我們將其遷移到 DataStore 中。為此,讓我們先更新 UserPreferences 以存儲排序順序:
data class UserPreferences(
val showCompleted: Boolean,
val sortOrder: SortOrder
)
從 SharedPreferences 遷移
為了能夠將排序順序遷移到 DataStore,我們需要更新 DataStore 構建器以向遷移列表傳遞 SharedPreferencesMigration。DataStore 能夠自動從 SharedPreferences 遷移到 DataStore。遷移可在 DataStore 中進行任何數(shù)據(jù)訪問之前運行。這意味著,必須在 DataStore.data 發(fā)出任何值之前和 DataStore.edit() 可以更新數(shù)據(jù)之前,成功完成遷移。
注意:由于鍵只能從 SharedPreferences 遷移一次,因此在代碼遷移到 DataStore 之后,您應停止使用舊 SharedPreferences。
private val dataStore: DataStore<Preferences> =
context.createDataStore(
name = USER_PREFERENCES_NAME,
migrations = listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
)
private object PreferencesKeys {
...
// Note: this has the the same name that we used with SharedPreferences.
val SORT_ORDER = stringPreferencesKey("sort_order")
}
所有鍵都將遷移到我們的 DataStore,并從用戶偏好設置 SharedPreferences 中刪除?,F(xiàn)在,我們可以從 Preferences 獲取并更新基于 SORT_ORDER 鍵的 SortOrder。
從 DataStore 中讀取排序順序
現(xiàn)在,讓我們更新 userPreferencesFlow 以同時檢索 map() 轉換中的排序順序:
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
// Get the sort order from preferences and convert it to a [SortOrder] object
val sortOrder =
SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
UserPreferences(showCompleted, sortOrder)
}
將排序順序保存到 DataStore
目前,UserPreferencesRepository 僅公開一種用于設置排序順序標志的同步方法,并且存在并發(fā)問題。我們公開了兩種更新排序順序的方法:enableSortByDeadline() 和 enableSortByPriority();這兩種方法都依賴當前的排序順序值,但如果在一個方法結束之前調用另一個方法,則最終值可能會出錯。
由于 Datastore 保證以事務方式進行數(shù)據(jù)更新,所以我們不會再遇到這個問題。接下來,讓我們一起執(zhí)行以下更改:
- 將
enableSortByDeadline()和enableSortByPriority()更新為使用dataStore.edit()的suspend函數(shù)。 - 在
edit()的轉換塊中,我們將從 Preferences 參數(shù)中獲取currentOrder,而不是從_sortOrderFlow字段中進行檢索。 - 我們可以直接在偏好設置中更新排序順序,而不是調用
updateSortOrder(newSortOrder)。
具體實現(xiàn)如下所示。
suspend fun enableSortByDeadline(enable: Boolean) {
// edit handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.edit { preferences ->
// Get the current SortOrder as an enum
val currentOrder = SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
)
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_PRIORITY) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_DEADLINE
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_PRIORITY
} else {
SortOrder.NONE
}
}
preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
}
}
suspend fun enableSortByPriority(enable: Boolean) {
// edit handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.edit { preferences ->
// Get the current SortOrder as an enum
val currentOrder = SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
)
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_DEADLINE) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_PRIORITY
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_DEADLINE
} else {
SortOrder.NONE
}
}
preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
}
}
[更新 TasksViewModel 以使用 UserPreferencesRepository]
現(xiàn)在,UserPreferencesRepository 在 DataStore 中存儲了顯示已完成標志和排序順序標志,并公開了 Flow<UserPreferences>。接下來,讓我們更新并使用 TasksViewModel。
移除 showCompletedFlow 和 sortOrderFlow,創(chuàng)建一個名為 userPreferencesFlow 的值,用 userPreferencesRepository.userPreferencesFlow 對該值進行初始化:
private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow
在 tasksUiModelFlow 創(chuàng)建中,將 showCompletedFlow 和 sortOrderFlow 替換為 userPreferencesFlow。請相應地替換參數(shù)。
調用 filterSortTasks 時,傳入 userPreferences 的 showCompleted 和 sortOrder。您的代碼應如下所示:
private val tasksUiModelFlow = combine(
repository.tasks,
userPreferencesFlow
) { tasks: List<Task>, userPreferences: UserPreferences ->
return@combine TasksUiModel(
tasks = filterSortTasks(
tasks,
userPreferences.showCompleted,
userPreferences.sortOrder
),
showCompleted = userPreferences.showCompleted,
sortOrder = userPreferences.sortOrder
)
}
現(xiàn)在,showCompletedTasks() 函數(shù)應更新為調用 userPreferencesRepository.updateShowCompleted()。由于該函數(shù)為掛起函數(shù),因此請在 viewModelScope 中創(chuàng)建新的協(xié)程:
fun showCompletedTasks(show: Boolean) {
viewModelScope.launch {
userPreferencesRepository.updateShowCompleted(show)
}
}
userPreferencesRepository 函數(shù) enableSortByDeadline() 和 enableSortByPriority() 現(xiàn)在屬于掛起函數(shù),因此還應在 viewModelScope 中啟動的新協(xié)程中調用它們:
fun enableSortByDeadline(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByDeadline(enable)
}
}
fun enableSortByPriority(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByPriority(enable)
}
}
清理 UserPreferencesRepository
讓我們移除不再需要的字段和方法。您應該可以刪除以下內容:
_sortOrderFlowsortOrderFlowupdateSortOrder()private val sortOrder: SortOrder
我們的應用現(xiàn)在應能成功編譯。運行一下,看看“顯示已完成”標志和排序順序標志是否保存成功。