Android DataStore
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
处理数据库。所以介于中间的部分,或者真的需要类型化的,真的有吗?
DataStore
以Flow
的方式提供数据,所以跑在协程里,可以不阻塞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
, get
扔Flow<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相关推荐
- Android Jetpack组件DataStore之Proto与Preferences存储详解与使用
一.介绍 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和 Flow 以异步.一致的事务方式存储数 ...
- JetPack—DataStore核心原理与使用
简介 首先,DataStore是Jetpack一部分,是一种数据存储解决方案. 其次,DataStore使用协程及flow以异步.一致的方式实现数据的存储. 最后是DataStore的实现,分为Pre ...
- Android开发笔记(一百七十八)更安全的数据仓库DataStore
虽然SharedPreferences用起来比较方便,但是在一些特殊场景会产生问题.比如共享参数保存的数据较多时,初始化共享参数会把整个文件加载进内存,加载耗时可能导致主线程堵塞.又如调用apply方 ...
- Android几种数据存储的对比(MMKV、DataStore、SharedPreference)
MMKV Github地址:https://github.com/Tencent/MMKV MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf ...
- Android Jatpack DataStore使用详解
简介 Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象.DataStore 使用 Kotlin 协程和流程以异步.一致的事务方式存储数据. 如果您 ...
- 【Android Jetpack】DataStore
目录 1. 前言 2. 分类 2.1 Preferences DataStore 和SharedPreferences的区别 3. 实践 3.1 Preferences DataStore 3.1.1 ...
- Android - 键值对存储 SharedPreference、DataStore、MMKV
一.概念 以键值对的方式保存少量的数据,通常用来存数程序的一些配置信息,如是否打开音效.是否使用震动效果.窗口状态.是否进入过向导界面等. 二.SharedPreference 路径:data/dat ...
- Android Jetpack之DataStore指南
介绍: 在官方尚未出手之前,存储键值对等小型数据集可能普遍采用两种方式,SharedPreferences或是MMKV(如果您需要支持大型或复杂数据集.部分更新或参照完整性,请考虑使用 Room,而不 ...
- 大佬教你极简方法来处理Android SharedPreferences设计与实现
起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack DataStore 组件横空出世--这是否意味着无论是腾讯三方还是Googl ...
- Android Loader机制
Android应用Loaders全面详解及源码浅析 加载器 LoaderCursor public class LoaderCursor extends Activity {@Overrideprot ...
最新文章
- 封装html5,封装Html5 Fullscreen API
- js获取当前Frame在父页面中的id
- C语言程序设计复习指导
- cout输出16进制_c++随堂测16优化方案
- 计算机二级最高分那个公式,计算机二级excel中常考公式及讲解[共24页].doc
- 简述关系型数据库和非关系型数据库
- mysql 两个结果集求差
- CNN:RCNN、SPPNet、Fast RCNN、Faster RCNN、YOLO V1 V2 V3、SSD、FCN、SegNet、U-Net、DeepLab V1 V2 V3、Mask RCNN
- 大数据平台架构与原型实现-读书笔记8
- Spring boot Shiro 用户认证
- 二维高斯核函数(python)
- cartographer代码框架(谷歌最近修改之后的)
- 稳定dns服务器,国内DNS服务器推荐 选对了让你网速开挂
- CAN通讯-使用Python收发CAN数据保姆级教程(包含完整代码)
- 如何尽早解决需求变更隐患,降低项目延期风险?
- 影像数据全院连通+集中管理,博为全院级PACS助力桑植县人民医院顺利通过二甲复审
- 虚拟机服务器做ghost,关于VMWare封装之后做Ghost的几种方法。
- 通过Teleport Ultra Teleport Ultra创建网站完整的镜象经验分享(注:资源里面可下载)
- 国能常发移动应用问题处理
- 计算机网络RIP实验
热门文章
- oracle seq nextval,在oracle中使用sequence.nextval创建表
- 腐蚀rust服务器命令_腐蚀rust服务器命令一览 腐蚀rust有哪些服务器命令
- uva 11137 Ingenuous Cubrency(完全背包)
- 弘辽科技:淘宝保险保证金怎么开通?它和消保保证金有什么区别?
- 快冲!淘宝无货源副业,傻瓜式操作,日赚300-500元!!
- 第六章 用正三和弦为含跳进旋律配和声
- python如何修改图片透明度_如何改变图像的不透明度
- iOS 获取APP名称 版本等
- 如何将HTML与win10桌面壁纸,如何在Windows 10中将页面设置为桌面背景 | MOS86
- Ubuntu 20.04.2 LTS安装微信(wine)