1. 事前準備
在先前的程式碼研究室中,我們已說明如何使用 Room (資料庫抽象層) 將資料儲存在 SQLite 資料庫中。本程式碼研究室會介紹 Jetpack DataStore。DataStore 以 Kotlin 協同程式和 Flow 為基礎而設計,共提供兩種不同的實作方式,一種是專門儲存類型物件的 Proto DataStore,另一種則是專門儲存鍵/值組合的 Preferences DataStore。
本程式碼研究室會說明如何使用 Preferences DataStore,Proto DataStore 則不在本程式碼研究室的說明範圍內。
必要條件
- 您熟悉 Android 架構元件
ViewModel
、LiveData
和Flow
,也瞭解如何使用ViewModelProvider.Factory
將ViewModel
執行個體化。 - 您熟悉並行的基礎知識。
- 您瞭解如何使用協同程式來處理長時間執行的工作。
課程內容
- DataStore 是什麼?您應使用 DataStore 的原因及時機為何?
- 如何將 Preference DataStore 新增至應用程式。
軟硬體需求
- Words 應用程式的範例程式碼 (與先前程式碼研究室中的 Words 應用程式解決方案程式碼相同)。
- 已安裝 Android Studio 的電腦。
下載本程式碼研究室的範例程式碼
在本程式碼研究室中,您將會從先前的解決方案程式碼擴充 Word 應用程式的功能。範例程式碼可能包含您在先前程式碼研究室中也熟悉的程式碼。
如要從 GitHub 取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Get from VCS」。
- 在「Get from Version Control」對話方塊中,確認您已為「Version Control」選取「Git」。
- 將提供的程式碼網址貼到「URL」方塊中。
- 您也可以將「Directory」變更為與建議預設值不同的內容。
- 按一下「Clone」。Android Studio 會開始擷取程式碼。
- 等待 Android Studio 開啟。
- 在程式碼研究室的範例程式碼、應用程式或解決方案程式碼中,選取正確的模組。
- 按一下「Run」按鈕 ,即可建構並執行程式碼。
2. 入門應用程式總覽
Words 應用程式包含兩個畫面:第一個畫面會顯示使用者可選取的字母,第二個畫面則會顯示開頭為所選字母的字詞清單。
這個應用程式可讓使用者透過選單選項,切換為以清單或格狀版面配置來顯示字母。
- 下載範例程式碼,然後在 Android Studio 中開啟並執行應用程式。系統會以線性版面配置顯示字母。
- 輕觸右上角的選單選項。版面配置會切換為格狀版面配置。
- 結束應用程式並重新啟動。您可以在 Android Studio 中使用「Stop ‘app'」(停止「應用程式」) 和「Run ‘app'」(執行「應用程式」) 選項。請注意,重新啟動應用程式後,字母會以線性版面配置顯示,而不是格狀。
請注意,系統不會保留使用者的選擇。本程式碼研究室會說明如何修正此問題。
建構項目
- 在本程式碼研究室中,您會瞭解如何使用 Preferences DataStore,保留 DataStore 中的版面配置設定。
3. Preferences DataStore 簡介
Preferences DataStore 適合用於簡單的小型資料集,例如儲存登入詳細資料、深色模式設定、字型大小等等。DataStore 不適用於複雜的資料集,例如線上雜貨店的商品目錄清單或學生資料庫。如果需要儲存大型或複雜的資料集,建議您使用 Room 而非 DataStore。
使用 Jetpack DataStore 程式庫就能建立簡單、安全且非同步的 API,可用來儲存資料。其提供兩種不同的導入方式:Preferences DataStore 和 Proto DataStore。雖然 Preferences DataStore 和 Proto DataStore 都能儲存資料,但卻使用不同的做法:
- Preferences DataStore 可依據鍵來存取和儲存資料,而不必事先界定結構定義 (資料庫模型)。
- Proto DataStore 使用通訊協定緩衝區來界定結構定義。使用通訊協定緩衝區 (或 Protobufs) 可讓您保留強類型資料。與 XML 和其他類似的資料格式相比,Protobufs 更快更簡單,而且更清晰明確。
Room 與 Datastore 的比較:適用時機
如果您的應用程式需要以 SQL 等結構化格式儲存大型/複雜的資料,建議您使用 Room。不過,如果您只想儲存簡單或少許資料,且這些資料可以儲存在鍵/值組合中,建議您使用 DataStore。
Proto DataStore 與 Preferences DataStore 的比較:使用時機
Proto DataStore 類型安全有效,但需要設定。如果您的應用程式資料夠簡單,可以儲存在鍵/值組合中,那麼易於設定的 Preferences DataStore 則較為適合。
將 Preferences DataStore 新增為依附元件
要將 DataStore 整合至應用程式,第一步是將其新增為依附元件。
- 在
build.gradle(Module: Words.app)
中新增下列依附元件:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
4. 建立 Preferences DataStore
- 新增名為
data
的套件,並在其中建立名為SettingsDataStore
的 Kotlin 類別。 - 在類型
Context
的SettingsDataStore
類別中加入建構函式參數。
class SettingsDataStore(context: Context) {}
- 在
SettingsDataStore
類別之外,宣告名為LAYOUT_PREFERENCES_NAME
的private const val
,並為其指派字串值layout_preferences
。這是你在下一步要執行個體化的 Preferences Datastore 名稱。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
- 請在類別外使用
preferencesDataStore
委托來建立DataStore
執行個體。由於您使用 Preferences Datastore,因此需要將Preferences
傳遞做為資料儲存庫類型。此外,請將name
資料儲存庫設為LAYOUT_PREFERENCES_NAME
。
完成的程式碼如下:
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCES_NAME
)
5. 實作 SettingsDataStore 類別
如前所述,Preferences DataStore 會以鍵/值組合的形式儲存資料。在這個步驟中,您會定義儲存版面配置設定所需的鍵,也會定義要寫入和讀取 Preferences DataStore 的函式。
鍵類型函式
有別於 Room,Preferences DataStore 並不會使用預先定義的結構定義,而是使用對應的鍵類型函式,來定義您儲存在 DataStore<Preferences>
執行個體中每個值的鍵。舉例來說,如要定義 int
值的鍵,請使用 intPreferencesKey()
;要定義 string
值的鍵,則使用 stringPreferencesKey()
。整體來說,這些函式名稱的前置字串會與所儲存鍵的資料類型相同。
在 data\SettingsDataStore
類別中實作以下內容:
- 如要導入
SettingsDataStore
類別,首先請建立用於儲存布林值的鍵,該布林值會指定使用者設定是否屬於線性版面配置。建立名為IS_LINEAR_LAYOUT_MANAGER
的private
類別屬性,並使用booleanPreferencesKey()
(傳入is_linear_layout_manager
鍵名稱做為函式參數) 來初始化。
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")
寫入 Preferences DataStore
現在,請開始使用鍵,並將布林值版面配置設定儲存在 DataStore
中。Preferences DataStore 提供 edit()
暫停函式,可以交易形式更新 DataStore
中的資料。函式的轉換參數接受程式碼區塊,您可以視需要更新值。轉換區塊的所有程式碼皆視為單一交易。原理上,交易作業會移至 Dispacter.IO
底下,因此在呼叫 edit()
函式時,別忘了將函式設為 suspend
。
- 建立一個名為
saveLayoutToPreferencesStore()
的suspend
函式,該函式採用以下兩個參數:版面配置設定布林值和Context
。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 實作上述函式,呼叫
dataStore
.edit()
,並傳遞程式碼區塊以儲存新的值。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
context.dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
}
}
從 Preferences DataStore 讀取
Preferences DataStore 會公開儲存在 Flow<Preferences>
的資料,只要偏好設定有所變更,該程式碼就會發出資料。您不想公開整個 Preferences
物件,只需公開 Boolean
值即可。因此,我們會對應 Flow<Preferences>
,並取得您所需的 Boolean
值。
- 公開根據
dataStore.data: Flow<Preferences>
建構的preferenceFlow: Flow<UserPreferences>
,進行對應以擷取Boolean
偏好設定。由於 Datastore 在首次執行時沒有任何內容,因此系統會預設傳回true
。
val preferenceFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
// On the first run of the app, we will use LinearLayoutManager by default
preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
}
- 如果下列匯入項目未自動匯入,請新增以下資訊:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
例外狀況處理
DataStore 從檔案讀取及寫入資料,系統可能會在存取資料時出現 IOExceptions
。您可以使用 catch()
運算子來擷取例外狀況,以處理這些問題。
- 若在讀取資料時發生錯誤,SharedPreference DataStore 會擲回
IOException
。在preferenceFlow
宣告中,請在map()
之前使用catch()
運算子擷取IOException
,並發出emptyPreferences()
。為求簡單,我們預計此處不會出現其他類型的例外情形;如果出現其他類型的例外狀況,請重新擲回該例外狀況。
val preferenceFlow: Flow<Boolean> = context.dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// On the first run of the app, we will use LinearLayoutManager by default
preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
}
你現在可以使用 data\SettingsDataStore
類別了!
6. 使用 SettingsDataStore 類別
在下一個工作中,您將會在 LetterListFragment
類別中使用 SettingsDataStore
。您要將觀察程式附加到版面配置設定,並據此更新使用者介面。
請在 LetterListFragment
中採取下列步驟:
- 宣告稱為
SettingsDataStore
且類型為SettingsDataStore
的private
類別變數。由於您後將會初始化這個變數,因此請將其設為lateinit
。
private lateinit var SettingsDataStore: SettingsDataStore
- 在
onViewCreated()
函式的末尾,請初始化新變數,然後將requireContext()
傳遞至SettingsDataStore
建構函式。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
讀取及觀察資料
- 在
LetterListFragment
的onViewCreated()
方法中,於SettingsDataStore
初始化底下,使用asLiveData
()
將preferenceFlow
轉換為Livedata
。請附加一個觀察程式,並傳遞到viewLifecycleOwner
做為擁有者。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- 在觀察程式內,將新的版面配置設定指派給
isLinearLayoutManager
變數。呼叫chooseLayout()
函式以更新 RecyclerView 版面配置。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
isLinearLayoutManager = value
chooseLayout()
})
完成的 onViewCreated()
函式應如下所示:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
isLinearLayoutManager = value
chooseLayout()
})
}
將版面配置設定寫入 DataStore
最後一個步驟則是在使用者輕觸選單選項時,將版面配置設定寫入 Preferences DataStore。您應以同步方式在協同程式中將資料寫入 Preferences DataStore。如要在片段中執行此操作,請使用名為 LifecycleScope
的 CoroutineScope
。
LifecycleScope
生命週期感知元件 (如片段) 為應用程式中的邏輯範圍以及與 LiveData
的互通層提供一流支援。系統會為每個 Lifecycle
物件定義 LifecycleScope
。Lifecycle
擁有者遭到刪除時,系統就會取消此範圍內啟動的所有協同程式。
- 在
LetterListFragment
的onOptionsItemSelected()
函式內,於R.id.
action_switch_layout
案件的結尾,使用lifecycleScope
來啟動協同程式。在launch
區塊內,呼叫saveLayoutToPreferencesStore()
以傳遞isLinearLayoutManager
和context
。
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_switch_layout -> {
...
// Launch a coroutine and write the layout setting in the preference Datastore
lifecycleScope.launch {
SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
}
...
return true
}
- 執行應用程式。按一下選單選項來變更應用程式的版面配置。
- 現在,請測試 Preferences DataStore 的持續性。將應用程式版面配置變更為格狀版面配置。結束應用程式並重新啟動 (您可以在 Android Studio 中使用「Stop ‘app'」(停止「應用程式」) 和 「Run ‘app'」(執行「應用程式」) 選項)。
重新啟動應用程式後,字母現在會以格狀版面配置顯示,而不是線性版面配置。您的應用程式已成功儲存使用者選取的版面配置!
請注意,雖然字母現在會以格狀版面配置顯示,但選單圖示未正確更新。我們接下來會說明如何解決這個問題。
7. 修正選單圖示錯誤
選單圖示錯誤,原因在於在 onViewCreated()
中,RecyclerView 版面配置的更新依據是版面配置設定而而不是選單圖示。只要在更新 RecyclerView 版面配置時同時重畫選單,即可解決這個問題。
重畫選項選單
建立選單後,系統就不會多此一舉地為每個頁框重畫相同的選單。invalidateOptionsMenu()
函式會指示 Android 重畫選項選單。
變更「選項」選單的內容 (例如新增選單項目、刪除項目或是變更選單文字或圖示) 時,您可以呼叫這個函式。在本例中,選單圖示已變更。呼叫此方法就會宣告「選項」選單已變更,並應重新建立選單。下次需要顯示時選項選單時,就會呼叫 onCreateOptionsMenu(android.view.Menu)
方法。
- 在
LetterListFragment
中的onViewCreated()
內,於preferenceFlow
觀察程式結尾,呼叫chooseLayout()
的底下,透過對activity
呼叫invalidateOptionsMenu()
來重畫選單。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- 再次執行應用程式,並變更版面配置。
- 結束應用程式並重新啟動。請注意,選單圖示現在已正確更新。
恭喜!您已成功將 Preferences DataStore 新增至應用程式,以便儲存使用者的選擇。
8. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方顯示的專案和模組中。
9. 總結
- DataStore 提供採用 Kotlin 協同程式和 Flow 的完全非同步 API,可確保資料的一致性。
- Jetpack DataStore 是一項資料儲存解決方案,可讓您使用通訊協定緩衝區來儲存鍵/值組合或類型物件。
- DataStore 提供兩種實作方式:Preferences DataStore 和 Proto DataStore。
- Preferences DataStore 不會使用預先定義的結構定義。
- Preferences DataStore 使用對應的鍵類型函式,定義每個需要儲存在
DataStore<Preferences>
執行個體中的值。舉例來說,想定義int
值的鍵,請使用intPreferencesKey()
。 - Preferences DataStore 提供
edit()
函式,可以交易形式更新DataStore
中的資料。