首先给个官网的的地址:应用架构:数据层 - DataStore - Android 开发者 | Android Developers
小伙伴们可以直接看官网的资料,本篇文章是对官网的部分细节进行补充
一、为什么要使用DataStore 代替SharedPreferences
SharedPreferences:
- DataStore出现之前,我们用的最多的存储方式毫无疑问是SP,其使用方式简单、易用,广受好评。然而google对SP的定义为轻量级存储,如果存储的数据少,使用起来没有任何问题,当需要存储数据比较多时,SP可能会导致以下问题:
- 1. SP第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿;
- 2. SP读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API;
- 3. commit() / apply()操作可能会造成ANR问题;
- commit()是同步提交,会在UI主线程中直接执行IO操作,当写入操作耗时比较长时就会导致UI线程被阻塞,进而产生ANR;apply()虽然是异步提交,但异步写入磁盘时,如果执行了Activity / Service中的onStop()方法,那么一样会同步等待SP写入完毕,等待时间过长时也会引起ANR问题。
DataStore:
总结:SharedPreferences卡进程,DataStore不卡进程
二、如何使用
-
添加依赖,在build.grade(:app)中添加:
implementation("androidx.datastore:datastore-preferences:1.0.0")
- 创建 Preferences DataStore
private val Context.mDataStore: DataStore<Preferences> by preferencesDataStore(name = "appData")
private val mContext =MyApplication.mContext
使用属性委托来创建,方便访问,并且可以保留为单例,只需要传入一个数据库名称即可,点开preferencesDataStore查看源码可以看到,只需要一个name参数,其他参数都是非必填,当初看到这个Context.mDataStore写法对新手有点蒙,各位小伙伴可以了解一下属性委托再看这个代码,preferencesDataStore返回的是PreferenceDataStoreSingletonDelegate,它的getValue方法如下,“委托必须提供一个操作符函数 getValue(),该函数具有以下参数: thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。 property —— 必须是类型 KProperty<*> 或其超类型“。这样我们就知道为啥使用Context加扩展写法了,如果直接在Application这样有上下文Context的地方可以直接写mDataStore而不是Context.mDataStore
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences>
-
使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。通常我们项目中需要存储类似userId,userName类似字段,我以它们两个举例说明:
private val USER_ID = intPreferencesKey("userId")
private val USER_NAME = stringPreferencesKey("userName")
-
存入数据:Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。
private suspend fun setUserName(userName: String) {
mContext.mDataStore.edit { settings ->
settings[USER_NAME] = userName
}
}
private suspend fun setUserId(userId: Int) {
mContext.mDataStore.edit { settings ->
settings[USER_ID] = userId
}
}
-
读取数据:(我直接取Flow的第一个值)
private suspend fun getUserId() {
mUserId = mDataStore.data.map { preferences ->
preferences[USER_ID] ?: 0
}.first()
}
private suspend fun getUserName() {
mUserName = mDataStore.data.map { preferences ->
preferences[USER_NAME] ?: "empty"
}.first()
}
-
如果是在Activity中可以直接使用lifecycleScope
lifecycleScope.launch {
getUserId()
}
-
如果想在整个应用中随时调用,不跟随某个组件的生命周期,可以在Application中声明一个全局的协程作用域,注意释放
override fun onCreate() {
mApplicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
mContext = applicationContext
}
companion object {
var mContext: Context by Delegates.notNull()
var mApplicationScope: CoroutineScope by Delegates.notNull()
}
override fun onLowMemory() {
super.onLowMemory()
mApplicationScope.cancel()
}
这样我们可以使用mApplicationScope随时存储和读取数据啦
-
我们不想读取和存储数据是异步操作,比如我们的网络请求一header里开始就需要userID和username判断用户是否登录,我们可以使用runBlocking阻塞进程
runBlocking {
//此处取Flow都是取第一个值,可以写两个取值操作,
DataStoreUtil.getUserId()
DataStoreUtil. getUserName()
//如果是没有调用flow.first()方法,无法取到username,可以把取值放在不同协程中,Flow就不会相互干扰了
// launch {
// getUserId()
// }
// launch {
// getUserName()
// }
}
-
附上代码地址:GitHub - scYao/DataStoreDmeo: DataStore Demo