Android DataStore

github blog
qq: 2383518170
wx: lzyprime

λ:

当前 DataStore 1.0.0

DataStore的封装已经试过好多方式。仍不满意。大概总结一下路数:

  1. DataStore<Preferences> 提供[]访问。

  2. 通过getValue, setValue 实现委托构造。

  3. 利用()运算符加suspend, 从而实现挂起效果。

这里最大的限制是[], getValue, setValue 是不能加suspend的。所以要么传CoroutineScope进来,要么加runBloacking。但runBlocking 就丧失了DataStore的优势,退化成 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 文档

当前DataStore 1.0.0,目的是替代之前的SharedPreference, 解决它的诸多问题。除了Preference简单的key-value形式,还有protobuf版本。但是感觉鸡肋,小数据key-value就够了,大数据建议Room处理数据库。所以介于中间的部分,或者真的需要类型化的,真的有吗?

DataStoreFlow的方式提供数据,所以跑在协程里,可以不阻塞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>

同时为了数据修改方便,提供了个edit的拓展函数,调用的就是updateData函数。

获取实例

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

preferencesDataStore 只为Context下的属性提供只读的委托:ReadOnlyProperty<Context, DataStore<Preferences>>

所以前边非要定成Context的拓展属性,属性名不一定非是这个, val Context.DS by ... 也可以。

搞清楚kotlin的属性委托拓展属性,就懂了这行代码。

preferencesDataStore相当于创建了个fileDir/<datastore_name>.preferences_pb的文件, 存数据。

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限制,所以在外边调不了构造。然后通过stringPreferencesKey(name: String)等一系列函数,创建特定类型的Key, 好处是限定了类型的范围,不会创出不支持类型的Key, 比如Key<UserInfo>Key<List<*>>

同时通过Preferences.Key<T>保证类型安全,明确存的是T类型数据。而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{}流转换, 在preference这个 "Map" 里取出UserId的值,有可能没有值。得到一个Flow<T>

在协程里取当前值Flow.first(), 或者实时监听变化。也可以runBlocking变成阻塞式的。当然这就会和SharedPreference一样的效果,阻塞UI, 导致卡顿或崩溃。尤其是第一次在data中取值,文件读入会花点时间。所以可以在初始化时,预热一下:

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

封装过程

[] 操作符

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

由于get set 函数无法加 suspend, 所以get只能以Flow的形式返回值. 而如果想实现set的效果,就要runBlocking, 这样DataStore就失去了优势。


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传进来的版本:

但是由于set过程不阻塞,如果立刻取值,可能任务执行的不及时,导致取到的是旧值。 而且如果scope生命结束仍没执行完,则保存失败。

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优化。

加入cache处理更新不及时问题,但有可能 预热DataStore 操作不及时,导致cache错乱。 get使用了runBlocking,仍有隐患。

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

    init {
        // 预热 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执行。

value class, ()操作符

  1. 内联类限定对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 的类型然后选择对应构造函数,匹配失败抛异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容