/DataStoreDemo

Jetpack DataStore使用

Primary LanguageKotlin

DataStore

DataStore 是 Android Jetpack 的一部分。Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和流程(Flow)以异步、一致的事务方式存储数据。官方建议如果当前在使用 SharedPreferences 存储数据,请考虑迁移到 DataStore。

DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。

  • Preferences DataStore 以键值对的形式存储在本地和 SharedPreferences 类似,此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。

Preferences DataStore 使用方式

先导入依赖

dependencies {
  // Preferences DataStore (SharedPreferences like APIs)  
  implementation "androidx.datastore:datastore-preferences:1.0.0-alpha06"
  // Typed DataStore (Typed API surface, such as Proto)
  implementation "androidx.datastore:datastore-core:1.0.0-alpha06"
}  

Preferences DataStore 的使用方式如下

//1.构建 DataStore
val dataStore: DataStore<Preferences> = context.createDataStore(name = PREFERENCE_NAME)

//2.Preferences DataStore 以键值对的形式存在本地,需要定义一个 key(比如:KEY_JACKIE)
//Preferences DataStore 中的 key 是 Preferences.Key<T> 类型
val KEY_JACKIE = stringPreferencesKey("username")
GlobalScope.launch {
    //3.存储数据
    dataStore.edit {
        it[KEY_JACKIE] = "jackie"
    }
    //4.获取数据
    val getName = dataStore.data.map {
        it[KEY_JACKIE]
    }.collect{ //flow 调用collect 开始消费数据
        Log.i(TAG, "onCreate: $it")  //打印出 jackie
    }
}

需要注意的是读取、写入数据都要在协程中进行,因为 DataStore 是基于 Flow 实现的。也可以看到没有 commit/apply() 方法,同时可以监听到操作成功或者失败结果。

Preferences DataStore 只支持 Int , Long , Boolean , Float , String 键值对数据,适合存储简单、小型的数据,并且不支持局部更新,如果修改了其中一个值,整个文件内容将会被重新序列化。

SharedPreferences 迁移到 Preferences DataStore

接下来我们来看看 SharedPreferences 迁移到 DataStore,在构建 DataStore 的时候传入 SharedPreferencesMigration,当 DataStore 构建完了之后,需要执行一次读取或者写入操作,即可完成迁移,迁移成功后,会自动删除 SharedPreferences 文件

val dataStoreFromPref = this.createDataStore(name = PREFERENCE_NAME_PREF ,migrations = listOf(SharedPreferencesMigration(this,OLD_PREF_NANE)))

我们原本的 SharedPreferences 数据如下

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="name">lsm</string>
    <boolean name="male" value="false" />
    <int name="age" value="30" />
    <float name="height" value="175.0" />
</map>

原本文件目录如下:

迁移后的文件目录如下:

可以看到迁移后原本的 SharedPreferences 被删除了,同时也可以看到 DataStore 的文件更小一些,在迁移的过程中发现一个有趣的情况,如果直接迁移后并不进行任意值的读取,在对应的目录上找不到迁移后的文件,只有当进行任意值的读取后,才会在对应的目录上找到文件。完整代码如下:

val dataStoreFromPref = this.createDataStore(name = PREFERENCE_NAME_PREF
                    , migrations = listOf(SharedPreferencesMigration(this, OLD_PREF_NANE)))
//迁移后需要手动读取一次,才可以找到迁移的文件            
val KEY_NAME = stringPreferencesKey("name")
GlobalScope.launch { 
    dataStoreFromPref.data.map { 
        it[KEY_NAME]
    }.collect {
        Log.i(TAG, "onCreate: ===============$it")
    }
}

下面我们继续来看 Proto DataStore,Proto DataStore 比 Preference DataStore 更加灵活,支持更多的类型

  • Preference DataStore 只支持 Int 、 Long 、 Boolean 、 Float 、 String,而 protocol buffers 支持的类型,Proto DataStore 都支持
  • Proto DataStore 使用了二进制编码压缩,体积更小,速度比 XML 更快

Proto DataStore 使用方式

因为 Proto DataStore 是存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地。

数据序列化常用的方式有 JSON、Protocol Buffers、FlatBuffers。Protocol Buffers 简称 Protobuf,共两个版本 proto2 和 proto3,大多数项目使用的 proto2,两者语法不一致,proto3 简化了 proto2 的语法,提高了开发效率。Proto DataStore 对着两者都支持,我们这里使用 proto 3。

新建Person.proto文件,添加一下内容:

syntax = "proto3";

option java_package = "com.hi.dhl.datastore.protobuf";
option java_outer_classname = "PersonProtos";

message Person {
    // 格式:字段类型 + 字段名称 + 字段编号
    string name = 1;
}

syntax :指定 protobuf 的版本,如果没有指定默认使用 proto2,必须是.proto文件的除空行和注释内容之外的第一行

option :表示一个可选字段

message 中包含了一个 string 类型的字段(name)。注意 := 号后面都跟着一个字段编号

每个字段由三部分组成:字段类型 + 字段名称 + 字段编号,在 Java 中每个字段会被编译成 Java 对象。

这些是简单的语法介绍,然后进行 Build 就可以看到生成的文件。

然后我们再来看具体的使用方式:

//1.构建Proto DataStore
val protoDataStore: DataStore<PersonProtos.Person> = this
    .createDataStore(fileName = "protos_file",serializer = PersonSerializer)

GlobalScope.launch(Dispatchers.IO) {
    protoDataStore.updateData { person ->
        //2.写入数据
        person.toBuilder().setName("jackie").build()
    }

    //3.读取数据
    protoDataStore.data.collect {
        Log.i(TAG, "onCreate: ============"+it.name)
    }

}

PersonSerializer 类实现如下:

object PersonSerializer: Serializer<PersonProtos.Person> {
    override val defaultValue: PersonProtos.Person
        get() {
            return PersonProtos.Person.getDefaultInstance()
        }

    override fun readFrom(input: InputStream): PersonProtos.Person {
        try {
            return PersonProtos.Person.parseFrom(input) // 是编译器自动生成的,用于读取并解析 input 的消息
        } catch (exception: Exception) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: PersonProtos.Person, output: OutputStream) =
        t.writeTo(output) // t.writeTo(output) 是编译器自动生成的,用于写入序列化消息
}

读取和写入也是都在协程当中,创建的文件在该目录下:

SharedPreferences 迁移到 Proto DataStore

接下来我们来看看 SharedPreferences 如何迁移到 Proto DataStore 当中

//1.创建映射关系
val sharedPrefsMigration =
    androidx.datastore.migrations.SharedPreferencesMigration<PersonProtos.Person>(this,OLD_PREF_NANE){
        sharedPreferencesView,person ->

        //获取SharedPreferences 数据
        val follow = sharedPreferencesView.getString(NAME,"")
        //写入数据,也就是说将数据映射到对应的类的属性中
        person.toBuilder().setName(follow).build()
    }
//2.构建 Protos DataStore 并传入 sharedPrefsMigration
val protoDataStoreFromPref = this.createDataStore(fileName = "protoDataStoreFile"
    ,serializer = PersonSerializer,migrations = listOf(sharedPrefsMigration))

GlobalScope.launch(Dispatchers.IO) {
    protoDataStoreFromPref.data.map {
        it.name
    }.collect{

    }
}

可以看到迁移首先需要创建映射关系,然后构建 Protos DataStore 并传入 sharedPrefsMigration,最后迁移完的 SharedPreferences 会被删除,就算你只迁移了一个数据,整个SharedPreferences 也会被删除,所以迁移是一定要把所有需要的数据都搬过去。最后是迁移后的目录

SharedPreferences vs DataStore 功能对比

应用场景

  • Preferences DataStore 以键值对的形式存储在本地和 SharedPreferences 类似,存取一些简单的字段等。
  • Proto DataStore 将数据作为自定义数据类型的实例进行存储。可以存取一些复杂的对象,适合保存一些重要对象的保存。