github blog
qq: 2383518170
wx: lzyprime

λ:

经过几番修改。对DataStore的封装方式初步定下,虽然还是不满意,但已经是目前能想到的最好的方式。等有了新想法再改。

目前:

// key
val UserId = stringPreferencesKey("user_id")// use:
val userId = DS[UserId] // 取值
DS[UserId] = "new user id" // 设值
// or delegate read and write:
var userId by UserId(defaultValue = "") // 需要设置属性为空时的默认值。userId: String
repo.login(userId)
userId = "new user id"
// or readOnly
val userId by UserId // 只读. userId: String?
if(!userId.isNullOrEmpty()) repo.login(userId)

DS:

// DS:
@JvmInline
value class DSManager(private val dataStore: DataStore<Preferences>) : DataStore<Preferences> {...
}val DS: DSManager by lazy {...}

提供了两套获取方式。一种是像map一样的访问风格。一种是靠属性委托的方式。运算符重载属性委托内联类(用于收缩函数范围)

记录一下是如何一步步混沌邪恶的。

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() }

封装过程

as Map

最常使用的也就是对某个数据的取值赋值。所以很容易想到重载operator get,operator set。也就是中括号[]运算符:

suspend operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): T? = data.map{ it[key] }.first()
suspend operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value:T) = edit { it[key] = value }// use:
scope.launch {val userId = context.dataStore[UserId]context.dataStore[UserId] = "new user id"
}

看着还行, 但是!!!, 这两个函数不允许加suspend 。除非不是运算符函数,而是普通函数:val userId = context.dataStore.get(UserId)

那用runBlocking做成阻塞式的?那不就开倒车了,get还好,但是set绝不能这么搞。

所以有了把CoroutineScope传进来的版本:

private val cache = mutablePreferencesOf()operator fun <T> get(key: Preferences.Key<T>): T? = cache[key] ?: runBlocking { data.map { it[key] }.first() }?.also { cache[key] = it }
operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, scope: CoroutineScope, value: T) {cache[key] = valuescope.launch(Dispatchers.IO) { edit { it[key] = value } }
}// use:
val userId = context.dataStore[UserId]
context.dataStore[UserId, lifecycleScope] = "new user id"
  • 此时这两个函数都不必在协程块里跑了。但是由于set过程不阻塞,相当于提交任务,如果立刻取值,可能任务执行的不及时,导致取值失败或错误。就加了cache: MutablePreference,同时也优化一下get操作的速度。
  • set 太丑了,而且CoroutineScope一般存活在某个View的生存周期内,View一死,set操作就被取消了。而塞完之值立马杀死View是很常见的,比如登录过程,登录成功后保存值,然后就跳转主页了。所以这个任务应该提到Application级别的CoroutineScope中。

然后就有了:

// UnsplashApplication.kt
@HiltAndroidApp
class UnsplashApplication : Application() {@ApplicationScope@Injectlateinit var applicationScope: CoroutineScopeinit {instance = this}companion object {private lateinit var instance: UnsplashApplicationoperator fun getValue(ref: Any?, property: KProperty<*>): UnsplashApplication = instance}
}// DS.kt
private val application by UnsplashApplication
operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value: T) {cache[key] = valueapplication.applicationScope.launch { edit { it[key] = value } }
}

但仍有问题:如果不通过set, 而是updateData函数。 cache就不会更新,而且get, set文件顶级写,import的时候是import package_path.get,污染环境。

所以通过内联类收缩一下范围, 同时在updateData时更新cache

@JvmInline
value class DSManager(private val dataStore: DataStore<Preferences>) : DataStore<Preferences> {override val data: Flow<Preferences> get() = dataStore.datainit {application.applicationScope.launch { cache += data.first() }}override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences {transform(cache)return dataStore.updateData(transform)}operator fun <T> set(key: Preferences.Key<T>, value: T) { ... }operator fun <T> get(key: Preferences.Key<T>): T? = ...suspend operator fun invoke(transform: suspend (MutablePreferences) -> Unit) = edit(transform)
}val DS by lazy {DSManager(application.dataStore)
}// operator invoke use:
DS {it -= UserIdit[Sig] = "xxx"
}val userId = DS[UserId]
DS[UserId] = "new user id"

同时,利用了invoke也就是括号()运算符,提供edit操作, 就有了现在的版本。但是仍会有风险在,还是cache问题,预热不及时等问题。保存副本,就有了数据不一致风险。

可以选择干掉cache,不提供set, getFlow<T>回去, application也不用站在全局。

用内联类,或者普通类,通过委托,实现DataStore<T>接口,同样上边的情况也可以用普通类处理,但是不要靠委托实现接口。否则override时,拿不到super.xxx()的行为。

普通类处理:

class DSManager(context: Context): DataStore<Preferences> by context.dataStore {operator fun <T> get(key: Preferences.Key<T>): Flow<T?> = data.map{ it[key] }suspend operator fun invoke(transform: suspend (MutablePreferences) -> Unit) = edit(transform)
}val DS by lazy {val application by UnsplashApplicationDSManager(context)
}// use
val userIdFlow = DS[UserId]
scope.launch { val userId = userIdFlow.first() }

value by delegate

如果场景严格一点:DataStore只提供指定key-value的访问,不允许其他地方自定义Key

向上面的访问方式,自然就限制不了,其他人可以在任何文件中定义一个Key, 然后取值赋值。

很容易做:单例,DataStore私有,指暴露想开放的变量。并且cache也不会有什么问题。把要暴露的值做成CoroutineScope的拓展属性,或者继续使用applicationScope。同时也可以控制是否可写,是否可删除:

object DS {private val ds by lazy {val application by UnsplashApplicationapplication.dataStore}private val cache = mutablePreferencesOf()private val UserId = stringPreferencesKey("user_id")var CoroutineScope.userId: Stringget() = cache[UserId] ?: runBlocking { ds.data.map { it[UserId].orEmpty() }.first() }set(value) {cache[UserId] = valuelaunch { ds.edit { it[UserId] = value } }} // orvar userId: Stringget() = cache[UserId] ?: runBlocking { ds.data.map { it[UserId].orEmpty() }.first() }set(value) {cache[UserId] = valueval application by UnsplashApplicationapplication.applicationScope.launch { ds.edit { it[UserId] = value } }} // 可删除:var userId: String?get() = cache[UserId] ?: runBlocking { ds.data.map { it[UserId] }.first() }set(value) {val application by UnsplashApplicationif(value == null) {cache[UserId] = valueapplication.applicationScope.launch { ds.edit { it -= UserId } }} else {cache[UserId] = valueapplication.applicationScope.launch { ds.edit { it[UserId] = value } }}}
}

可以。但是每个变量都要写这么一份重复内容,太废了。这不就是属性委托编译器处理后的效果。所以做成委托:

object DS {private val ds by lazy {val application by UnsplashApplicationapplication.dataStore}private val cache = mutablePreferencesOf()private fun <T> safeKeyDelegate(key: Preferences.Key<T>, defaultValue: T) =object : ReadWriteProperty<Any?, T> {override fun getValue(thisRef: Any?, property: KProperty<*>): T =cache[key] ?: runBlocking { ds.data.map { it[key] ?: defaultValue }.first() }override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {cache[key] = valueapplication.applicationScope.launch { ds.edit { it[key] = value } }}}var userId: String by safeKeyDelegate(stringPreferencesKey("user_id"), "")
}

ReadWriteProperty<Any?, T>处理一下。可删除:ReadWriteProperty<Any?, T?>, 限定可见范围:ReadWriteProperty<UserInfo, T>

使用:

login(DS.userId)
DS.userId = "new user id"

key by delegate

条件放宽一点,可以自定义Key, 然后通过Key去DataStore里获取值:

operator fun <T> Preferences.Key<T>.invoke(defaultValue: T) = object : ReadWriteProperty<Any?, T> {override fun getValue(thisRef: Any?, property: KProperty<*>): T =cache[key] ?: runBlocking { ds.data.map { it[key] ?: defaultValue }.first() }override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {cache[key] = valueapplication.applicationScope.launch { ds.edit { it[key] = value } }}}operator fun <T> Preferences.Key<T>.provideDelegate(ref: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, T?> {override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {if(value == null) {...} else {...}}override fun getValue(thisRef: Any?, property: KProperty<*>): T? = ...
}private val UserId = stringPreferencesKey("user_id")

使用:


// 有默认值,读写, var
var userId by UserId("default value")
login(userId)
userId = "xxx"//有默认值,只读,val
val  userId by UserId("default value")// 无默认值(可删除),读写,var
var userId:String? by UserId// 无默认值(可删除),只读, val
val userId:String? by UserId

~λ:

大概就这些套路,根据项目实际情况,选择封装方式。排列组合一下。

Android DataStore相关推荐

  1. Android Jetpack组件DataStore之Proto与Preferences存储详解与使用

    一.介绍 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和 Flow 以异步.一致的事务方式存储数 ...

  2. JetPack—DataStore核心原理与使用

    简介 首先,DataStore是Jetpack一部分,是一种数据存储解决方案. 其次,DataStore使用协程及flow以异步.一致的方式实现数据的存储. 最后是DataStore的实现,分为Pre ...

  3. Android开发笔记(一百七十八)更安全的数据仓库DataStore

    虽然SharedPreferences用起来比较方便,但是在一些特殊场景会产生问题.比如共享参数保存的数据较多时,初始化共享参数会把整个文件加载进内存,加载耗时可能导致主线程堵塞.又如调用apply方 ...

  4. Android几种数据存储的对比(MMKV、DataStore、SharedPreference)

    MMKV Github地址:https://github.com/Tencent/MMKV MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf ...

  5. Android Jatpack DataStore使用详解

    简介 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和流程以异步.一致的事务方式存储数据. 如果您 ...

  6. 【Android Jetpack】DataStore

    目录 1. 前言 2. 分类 2.1 Preferences DataStore 和SharedPreferences的区别 3. 实践 3.1 Preferences DataStore 3.1.1 ...

  7. Android - 键值对存储 SharedPreference、DataStore、MMKV

    一.概念 以键值对的方式保存少量的数据,通常用来存数程序的一些配置信息,如是否打开音效.是否使用震动效果.窗口状态.是否进入过向导界面等. 二.SharedPreference 路径:data/dat ...

  8. Android Jetpack之DataStore指南

    介绍: 在官方尚未出手之前,存储键值对等小型数据集可能普遍采用两种方式,SharedPreferences或是MMKV(如果您需要支持大型或复杂数据集.部分更新或参照完整性,请考虑使用 Room,而不 ...

  9. 大佬教你极简方法来处理Android SharedPreferences设计与实现

    起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack DataStore 组件横空出世--这是否意味着无论是腾讯三方还是Googl ...

  10. Android Loader机制

    Android应用Loaders全面详解及源码浅析 加载器 LoaderCursor public class LoaderCursor extends Activity {@Overrideprot ...

最新文章

  1. 封装html5,封装Html5 Fullscreen API
  2. js获取当前Frame在父页面中的id
  3. C语言程序设计复习指导
  4. cout输出16进制_c++随堂测16优化方案
  5. 计算机二级最高分那个公式,计算机二级excel中常考公式及讲解[共24页].doc
  6. 简述关系型数据库和非关系型数据库
  7. mysql 两个结果集求差
  8. CNN:RCNN、SPPNet、Fast RCNN、Faster RCNN、YOLO V1 V2 V3、SSD、FCN、SegNet、U-Net、DeepLab V1 V2 V3、Mask RCNN
  9. 大数据平台架构与原型实现-读书笔记8
  10. Spring boot Shiro 用户认证
  11. 二维高斯核函数(python)
  12. cartographer代码框架(谷歌最近修改之后的)
  13. 稳定dns服务器,国内DNS服务器推荐 选对了让你网速开挂
  14. CAN通讯-使用Python收发CAN数据保姆级教程(包含完整代码)
  15. 如何尽早解决需求变更隐患,降低项目延期风险?
  16. 影像数据全院连通+集中管理,博为全院级PACS助力桑植县人民医院顺利通过二甲复审
  17. 虚拟机服务器做ghost,关于VMWare封装之后做Ghost的几种方法。
  18. 通过Teleport Ultra Teleport Ultra创建网站完整的镜象经验分享(注:资源里面可下载)
  19. 国能常发移动应用问题处理
  20. 计算机网络RIP实验

热门文章

  1. oracle seq nextval,在oracle中使用sequence.nextval创建表
  2. 腐蚀rust服务器命令_腐蚀rust服务器命令一览 腐蚀rust有哪些服务器命令
  3. uva 11137 Ingenuous Cubrency(完全背包)
  4. 弘辽科技:淘宝保险保证金怎么开通?它和消保保证金有什么区别?
  5. 快冲!淘宝无货源副业,傻瓜式操作,日赚300-500元!!
  6. 第六章 用正三和弦为含跳进旋律配和声
  7. python如何修改图片透明度_如何改变图像的不透明度
  8. iOS 获取APP名称 版本等
  9. 如何将HTML与win10桌面壁纸,如何在Windows 10中将页面设置为桌面背景 | MOS86
  10. Ubuntu 20.04.2 LTS安装微信(wine)