美文网首页
Android DataStore

Android DataStore

作者: lzyprime | 来源:发表于2021-09-02 03:31 被阅读0次

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

    相关文章

      网友评论

          本文标题:Android DataStore

          本文链接:https://www.haomeiwen.com/subject/hckqwltx.html