DataStore使用介绍

1,707 阅读5分钟

什么是DataStore

官网中对它的描述

一个数据存储解决方案。使用Kotlin协程和Flow异步、一致和事务性地存储数据

并且很贴心的建议 「如果您目前使用SharedPreferences存储数据,请考虑迁移到DataStore」

从官网的描述,大致能够知道 DataStore干的事情和SharedPreferences差不多。都是对少量的数据进行存储。

为什么会有DataStore

在官网中,明确建议我们迁移到DataStore,而不是继续使用SharedPreferences

先放上一个官方的总结

功能SharedPreferencesPreferences DataStoreProto DataStore
异步✅(仅用于读取更改的值) 通过 listener✅ 通过 Flow✅ 通过 Flow
同步
安全调用UI线程
发出错误信号
不受运行时异常的影响
具有强一致性保证的事务性Api
处理数据迁移
类型安全

表格来源 Android 官网

通过上面的总结。可以看到。SharedPreferences相比于DataStore存在着不少的安全隐患。Google在Jetpack中,给笔者的感觉就是尽量将错误都给收到内部,从Kotlin的语法糖。到ViewBinding对于一些安全隐患尤为看中,对于开发者来说。也是件好事情。

Preferences DataStore如何使用

官网中,DataStore提供了两种不同的实现

  • Preferences DataStore使用键存储和访问数据。此实现不需要预定义的架构,并且不提供类型安全性。
  • Proto DataStore将数据存储为自定义数据类型的实例。此实现要求您使用协议缓冲区定义架构,但它提供类型安全性。

这一小节先说一下 Preferences DataStore 如何使用。

添加依赖

笔者针对 Preferences DataStore归纳总结其使用。首先肯定是添加依赖

// Preferences DataStore (SharedPreferences like APIs)
dependencies {
  implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
}
// 无Android环境下使用.
dependencies {
  implementation "androidx.datastore:datastore-preferences-core:1.0.0-alpha05"
}

Android开发者其实只需要添加上面一行即可。目前最新版本依然是alpha版。项目中还是慎用比较好。

创建DataStore

一样从官网中得知。先创建一个DataStore,使用的是Context的拓展函数createDataStore,其中name为必填项。

val dataStore: DataStore<Preferences> = context.createDataStore(
  name = "allens_form_ds"
)

和SharePreferences一样,name对应的一个文件的名称,如下图,可以看到其对应的文件。

添加属性

既然Datastore对标的是SharePreferences。我们先想一下SharePreferences如何添加属性的

 getSharedPreferences("allens_form_sp", Context.MODE_PRIVATE)
      .edit()
      .putString("name", "在雨季等你")
      .putInt("age", 22)
      .commit()

在看一下DataStore。

GlobalScope.launch {
	//获取DataStore 对标 getSharedPreferences
      val dataStore: DataStore<Preferences> = createDataStore(
          name = "allens_form_ds"
      )
      //创建key  
      val userName = preferencesKey<String>("name")
      val userAge = preferencesKey<Int>("age")
      dataStore
          .edit { value ->
              value[userName] = "在雨季等你"
              value[userAge] = 22
          }
  }

与SharePreferences区别

使用上几乎和SharePreferences一样。需要注意的点

  • key 需要使用preferencesKey或者preferencesSetKey去创建
  • edit是一个挂起函数,需要放在协程中去使用

与SharePreferences相同点

和SharePreferences相同的点。都只支持基本的数据类型,在preferencesKey方法中也能窥视

public inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {
    return when (T::class) {
        Int::class -> {
            Preferences.Key<T>(name)
        }
        String::class -> {
            Preferences.Key<T>(name)
        }
        Boolean::class -> {
            Preferences.Key<T>(name)
        }
        Float::class -> {
            Preferences.Key<T>(name)
        }
        Long::class -> {
            Preferences.Key<T>(name)
        }
        Double::class -> {
            Preferences.Key<T>(name)
        }
        Set::class -> {
            throw IllegalArgumentException("Use `preferencesSetKey` to create keys for Sets.")
        }
        else -> {
            throw IllegalArgumentException("Type not supported: ${T::class.java}")
        }
    }
}

获取属性

DataStore获取属性使用的是Flow流的方式,对其不了解的小伙伴可以先花了10分钟看一下官网对着一小节的示例,有个大致的印象点击查看异步流

dataStore.data方法返回就是一个Flow.可自行对Flow进行处理。通过key获取我们想获取的属性,或者对Flow进行map等其他的操作将保存的属性进行转换等。

  • 获取某一个属性
dataStore.data
	//可通过Flow collect 属性获取流在流中进行判断
    .collect {
    	//通过传入key去获取对应的value
        println("user name:${it[userName]}")
    }
  • 获取多个属性
dataStore.data
    .collect {
        println("user name:${it[userName]}")
        println("user age:${it[userAge]}")
    }

还可以这样使用Flow的onEach操作符去处理

dataStore.data
    .onEach {
        println("user name:${it[userName]}")
        println("user age:${it[userAge]}")
    }
    .collect()
  • 异常处理

异常控制是属于Flow控制流的东西。对于本文来说,有点拖沓,笔者这里也就简单带过。

dataStore.data
    .onEach {
        println("user name:${it[userName]}")
        println("user age:${it[userAge]}")
        //模拟异常
        throw NullPointerException()
    }
    //处理异常
    .catch { println("error $it") }
    //成功失败都走这里。类似于finly
    .onCompletion { println("complete") }
    .collect()

总之通过Flow可以做到很多事情,可以转换使用map等。依然建议先看一下Flow在上手DataStore

Proto DataStore使用

在使用Proto DataStore之前,还是有一个问题,既然有了Preferences DataStore还要他干嘛。

为什么要有Proto DataStore

其实在上面小节学习Preferences DataStore的时候,我们发现其只支持一些基本的数据类型,无法做到自定义类型的存储,Proto DataStore就是解决这个问题而诞生的。

配置依赖

Proto DataStore相比于Preferences DataStore,使用还是比较复杂的。下面笔者也将其配置过程整理如下。方便读者去使用和配置

  • 添加Protobuf插件

appbuild.gradle中配置插件。如果依赖了其他module,那么子module也需要完成下面配置.

plugins {
    id "com.google.protobuf" version "0.8.12"
}
  • 添加依赖
dependencies {
    //Android 只需要添加这一个就够了
    implementation  "androidx.datastore:datastore:1.0.0-alpha05"
    //Protobuf依赖
    implementation  "com.google.protobuf:protobuf-javalite:3.14.0"
}
  • 配置Protobuf

build.gradle中添加下列节点。注意是和android同一级节点

//配置Protobuf
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

创建Protobuf对象

app/src/main/proto下创建user_prefs.proto文件

//语言版本
syntax = "proto3";

//包名
option java_package = "com.allens.okdatastore";
option java_multiple_files = true;

//每一个新的类型用 message 表示
message UserPreferences {
  bool show_completed = 1;
  int32 age = 2;
  string name = 3;
  float price = 4;
}

完成以后一定要rebuild一下。编译器会帮我创建好可用的Java对象

有读者会发现,自己写的这个user_prefs.proto可读性非常差,这里也推荐Android Studio的一个插件

代码看起来不会那么难看。好歹是有个颜色~~

Proto语言

在上面定义的user_prefs.proto文件使用到的是Proto语言,对于Android开发来说,不需要掌握那么深。详细的信息可以查看Proto语言指南

Protocol Buffers (ProtocolBuffer/ protobuf )是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。同XML相比,Protocol buffers在序列化结构化数据方面有许多优点:

  • 更简单
  • 数据描述文件只需原来的1/10至1/3
  • 解析速度是原来的20倍至100倍
  • 减少了二义性
  • 生成了更容易在编程中使用的数据访问类

序列化

为了告诉DataStore如何读写在原始文件中定义的数据类型,我们需要实现一个Serializer。创建一个新的类UserPreferencesSerializer

注意了,如果在上面没有rebuild,可能会找不到UserPreferences这个类

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}

写入数据

val dataStore = createDataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

dataStore.updateData { preferences ->
    preferences.toBuilder()
        .setShowCompleted(false)
        .setAge(22)
        .setName("江海洋")
        .setPrice(10.5f)
        .build()
}

读取数据

val dataStore = createDataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

dataStore.data
    .collect {
        println("age:${it.age}")
        println("name:${it.name}")
        println("price:${it.price}")
        println("completed:${it.showCompleted}")
    }

SharedPreferences迁移

使用起来也非常简单

createDataStore(
  //迁移到DataStore的名字
  name = "user",
  migrations = listOf(
      SharedPreferencesMigration(
          context = this@MainActivity,
          //原来SharedPreferences的名字
          sharedPreferencesName = "user"
      )
  )
)

需要注意的是执行完成以后不是立马就能迁移成功的,需要执行一次读取或者写入操作才会生效。迁入成功后会删除原有的SharedPreferences

对于DataStore的一些看法

优点

首先一点。笔者非常认同DataStore的设计理念,「不在单纯像SharedPreferences一样去设置一个String类型的参数作为key」。而是强制使用preferencesKey将key与设置的类型强行进行关联可以发现。google一直在想着帮助开发者去减少因为错误类型而带来的crash。

其次引入Flow作为事件流去使用。能够帮助开发者更好的处理事件。像写RxJava那样爽。

最后呢,DataStore方法强制要求在协程中去使用。也能够避免阻塞线程做到了安全调用UI线程的功能

基于上诉3点,笔者在学习过程中认为其设计思想,以及对SharedPreferences的0成本迁移。确实可以做到替换SharedPreferences

缺点

咋也不能一个劲的夸她,毕竟到目前为止还是alpha。对比SharedPreferences可以说除了API相对来说难用一点,基本上完胜。那和MMKV相比呢?

熟悉MMKV的读者可能知道。(不熟悉的读者可以去看一下官网的介绍MMKV中文文档)。 MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强

DataStore在上面我们可看到了,本质上还是以文件的形式去存储。而且在使用自定义属性的时候。要去写Proto语言.也是比较麻烦的。使用MMKV只需要一行代码即可。这一点完败。更可恨的是MMKV也能够支持SharedPreferences迁移。

对比实际开发项目来说,笔者认为MMVK的优势确实要比DataStore要好。其API以及其mmap的方式更佳适合做少量数据存储。

实际中发现DataStore的问题(这里误认子弟了)

2021-1-22 更新

这里误认子弟了。这个设计是非常好的设计。是笔者自己的思想还停留在 SharedPreferences的层面。如同DataStore的作者已经在了第10层。我还在第2层。去评判这个库。下面是其作者回复。

数据存储。数据是一个不应该完成的流。它应该在每次数据存储中发生更改时发出。

本身Flow会监听数据的变化。而笔者误认为是一个错误。丢脸了。


笔者这边先写一个小示例

suspend fun main() {
    (1..3).asFlow()
        .onCompletion { println("完成") }
        .collect { println("$it") }
}

上面的代码执行,笔者相信大家都知道

1
2
3
完成

然后Flow流程应该是结束的。但是在1.0.0-alpha05版本的DataStore实际上发现Flow无法结束

🌰

笔者先在创建一个DataStore并且添加一个name属性.这里就不放代码了,然后去查询name,(笔者这里使用runBlocking测试使用。实际项目不要这么用)

viewBinding.linear.addView(createButton("Test") {
    runBlocking {
        val dataStore: DataStore<Preferences> = createDataStore(
            name = "test"
        )
        val key = preferencesKey<String>("name")
        dataStore.data
            .onCompletion { println("完成") }
            .collect { println("${it[key]}") }

    }
})

按照想法,应该是打印出name然后打印完成,和上面的123完成一样才对。可实际结果是,打印name以后协程一直挂起,也不会结束。 具体原因还不清楚。知道的小伙伴可以告知一下。

OKDataStore

上面都在介绍DataStore的使用。下面介绍一下笔者针对DataStroe的一些小封装。笔者发现。DataStore最初的设计是将返回类型与key强项绑定。防止发生错误。初衷是好的,但是却牺牲了便捷性。即便是使用MMKV,只需要一个String类型的key即可去保存属性,笔者认为,如果只是简单的数据类型保存。不需要那么可以去使用,反而有点画蛇添足。

保存数据

OKDataStore就仿造SharedPreferences去添加和保存数据,用法如下。

runBlocking {
    createOkDataStore("user")
        .edit()
        .putString("name", "江海洋")
        .putInt("age", 21)
        .putBoolean("isBody", true)
        .putFloat("size", 20f)
        .putLong("long", 100L)
        .putStringSet("setData", setOf("a", "b", "c", "d"))
        .commit()

}

对于自定义的类型,笔者这边依然保留了Proto DataStore的使用方式。考虑到实际项目中,真要有一些自定义的属性,可完全可以使用json的形式去保存String.所以也对这一块不太感冒

runBlocking {
    val dataStore = createOkDataStore(
        fileName = "user_prefs.pb",
        serializer = UserPreferencesSerializer
    )

    dataStore.updateData { preferences ->
        preferences.toBuilder()
            .setShowCompleted(false)
            .setAge(22)
            .setName("江海洋")
            .setPrice(10.5f)
            .build()
    }
}

获取数据

获取数据,笔者这边考虑到其实一般来说想获取的数据不会那么复杂,但是又有需求可能需要设置默认的选项,毕竟SharedPreferencesMMKV都是可以这么设置的。这么设计的好处不言而喻,故此,在OKDataStore中也加入了默认选项。并且可以去获取指定类型。依然保留的Flow,笔者认为,Flowkotlin中还是比较好用的。

runBlocking {
    createOkDataStore("user")
    	.getInt("age", 10000)
        .collect {
            println("data:$it")
        }
}

如果是错误类型可以使用catch操作符去处理,例如

runBlocking {
    createOkDataStore("user")
    	//故意写错 这样会出现类型错误
    	.getInt("name", 10000)
        .catch {
            println("获取name error:${it.message}")
        }
        .collect {
            println("data:$it")
        }
}

也可以直接使用Flow的特性自行处理所有结果,其效果和datastore.data是一样的。读者可以自行去处理,不过依然有上面小节说展示的问题Flow无法结束

runBlocking {
    createOkDataStore("user")
    	.flow()
        .collect {
            println("data:$it")
        }
}

获取自定义类型,依然沿用了DataStore的方式

runBlocking {
    val dataStore = createOkDataStore(
        fileName = "user_prefs.pb",
        serializer = UserPreferencesSerializer
    )
    dataStore.flow()
        .collect {
            println("age:${it.age}")
            println("name:${it.name}")
            println("price:${it.price}")
            println("completed:${it.showCompleted}")
        }
}

为了解决DataStore无法结束Flow的问题。笔者这边自定义了一个Throwable去中断Flow,当然这个选项也是可以关闭的,默认是开启的,代码如下。只需要将cancel设置成false即可关闭,读者需要自定处理Flow带来的问题。至于为什么会这样。笔者这里也不是很清楚。BUG? 还是设计如此? 有知道的小伙伴可以告知!

fun Context.createOkDataStore(
    name: String,
    cancel: Boolean = true,
    migrations: List<DataMigration<Preferences>> = listOf()
): OKDataStore {
    return OKDataStoreImpl(name = name, context = this, cancel = cancel, migrations = migrations)
}

设计思路

笔者发现,DataStore设计中,其实只有一个事件流,然后返回一个Preferences。在Preferences通过key在获取保存的参数,笔者这边将这个事件流通过flatMapConcat操作符去变成多个事件流。然后再去处理对应的流。

private fun <T> getValueFormKey(key: String, default: T): Flow<T> {
    return dataStore.data
        .map { it.asMap() }
        .flatMapConcat { requestFlow(it, key, default) }
        .cancellable()
        .map { it.second as T }
}

但是毕竟坑的就是flatMapConcat操作符,处于预览状态。唉,天知道后续会咋样。

最后放上项目源码,大家可以看一下。OKDataStore