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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容