美文网首页Android数据存储JetPack
Jetpack Preferences DataStore 入门

Jetpack Preferences DataStore 入门

作者: XFY9326 | 来源:发表于2020-12-09 15:58 被阅读0次

    1. 简介

    DataStore是Google Android Jetpack组件新推出的数据存储解决方案,其主要优点如下:

    • 允许使用Protocol-Buffers存储键值对或类型化对象
    • 使用Kotlin协程和Flow来异步、一致和事务性地存储数据

    DataStore并不被建议用来存储大量复杂的数据,并且无法局部的更新数据,如果有类似的需求可以使用Room组件替代。

    由于使用了Kotlin协程和Flow相关的知识,所以建议在使用之前先在Kotlin协程与Flow官方文档进行了解。(注:英语不好的可以翻译或者搜索相关中文教程)

    2. Preferences DataStore 与 Proto DataStore

    可以这样简单的理解两者的区别:
    Preferences DataStore与SharedPreferences类似,通过键值对存储数据,不保证类型安全。
    Proto DataStore通过Protocol-Buffers定义存储数据类型以及结构,保证类型安全。

    注:本文只介绍Preferences DataStore的使用方式,因为这足够满足多数情况下的使用了。如果想要进一步了解Proto DataStore,建议前往DataStore官方教程Protocol-Buffers官方教程查看最新文档。

    3. 依赖导入(按需导入)

    DataStore API更新动态与最新版本查询

    dependencies {
        // Typed DataStore (Proto DataStore)
        implementation "androidx.datastore:datastore:1.0.0"
        // Typed DataStore (没有Android依赖项,包含仅适用于 Kotlin 的核心 API)
        implementation "androidx.datastore:datastore-core:1.0.0"
        // 可选 - RxJava2 支持
        implementation "androidx.datastore:datastore-rxjava2:1.0.0"
        // 可选 - RxJava3 支持
        implementation "androidx.datastore:datastore-rxjava3:1.0.0"
    
        // Preferences DataStore(可以直接使用)
        implementation "androidx.datastore:datastore-preferences:1.0.0"
        // Preferences DataStore (没有Android依赖项,包含仅适用于 Kotlin 的核心 API)
        implementation "androidx.datastore:datastore-preferences-core:1.0.0"
        // 可选 - RxJava2 支持
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
        // 可选 - RxJava3 支持
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
    }
    

    注1:2021.1.15 自alpha06开始修改了Preference.Key的API,本文已更新
    注2:2021.2.24 自alpha07开始废弃了Context.createDataStore的API,本文已更新
    注3:2021.8.4 DataStore 1.0.0 release

    4. Preferences DataStore 入门

    4.1 初始化DataStore

    官方示例(创建名称为settings的DataStore):

    val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
    

    根据官方注释的说明,该操作用于创建SingleProcessDataStore的实例,用户负责确保一次操作一个文件的SingleProcessDataStore的实例永远不会超过一个。
    如果使用RxJava的话需要使用RxPreferenceDataStoreBuilder替代
    因此为了防止出错,方便管理,个人建议使用单例模式进行DataStore实例的管理,但是由于需要使用Context对象才能够实例化,所以可以通过使用Application的静态context变量的方式实现。
    因为DataStore必须使用by委托的方式创建,所以在非Context类下创建较为麻烦,因此最好使用Application的静态Context方式作为媒介创建DataStore。

    // App.kt
    class App : Application() {
        companion object {
            lateinit var instance: App
        }
    
        override fun onCreate() {
            super.onCreate()
            instance = this
        }
    }
    
    // SettingsDataStore.kt
    object {
        // 创建DataStore
        private val App.dataStore: DataStore<Preferences> by createDataStore(
            name = "settings"
        )
        // 对外开放的DataStore变量
        val dataStore = App.instance.dataStore
    }
    

    创建的DataStore存储文件将会被放置在 "/data/data/{包名}/files/datastore/{DataStore名称}.preferences_pb"

    4.2 键(Key)创建

    官方示例(创建名为example_counter的Int类型的键):

    val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
    

    通过preferencesKey可以创建的数据类型为:Int,String,Boolean,Float,Long,Double。
    如果想要创建Set<T>类型的键,必须使用以下方法:

    val EXAMPLE_COUNTER_SET = stringSetPreferencesKey("example_counter_set")
    

    通过preferencesSetKey可以创建的数据类型目前仅支持String。

    如果希望能够将变量名作为键名,可以使用如下方法建立委托方法:

    fun booleanPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Boolean>> { _, property -> booleanPreferencesKey(property.name) }
    
    fun stringPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<String>> { _, property -> stringPreferencesKey(property.name) }
    
    fun intPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Int>> { _, property -> intPreferencesKey(property.name) }
    
    fun longPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Long>> { _, property -> longPreferencesKey(property.name) }
    
    fun floatPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Float>> { _, property -> floatPreferencesKey(property.name) }
    
    fun doublePreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Double>> { _, property -> doublePreferencesKey(property.name) }
    
    fun stringSetPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Set<String>>> { _, property -> stringSetPreferencesKey(property.name) }
    

    这样就可以通过以下方式实现键的创建:

    val example_counter by intPreferencesKey()
    

    4.3 数据读取

    官方示例(读取EXAMPLE_COUNTER键的值,若为null即不存在,则使用0作为默认值):

    val exampleCounterFlow: Flow<Int> = dataStore.data.map { preferences ->
        // 无类型安全
        preferences[EXAMPLE_COUNTER] ?: 0
    }
    

    dataStore.data本质上返回的是一个Flow<Preference>对象,此处的Preference仅能够进行读取操作,接着通过Flow提供的map方法转换接下来传递的数据。
    如果想要一次性读取多个数据,或者读取数据为一个data class,可以采用如下方式:

    data class Example(val value_1: Int, val value_2: String?)
    
    val key_1 = intPreferencesKey("key_1")
    val key_2 = stringPreferencesKey("key_2")
    
    val exampleFlow: Flow<Example> = dataStore.data.map { preferences ->
        Example(preferences[key_1] ?: 0, preferences[key_2])
    }
    

    DataStore会使用内存缓存的方式加快同一数据二次读取速度,因此多数情况下并不需要手动设置缓存相关的代码。
    通过Flow API,实际读取到数据可以主要通过以下两种方式:

    // 需要在协程函数内部或suspend函数下运行,仅读取一次最新数据
    exampleFlow.first()
    
    // 需要在协程函数内部或suspend函数下运行,会监听数据变化并返回最新数据
    exampleFlow.collect { data ->
        println(data)
    }
    

    4.4 数据修改

    官方示例(对EXAMPLE_COUNTER键的值进行从0开始的自增):

    suspend fun incrementCounter() {
        dataStore.edit { settings ->
            val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
            settings[EXAMPLE_COUNTER] = currentCounterValue + 1
        }
    }
    

    DataStore提供的edit()方法可以将传入的操作视作单个事务进行修改,因此满足了数据的一致和事务性。
    示例的lambda函数中传入的settings为MutablePreferences对象,提供了数据的读取与修改操作。
    对于数据较大的批量的修改,建议可以合并到一个事务内进行以提高IO效率。

    4.5 异步

    由于DataStore使用了Kotlin提供的Flow作为数据获取的方式,因此满足的IO操作异步的需求。但是并非所有的IO操作都可以立即迁移为异步执行,所以官方文档中指出可以使用以下方法临时解决问题:

    // 普通的堵塞方式读取数据,可能会导致死锁,最好别用
    val exampleData = runBlocking { dataStore.data.first() }
    
    // 在LifeCycle提供的协程方法中读取数据
    override fun onCreate(savedInstanceState: Bundle?) {
        lifecycleScope.launch {
            dataStore.data.first()
            // 可以在此处理 IOException
        }
    }
    

    5. 从SharedPreferences中迁移

    示例代码:

    val dataStore = context.preferenceDataStore(
        name = "{DataStore名称}",
        migrations = listOf(SharedPreferencesMigration(context, "{SharedPreferences名称}"))
    )
    

    默认情况下完成迁移后将会删除原始SharedPreferences的xml文件,可以通过参数调整。
    注:此处的SharedPreferencesMigration并非该类的原始构造方法,而是androidx.datastore.preferences包下的kotlin函数。

    6. 在PreferenceFragmentCompat中使用DataStore

    Google目前已经在PreferenceFragment上提供可以使用其他数据源的兼容性接口,首先手动实现基于DataStore的抽象类androidx.preference.PreferenceDataStore,然后在PreferenceFragmentCompat中获取PreferenceManager,最后通过PreferenceManager的以下方法就可以将默认的SharedPreference存储方式替换为DataStore了。

     public void setPreferenceDataStore(PreferenceDataStore dataStore)
    

    注:虽然抽象类名字为PreferenceDataStore,但是本身与DataStore并没有关系

    以下为笔者实现的PreferenceDataStore

    open class DataStorePreferenceAdapter(private val dataStore: DataStore<Preferences>, scope: CoroutineScope) : PreferenceDataStore() {
        private val prefScope = CoroutineScope(scope.coroutineContext + SupervisorJob() + Dispatchers.IO)
    
        private val dsData = dataStore.data.shareIn(prefScope, SharingStarted.Eagerly, 1)
    
        private fun <T> putData(key: Preferences.Key<T>, value: T?) {
            prefScope.launch {
                dataStore.edit {
                    if (value != null) it[key] = value else it.remove(key)
                }
            }
        }
    
        private fun <T> readNullableData(key: Preferences.Key<T>, defValue: T?): T? {
            return runBlocking(prefScope.coroutineContext) {
                dsData.map {
                    it[key] ?: defValue
                }.firstOrNull()
            }
        }
    
        private fun <T> readNonNullData(key: Preferences.Key<T>, defValue: T): T {
            return runBlocking(prefScope.coroutineContext) {
                dsData.map {
                    it[key] ?: defValue
                }.first()
            }
        }
    
        override fun putString(key: String, value: String?) = putData(stringPreferencesKey(key), value)
    
        override fun putStringSet(key: String, values: Set<String>?) = putData(stringSetPreferencesKey(key), values)
    
        override fun putInt(key: String, value: Int) = putData(intPreferencesKey(key), value)
    
        override fun putLong(key: String, value: Long) = putData(longPreferencesKey(key), value)
    
        override fun putFloat(key: String, value: Float) = putData(floatPreferencesKey(key), value)
    
        override fun putBoolean(key: String, value: Boolean) = putData(booleanPreferencesKey(key), value)
    
    
        override fun getString(key: String, defValue: String?): String? = readNullableData(stringPreferencesKey(key), defValue)
    
        override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? = readNullableData(stringSetPreferencesKey(key), defValues)
    
        override fun getInt(key: String, defValue: Int): Int = readNonNullData(intPreferencesKey(key), defValue)
    
        override fun getLong(key: String, defValue: Long): Long = readNonNullData(longPreferencesKey(key), defValue)
    
        override fun getFloat(key: String, defValue: Float): Float = readNonNullData(floatPreferencesKey(key), defValue)
    
        override fun getBoolean(key: String, defValue: Boolean): Boolean = readNonNullData(booleanPreferencesKey(key), defValue)
    }
    

    7. 总结

    相比于漏洞百出到就连Google都不想修复的SharedPreferences,DataStore确实提供了一套简单可用的异步数据存储方案,不管是Kotlin协程还是Flow,都极大程度的提高了使用的体验。
    与腾讯已经开源并稳定使用的MMKV相比,使用官方组件最大的好处就是与其他组件的相互兼容性,并且如果已经使用了Kotlin协程库,使用DataStore可以减少App的体积。
    目前DataStore已经release,总体使用效果还是不错的。不过如果项目大都是静态存储的数据(不需要观察数据更新)或者没有任何多进程等同步的需求,那么也没必要马上迁移到DataStore中。不过DataStore依然存在的一个问题就是无法直观的看到与修改已经存放的数据,这需要Android Studio后续的更新支持。


    Made By XFY9326

    相关文章

      网友评论

        本文标题:Jetpack Preferences DataStore 入门

        本文链接:https://www.haomeiwen.com/subject/acrhgktx.html