Android Jetpack - DataStore 指南

語言: CN / TW / HK

介紹:

在官方尚未出手之前,儲存鍵值對等小型資料集可能普遍採用兩種方式,SharedPreferences或是MMKV(如果您需要支援大型或複雜資料集、部分更新或參照完整性,請考慮使用 Room,而不是 DataStore。DataStore 非常適合簡單的小型資料集,不支援部分更新或參照完整性。) MMKV這次暫時不提及,因為DatStore本身對比的也就是SharedPreferences,而且官方也是明確的建議我們遷移到DataStore。 DataStore包含了兩種實現方式: - Preferences DataStore僅使用鍵儲存和訪問值資料。此實現不需要預定義的架構,並且不提供型別安全性。 - Proto DataStore將資料儲存為自定義資料型別的例項。此實現要求您使用協議緩衝區(protobuf - PB協議)定義架構,但它提供型別安全性。

與SharedPreferences的對比:

首先我們來看官方的一張對比 功能 | SharedPreferences | PreferencesDataStore | ProtoDataStore | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- | | 非同步 API | ✅(僅用於通過監聽器讀取已更改的值) | ✅(通過 Flow 以及 RxJava 2 和 3 Flowable) | ✅(通過 Flow 以及 RxJava 2 和 3 Flowable) | | 同步 API | ✅(但無法在介面執行緒上安全呼叫) | ❌ | ❌ | | 可在介面執行緒上安全呼叫 | ❌ | ✅(這項工作已在後臺移至 Dispatchers.IO) | ✅(這項工作已在後臺移至 Dispatchers.IO) | | 可以提示錯誤 | ❌ | ✅ | ✅ | | 不受執行時異常影響 | ❌ | ✅ | ✅ | | 包含一個具有強一致性保證的事務性 API | ❌ | ✅ | ✅ | | 處理資料遷移 | ❌ | ✅ | ✅ | | 型別安全 | ❌ | ❌ | ✅ 使用協議緩衝區

我們先暫時只看PreferencesDataStoreSharedPreferences 首先同步API和非同步API這兩點區別是沒有問題的。 SharedPreferences: - apply()來完成非同步操作:會立即更改記憶體中的 SharedPreferences 物件,但會將更新非同步寫入磁碟。而且apply()還有個問題就是,雖然他本身是非同步的來完成IO操作,但是在SharedPreferencesImpl.EditorImpl.apply()中會新增到QueuedWork中,當Service或者Activity啟動或停止時,具體可見ActivityThread中handleServiceArgshandleStopServicehandlePauseActivityhandleStopActivity均會執行QueuedWork.waitToFinish()等待資料寫入的完成,因為要保證資料不會丟失,但是我們也知道,onPause() 是不適合執行耗時操作的,因為當你期待另一個Activity的時候,會先onPause當前Activity,這很明顯,假如你寫入了較多內容,然後立馬啟動了另一個Activity,結果在onPause()被阻塞,就很容易導致ANR。 - commit()來實現同步操作,但應避免從主執行緒呼叫它,因為它可能會阻塞UI執行緒,這點沒什麼好說的,而且會返回Boolean值來表示寫入是否成功。 詳細的可以檢視SharedPreferences.Editor介面提供的註釋,具體的實現在SharedPreferencesImpl.EditorImpl我這裡就不貼原始碼了。 回到DataStore,PreferencesDataStore本身是基於攜程Flow來實現的,所以非同步API這點沒有任何問題,不過至於同步的使用方式,放到後面來說,我們先看普遍的非同步使用方式。 可以參考官方出的教學:https://developer.android.com/codelabs/android-preferences-datastore#0 我就不一一複述了。

使用:

````kotlin private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(     name = USER_PREFERENCES_NAME ) ` 首先是通過委託拿到DataStore```單例.注意Java版本有所區別,Java版本需要Rxjava的支援,具體依賴的匯入可參考官方文件:https://developer.android.com/topic/libraries/architecture/datastore

````kotlin public fun preferencesDataStore( name: String, corruptionHandler: ReplaceFileCorruptionHandler? = null, produceMigrations: (Context) -> List> = { listOf() }, scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) ): ReadOnlyProperty> { return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope) }

internal class PreferenceDataStoreSingletonDelegate internal constructor( private val name: String, private val corruptionHandler: ReplaceFileCorruptionHandler?, private val produceMigrations: (Context) -> List>, private val scope: CoroutineScope ) : ReadOnlyProperty> {

private val lock = Any()

@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStore<Preferences>? = null

override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
    return INSTANCE ?: synchronized(lock) {
        if (INSTANCE == null) {
            val applicationContext = thisRef.applicationContext

            INSTANCE = PreferenceDataStoreFactory.create(
                corruptionHandler = corruptionHandler,
                migrations = produceMigrations(applicationContext),
                scope = scope
            ) {
                applicationContext.preferencesDataStoreFile(name)
            }
        }
        INSTANCE!!
    }
}

} 本質上還是走的PreferenceDataStoreFactory.create()```建立,是一個非常標準的雙重檢查鎖單例。 來看一下create()函式的引數吧 - corruptionHandler: 異常處理,當反序列化錯誤時會走到這,可以用於讀取錯誤是返回預設值或捕獲異常。 - migrations: 用於遷移SharedPreferences到PreferenceDataStore - scope: 協程的作用域,指定IO操作及資料轉換的執行的協程作用域 - produceFile: 基於提供的Context和name建立或讀取對應的檔案,預設路徑為this.applicationContext.filesDir+datastore/fileName` 推薦做法也是通過PreferenceDataStoreFactory來建立DataStore例項並作為單例注入需要它的類中。

讀取:

dataStore.data     .catch { exception ->         // 有異常丟擲         if (exception is IOException) { // 使用預設空值             emit(emptyPreferences())         } else { // 其他異常則繼續丟擲             throw exception         }     }.map { preferences ->         // 資料轉化     }.collect { // 收集資料 } emptyPreferences()可以參考上面提到的官方教學示例,裡面會詳細介紹PreferenceData的KV 可以看出dataStore返回的是一個Flow,你可以很方便的轉換成你所需要的資料。 對於Java則是返回的Flowable,和Flow差別不大。

寫入:

``` val INT_KEY = intPreferencesKey("int_key") dataStore.edit { preferences -> preferences[INT_KEY] = 1 }

// 呼叫 public suspend fun DataStore.edit( transform: suspend (MutablePreferences) -> Unit ): Preferences { return this.updateData { // It's safe to return MutablePreferences since we freeze it in // PreferencesDataStore.updateData() it.toMutablePreferences().apply { transform(this) } } } ``` 直接呼叫edit函式,不過要注意的是edit可能會丟擲異常。 同時還有兩點需要注意: 1. 在transform執行完之前,在transform裡改變的值並不會馬上更新到DataStore,執行完才會,所以在edit()函式成功返回之前不要認為資料已經寫入成功。 2. 不要儲存在transform中提供的MutablePreferences的引用,在transform外的對preferences操作並不會更新到DataStore,因為這很明顯破壞了原設計的原子性與事務性。

再回到上圖中,DataStore 的主要優勢之一是非同步API,所以本身並未提供同步API呼叫,但實際上可能不一定始終能將周圍的程式碼更改為非同步程式碼。如果您使用的現有程式碼庫採用同步磁碟 I/O,或者您的依賴項不提供非同步API,就可能出現這種情況。 這個時候可以採用協程本身提供 runBlocking()以幫助消除同步與非同步程式碼之間的差異。您可以使用 runBlocking() 從 DataStore 同步讀取資料。RxJava 在 Flowable 上提供阻塞方法。會阻塞發起呼叫的執行緒,直到 DataStore 返回資料。 kotlin runBlocking { context.dataStore.data.first() } runBlocking()會執行一個新的協程並阻塞當前執行緒直到內部邏輯完成,所以儘量避免在UI執行緒呼叫。 而且還需要注意的一點是,不用在初始讀取時呼叫runBlocking,會阻塞當前執行的執行緒,因為初始讀取會有較多的IO操作,耗時較長。 更為推薦的做法則是先非同步讀取到記憶體後,後續有需要可直接從記憶體中拿,而非運行同步程式碼阻塞式獲取。

總結:

總的來說還是很推薦使用DataStore,與協程的搭配,用起來也是非常的便利,至於PB協議的ProtoDataStore,可以參考官方的示例來實踐,差別主要是還是集中在PB檔案的處理。