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
如果想要一次性读取多个数据,或者读取数据为一个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