I'm Prime

十方三世,尽在一念

View on GitHub

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