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

相关推荐
雨白2 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹3 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空5 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭5 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日6 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安6 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑6 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟11 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡12 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0012 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体