DataStore的基础用法

Posted microhex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DataStore的基础用法相关的知识,希望对你有一定的参考价值。

文章目录

0. 简介

Google在推出JetPack组件以来,一直推荐我们使用DataStore组件替代到我们第一天学android就知道的SharedPreferences组件,原因很简单,因为当年的SharedPreferences存在居多的问题,DataStore就是为了解决这些问题而来的。

1. SP的缺点

至于 SP到底存在哪些问题,我们可以直接查看 DataStore源码上的注释:

  1. Synchronous API encourages StrictMode violations
  2. apply() and commit() have no mechanism of signalling errors
  3. apply() will block the UI thread on fsync()
  4. Not durable – it can returns state that is not yet persisted
  5. No consistency or transactional semantics
  6. Throws runtime exception on parsing errors
  7. Exposes mutable references to its internal state

用我们蹩脚的英语逐字逐句的翻译一遍:

  1. 同步的API鼓励违反StrictMode模式
  2. apply()和commit()方法没有错误信号机制
  3. apply()方法将界面重绘时会在阻塞UI线程
  4. 不耐用 - 它可以返回状态,但是并不能将状态持久化
  5. 没有一致性或者事物语义
  6. 解析出现错误时,直接抛出运行时异常
  7. 在其内部的状态中,暴露其可变的引用

老外写的问题直译过来一般都比较难懂,除非讲的很简单清楚。那么我就说两句人话,大概说一下我所认为的 SP所存在的问题吧:

  1. 不支持跨进程,使用 MODE_MULTI_PROCESS模式也没鸟用。而且在跨进程中,频繁的读写可能导致数据损坏或者丢失;
  2. 懒加载模式下读取SP文件,可能会导致 getXXX() 阻塞。所以建议提前异步初始化 SP;
  3. sp文件的中的数据全部都保存在内存中,所以 SP对大数据量少儿不宜
  4. edit()方法每次都会新建一个EditorImpl对象。建议一次edit(),多次putXXX;
  5. 无论是 commit()还是 apply(),针对任何修改都是全量写入。这种情况下,对于高频的修改配置项存放在单独的SP文件中;
  6. commit()同步保存,有返回值;apply()异步保存,无返回值。
  7. onPause() onReceive()方法中使用异步写操作执行完成,可能会造成卡顿或者ANR。

当然这里并不是把SP贬得一无是处啊,正所谓存在即合理,当我们不涉及到跨进程,并且存储数据量比较少的情况下,SP还是相当不错的选择。

2. DataStore的基础用法

首先需要声明的一点是,DataStore存在两个版本的,一种是类似于SP,基于普通文件的读写;一种是基于Google protobuf模式的,这里的 protobufGoogle自研的一种数据结构,平时用到的也比较少,我以前博客里面也写过类似的,这里就先只介绍第一种基于文件的逻辑:

下面介绍一下 DataStore的基础用法:

首先需要引入:

 implementation("androidx.datastore:datastore:1.0.0")

首先需要明确一点,既然我们的DataStore是兼容当前使用的SP的,那么它就应该支持SP的存储类型,而且我们也知道SP支持的数据类型为Int,Long,Float,Boolean,StringStringSet;此时DataStore不仅支持以上六种数据结构,还支持一种额外的Double类型。

创建一个DataStore对象:

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "dataStore_data")

首先我们需要读取一个Int型的对象:

val keyName = intPreferencesKey("example_counter")
val keyNameFlow: Flow<Int> = context.dataStore.data
  .map  preferences -> preferences[keyName] ?: 0


然后我们写入一个 Int型的对象:

val keyName = intPreferencesKey("example_counter")

suspend fun incrementCounter() 
  context.dataStore.edit  settings ->
    val currentCounterValue = settings[keyName] ?: 0
    settings[keyName] = currentCounterValue + 1
  

第一眼看上去很懵逼,写的什么玩意儿啊。没错,我学 DataStore的第一天也是这么想法,SP比这玩意儿香一万倍都不止啊,这么多新的东西我不知道,而且感觉写起来也是很拉跨,写一个简单的存储这么多代码。

首先,的确一个新的知识点出来,大家内心肯定是抗拒的,因为要去理解和实践,这个本身就比较耗时间和精力。但是大家都一样,所以还是需要去迎接变化。

首先,我们按照简单的来,我们都知道SP是基于XML文件的Key-Value结构,那么 DataStore作为它的兼容类,也必然兼容这种Key-Value结构。那么DataStoreKeyString型的么?然而并不是,它是一种Preferences.Key类型,具体类型为:androidx.datastore.preferences.core.Preferences.Key, 可以分为以下几种类型:

  1. intPreferencesKey -> Preferences.Key<Int> 保存Int类型数据
  2. doublePreferencesKey -> Preferences.Key<Double> 保存Double类型数据
  3. stringPreferencesKey -> Preferences.Key<String> 保存String类型数据
  4. booleanPreferencesKey ->Preferences.Key<Boolean> 保存Boolean类型数据
  5. floatPreferencesKey -> Preferences.Key<Float> 保存Float类型数据
  6. longPreferencesKey -> Preferences.Key<Long> 保存Long类型数据
  7. stringSetPreferencesKey -> Preferences.Key<Set<String>> 保存Set<String>类型数据

有了Key之后,我们需要看看DataStore如果存储和读取数据的。

a. DataStore怎么写

SPEditor,同理DataStore也有edit方法:

public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences 
    return this.updateData 
        // It's safe to return MutablePreferences since we freeze it in
        // PreferencesDataStore.updateData()
        it.toMutablePreferences().apply  transform(this) 
    


首先它是一个suspend函数,只能在协程体中运行,每当遇到 suspend函数以挂起的方式运行时,并不会阻塞主线程运行。

既然是suspend函数,那么我们就可以有同步异步的方式对数据进行写入:

同步方式:

private suspend fun saveSyncIntData(key : String, value:Int) 
    globalDataStore.edit  mutablePreferences -> mutablePreferences[intPreferencesKey(key)] = value 


异步方式:
这个就很简单了,可以随意发挥了,在同步方法上套一个runBlocking就行了:

private fun saveIntData(key: String, value:Int) = runBlocking  saveSyncIntData(key,value) 

b. DataStore怎么读

按照以上的惯例,肯定也会存在同步读取异步读取的两种方法。首先需要明确一点,DataStoredata返回的是Flow类型,Flow是一种流式接口,类似于RxJava中的 Observable那样,存在很多操作符可以对数据进行变换,时间允许的情况下,可以写一篇关于Flow文章。

首先我们获取到同步的读

private fun readSyncIntData(key: String, defaultValue: Int) : Flow<Int> = dataStore.data.catch 

        if(it is IOException) 
            it.printStackTrace()
            emit(emptyPreferences())
         else 
            throw it
        

    .map  it[intPreferencesKey(key)] ?: defaultValue 

对代码进行解读一下, dataStore.data返回类型是Flow类型,对Flow进行catch检查是否存在异常,然后map转换一下,然后得到Flow<Int>,最终并返回。

写完了同步的读,那么异步的读为:

private fun readIntData(key: String, defaultValue : Int) : Int 
    var resultValue = defaultValue

    runBlocking 
        dataStore.data.first 
            resultValue = it[intPreferencesKey(key)] ?: resultValue
            true
        
    

    return resultValue


异步的读取直接返回了具体类型的数据,这里的first操作符是取第一个的意思。

基本上,我们对DataStore的操作有了一个简单的了解,重要的还是自己去实践,不难也不算容易。

3. 从SP迁移到DataStore

大概也就分两个步骤:

  1. 需要一个 SharedPreferencesMigration,这个迁移类并不算难,也就需要你传入ContextSP的文件名即可:
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = "dataStore_setting",
    produceMigrations =  context ->
        listOf( SharedPreferencesMigration(context, "sp_name"))
    )

  1. dataStore生成完成之后,需要执行一个读或者写的操作,SharedPreferences的数据将会被迁移到dataStore中,同时SharedPreferences文件也将会被删除。
  • 使用SP的文件夹

  • 迁移到DataStore的文件夹

    可以看到SP文件被删除了,然后dataStore的文件目录为/data/data/package_name/files/xxx.preferences_pb

4. DataStore的封装类

为了方便操作,我这边封装了DataStore的逻辑,读写起来会更方便一点,方法部分代码为:

我们使用时也很简单,直接代码为:

DataStoreUtils.putData("int_value",100)
DataStoreUtils.putData("long_value",100L)
DataStoreUtils.putData("float_value",100.0f)
DataStoreUtils.putData("double_value",100.00)
DataStoreUtils.putData("boolean_value",true)
DataStoreUtils.putData("string_value","hello world")

val intValue = DataStoreUtils.getData("int_value", 0)
val longValue = DataStoreUtils.getData("long_value", 0L)
val floatValue = DataStoreUtils.getData("float_value", 0.0f)
val doubleValue = DataStoreUtils.getData("double_value", 0.00)
val booleanValue = DataStoreUtils.getData("boolean_value", false)
val stringValue = DataStoreUtils.getData("string_value", "hello")

当然,这只是异步的读取/存储方式,当然我们还有同步的获取方式:

lifecycle.coroutineScope.launch 

  //  读取
  DataStoreUtils.getSyncData("int_value",0).collect(object : FlowCollector<Int> 
      override suspend fun emit(value: Int) 
                   Log.d("TAG","get sync data : $value")
               
           )
           
  // 写入
  DataStoreUtils.putSyncData("int_value", 1)
  
  

当然,具体的源码可以看这个了.

5. 个人结论

总体来说,DataStore如果高度封装,其实使用方式上和SP基本上没什么区别,它解决了SP所存在的诟病,但是就目前而言,对它的性能还是未知的,这个可能需要后续的线上检验了,当然谷爹出品的东西,应该没什么太大的问题。当然了,学习DataStore其实对Kotlin还是有很高的门槛的,其中协程高阶函数Flow等相关知识点还是存在一个相当陡峭的学习坡度的。

以上是关于DataStore的基础用法的主要内容,如果未能解决你的问题,请参考以下文章

DataStore的基础用法

DataStore的基础用法

Android Jetpack系列之DataStore

Google Datastore 插入/更新查询中如何使用长 ID?

在 Google Cloud Datastore 上使用动态类型

以增量方式将数据从 GCP Datastore 移动到 BigQuery 的最佳做法