Jetpack Preferences DataStore 入門

1. 簡介

DataStore是Google Android Jetpack組件新推出的數(shù)據(jù)存儲解決方案避凝,其主要優(yōu)點如下:

  • 允許使用Protocol-Buffers存儲鍵值對或類型化對象
  • 使用Kotlin協(xié)程和Flow來異步故黑、一致和事務(wù)性地存儲數(shù)據(jù)

DataStore并不被建議用來存儲大量復(fù)雜的數(shù)據(jù),并且無法局部的更新數(shù)據(jù),如果有類似的需求可以使用Room組件替代荒勇。

由于使用了Kotlin協(xié)程和Flow相關(guān)的知識,所以建議在使用之前先在Kotlin協(xié)程與Flow官方文檔進(jìn)行了解。(注:英語不好的可以翻譯或者搜索相關(guān)中文教程)

2. Preferences DataStore 與 Proto DataStore

可以這樣簡單的理解兩者的區(qū)別:
Preferences DataStore與SharedPreferences類似压储,通過鍵值對存儲數(shù)據(jù),不保證類型安全源譬。
Proto DataStore通過Protocol-Buffers定義存儲數(shù)據(jù)類型以及結(jié)構(gòu)渠脉,保證類型安全。

注:本文只介紹Preferences DataStore的使用方式瓶佳,因為這足夠滿足多數(shù)情況下的使用了芋膘。如果想要進(jìn)一步了解Proto DataStore,建議前往DataStore官方教程Protocol-Buffers官方教程查看最新文檔霸饲。

3. 依賴導(dǎo)入(按需導(dǎo)入)

DataStore API更新動態(tài)與最新版本查詢

dependencies {
    // Typed DataStore (Proto DataStore)
    implementation "androidx.datastore:datastore:1.0.0"
    // Typed DataStore (沒有Android依賴項为朋,包含僅適用于 Kotlin 的核心 API)
    implementation "androidx.datastore:datastore-core:1.0.0"
    // 可選 - RxJava2 支持
    implementation "androidx.datastore:datastore-rxjava2:1.0.0"
    // 可選 - RxJava3 支持
    implementation "androidx.datastore:datastore-rxjava3:1.0.0"

    // Preferences DataStore(可以直接使用)
    implementation "androidx.datastore:datastore-preferences:1.0.0"
    // Preferences DataStore (沒有Android依賴項,包含僅適用于 Kotlin 的核心 API)
    implementation "androidx.datastore:datastore-preferences-core:1.0.0"
    // 可選 - RxJava2 支持
    implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
    // 可選 - RxJava3 支持
    implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
}

注1:2021.1.15 自alpha06開始修改了Preference.Key的API厚脉,本文已更新
注2:2021.2.24 自alpha07開始廢棄了Context.createDataStore的API习寸,本文已更新
注3:2021.8.4 DataStore 1.0.0 release

4. Preferences DataStore 入門

4.1 初始化DataStore

官方示例(創(chuàng)建名稱為settings的DataStore):

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

根據(jù)官方注釋的說明,該操作用于創(chuàng)建SingleProcessDataStore的實例傻工,用戶負(fù)責(zé)確保一次操作一個文件的SingleProcessDataStore的實例永遠(yuǎn)不會超過一個霞溪。
如果使用RxJava的話需要使用RxPreferenceDataStoreBuilder替代
因此為了防止出錯,方便管理中捆,個人建議使用單例模式進(jìn)行DataStore實例的管理鸯匹,但是由于需要使用Context對象才能夠?qū)嵗钥梢酝ㄟ^使用Application的靜態(tài)context變量的方式實現(xiàn)泄伪。
因為DataStore必須使用by委托的方式創(chuàng)建殴蓬,所以在非Context類下創(chuàng)建較為麻煩,因此最好使用Application的靜態(tài)Context方式作為媒介創(chuàng)建DataStore蟋滴。

// App.kt
class App : Application() {
    companion object {
        lateinit var instance: App
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

// SettingsDataStore.kt
object {
    // 創(chuàng)建DataStore
    private val App.dataStore: DataStore<Preferences> by createDataStore(
        name = "settings"
    )
    // 對外開放的DataStore變量
    val dataStore = App.instance.dataStore
}

創(chuàng)建的DataStore存儲文件將會被放置在 "/data/data/{包名}/files/datastore/{DataStore名稱}.preferences_pb"

4.2 鍵(Key)創(chuàng)建

官方示例(創(chuàng)建名為example_counter的Int類型的鍵):

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

通過preferencesKey可以創(chuàng)建的數(shù)據(jù)類型為:Int染厅,String,Boolean津函,F(xiàn)loat肖粮,Long,Double尔苦。
如果想要創(chuàng)建Set<T>類型的鍵涩馆,必須使用以下方法:

val EXAMPLE_COUNTER_SET = stringSetPreferencesKey("example_counter_set")

通過preferencesSetKey可以創(chuàng)建的數(shù)據(jù)類型目前僅支持String行施。

如果希望能夠?qū)⒆兞棵鳛殒I名,可以使用如下方法建立委托方法:

fun booleanPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Boolean>> { _, property -> booleanPreferencesKey(property.name) }

fun stringPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<String>> { _, property -> stringPreferencesKey(property.name) }

fun intPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Int>> { _, property -> intPreferencesKey(property.name) }

fun longPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Long>> { _, property -> longPreferencesKey(property.name) }

fun floatPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Float>> { _, property -> floatPreferencesKey(property.name) }

fun doublePreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Double>> { _, property -> doublePreferencesKey(property.name) }

fun stringSetPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Set<String>>> { _, property -> stringSetPreferencesKey(property.name) }

這樣就可以通過以下方式實現(xiàn)鍵的創(chuàng)建:

val example_counter by intPreferencesKey()

4.3 數(shù)據(jù)讀取

官方示例(讀取EXAMPLE_COUNTER鍵的值凌净,若為null即不存在,則使用0作為默認(rèn)值):

val exampleCounterFlow: Flow<Int> = dataStore.data.map { preferences ->
    // 無類型安全
    preferences[EXAMPLE_COUNTER] ?: 0
}

dataStore.data本質(zhì)上返回的是一個Flow<Preference>對象屋讶,此處的Preference僅能夠進(jìn)行讀取操作冰寻,接著通過Flow提供的map方法轉(zhuǎn)換接下來傳遞的數(shù)據(jù)。
如果想要一次性讀取多個數(shù)據(jù)皿渗,或者讀取數(shù)據(jù)為一個data class斩芭,可以采用如下方式:

data class Example(val value_1: Int, val value_2: String?)

val key_1 = intPreferencesKey("key_1")
val key_2 = stringPreferencesKey("key_2")

val exampleFlow: Flow<Example> = dataStore.data.map { preferences ->
    Example(preferences[key_1] ?: 0, preferences[key_2])
}

DataStore會使用內(nèi)存緩存的方式加快同一數(shù)據(jù)二次讀取速度,因此多數(shù)情況下并不需要手動設(shè)置緩存相關(guān)的代碼乐疆。
通過Flow API划乖,實際讀取到數(shù)據(jù)可以主要通過以下兩種方式:

// 需要在協(xié)程函數(shù)內(nèi)部或suspend函數(shù)下運行,僅讀取一次最新數(shù)據(jù)
exampleFlow.first()

// 需要在協(xié)程函數(shù)內(nèi)部或suspend函數(shù)下運行挤土,會監(jiān)聽數(shù)據(jù)變化并返回最新數(shù)據(jù)
exampleFlow.collect { data ->
    println(data)
}

4.4 數(shù)據(jù)修改

官方示例(對EXAMPLE_COUNTER鍵的值進(jìn)行從0開始的自增):

suspend fun incrementCounter() {
    dataStore.edit { settings ->
        val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
        settings[EXAMPLE_COUNTER] = currentCounterValue + 1
    }
}

DataStore提供的edit()方法可以將傳入的操作視作單個事務(wù)進(jìn)行修改琴庵,因此滿足了數(shù)據(jù)的一致和事務(wù)性。
示例的lambda函數(shù)中傳入的settings為MutablePreferences對象仰美,提供了數(shù)據(jù)的讀取與修改操作迷殿。
對于數(shù)據(jù)較大的批量的修改,建議可以合并到一個事務(wù)內(nèi)進(jìn)行以提高IO效率咖杂。

4.5 異步

由于DataStore使用了Kotlin提供的Flow作為數(shù)據(jù)獲取的方式庆寺,因此滿足的IO操作異步的需求。但是并非所有的IO操作都可以立即遷移為異步執(zhí)行诉字,所以官方文檔中指出可以使用以下方法臨時解決問題:

// 普通的堵塞方式讀取數(shù)據(jù)懦尝,可能會導(dǎo)致死鎖,最好別用
val exampleData = runBlocking { dataStore.data.first() }

// 在LifeCycle提供的協(xié)程方法中讀取數(shù)據(jù)
override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        dataStore.data.first()
        // 可以在此處理 IOException
    }
}

5. 從SharedPreferences中遷移

示例代碼:

val dataStore = context.preferenceDataStore(
    name = "{DataStore名稱}",
    migrations = listOf(SharedPreferencesMigration(context, "{SharedPreferences名稱}"))
)

默認(rèn)情況下完成遷移后將會刪除原始SharedPreferences的xml文件壤圃,可以通過參數(shù)調(diào)整陵霉。
注:此處的SharedPreferencesMigration并非該類的原始構(gòu)造方法,而是androidx.datastore.preferences包下的kotlin函數(shù)伍绳。

6. 在PreferenceFragmentCompat中使用DataStore

Google目前已經(jīng)在PreferenceFragment上提供可以使用其他數(shù)據(jù)源的兼容性接口撩匕,首先手動實現(xiàn)基于DataStore的抽象類androidx.preference.PreferenceDataStore,然后在PreferenceFragmentCompat中獲取PreferenceManager墨叛,最后通過PreferenceManager的以下方法就可以將默認(rèn)的SharedPreference存儲方式替換為DataStore了止毕。

 public void setPreferenceDataStore(PreferenceDataStore dataStore)

注:雖然抽象類名字為PreferenceDataStore,但是本身與DataStore并沒有關(guān)系

以下為筆者實現(xiàn)的PreferenceDataStore

open class DataStorePreferenceAdapter(private val dataStore: DataStore<Preferences>, scope: CoroutineScope) : PreferenceDataStore() {
    private val prefScope = CoroutineScope(scope.coroutineContext + SupervisorJob() + Dispatchers.IO)

    private val dsData = dataStore.data.shareIn(prefScope, SharingStarted.Eagerly, 1)

    private fun <T> putData(key: Preferences.Key<T>, value: T?) {
        prefScope.launch {
            dataStore.edit {
                if (value != null) it[key] = value else it.remove(key)
            }
        }
    }

    private fun <T> readNullableData(key: Preferences.Key<T>, defValue: T?): T? {
        return runBlocking(prefScope.coroutineContext) {
            dsData.map {
                it[key] ?: defValue
            }.firstOrNull()
        }
    }

    private fun <T> readNonNullData(key: Preferences.Key<T>, defValue: T): T {
        return runBlocking(prefScope.coroutineContext) {
            dsData.map {
                it[key] ?: defValue
            }.first()
        }
    }

    override fun putString(key: String, value: String?) = putData(stringPreferencesKey(key), value)

    override fun putStringSet(key: String, values: Set<String>?) = putData(stringSetPreferencesKey(key), values)

    override fun putInt(key: String, value: Int) = putData(intPreferencesKey(key), value)

    override fun putLong(key: String, value: Long) = putData(longPreferencesKey(key), value)

    override fun putFloat(key: String, value: Float) = putData(floatPreferencesKey(key), value)

    override fun putBoolean(key: String, value: Boolean) = putData(booleanPreferencesKey(key), value)


    override fun getString(key: String, defValue: String?): String? = readNullableData(stringPreferencesKey(key), defValue)

    override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? = readNullableData(stringSetPreferencesKey(key), defValues)

    override fun getInt(key: String, defValue: Int): Int = readNonNullData(intPreferencesKey(key), defValue)

    override fun getLong(key: String, defValue: Long): Long = readNonNullData(longPreferencesKey(key), defValue)

    override fun getFloat(key: String, defValue: Float): Float = readNonNullData(floatPreferencesKey(key), defValue)

    override fun getBoolean(key: String, defValue: Boolean): Boolean = readNonNullData(booleanPreferencesKey(key), defValue)
}

7. 總結(jié)

相比于漏洞百出到就連Google都不想修復(fù)的SharedPreferences漠趁,DataStore確實提供了一套簡單可用的異步數(shù)據(jù)存儲方案扁凛,不管是Kotlin協(xié)程還是Flow,都極大程度的提高了使用的體驗闯传。
與騰訊已經(jīng)開源并穩(wěn)定使用的MMKV相比谨朝,使用官方組件最大的好處就是與其他組件的相互兼容性,并且如果已經(jīng)使用了Kotlin協(xié)程庫,使用DataStore可以減少App的體積字币。
目前DataStore已經(jīng)release则披,總體使用效果還是不錯的。不過如果項目大都是靜態(tài)存儲的數(shù)據(jù)(不需要觀察數(shù)據(jù)更新)或者沒有任何多進(jìn)程等同步的需求洗出,那么也沒必要馬上遷移到DataStore中士复。不過DataStore依然存在的一個問題就是無法直觀的看到與修改已經(jīng)存放的數(shù)據(jù),這需要Android Studio后續(xù)的更新支持翩活。


Made By XFY9326

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末阱洪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菠镇,更是在濱河造成了極大的恐慌冗荸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件利耍,死亡現(xiàn)場離奇詭異蚌本,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)隘梨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門魂毁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人出嘹,你說我怎么就攤上這事席楚。” “怎么了税稼?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵烦秩,是天一觀的道長。 經(jīng)常有香客問我郎仆,道長只祠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任扰肌,我火速辦了婚禮抛寝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曙旭。我一直安慰自己盗舰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布桂躏。 她就那樣靜靜地躺著钻趋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪剂习。 梳的紋絲不亂的頭發(fā)上蛮位,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天较沪,我揣著相機(jī)與錄音,去河邊找鬼失仁。 笑死尸曼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的萄焦。 我是一名探鬼主播控轿,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼楷扬!你這毒婦竟也來了解幽?” 一聲冷哼從身側(cè)響起贴见,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烘苹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后片部,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镣衡,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年档悠,在試婚紗的時候發(fā)現(xiàn)自己被綠了廊鸥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡辖所,死狀恐怖惰说,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缘回,我是刑警寧澤吆视,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站酥宴,受9級特大地震影響啦吧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拙寡,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一授滓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肆糕,春花似錦般堆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绍申,卻和暖如春噩咪,著一層夾襖步出監(jiān)牢的瞬間顾彰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工胃碾, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留涨享,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓仆百,卻偏偏與公主長得像厕隧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俄周,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容