Jetpack(七) 之 Data Store

您可能經常需要存儲較小或簡單的數(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)的可變引用
  • 通過類型化鍵提供類似于 MapMutableMap 的 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ù),以便我們能夠更新 UserPreferencesshowCompleted 屬性,此函數(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。

移除 showCompletedFlowsortOrderFlow,創(chuàng)建一個名為 userPreferencesFlow 的值,用 userPreferencesRepository.userPreferencesFlow 對該值進行初始化:

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

tasksUiModelFlow 創(chuàng)建中,將 showCompletedFlowsortOrderFlow 替換為 userPreferencesFlow。請相應地替換參數(shù)。

調用 filterSortTasks 時,傳入 userPreferencesshowCompletedsortOrder。您的代碼應如下所示:

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

讓我們移除不再需要的字段和方法。您應該可以刪除以下內容:

  • _sortOrderFlow
  • sortOrderFlow
  • updateSortOrder()
  • private val sortOrder: SortOrder

我們的應用現(xiàn)在應能成功編譯。運行一下,看看“顯示已完成”標志和排序順序標志是否保存成功。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容