Jetpack Proto DataStore

1,910 阅读5分钟

Google Jetpack 新出的DataStore用来代替SharePreferences的使用,DataStore有两种实现方式,一种是Preferences DataStore,一种是Proto DataStore,下面文章内容主要介绍第二种Proto DataStore的使用。
第一种Jetpack Preferences DataStore使用详解地址
Github的项目地址

一:添加依赖

在app的build.grale里添加如下依赖

implementation('androidx.datastore:datastore:1.0.0-alpha04') {
  exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
  exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}
implementation('androidx.datastore:datastore-core:1.0.0-alpha04') {
     exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}

因为会报因为会报Duplicate class kotlinx.coroutines.AbstractCoroutine found in modules jetified-kotlinx-coroutines-core-jvm-1.3.9.jar 所以上面的 需要
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'

二:下载插件,并添加proto需要的配置

要支持.proto结尾的文件,需要先下载插件,另外gradle需要是5.6以上,必须是java8。
配置插件参考地址1
配置插件参考地址2

1:在project的build.gradle里添加 classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.14',并添加上仓库mavenCentral()

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.14'
  }
}

2:在app的build.gradle里添加 apply plugin: 'com.google.protobuf'

apply plugin: 'com.google.protobuf'

3: 在app的build.gradle里添加 proto的文件位置

// 设置 proto 文件位置
android{
    sourceSets {
        main {
            proto {
                // proto 文件默认路径是 src/main/proto
                // 可以通过 srcDir 修改 proto 文件的位置
                srcDir 'src/main/proto'
            }
        }
    }
}

4: 使用3.8.0之后的配置方式,在app的build.gradle里添加如下代码

/**
 * 注意配置 protoc 命令,分为不同的版本,版本不同配置的方式不同,
 * 网上大部分都是 3.0.x ~ 3.7.x 的配置方式,关于这种配置方法,可以查看 [protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin)  文档,这里不在演示了,也不建议使用
 * 在此项目中使用的是 3.8 以后 的配置的配置,
 */
protobuf {
    // 设置 protoc 的版本
    protoc {
        // //从仓库下载 protoc 这里的版本号需要与依赖 com.google.protobuf:protobuf-javalite:xxx 版本相同
        artifact = 'com.google.protobuf:protoc:3.10.0'
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option "lite"
                }
            }
        }
    }

    // 默认生成目录 $buildDir/generated/source/proto 通过 generatedFilesBaseDir 改变生成位置
   //  generatedFilesBaseDir = "$projectDir/src/main"
}

dependencies {
  // You need to depend on the lite runtime library, not protobuf-java
  implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
}

三:定义一个协议 并在app/src/main/proto/ 文件夹下创建一个.proto的协议文件

在app/src/main/ 下新建一个proto文件夹。并且创建一个persons.proto的文件,文件内容如下:

syntax = "proto3";

option java_package = "com.example.jepcaktestapp.datastore.proto";
option java_outer_classname = "PersonProtos";

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

下面对文件的内容简单做个解释,后序会对proto3语法做个简单介绍:

  • syntax 声明proto的版本,比如 这里syntax = "proto3";
  • option java_package 定义生成的类的包名
  • option java_outer_classname 定义生成的类的类名
  • message 声明的是内部类, message里面的格式是 字段类型+字段名称+字段编号
  • 类型以Java的对应关系:string->String, int32->int , int64->long, bool->Boolean,float->float,double->double

四:编译

执行Rebuild Project,每次 proto 文件内容变更的时候,都要重新点击 Build -> Rebuild Project 即可生成对应的 Java 文件
该Java 文件(上面proto文件我们定义的PersonProtos),该文件的 默认生成目录 app/build/generated/source/proto/包名/类名
可以通过 generatedFilesBaseDir 改变此文件的生成位置

五:使用

1:创建

// 指定名字
private val PROTO_NAME = "proto_datastore.pb"
var dataStore: DataStore<PersonProtos.Person> = context.createDataStore(
    fileName = PROTO_NAME,
    serializer = PersonSerializer
)

object PersonSerializer :Serializer<PersonProtos.Person>{

    override val defaultValue: PersonProtos.Person
        get() = PersonProtos.Person.getDefaultInstance()

    override fun readFrom(input: InputStream): PersonProtos.Person {
        try {
            return PersonProtos.Person.parseFrom(input)
        }catch (e:IOException){
            throw CorruptionException("Cannot read proto.", e)
        }
    }

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

2:存储

// 拿count属性来说
suspend fun putCount(count:Int) {
 dataStore.updateData { it.toBuilder().setCount(count).build() }
}

3:获取

// 拿count属性来说
suspend fun getCount():Int =  dataStore.data.map { it.count }.first()

4:关键代码

class ProtoDataStoreImpl(val context: Application){

    // 指定名字
    private val PROTO_NAME = "proto_datastore.pb"

    var dataStore: DataStore<PersonProtos.Person> = context.createDataStore(
        fileName = PROTO_NAME,
        serializer = PersonSerializer
    )

    private val shardPrefsMigration = SharedPreferencesMigration<PersonProtos.Person>(context, SpUtils.SHARE_PREFERENCES_NAME) {
            sharedPreferencesView, person ->
            // 获取 SharedPreferences 的数据
            val count = sharedPreferencesView.getInt(SpKeys.KEY_COUNT,0)
            // 存储到dataStore中
            person.toBuilder().setCount(count).setName(name).setFlag(flag).setPrice(price).setTime(time).build()
        }

    suspend fun putCount(count:Int) {
        dataStore.updateData { it.toBuilder().setCount(count).build() }
    }
    suspend fun getCount():Int =  dataStore.data.map { it.count }.first()

    fun spToDataStore(){
        dataStore = context.createDataStore(
            fileName = PROTO_NAME,
            serializer = PersonSerializer,
            migrations = listOf(shardPrefsMigration)
        )
    }
}

object StoreFactory {
    fun provideProtoDataStore(context: Application):ProtoDataStoreImpl{
        return ProtoDataStoreImpl(context)
    }
}

object PersonSerializer :Serializer<PersonProtos.Person>{

    override val defaultValue: PersonProtos.Person
        get() = PersonProtos.Person.getDefaultInstance()

    override fun readFrom(input: InputStream): PersonProtos.Person {
        try {
            return PersonProtos.Person.parseFrom(input)
        }catch (e:IOException){
            throw CorruptionException("Cannot read proto.", e)
        }
    }

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


class MainActivity : AppCompatActivity() ,View.OnClickListener ,CoroutineScope by MainScope(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tv_proto_data_store.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v){
            tv_preferences_data_store->{
              launch (Dispatchers.Main){
                  // 设置
                  StoreFactory.providePreferencesDataStore(application).putInt(PreferencesKeys.KEY_COUNT,1)
                  // 获取
                  val count = StoreFactory.providePreferencesDataStore(application).getInt(PreferencesKeys.KEY_COUNT)
              }
            }
        }        
    }
    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }
}

5:proto datastore生成的位置

我们可以使用adb shell
run-as 包名
ls
查看到files文件夹
proto datastore的文件就存放在files的datastore文件夹里
文件名字就是我们定义的proto_datastore.pb

六:SP转Proto DataStore

主要是获取Sp的值,在一一对应设置到Proto里,关键代码如下:

private val shardPrefsMigration = SharedPreferencesMigration<PersonProtos.Person>(context, SpUtils.SHARE_PREFERENCES_NAME) {
            sharedPreferencesView, person ->
            // 获取 SharedPreferences 的数据
            val count = sharedPreferencesView.getInt(SpKeys.KEY_COUNT,0)
            person.toBuilder().setCount(count).build()
        }
        
var dataStore: DataStore<PersonProtos.Person> = context.createDataStore(
            fileName = PROTO_NAME,
            serializer = PersonSerializer,
            migrations = listOf(shardPrefsMigration)
) 

七:Pro DataStore跟 Preferences DataStore 有何区别?

  • Preference DataStore 主要是为了解决 SharedPreferences 所带来的性能问题
  • Proto DataStore 比 Preference DataStore 更加灵活,支持更多的类型
  • Preference DataStore 支持 Int 、 Long 、 Boolean 、 Float 、 String,Double,Set
  • protocol buffers 支持的类型,Proto DataStore 都支持
  • Preference DataStore 以 XML 的形式存储 key-value 数据,可读性很好
  • Proto DataStore 使用了二进制编码压缩,体积更小,速度比 XML 更快

八:proto3语法简介

例子,定义一个文件如下:

syntax = "proto3";

option java_package = "com.example.jepcaktestapp.datastore.proto";
option java_outer_classname = "PersonProtos";

message Teacher{
   string grade = 1;
}

message Person {
    // 格式:字段类型 + 字段名称 + 字段编号
    string name = 1;
    int32 count = 2;
    bool flag = 3;
    float price = 4;
    int64 time = 5;
    double money = 6;
    Teacher teacher = 7;
    enum Weekday{
        SUN = 0;
        MON = 1;
        TUE = 2;
        WED = 3;
        THU = 4;
        FRI = 5;
        SAT = 6;
    }
    Weekday weekday = 8;
    repeated string course = 9;
}
  • syntax :指定 protobuf 的版本,如果没有指定默认使用 proto2
  • option :表示一个可选字段
    • java_package 是 指定生成 java 类所在的包名
    • java_outer_classname : 指定生成 java 类的名字
  • 在一个 proto 文件中,可以定义多个 message 如果messageA里有messageB,表示B是A的内部类
  • message里内容格式 字段类型 + 字段名称 + 字段编号,比如 string name = 1; string是类型,name是名称,等号右边1表示编号

字段类型

proto typejava type默认值
doubledouble0
floatfloat0
int32int0
int64long0
boolbooleanfalse
stringString""
repeatedList空集合

也可以用其他的消息类型,作为类型,例子如下:

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

message Teacher{
    ......
}

一个proto文件里面可以有多个message,message之间也可以嵌套。例如:

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

这些message会被编译成静态内部类。
message下也可以用枚举,而且枚举的第一个值必须是0。(是为了兼容proto2的语法)

message Person {
 enum Weekday{
        SUN = 0;
        MON = 1;
        TUE = 2;
        WED = 3;
        THU = 4;
        FRI = 5;
        SAT = 6;
  }
  Weekday weekday = 1;
}