Android DataStore

github blog
qq: 2383518170
wx: lzyprime

λ:

當(dāng)前 DataStore 1.0.0

DataStore的封裝已經(jīng)試過好多方式。仍不滿意它抱。大概總結(jié)一下路數(shù):

  1. DataStore<Preferences> 提供[]訪問断国。

  2. 通過getValue, setValue 實(shí)現(xiàn)委托構(gòu)造页眯。

  3. 利用()運(yùn)算符加suspend, 從而實(shí)現(xiàn)掛起效果赊锚。

這里最大的限制是[], getValue, setValue 是不能加suspend的。所以要么傳CoroutineScope進(jìn)來赁还,要么加runBloacking并齐。但runBlocking 就喪失了DataStore的優(yōu)勢漏麦,退化成 SharedPreference.

// api preview
val kUserId = stringPreferencesKey("user_id")

// 1.
val userId: String? = anyDataStore[kUserId]
val userId: String = anyDataStore[kUserId, "0"]
anyDataStore[kUserId] = "<new value>"

// 2.
var userId: String by anyDataStore(...)
userId = "<new value>"

DataStore API

DataStore 文檔

當(dāng)前DataStore 1.0.0,目的是替代之前的SharedPreference, 解決它的諸多問題况褪。除了Preference簡單的key-value形式撕贞,還有protobuf版本。但是感覺雞肋测垛,小數(shù)據(jù)key-value就夠了捏膨,大數(shù)據(jù)建議Room處理數(shù)據(jù)庫。所以介于中間的部分食侮,或者真的需要類型化的号涯,真的有嗎?

DataStoreFlow的方式提供數(shù)據(jù)锯七,所以跑在協(xié)程里链快,可以不阻塞UI。

interface

DataStore的接口非常簡單眉尸,一個data, 一個fun updateData:

// T = Preferences
public interface DataStore<T> {
    public val data: Flow<T>
    public suspend fun updateData(transform: suspend (t: T) -> T): T
}

public suspend fun DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit): Preferences {
    return this.updateData { it.toMutablePreferences().apply { transform(this) } }
}

data: Flow<Preferences>域蜗。 Preferences可以看作是個Map<Preferences.Key<*>, Any>

同時為了數(shù)據(jù)修改方便效五,提供了個edit的拓展函數(shù)地消,調(diào)用的就是updateData函數(shù)。

獲取實(shí)例

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

preferencesDataStore 只為Context下的屬性提供只讀的委托:ReadOnlyProperty<Context, DataStore<Preferences>>畏妖。

所以前邊非要定成Context的拓展屬性,屬性名不一定非是這個, val Context.DS by ... 也可以疼阔。

搞清楚kotlin的屬性委托拓展屬性戒劫,就懂了這行代碼半夷。

preferencesDataStore相當(dāng)于創(chuàng)建了個fileDir/<datastore_name>.preferences_pb的文件, 存數(shù)據(jù)。

Preferences.Key

public abstract class Preferences internal constructor() {
    public class Key<T> internal constructor(public val name: String){ ... }
}

//create: 
val USER_ID = stringPreferencesKey("user_id")
val Guide = booleanPreferencesKey("guide")

都被加了internal限制迅细,所以在外邊調(diào)不了構(gòu)造巫橄。然后通過stringPreferencesKey(name: String)等一系列函數(shù),創(chuàng)建特定類型的Key茵典, 好處是限定了類型的范圍湘换,不會創(chuàng)出不支持類型的Key, 比如Key<UserInfo>Key<List<*>>统阿。

同時通過Preferences.Key<T>保證類型安全彩倚,明確存的是T類型數(shù)據(jù)。而SharedPreference, 可以沖掉之前的值類型:

SharedPreference.edit{
    it["userId"] = 1 
    it["userId"] = "new user id"
}

使用:

// 取值 -------------
val userIdFlow: Flow<String> = context.dataStore.data.map { preferences ->
    // No type safety.
    preferences[USER_ID].orEmpty()
}

anyCoroutineScope.launch {
    repo.login(userIdFlow.first())
    userIdFlow.collect { 
        ...
    }
}

// or
val userId = runBlocking {
    userIdFlow.first()
}

// 更新值 ------------
anyCoroutineScope.launch {
    context.dataStore.edit {
        it[USER_ID] = "new user id"
    }
}

Flow<Preference>.map{}流轉(zhuǎn)換扶平, 在preference這個 "Map" 里取出UserId的值帆离,有可能沒有值。得到一個Flow<T>结澄。

在協(xié)程里取當(dāng)前值Flow.first(), 或者實(shí)時監(jiān)聽變化哥谷。也可以runBlocking變成阻塞式的。當(dāng)然這就會和SharedPreference一樣的效果麻献,阻塞UI, 導(dǎo)致卡頓或崩潰们妥。尤其是第一次在data中取值,文件讀入會花點(diǎn)時間勉吻。所以可以在初始化時王悍,預(yù)熱一下:

anyCoroutineScope.launch { context.dataStore.data.first() }

封裝過程

[] 操作符

1. return Flow<T?> || Flow<T>

由于get set 函數(shù)無法加 suspend, 所以get只能以Flow的形式返回值. 而如果想實(shí)現(xiàn)set的效果,就要runBlocking餐曼, 這樣DataStore就失去了優(yōu)勢压储。


operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): Flow<T?> = data.map{ it[key] }

operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>, defaultValue: T): Flow<T> = data.map{ it[key] }

// operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value: T?) = runBlocking {
//    edit { if(value != null) it[key] = value else it -= key }
// }

// use:
val userId: Flow<String?> = anyDataStore[kUserId]
val userId: Flow<String> = anyDataStore[kUserId, ""]
// anyDataStore[kUserId] = "<new value>"

2. 為了解決set, 有了把CoroutineScope傳進(jìn)來的版本:

但是由于set過程不阻塞,如果立刻取值源譬,可能任務(wù)執(zhí)行的不及時集惋,導(dǎo)致取到的是舊值。 而且如果scope生命結(jié)束仍沒執(zhí)行完踩娘,則保存失敗刮刑。

operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, scope: CoroutineScope, value: T?) {
    scope.launch {
        edit { if(value != null) it[key] = value else it -= key }
    }
}

// use:
anyDataStore[kUserId, anyScope] = "<new value>"

3. 包裹DataStore, 加cache優(yōu)化。

加入cache處理更新不及時問題养渴,但有可能 預(yù)熱DataStore 操作不及時雷绢,導(dǎo)致cache錯亂。 get使用了runBlocking理卑,仍有隱患翘紊。

class DS(
    private val dataStore: DataStore<Preferences>,
    private val scope: CoroutineScope,
) {
    private val cache = mutablePreferencesOf()

    init {
        // 預(yù)熱 DataStore
        scope.launch {
            cache += dataStore.data.first()
        }
    }

    operator fun <T> get(key: Preferences.Key<T>): T? =
        cache[key] ?: runBlocking {
            dataStore.data.map { it[key] }.first()?.also { cache[key] = it }
        }

    operator fun <T> set(key:Preferences.Key<T>, value:T?) {
        if(value != null) cache[key] = value
        scope.launch {
            dataStore.edit { if(value != null) it[key] = value else it -= key }
        }
    }

    companion object {
        private const val STORE_NAME = "global_store"
        private val Context.dataStore by preferencesDataStore(STORE_NAME)
    }
}

// use:
// val ds: DS // 依賴注入或instance拿到單例
val userId = ds[kUserId]
ds[kUserId] = "<new value>"

總之[]難解決的是runBlocking執(zhí)行。

value class, ()操作符

  1. 內(nèi)聯(lián)類限定對DataStore的訪問藐唠。[]只提供get操作帆疟,返回Flow鹉究。
  2. 通過()操作符暴露DataStore<T>.edit.
@JvmInline
value class DS(private val dataStore: DataStore<Preferences>) {

    operator fun <T> get(key: Preferences.Key<T>) =
        dataStore.data.map { it[key] }
    
    suspend operator fun invoke(block: suspend (MutablePreferences) -> Unit) = 
        dataStore.edit(block)

    companion object {
        private const val STORE_NAME = "global_store"
        private val Context.dataStore by preferencesDataStore(STORE_NAME)
    }
}

// use
val userId = ds[kUserId]
suspend {
    ds {
        it[kUserId] = "<new value>"
        it -= kUserId
    }
}

屬性委托

abstract class PreferenceItem<T>(flow: Flow<T>) : Flow<T> by flow {
    abstract suspend fun update(v: T?)
}

operator fun <T> DataStore<Preferences>.invoke(
    buildKey: (name: String) -> Preferences.Key<T>,
    defaultValue: T,
) = ReadOnlyProperty<Any?, PreferenceItem<T>> { _, property ->
    val key = buildKey(property.name)
    object : PreferenceItem<T>(data.map { it[key] ?: defaultValue }) {
        override suspend fun update(v: T?) {
            edit {
                if (v == null) {
                    it -= key
                } else {
                    it[key] = v
                }
            }
        }
    }
}

// use
val userId: PreferenceItem<String> by anyDataStore(::stringPreferencesKey, "0")

suspend {
    userId.update("<new value>")
}

Preferences.Key<T>可以通過判別 T 的類型然后選擇對應(yīng)構(gòu)造函數(shù),匹配失敗拋異常踪宠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末自赔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子柳琢,更是在濱河造成了極大的恐慌绍妨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柬脸,死亡現(xiàn)場離奇詭異他去,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)肖粮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門孤页,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涩馆,你說我怎么就攤上這事行施。” “怎么了魂那?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵蛾号,是天一觀的道長。 經(jīng)常有香客問我涯雅,道長鲜结,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任活逆,我火速辦了婚禮精刷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔗候。我一直安慰自己怒允,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布锈遥。 她就那樣靜靜地躺著纫事,像睡著了一般。 火紅的嫁衣襯著肌膚如雪所灸。 梳的紋絲不亂的頭發(fā)上丽惶,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機(jī)與錄音爬立,去河邊找鬼钾唬。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的知纷。 我是一名探鬼主播壤圃,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陵霉,長吁一口氣:“原來是場噩夢啊……” “哼琅轧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起踊挠,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤乍桂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后效床,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體睹酌,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年剩檀,在試婚紗的時候發(fā)現(xiàn)自己被綠了憋沿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沪猴,死狀恐怖辐啄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情运嗜,我是刑警寧澤壶辜,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站担租,受9級特大地震影響砸民,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奋救,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一岭参、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尝艘,春花似錦演侯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至隘梨,卻和暖如春程癌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轴猎。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工嵌莉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捻脖。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓锐峭,卻偏偏與公主長得像中鼠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沿癞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

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

  • 1. 簡介 DataStore是Google Android Jetpack組件新推出的數(shù)據(jù)存儲解決方案援雇,其主要優(yōu)...
    XFY9326閱讀 6,466評論 0 3
  • hibernate 面試題小集 Hibernate有哪幾種查詢數(shù)據(jù)的方式 3種:hql、條件查詢QBC(Query...
    dbc94a66f502閱讀 232評論 0 0
  • 第一部分: application 應(yīng)用程式 應(yīng)用椎扬、應(yīng)用程序application framework 應(yīng)用程式框...
    谷雨2058閱讀 1,873評論 0 1
  • 很實(shí)用的編程英語詞庫惫搏,共收錄一千五百余條詞匯。 第一部分: application 應(yīng)用程式 應(yīng)用蚕涤、應(yīng)用程序app...
    春天的蜜蜂閱讀 1,356評論 0 22
  • Java 常見英語單詞 (1.0 版本) 1. Java 基礎(chǔ)常見英語詞匯(70 個) OO: object-or...
    Nemo359閱讀 2,176評論 0 0