Preferences DataStore

1. 事前準備

在先前的程式碼研究室中,我們已說明如何使用 Room (資料庫抽象層) 將資料儲存在 SQLite 資料庫中。本程式碼研究室會介紹 Jetpack DataStore。DataStore 以 Kotlin 協同程式和 Flow 為基礎而設計,共提供兩種不同的實作方式,一種是專門儲存類型物件的 Proto DataStore,另一種則是專門儲存鍵/值組合的 Preferences DataStore。

本程式碼研究室會說明如何使用 Preferences DataStore,Proto DataStore 則不在本程式碼研究室的說明範圍內。

必要條件

  • 您熟悉 Android 架構元件 ViewModelLiveDataFlow,也瞭解如何使用 ViewModelProvider.FactoryViewModel 執行個體化。
  • 您熟悉並行的基礎知識。
  • 您瞭解如何使用協同程式來處理長時間執行的工作。

課程內容

  • DataStore 是什麼?您應使用 DataStore 的原因及時機為何?
  • 如何將 Preference DataStore 新增至應用程式。

軟硬體需求

  • Words 應用程式的範例程式碼 (與先前程式碼研究室中的 Words 應用程式解決方案程式碼相同)。
  • 已安裝 Android Studio 的電腦。

下載本程式碼研究室的範例程式碼

在本程式碼研究室中,您將會從先前的解決方案程式碼擴充 Word 應用程式的功能。範例程式碼可能包含您在先前程式碼研究室中也熟悉的程式碼。

如要從 GitHub 取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Get from VCS」

61c42d01719e5b6d.png

  1. 在「Get from Version Control」對話方塊中,確認您已為「Version Control」選取「Git」

9284cfbe17219bbb.png

  1. 將提供的程式碼網址貼到「URL」方塊中。
  2. 您也可以將「Directory」變更為與建議預設值不同的內容。

5ddca7dd0d914255.png

  1. 按一下「Clone」。Android Studio 會開始擷取程式碼。
  2. 等待 Android Studio 開啟。
  3. 在程式碼研究室的範例程式碼、應用程式或解決方案程式碼中,選取正確的模組。

2919fe3e0c79d762.png

  1. 按一下「Run」按鈕 8de56cba7583251f.png,即可建構並執行程式碼。

2. 入門應用程式總覽

Words 應用程式包含兩個畫面:第一個畫面會顯示使用者可選取的字母,第二個畫面則會顯示開頭為所選字母的字詞清單。

這個應用程式可讓使用者透過選單選項,切換為以清單或格狀版面配置來顯示字母。

  1. 下載範例程式碼,然後在 Android Studio 中開啟並執行應用程式。系統會以線性版面配置顯示字母。
  2. 輕觸右上角的選單選項。版面配置會切換為格狀版面配置。
  3. 結束應用程式並重新啟動。您可以在 Android Studio 中使用「Stop ‘app'」(停止「應用程式」) f782441b99bdd0a4.png 和「Run ‘app'」(執行「應用程式」) d203bd07cbce5954.png 選項。請注意,重新啟動應用程式後,字母會以線性版面配置顯示,而不是格狀。

請注意,系統不會保留使用者的選擇。本程式碼研究室會說明如何修正此問題。

建構項目

  • 在本程式碼研究室中,您會瞭解如何使用 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 整合至應用程式,第一步是將其新增為依附元件。

  1. 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

  1. 新增名為 data 的套件,並在其中建立名為 SettingsDataStore 的 Kotlin 類別。
  2. 在類型 ContextSettingsDataStore 類別中加入建構函式參數。
class SettingsDataStore(context: Context) {}
  1. SettingsDataStore 類別之外,宣告名為 LAYOUT_PREFERENCES_NAMEprivate const val,並為其指派字串值 layout_preferences。這是你在下一步要執行個體化的 Preferences Datastore 名稱。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. 請在類別外使用 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 類別中實作以下內容:

  1. 如要導入 SettingsDataStore 類別,首先請建立用於儲存布林值的鍵,該布林值會指定使用者設定是否屬於線性版面配置。建立名為 IS_LINEAR_LAYOUT_MANAGERprivate 類別屬性,並使用 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

  1. 建立一個名為 saveLayoutToPreferencesStore()suspend 函式,該函式採用以下兩個參數:版面配置設定布林值和 Context
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. 實作上述函式,呼叫 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 值。

  1. 公開根據 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
   }
  1. 如果下列匯入項目未自動匯入,請新增以下資訊:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

例外狀況處理

DataStore 從檔案讀取及寫入資料,系統可能會在存取資料時出現 IOExceptions。您可以使用 catch() 運算子來擷取例外狀況,以處理這些問題。

  1. 若在讀取資料時發生錯誤,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 中採取下列步驟:

  1. 宣告稱為 SettingsDataStore 且類型為 SettingsDataStoreprivate 類別變數。由於您後將會初始化這個變數,因此請將其設為 lateinit
private lateinit var SettingsDataStore: SettingsDataStore
  1. onViewCreated() 函式的末尾,請初始化新變數,然後將 requireContext() 傳遞至 SettingsDataStore 建構函式。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

讀取及觀察資料

  1. LetterListFragmentonViewCreated() 方法中,於 SettingsDataStore 初始化底下,使用 asLiveData()preferenceFlow 轉換為 Livedata。請附加一個觀察程式,並傳遞到 viewLifecycleOwner 做為擁有者。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. 在觀察程式內,將新的版面配置設定指派給 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。如要在片段中執行此操作,請使用名為 LifecycleScopeCoroutineScope

LifecycleScope

生命週期感知元件 (如片段) 為應用程式中的邏輯範圍以及與 LiveData 的互通層提供一流支援。系統會為每個 Lifecycle 物件定義 LifecycleScopeLifecycle 擁有者遭到刪除時,系統就會取消此範圍內啟動的所有協同程式。

  1. LetterListFragmentonOptionsItemSelected() 函式內,於 R.id.action_switch_layout 案件的結尾,使用 lifecycleScope 來啟動協同程式。在 launch 區塊內,呼叫 saveLayoutToPreferencesStore() 以傳遞 isLinearLayoutManagercontext
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
       }
  1. 執行應用程式。按一下選單選項來變更應用程式的版面配置。

  1. 現在,請測試 Preferences DataStore 的持續性。將應用程式版面配置變更為格狀版面配置。結束應用程式並重新啟動 (您可以在 Android Studio 中使用「Stop ‘app'」(停止「應用程式」) f782441b99bdd0a4.png 和 「Run ‘app'」(執行「應用程式」) d203bd07cbce5954.png 選項)。

cd2c31f27dfb5157.png

重新啟動應用程式後,字母現在會以格狀版面配置顯示,而不是線性版面配置。您的應用程式已成功儲存使用者選取的版面配置!

請注意,雖然字母現在會以格狀版面配置顯示,但選單圖示未正確更新。我們接下來會說明如何解決這個問題。

7. 修正選單圖示錯誤

選單圖示錯誤,原因在於在 onViewCreated() 中,RecyclerView 版面配置的更新依據是版面配置設定而而不是選單圖示。只要在更新 RecyclerView 版面配置時同時重畫選單,即可解決這個問題。

重畫選項選單

建立選單後,系統就不會多此一舉地為每個頁框重畫相同的選單。invalidateOptionsMenu() 函式會指示 Android 重畫選項選單。

變更「選項」選單的內容 (例如新增選單項目、刪除項目或是變更選單文字或圖示) 時,您可以呼叫這個函式。在本例中,選單圖示已變更。呼叫此方法就會宣告「選項」選單已變更,並應重新建立選單。下次需要顯示時選項選單時,就會呼叫 onCreateOptionsMenu(android.view.Menu) 方法。

  1. LetterListFragment 中的 onViewCreated() 內,於 preferenceFlow 觀察程式結尾,呼叫 chooseLayout() 的底下,透過對 activity 呼叫 invalidateOptionsMenu() 來重畫選單。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. 再次執行應用程式,並變更版面配置。
  2. 結束應用程式並重新啟動。請注意,選單圖示現在已正確更新。

1c8cf63c8d175aad.png

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

10. 瞭解詳情

網誌

偏好使用 Jetpack DataStore 儲存資料