【Android】还在用SharedPreferences?DataStorePreference 的两种封装方式了解一下

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,这一期就此完结。

相关推荐
诸神黄昏EX1 小时前
Android 分区相关介绍
android
大白要努力!2 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-5 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen8 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年15 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿17 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神18 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛19 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee