使用 DataStore 將偏好設定儲存在本機

1. 事前準備

簡介

在本單元中,您已瞭解如何使用 SQL 和 Room 將資料儲存在本機裝置。SQL 和 Room 是功能強大的工具。不過,如果您不需要儲存關聯資料,DataStore 可提供簡單的解決方案。DataStore Jetpack 元件採用一種絕佳的方式,以較低的負擔儲存小型且簡易的資料集。DataStore 採用兩種實作方式:Preferences DataStoreProto DataStore

  • Preferences DataStore 會儲存鍵/值組合。這些值可以是 Kotlin 的基本資料類型,例如 StringBooleanInteger。但它不會儲存複雜的資料集,亦不需要預先定義的結構定義。Preferences Datastore 的主要用途是將使用者偏好設定儲存到自己的裝置。
  • Proto DataStore 會儲存自訂資料類型。它需要預先定義的結構定義,可將 Proto 定義對應至物件結構。

此程式碼研究室僅適用 Preferences DataStore,但您可以在 DataStore 說明文件中進一步瞭解 Proto DataStore

Preferences DataStore 是儲存使用者控管設定的絕佳方式。在本程式碼研究室中,您將瞭解如何實作 DataStore 以達到上述目的!

需求條件:

軟硬體需求

  • 已安裝 Android Studio 且連上網路的電腦
  • 裝置或模擬器
  • Dessert Release 應用程式的範例程式碼

建構項目

Dessert Release 應用程式會顯示 Android 版本清單。應用程式列中的圖示可將版面配置切換成格狀檢視和清單檢視。

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

在目前的狀態下,應用程式不會保存版面配置選項。關閉應用程式時,不會儲存版面配置選項,而且設定會恢復為預設選項。在本程式碼研究室中,您會將 DataStore 新增至 Dessert Release 應用程式,並用於儲存版面配置選項偏好設定。

2. 下載範例程式碼

點選下方連結即可下載這個程式碼研究室的所有程式碼:

您也可以視需要從 GitHub 複製 Dessert Release 程式碼:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout starter
  1. 在 Android Studio 中開啟 basic-android-kotlin-compose-training-dessert-release 資料夾。
  2. 在 Android Studio 中開啟 Dessert Release 應用程式程式碼。

3. 設定依附元件

請將以下內容新增至 app/build.gradle.kts 檔案的 dependencies 中:

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. 實作使用者偏好設定存放區

  1. data 套件中,建立名為 UserPreferencesRepository 的新類別。

c4c2e90902898001.png

  1. UserPreferencesRepository 建構函式中,定義不公開值屬性,用於表示 Preferences 類型的 DataStore 物件例項。
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore 會儲存鍵/值組合。您必須定義索引鍵,才能存取值。

  1. UserPreferencesRepository 類別中建立 companion object
  2. 使用 booleanPreferencesKey() 函式定義索引鍵,並將名稱 is_linear_layout 傳遞給這個函式。與 SQL 資料表名稱的做法類似,索引鍵必須使用底線格式。這個索引鍵的用途是存取布林值,藉此瞭解是否應顯示線性版面配置。
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
    private companion object {
        val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    }
    ...
}

寫入 DataStore

將 lambda 傳遞至 edit() 方法即可在 DataStore 中建立和修改值。lambda 會傳遞 MutablePreferences 的執行個體,可用於更新 DataStore 中的值。此 lambda 中的所有更新都會做為單一交易來執行。另外,更新是 atomic 作業,即一次完成的作業。這類更新可避免更新部分值,而不更新其他值的情況。

  1. 建立暫停函式並呼叫 saveLayoutPreference()
  2. saveLayoutPreference() 函式中,對 dataStore 物件呼叫 edit() 方法。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. 如要讓程式碼更易讀,請定義 lambda 主體中提供的 MutablePreferences 名稱。使用該屬性設定一個值,以及您定義的索引鍵和傳遞至 saveLayoutPreference() 函式的布林值。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

從 DataStore 讀取

現在,您已建立將 isLinearLayout 寫入 dataStore 的方法,請按照下列步驟執行讀取作業:

  1. UserPreferencesRepository 中建立類型為 Flow<Boolean> 且名為 isLinearLayout 的屬性。
val isLinearLayout: Flow<Boolean> =
  1. 您可以使用 DataStore.data 屬性來公開 DataStore 值。將 isLinearLayout 設定為 DataStore 物件的 data 屬性。
val isLinearLayout: Flow<Boolean> = dataStore.data

data 屬性是 Preferences 物件的 FlowPreferences 物件包含 DataStore 中的所有鍵/值組合。每次更新 DataStore 中的資料時,都會將新的 Preferences 物件發出至 Flow

  1. 使用對應函式將 Flow<Preferences> 轉換為 Flow<Boolean>

這個函式接受使用目前 Preferences 物件的 lambda 做為參數。您可以指定之前定義的索引鍵,以取得版面配置偏好設定。請注意,如果尚未呼叫 saveLayoutPreference,則該值可能不存在,因此您必須提供預設值。

  1. 指定 true 以預設為線性版面配置檢視畫面。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

例外狀況處理

裝置上的檔案系統可能會在與您互動時發生問題,例如檔案可能不存在、磁碟已滿或已卸載。DataStore 讀取及寫入檔案資料時,如果存取 DataStore,可能會發生 IOExceptions。您可以使用 catch{} 運算子擷取例外狀況並處理這些錯誤。

  1. 在隨附物件中,實作不可變動的 TAG 字串屬性,用於進行記錄。
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. 若讀取資料時發生錯誤,Preferences DataStore 會擲回 IOException。在 isLinearLayout 初始化區塊中,請在 map() 前使用 catch{} 運算子擷取 IOException
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. 如果擷取區塊中有 IOexception,請記錄錯誤並發出 emptyPreferences()。如果擲回其他類型的例外狀況,則建議再次擲回該例外狀況。如果存在錯誤,請發出 emptyPreferences(),對應函式仍可對應至預設值。
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {
        if(it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }

5. 初始化 DataStore

在本程式碼研究室中,必須手動處理依附元件插入作業。因此,您必須手動提供 Preferences DataStore 類別與 UserPreferencesRepository。請按照下列步驟將 DataStore 插入 UserPreferencesRepository

  1. 尋找 dessertrelease 套件。
  2. 在這個目錄中,建立名為 DessertReleaseApplication 的新類別,並實作 Application 類別 (即 DataStore 的容器)。
class DessertReleaseApplication: Application() {
}
  1. DessertReleaseApplication.kt 檔案內 (但在 DessertReleaseApplication 類別之外),宣告名為 LAYOUT_PREFERENCE_NAMEprivate const val
  2. 指派字串值 layout_preferences 做為 LAYOUT_PREFERENCE_NAME 的變數,您可以在下一步進行例項化時將其設為 Preferences Datastore 的名稱。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. 同樣在 DessertReleaseApplication.kt 檔案內 (但在 DessertReleaseApplication 類別主體之外),使用 preferencesDataStore 委派建立類型為 DataStore<Preferences> 且名為 Context.dataStore 的不公開值屬性。針對 preferencesDataStore 委派的 name 參數傳遞 LAYOUT_PREFERENCE_NAME
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. DessertReleaseApplication 類別主體中,建立 UserPreferencesRepositorylateinit var 執行個體。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository
}
  1. 覆寫 onCreate() 方法。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
    }
}
  1. onCreate() 方法內,建構使用 dataStore 做為參數的 UserPreferencesRepository 來初始化 userPreferencesRepository
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
        userPreferencesRepository = UserPreferencesRepository(dataStore)
    }
}
  1. AndroidManifest.xml 檔案的 <application> 標記中新增以下行。
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

這種方法會將 DessertReleaseApplication 類別定義為應用程式的進入點。本程式碼的目的是在啟動 MainActivity 之前,初始化 DessertReleaseApplication 類別中定義的依附元件。

6. 使用 UserPreferencesRepository

將存放區提供給 ViewModel

您現在可透過插入依附元件的方式在 DessertReleaseViewModel 中使用 UserPreferencesRepository

  1. DessertReleaseViewModel 中建立 UserPreferencesRepository 屬性,做為建構函式參數。
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. ViewModel 的隨附物件 (位於 viewModelFactory initializer 區塊) 中,使用以下程式碼取得 DessertReleaseApplication 的例項。
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. 建立 DessertReleaseViewModel 的執行個體並傳遞 userPreferencesRepository
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

UserPreferencesRepository 現在可透過 ViewModel 存取。後續步驟為使用之前實作的 UserPreferencesRepository 讀寫功能。

儲存版面配置偏好設定

  1. DessertReleaseViewModel 中編輯 selectLayout() 函式,即可存取偏好設定存放區並更新版面配置偏好設定。
  2. 請注意,寫入 DataStore 會使用 suspend 函式以非同步方式來完成。請啟動新的協同程式,呼叫偏好設定存放區的 saveLayoutPreference() 函式。
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

讀取版面配置偏好設定

在本節中,您將重構 ViewModel 中的現有 uiState: StateFlow,反映存放區中的 isLinearLayout: Flow

  1. 刪除將 uiState 屬性初始化為 MutableStateFlow(DessertReleaseUiState) 的程式碼。
val uiState: StateFlow<DessertReleaseUiState> =

存放區中的線性版面配置偏好設定具有兩個可能的值,即 true 或 false,格式為 Flow<Boolean>。這個值必須對應至 UI 狀態。

  1. StateFlow 設定為在 isLinearLayout Flow 上呼叫的 map() 集合轉換的結果。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. 傳回 DessertReleaseUiState 資料類別的執行個體,並傳遞 isLinearLayout Boolean。畫面會根據此 UI 狀態,來判斷要顯示的正確字串和圖示。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout 是採用冷流程Flow。不過,為了向 UI 提供狀態,最好使用熱流程 (例如 StateFlow),這樣一來,狀態始終會立即在 UI 中顯示。

  1. 使用 stateIn() 函式將 Flow 轉換為 StateFlow
  2. stateIn() 函式可接受三個參數:scopestartedinitialValue。分別針對這些參數傳遞 viewModelScopeSharingStarted.WhileSubscribed(5_000)DessertReleaseUiState()
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. 啟動應用程式。請注意,您可以點選切換圖示,在格線版面配置與線性版面配置之間進行切換。

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

恭喜!您已成功將 Preferences DataStore 新增至應用程式,可用於儲存使用者的版面配置偏好設定。

7. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些 git 指令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout main

另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。

如要查看程式碼解答,請前往 GitHub