1. 事前準備
簡介
在本單元中,您已瞭解如何使用 SQL 和 Room 將資料儲存在本機裝置。SQL 和 Room 是功能強大的工具。不過,如果您不需要儲存關聯資料,DataStore 可提供簡單的解決方案。DataStore Jetpack 元件採用一種絕佳的方式,以較低的負擔儲存小型且簡易的資料集。DataStore 採用兩種實作方式:Preferences DataStore 和 Proto DataStore。
Preferences DataStore會儲存鍵/值組合。這些值可以是 Kotlin 的基本資料類型,例如String、Boolean和Integer。但它不會儲存複雜的資料集,亦不需要預先定義的結構定義。Preferences Datastore的主要用途是將使用者偏好設定儲存到自己的裝置。Proto DataStore會儲存自訂資料類型。它需要預先定義的結構定義,可將 Proto 定義對應至物件結構。
此程式碼研究室僅適用 Preferences DataStore,但您可以在 DataStore 說明文件中進一步瞭解 Proto DataStore。
Preferences DataStore 是儲存使用者控管設定的絕佳方式。在本程式碼研究室中,您將瞭解如何實作 DataStore 以達到上述目的!
需求條件:
- 在「使用 Room 讀取及更新資料」程式碼研究室中完成「Compose 中的 Android 基本概念」課程。
軟硬體需求
- 已安裝 Android Studio 且連上網路的電腦
- 裝置或模擬器
- Dessert Release 應用程式的範例程式碼
建構項目
Dessert Release 應用程式會顯示 Android 版本清單。應用程式列中的圖示可將版面配置切換成格狀檢視和清單檢視。

在目前的狀態下,應用程式不會保存版面配置選項。關閉應用程式時,不會儲存版面配置選項,而且設定會恢復為預設選項。在本程式碼研究室中,您會將 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
- 在 Android Studio 中開啟
basic-android-kotlin-compose-training-dessert-release資料夾。 - 在 Android Studio 中開啟 Dessert Release 應用程式程式碼。
3. 設定依附元件
請將以下內容新增至 app/build.gradle.kts 檔案的 dependencies 中:
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. 實作使用者偏好設定存放區
- 在
data套件中,建立名為UserPreferencesRepository的新類別。

- 在
UserPreferencesRepository建構函式中,定義不公開值屬性,用於表示Preferences類型的DataStore物件例項。
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
}
DataStore 會儲存鍵/值組合。您必須定義索引鍵,才能存取值。
- 在
UserPreferencesRepository類別中建立companion object。 - 使用
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 作業,即一次完成的作業。這類更新可避免更新部分值,而不更新其他值的情況。
- 建立暫停函式並呼叫
saveLayoutPreference()。 - 在
saveLayoutPreference()函式中,對dataStore物件呼叫edit()方法。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- 如要讓程式碼更易讀,請定義 lambda 主體中提供的
MutablePreferences名稱。使用該屬性設定一個值,以及您定義的索引鍵和傳遞至saveLayoutPreference()函式的布林值。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT] = isLinearLayout
}
}
從 DataStore 讀取
現在,您已建立將 isLinearLayout 寫入 dataStore 的方法,請按照下列步驟執行讀取作業:
- 在
UserPreferencesRepository中建立類型為Flow<Boolean>且名為isLinearLayout的屬性。
val isLinearLayout: Flow<Boolean> =
- 您可以使用
DataStore.data屬性來公開DataStore值。將isLinearLayout設定為DataStore物件的data屬性。
val isLinearLayout: Flow<Boolean> = dataStore.data
data 屬性是 Preferences 物件的 Flow。Preferences 物件包含 DataStore 中的所有鍵/值組合。每次更新 DataStore 中的資料時,都會將新的 Preferences 物件發出至 Flow。
- 使用對應函式將
Flow<Preferences>轉換為Flow<Boolean>。
這個函式接受使用目前 Preferences 物件的 lambda 做為參數。您可以指定之前定義的索引鍵,以取得版面配置偏好設定。請注意,如果尚未呼叫 saveLayoutPreference,則該值可能不存在,因此您必須提供預設值。
- 指定
true以預設為線性版面配置檢視畫面。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
例外狀況處理
裝置上的檔案系統可能會在與您互動時發生問題,例如檔案可能不存在、磁碟已滿或已卸載。DataStore 讀取及寫入檔案資料時,如果存取 DataStore,可能會發生 IOExceptions。您可以使用 catch{} 運算子擷取例外狀況並處理這些錯誤。
- 在隨附物件中,實作不可變動的
TAG字串屬性,用於進行記錄。
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
const val TAG = "UserPreferencesRepo"
}
- 若讀取資料時發生錯誤,
Preferences DataStore會擲回IOException。在isLinearLayout初始化區塊中,請在map()前使用catch{}運算子擷取IOException。
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- 如果擷取區塊中有
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。
- 尋找
dessertrelease套件。 - 在這個目錄中,建立名為
DessertReleaseApplication的新類別,並實作Application類別 (即 DataStore 的容器)。
class DessertReleaseApplication: Application() {
}
- 在
DessertReleaseApplication.kt檔案內 (但在DessertReleaseApplication類別之外),宣告名為LAYOUT_PREFERENCE_NAME的private const val。 - 指派字串值
layout_preferences做為LAYOUT_PREFERENCE_NAME的變數,您可以在下一步進行例項化時將其設為Preferences Datastore的名稱。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- 同樣在
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
)
- 在
DessertReleaseApplication類別主體中,建立UserPreferencesRepository的lateinit 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
}
- 覆寫
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()
}
}
- 在
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)
}
}
- 在
AndroidManifest.xml檔案的<application>標記中新增以下行。
<application
android:name=".DessertReleaseApplication"
...
</application>
這種方法會將 DessertReleaseApplication 類別定義為應用程式的進入點。本程式碼的目的是在啟動 MainActivity 之前,初始化 DessertReleaseApplication 類別中定義的依附元件。
6. 使用 UserPreferencesRepository
將存放區提供給 ViewModel
您現在可透過插入依附元件的方式在 DessertReleaseViewModel 中使用 UserPreferencesRepository。
- 在
DessertReleaseViewModel中建立UserPreferencesRepository屬性,做為建構函式參數。
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
- 在
ViewModel的隨附物件 (位於viewModelFactory initializer區塊) 中,使用以下程式碼取得DessertReleaseApplication的例項。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
- 建立
DessertReleaseViewModel的執行個體並傳遞userPreferencesRepository。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
DessertReleaseViewModel(application.userPreferencesRepository)
}
}
}
}
UserPreferencesRepository 現在可透過 ViewModel 存取。後續步驟為使用之前實作的 UserPreferencesRepository 讀寫功能。
儲存版面配置偏好設定
- 在
DessertReleaseViewModel中編輯selectLayout()函式,即可存取偏好設定存放區並更新版面配置偏好設定。 - 請注意,寫入
DataStore會使用suspend函式以非同步方式來完成。請啟動新的協同程式,呼叫偏好設定存放區的saveLayoutPreference()函式。
fun selectLayout(isLinearLayout: Boolean) {
viewModelScope.launch {
userPreferencesRepository.saveLayoutPreference(isLinearLayout)
}
}
讀取版面配置偏好設定
在本節中,您將重構 ViewModel 中的現有 uiState: StateFlow,反映存放區中的 isLinearLayout: Flow。
- 刪除將
uiState屬性初始化為MutableStateFlow(DessertReleaseUiState)的程式碼。
val uiState: StateFlow<DessertReleaseUiState> =
存放區中的線性版面配置偏好設定具有兩個可能的值,即 true 或 false,格式為 Flow<Boolean>。這個值必須對應至 UI 狀態。
- 將
StateFlow設定為在isLinearLayout Flow上呼叫的map()集合轉換的結果。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- 傳回
DessertReleaseUiState資料類別的執行個體,並傳遞isLinearLayout Boolean。畫面會根據此 UI 狀態,來判斷要顯示的正確字串和圖示。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
UserPreferencesRepository.isLinearLayout 是採用冷流程的 Flow。不過,為了向 UI 提供狀態,最好使用熱流程 (例如 StateFlow),這樣一來,狀態始終會立即在 UI 中顯示。
- 使用
stateIn()函式將Flow轉換為StateFlow。 stateIn()函式可接受三個參數:scope、started和initialValue。分別針對這些參數傳遞viewModelScope、SharingStarted.WhileSubscribed(5_000)和DessertReleaseUiState()。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DessertReleaseUiState()
)
- 啟動應用程式。請注意,您可以點選切換圖示,在格線版面配置與線性版面配置之間進行切換。

恭喜!您已成功將 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。