DataStorePreference 的两种封装方案
前言
MMKV、ShredPreference、DataStore到底怎么选,性能对比如何?首先感谢扔物线大佬的科普【MMKV 天下无敌无脑选?那你这几年可能被骗了】,有大佬顶在前面我只需要在大佬的胯下小心输出即可,如果觉得有性能问题或其他疑问完全可以找大佬探讨下,这不是本文的重点。
既然我们已经通过大佬的讲解知道了为什么要用 DataStore,其实我们也就知道了 MMKV 和 DataStore 其实是不同的使用场景,而 DataStore(Preference)可以说是全场景化取代 ShredPreference 的方案,也是更符合普通 App 应用的常规 KV 储存框架。
如果你的项目是 Kotlin 构架的,那么 DataStore 是必不可少也是强烈推荐使用的。如果你的项目还是 Java 语言构建的,别慌,目前 DataStore 也已经支持 RxJava2 和 RxJava3 的框架。
下来说下 DataStore 的优缺点是 .. 咦?等等..偏题了啊,我不是科普 DataStore 的啊,有这方面的问题可以自行搜索相关文章或者看大佬的讲解。我是真的只是探讨下 DataStore 的几种封装方式而已。
接下来我就默认你已经了解 DataStore 了,那么它给你最大的痛点是什么?最让人难以接受的就是那相对难用的 Api 了,完全没有 ShredPreference 那么简单易用,那么接下来就一起看看大家常用的两种封装方案。
一、使用委托的方案
我们先定义一个基本的操作类,在定义一个持有类,通过接口委托实例的方案让每一个数据仓库都持有一个自己的 DataStore 对象。
基类的操作类:
我以 Kotlin 项目为例,转为 Flow 对象,在协程中调用与接收。如果你是Java项目可以转换为 RxJava 对象效果是类似的。
kotlin
/**
* 默认的 DataStore 操作类 (Core)
*/
open class DataStorePreference<V>(
private val dataStore: DataStore<Preferences>, //DataStore对象
val key: Preferences.Key<V>, //存储key
open val default: V? //存储的Value默认值
) {
//KV的存储实现,通过 implicit receiver 特性简化this,存入新的值
suspend fun put(block: suspend V?.(Preferences) -> V?): Preferences =
dataStore.edit { preferences ->
val value = block(preferences[key] ?: default, preferences)
if (value == null) {
preferences.remove(key)
} else {
preferences[key] = value
}
}
//设置KV的快速入口
suspend fun put(value: V?): Preferences = put { value }
//转换为Flow对象接收
fun asFlow(): Flow<V?> =
dataStore.data.map { it[key] ?: default }
//转换为LiveData对象接收
fun asLiveData(): LiveData<V?> = asFlow().asLiveData()
//获取通过Flow获取到存储的Value
suspend fun get(): V? = asFlow().first()
//移除Key
suspend fun remove(): Preferences = dataStore.edit { preferences ->
preferences.remove(key)
}
}
我们通过一个非单例的 DataStoreOwner 对象来执行具体的操作:
kotlin
// 定义接口,方便Kotlin委托实现
interface IDataStoreOwner {
val context: Context get() = application
val dataStore: DataStore<Preferences> //空实现
fun intPreference(default: Int? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Int>> =
PreferenceProperty(::intPreferencesKey, default)
fun doublePreference(default: Double? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Double>> =
PreferenceProperty(::doublePreferencesKey, default)
fun longPreference(default: Long? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Long>> =
PreferenceProperty(::longPreferencesKey, default)
fun floatPreference(default: Float? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Float>> =
PreferenceProperty(::floatPreferencesKey, default)
fun booleanPreference(default: Boolean? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Boolean>> =
PreferenceProperty(::booleanPreferencesKey, default)
fun stringPreference(default: String? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<String>> =
PreferenceProperty(::stringPreferencesKey, default)
fun stringSetPreference(default: Set<String>? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<Set<String>>> =
PreferenceProperty(::stringSetPreferencesKey, default)
fun byteArrayPreference(default: ByteArray? = null): ReadOnlyProperty<IDataStoreOwner, DataStorePreference<ByteArray>> =
PreferenceProperty(::byteArrayPreferencesKey, default)
//自定义类,封装 KV 对象,内部使用 DataStorePreference 具体操作 KV 的存取
class PreferenceProperty<V>(
private val key: (String) -> Preferences.Key<V>,
private val default: V? = null,
) : ReadOnlyProperty<IDataStoreOwner, DataStorePreference<V>> {
//做缓存,如果已经取出了,不需要重复取
private var cache: DataStorePreference<V>? = null
override fun getValue(owner: IDataStoreOwner, property: KProperty<*>): DataStorePreference<V> =
cache ?: DataStorePreference(owner.dataStore, key(property.name), default).also { cache = it }
}
companion object {
internal lateinit var application: Application
}
}
我们使用的时候就可以通过自定义委托,指定储存范围,例如 :
less
@Singleton
class AuthRepository @Inject constructor() : BaseRepository(), store by DataStoreOwner("auth") {
val userId by intPreference()
val userToken by stringPreference()
}
首先 AuthRepository 必须是全局单例的,其次用户的 id 和 token 必须通过 AuthRepository 这个数据仓库来取,如果你通过工具类直接在 Activity 中取是拿不到的,因为数据分区了,AuthRepository 中的 DataStore持有者在 "auth" 分区中。
这一点也是正是数据仓库的意义所在,当然如果你不想和业务数据混合,你可以单独按功能分为不同的数据仓库。
例如 AuthRepository 专门调用用户信息的接口等信息,再定义一个 UserDataStoreRepository 专门用于 DataStore 的数据仓库,当然我也更建议这么做。
kotlin
@Singleton
class AuthRepository @Inject constructor() : BaseRepository() {
suspend inline fun fetchUserProfile(): OkResult<UserProfile> {
return httpRequest {
ProfileRetrofit.apiService.fetchUserProfile()
}
}
}
@Singleton
class UserDataStoreRepository @Inject constructor() : DataStoreOwner("user") {
val userId by intPreference()
val userToken by stringPreference()
}
如果当前页面需要用到 User 的信息,那么在当前页面的 ViewModel 直接注入 UserDataStoreRepository 即可:
kotlin
@HiltViewModel
class LoginViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val userStoreRepository: UserDataStoreRepository,
) : BaseEISViewModel<AuthEffect, AuthIntent, AuthUiState>() {
//存取KV
private fun putPreference() {
viewModelScope.launch(Dispatchers.IO) {
userStoreRepository.userId.put(Random.nextInt(0, 11))
val languages = listOf("123", "456", "789", "456", "765")
userStoreRepository.userToken.put(languages.random())
}
}
private fun getPreference() {
viewModelScope.launch {
val idDeferred = async(Dispatchers.IO) {
userStoreRepository.userId.get() ?: 0
}
val tokenDeferred = async(Dispatchers.IO) {
userStoreRepository.userToken.get() ?: "-"
}
val (idResult, tokenResult) = awaitAll(idDeferred, tokenDeferred)
val message = "获取到KV的值,id:${idResult as Int} token${tokenResult as String}"
}
}
...
}
存取就可以直接用 get 和 put 即可,内部做了缓存处理。
二、使用工具类的方案
这么麻烦,还得专门搞个数据仓库去管理?我能不能像 ShredPreference 一样全局一个用法,哪里需要哪里用?管他 ViewModel,Activity Fragment 还是 View ,我想在哪用就在哪里用?
当然也可以,为了像 ShredPreference 一样简单的使用 DataStore ,我们也能使用一个单例工具类封装起来。
kotlin
//别名
typealias EasyStore = EasyDataStore
object EasyDataStore : DataStoreOwner() {
suspend fun <V> put(key: String, value: V) {
when (value) {
is Long -> putLongData(key, value)
is String -> putStringData(key, value)
is Int -> putIntData(key, value)
is Boolean -> putBooleanData(key, value)
is Float -> putFloatData(key, value)
is Double -> putDoubleData(key, value)
is ByteArray -> putByteArrayData(key, value)
is Set<*> -> {
val set = value as Set<*>
if (set.isEmpty() || set.first() is String) {
putStringSetData(key, set as Set<String>)
} else {
throw IllegalArgumentException("This type can't be saved into DataStore")
}
}
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
}
/**
* 取数据
*/
@Suppress("IMPLICIT_CAST_TO_ANY")
suspend fun <V> get(key: String, defaultValue: V): V {
val data = when (defaultValue) {
is Int -> getIntData(key, defaultValue)
is Long -> getLongData(key, defaultValue)
is String -> getStringData(key, defaultValue)
is Boolean -> getBooleanData(key, defaultValue)
is Float -> getFloatData(key, defaultValue)
is Double -> getDoubleData(key, defaultValue)
is ByteArray -> getByteArrayData(key, defaultValue)
is Set<*> -> {
val set = defaultValue as Set<*>
if (set.isEmpty() || set.first() is String) {
getStringSetData(key, set as Set<String>)
} else {
throw IllegalArgumentException("This value cannot be get form the Data Store")
}
}
else -> throw IllegalArgumentException("This value cannot be get form the Data Store")
}
return data as V
}
/**
* 移除数据
*/
suspend inline fun <reified V> remove(key: String) {
when (V::class) {
Int::class -> removeIntData(key)
Long::class -> removeLongData(key)
String::class -> removeStringData(key)
Boolean::class -> removeBooleanData(key)
Float::class -> removeFloatData(key)
Double::class -> removeDoubleData(key)
ByteArray::class -> removeByteArrayData(key)
Set::class -> removeStringSetData(key)
else -> throw IllegalArgumentException("Unsupported type")
}
}
/**
* 移除Int数据
*/
suspend fun removeIntData(key: String) {
DataStorePreference(dataStore, intPreferencesKey(key), null).remove()
}
/**
* 移除Long数据
*/
suspend fun removeLongData(key: String) {
DataStorePreference(dataStore, longPreferencesKey(key), null).remove()
}
/**
* 移除String数据
*/
suspend fun removeStringData(key: String) {
DataStorePreference(dataStore, stringPreferencesKey(key), null).remove()
}
/**
* 移除Boolean数据
*/
suspend fun removeBooleanData(key: String) {
DataStorePreference(dataStore, booleanPreferencesKey(key), null).remove()
}
/**
* 移除Float数据
*/
suspend fun removeFloatData(key: String) {
DataStorePreference(dataStore, floatPreferencesKey(key), null).remove()
}
/**
* 移除Double数据
*/
suspend fun removeDoubleData(key: String) {
DataStorePreference(dataStore, doublePreferencesKey(key), null).remove()
}
/**
* 移除ByteArray数据
*/
suspend fun removeByteArrayData(key: String) {
DataStorePreference(dataStore, byteArrayPreferencesKey(key), null).remove()
}
/**
* 移除StringSet数据
*/
suspend fun removeStringSetData(key: String) {
DataStorePreference(dataStore, stringSetPreferencesKey(key), null).remove()
}
/**
* 取出Int数据
*/
private suspend fun getIntData(key: String, default: Int = 0): Int =
DataStorePreference(dataStore, intPreferencesKey(key), default).get() ?: default
/**
* 取出Long数据
*/
private suspend fun getLongData(key: String, default: Long = 0L): Long =
DataStorePreference(dataStore, longPreferencesKey(key), default).get() ?: default
/**
* 取出String数据
*/
private suspend fun getStringData(key: String, default: String = ""): String =
DataStorePreference(dataStore, stringPreferencesKey(key), default).get() ?: default
/**
* 取出Boolean数据
*/
private suspend fun getBooleanData(key: String, default: Boolean = false): Boolean =
DataStorePreference(dataStore, booleanPreferencesKey(key), default).get() ?: default
/**
* 取出Float数据
*/
private suspend fun getFloatData(key: String, default: Float = 0F): Float =
DataStorePreference(dataStore, floatPreferencesKey(key), default).get() ?: default
/**
* 取出Double数据
*/
private suspend fun getDoubleData(key: String, default: Double = 0.0): Double =
DataStorePreference(dataStore, doublePreferencesKey(key), default).get() ?: default
/**
* 取出ByteArray数据
*/
private suspend fun getByteArrayData(key: String, default: ByteArray = ByteArray(0)): ByteArray =
DataStorePreference(dataStore, byteArrayPreferencesKey(key), default).get() ?: default
/**
* 取出StringSet数据
*/
private suspend fun getStringSetData(key: String, default: Set<String> = emptySet()): Set<String> =
DataStorePreference(dataStore, stringSetPreferencesKey(key), default).get() ?: default
/**
* 存放Int数据
*/
private suspend fun putIntData(key: String, value: Int) =
DataStorePreference(dataStore, intPreferencesKey(key), value).put(value)
/**
* 存放Long数据
*/
private suspend fun putLongData(key: String, value: Long) =
DataStorePreference(dataStore, longPreferencesKey(key), value).put(value)
/**
* 存放String数据
*/
private suspend fun putStringData(key: String, value: String) =
DataStorePreference(dataStore, stringPreferencesKey(key), value).put(value)
/**
* 存放Boolean数据
*/
private suspend fun putBooleanData(key: String, value: Boolean) =
DataStorePreference(dataStore, booleanPreferencesKey(key), value).put(value)
/**
* 存放Float数据
*/
private suspend fun putFloatData(key: String, value: Float) =
DataStorePreference(dataStore, floatPreferencesKey(key), value).put(value)
/**
* 存放Double数据
*/
private suspend fun putDoubleData(key: String, value: Double) =
DataStorePreference(dataStore, doublePreferencesKey(key), value).put(value)
/**
* 存放ByteArray数据
*/
private suspend fun putByteArrayData(key: String, value: ByteArray) =
DataStorePreference(dataStore, byteArrayPreferencesKey(key), value).put(value)
/**
* 存放Set<String>数据
*/
private suspend fun putStringSetData(key: String, value: Set<String>) =
DataStorePreference(dataStore, stringSetPreferencesKey(key), value).put(value)
/**
* 清空数据(异步)
*/
suspend fun clear() {
dataStore.edit {
it.clear()
}
}
}
我们和上面是一样的步骤,还是判断存入的数据类型,根据对于的key去存储对于的值,存取都是类似的逻辑,我们只是指定了全局统一的分区,然后使用单例类统一的处理存取删除等操作。
使用起来也简单:
javascript
//存
viewModelScope.launch(Dispatchers.IO) {
EasyDataStore.put("counter", Random.nextInt(0, 10))
val languages = listOf("zh", "en", "hk", "vn", "sg")
EasyDataStore.put("language", languages.random())
}
//取
val counterDeferred = async(Dispatchers.IO) {
EasyDataStore.get("counter", -1)
}
为什么每次存取都要协程?能不能去掉协程?可以是可以,但是用主线程同步的方法去存取...这...这不是它的特性吗,...要不,我还是觉得您比较适合 ShredPreference 工具吧。
我们能不能把第一个步骤的委托实现对象给这个工具类?
当然可以,此时这个存储区间就是全局的了哦:
less
@Singleton
class UserDataStoreRepository @Inject constructor() : store by EasyStore {
val userId by intPreference()
val userToken by stringPreference()
}
其他的就没变化,只是之前是指定了分区,现在是全局分区,此时在 UserDataStoreRepository 中就可以材质 userId 和 token,你同时也可以使用 EasyDataStore 工具类去存取这个值。
javascript
viewModelScope.launch(Dispatchers.IO) {
EasyDataStore.put("userId", 1)
EasyDataStore.put("userToken", "123456")
}
//取
val counterDeferred = async(Dispatchers.IO) {
EasyDataStore.get("userId", 0)
EasyDataStore.get("userToken", "")
}
注意这些区分即可。
总结
两种方案,我建议选用其中一种来完成即可,两种方案如果混用可能导致存储区域的不同拿不到预期的值。
例如如果使用接口委托分区存储的方案,你就不能用工具类全局存储,如果你使用接口委托全局存储的方案,就可以使用全局存储。但是会造成逻辑分散,权限下放,继任者或同事可能不太理解。
我个人建议如果想要收紧权限(或者说为了KPI?报表文档好看?),那就统一使用接口委托分区存储的方案,使用单独的数据仓库来管理,如果感觉权限无所谓直接用全局工具类就行了,也没啥毛病的。
本文源码在文中已经全部展出,最后惯例,如果有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码/注释有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
Ok,这一期就此完结。