Jetpack Preferences DataStore 入门

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 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 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类型的键,必须使用以下方法:

val EXAMPLE_COUNTER_SET = stringSetPreferencesKey("example_counter_set")

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

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

fun booleanPreferencesKey() = ReadOnlyProperty> { _, property -> booleanPreferencesKey(property.name) }

fun stringPreferencesKey() = ReadOnlyProperty> { _, property -> stringPreferencesKey(property.name) }

fun intPreferencesKey() = ReadOnlyProperty> { _, property -> intPreferencesKey(property.name) }

fun longPreferencesKey() = ReadOnlyProperty> { _, property -> longPreferencesKey(property.name) }

fun floatPreferencesKey() = ReadOnlyProperty> { _, property -> floatPreferencesKey(property.name) }

fun doublePreferencesKey() = ReadOnlyProperty> { _, property -> doublePreferencesKey(property.name) }

fun stringSetPreferencesKey() = ReadOnlyProperty>> { _, property -> stringSetPreferencesKey(property.name) }

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

val example_counter by intPreferencesKey()

4.3 数据读取

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

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

dataStore.data本质上返回的是一个Flow对象,此处的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 = 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, scope: CoroutineScope) : PreferenceDataStore() {
    private val prefScope = CoroutineScope(scope.coroutineContext + SupervisorJob() + Dispatchers.IO)

    private val dsData = dataStore.data.shareIn(prefScope, SharingStarted.Eagerly, 1)

    private fun  putData(key: Preferences.Key, value: T?) {
        prefScope.launch {
            dataStore.edit {
                if (value != null) it[key] = value else it.remove(key)
            }
        }
    }

    private fun  readNullableData(key: Preferences.Key, defValue: T?): T? {
        return runBlocking(prefScope.coroutineContext) {
            dsData.map {
                it[key] ?: defValue
            }.firstOrNull()
        }
    }

    private fun  readNonNullData(key: Preferences.Key, 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?) = 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?): Set? = 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 入门)