JetPack—DataStore核心原理与使用

507 阅读3分钟

简介

  • 首先,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,保证数据一致性,不会造成数据损坏
  • 可以监听到操作成功或者失败结果