简介
- 首先,DataStore是Jetpack一部分,是一种数据存储解决方案。
- 其次,DataStore使用协程及flow以异步、一致的方式实现数据的存储。
- 最后是DataStore的实现,分为Preferences DataStore和Proto DataStore:
- Preferences DataStore 类似于SharedPreferences,键值对存储,本篇的主要介绍。
- Proto DataStore 将数据作为自定义数据类型的实例进行存储,基于Google protobuf实现。
DataStore和SharedPreferences(SP)
- SP存在的问题:
- 内存浪费问题,加载的数据会一直存在在内存中。
- get方法可能会阻塞主线程。
- apply方法虽然为异步,也可能会发生ANR。
- SP也无法保证类型安全。
- SP不支持跨进程。
- DataStore的特点:
- 基于协程和Flow实现,保证了主线程的安全性。
- 以事务的方式进行处理,保证了操作的原子性、一致性、隔离性及持久性。
- Preferences DataStore可以支持SP的迁移,保证数据的完整性。
- 说明:
- 此处只是列出SP存在的问题,并没有SP一无是处的的说法,DataStore在SP的基础上解决了不少的问题,但是也没有说SP存在的问题已经全部解决了。
- 至少,DataStore在SP的基础上解决了如类型安全、阻塞导致anr等问题,确实这方面优于SP。
使用
Preferences DataStore
基本使用流程
引入
def dataStoreVersion = '1.0.0-beta01'
implementation "androidx.datastore:datastore-preferences:$dataStoreVersion"
创建DataStore
//指定DataStore的文件名
//对应最终件:/data/data/org.geekbang.aac/files/datastore/user_preferences.preferences_pb
private const val USER_PREFERENCES_NAME = "user_preferences"
//扩展属性DataStore,实际类型为DataStore<Preferences>
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME,//指定名称
produceMigrations = {context -> //指定要恢复的sp文件,无需恢复可不写
listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
}
)
定义Key
val SORT_ORDER = stringPreferencesKey("sort_order")
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
//... 通过查看源码可以看到支持的其它数据类型
存储
//edit要在suspend函数中
override suspend fun updateShowCompleted(showCompleted: Boolean) {
dataStore.edit { preferences ->
//...这里可以做一些数据的逻辑处理
preferences[SHOW_COMPLETED] = showCompleted
// 整个tranform中的所有代码块被视为单个事务
}
}
读取
override val userPreferencesFlow = dataStore.data
.catch { exception ->
if (exception is IOException) {//进行IO异常处理,确保能得到默认值
Log.e(TAG, "Error reading preferences.", exception)
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
//真正的获取存储的一个字段
val sortOrder = SortOrder.valueOf(preferences[SORT_ORDER] ?: SortOrder.NONE.name)
val showCompleted = preferences[SHOW_COMPLETED] ?: false
UserPreferences(showCompleted, sortOrder)
}
使用总结
一个对应的preferences_pb文件对应一个.kt文件,里面包含了文件名定义,DataStore定义,Key定义,存取方法定义;例如:
//TaskConfigDataStore.kt
/**
* 文件名
*/
private const val TASK_CONFIG_PREFERENCES_FILE_NAME = "task_config_pre"
/**
* dataStore对象
*/
val Context.taskConfigDataStore : DataStore<Preferences> by preferencesDataStore(
name = TASK_CONFIG_PREFERENCES_FILE_NAME
)
/** Keys **/
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
val OPEN_COUNT = intPreferencesKey("open_count")
//other keys
/** 存取方法 **/
fun getShowCompleted(context: Context): Flow<Boolean>
= context.taskConfigDataStore.data
.catch { e->
if(e is IOException){
emptyPreferences()
}else{
throw e
}
}.map { pre->
pre[SHOW_COMPLETED] ?: false
}
suspend fun setShowCompleted(context: Context,showComplete: Boolean){
context.taskConfigDataStore.edit { pre->
pre[SHOW_COMPLETED] = showComplete
}
}
// other method
ProtoBuf DataStore
基本使用流程
接入protobuf,以最新的为准 详情信息可参考
protobuf-gradle-plugin
,想详细了解protobuf基础知识,可参考
Protobuf 终极教程
- 在xxx.build中加入:
plugins {
//other...
id "com.google.protobuf" version "0.8.16"
}
- dependencies
// protobuf
def protobufVersion = "3.10.0"
// 3.0.0后Android建议使用javalite
implementation "com.google.protobuf:protobuf-javalite:$protobufVersion"
- 增加protobuf 的块
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
- 在src/main/目录下建立proto文件,3.8.0以后自动识别此目录下的.proto文件
引入dataStore库
// dataStore
def dataStoreVersion = '1.0.0-beta01'
implementation "androidx.datastore:datastore:$dataStoreVersion"
建立proto文件后,进行rebuild
syntax = "proto3";
option java_package = "org.geekbang.aac";
option java_multiple_files = true;
message UserPreferences {
bool show_completed = 1;
enum SortOrder {
UNSPECIFIED = 0;
NONE = 1;
BY_DEADLINE = 2;
BY_PRIORITY = 3;
BY_DEADLINE_AND_PRIORITY = 4;
}
SortOrder sort_order = 2;
}
创建Serializer的实现,告诉框架如何读写,这个接口明确规定要有默认值,以便在尚未创建任何文件时使用,这是必要流程,基本是固定写法,用编译器生成的Java类对应api即可
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
定义创建DataStore对象
//老的sp的文件名
private const val USER_PREFERENCES_NAME = "user_preferences"
//新的文件名,对应目录 /data/data/com.codelab.android.datastore/files/datastore/user_prefs.pb
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
//老的对应的key
private const val SORT_ORDER_KEY = "sort_order"
// Build the DataStore
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = UserPreferencesSerializer,
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
USER_PREFERENCES_NAME
) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
// 定义从SharedPreferences到UserPreference的映射
if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
currentData.toBuilder().setSortOrder(
SortOrder.valueOf(
sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
)
).build()
} else {
currentData
}
}
)
}
)
存储
//必须是挂起函数,决定其要在协程中使用
suspend fun updateShowCompleted(completed: Boolean) {
//Proto DataStore 提供了一个updateData() 函数,
//用于以事务方式更新存储的对象
//为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据
userPreferencesStore.updateData { currentPreferences ->//当前文件对应的对象
currentPreferences.toBuilder().setShowCompleted(completed).build()//对当前对象进行修改
}
}
读取
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading > data
if (exception is IOException) {
Log.e(TAG, "Error reading sort order preferences.", exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
//单独获取时是阻塞的,在实际使用中建议是异步的,在Kotlin项目中可以使用协程异步实现
suspend fun getUserPreferencesFlowData() = userPreferencesFlow.first()
使用总结
这个是面向相对复杂的对象结构(例如用户信息的本地缓存)的场景下使用,一般以一个proto文件为单位,相关定义,方法做好整体分类即可。
以上就是JetPack内容中的DataStore原理与使用;更多jetpack的技术或者Android知识进阶可以参考前往传送直达↓↓↓ :link.juejin.cn/?target=htt…这个文档里面查找获取。
DataStore的优势
- DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
- 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
- 没有 apply() 和 commit() 等等数据持久的方法
- 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
- 可以监听到操作成功或者失败结果