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。