Jetpack组件之DataStore

Jetpack组件之DataStore

Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager
Android Jetpack组件(九)DataStore
Android Jetpack组件(十)App Startup
Android Jetpack组件(十一)Compose

疫情距离我最近的一次,隔离的第10天,居家办公的第8天,希望疫情早点过去,结束隔离✊。

首语

数据持久化指将哪些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,数据依然不会丢失。

Android系统中主要提供了三种方式来实现数据持久化功能。即文件存储、SharedPreferences存储及数据库存储。其中SharedPreferences是使用键值对的方式来存储轻量型数据,使用比较简单,且程序卸载后也会一并清除,不会残留数据。但是SharedPreferences也存在很多缺点,它是对磁盘进行I/O操作,会引起性能问题,导致ANR,且多线程场景下效率低下、存储延迟,存储较大数据如json或html会频繁引起GC,导致界面卡顿,曾经在项目开发中使用SharedPreferences碰到数据缓存延迟的情况,后面就使用了腾讯的MMKV

现在,Google推出DataStore,旨在代替SharedPreferences,克服其大部分缺点。

Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。

对比

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

  • Preferences DataStore 由类 DataStore 和 Preferences 实现,使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 将数据作为自定义数据类型的实例序列化存储在磁盘。此实现要求您使用协议缓冲区(Protocol Buffers)来定义架构,但可以确保类型安全。

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

  • 更简单。

  • 数据描述文件只需原来的1/10至1/3。

  • 解析速度是原来的20倍至100倍。

  • 减少了二义性。

  • 生成了更容易在编程中使用的数据访问类。

下图是Google对SharedPreferences与DataStore两种不同实现的对比。
DataStore对比

依赖

// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0"

使用

在两种实现中,除非另外特指,否则 DataStore 会将首选项存储在文件中,并且所有的数据操作都会在 Dispatchers.IO 上执行。

Preferences DataStore

创建

使用由PreferencesDataStore创建的属性委托来创建 Datastore<Preferences> 实例。在 kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore 保留为单例。

val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
读取

由于 Preferences DataStore 不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。。然后,使用 DataStore.data 属性读取内容。

val USER_INFO = intPreferencesKey("user_info")
dataStore.data.collect {
	val i = it.toPreferences()[USER_INFO]
    tvContent.text=i.toString()
}
写入

Preferences DataStore 提供了一个edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务,查看源码可知。

 dataStore.edit { settings ->
            val currentCounterValue = settings[USER_INFO] ?: 0
            settings[USER_INFO] = currentCounterValue + 1
        }

Proto DataStore

Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化的对象保留在磁盘上。

导入
  1. 导入plugins 插件。在app的build.gradle中添加如下代码。
plugins {
    id "com.android.application"
    id "kotlin-android"
    id "com.google.protobuf" version "0.8.12"
}
  1. 添加依赖。
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
  1. 配置 protoc 命令,与dependencies同级。
protobuf {
    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"
}
  1. 设置proto文件位置。
android {
    sourceSets {
        main {
            proto {
                // proto 文件默认路径是 src/main/proto
                // 可以通过 srcDir 修改 proto 文件的位置
                srcDir 'src/main/proto'
            }
        }
    }
}
  1. 编译项目。

在app/src/main目录下新建一个文件夹proto,然后在文件夹proto下新建一个.proto类型的文件UserPrefs,编写proto文件及其字段,重新构建项目。

// 固定的,还有proto2
syntax="proto3";

// 格式:包名 + . + 文件名
option java_package = "com.yhj.kotlincomponent.protobuf";
//可以生成单独的.java每个生成的类的文件
option java_multiple_files = true;

message Settings {
  int32 count = 1;
}

这里推荐安装 Protocol Buffer Editor插件,它为协议缓冲区文件提供编辑器支持。语法高亮、编辑器增强功能等有点,调试起来非常方便。

对于proto3语法,使用技巧,参考Google proto3教程,讲解详细。

构建完成后,就可以看到app\src\main\debug目录下生成了protobuf文件目录,里面包含Settings、SettingsOrBuilder、UserPrefs文件。

创建

定义一个实现 Serializer<T> 的类,其中 T 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。

使用由 dataStore 创建的属性委托来创建 DataStore<T> 的实例,其中 T 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore 上面中定义的序列化器类的名称。

object SettingsSerializer : Serializer<Settings> {
    override val defaultValue: Settings = Settings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

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

val Context.settingsDataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)
写入

Proto DataStore 提供了一个updateData() 函数,用于以事务方式更新存储的对象。updateData() 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。

 settingsDataStore.updateData { currentSettings ->
                currentSettings.toBuilder()
                    .setCount(currentSettings.count + 1)
                    .build()
            }
读取

使用 DataStore.data来获取存储的数据。

settingsDataStore.data.collect {
    Log.e("yhj", it.count.toString(), )
}

迁移SharedPreferences

在创建DataStore时,preferencesDataStore参数里包含produceMigrations参数,用来迁移SharedPreferences,需要执行一次读取或者写入操作,DataStore才会自动合并,迁移成功后会删除原有的SharedPreferences文件,Proto DataStore 用法相同。

val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", produceMigrations = { con ->
        listOf(SharedPreferencesMigration(con, "app_cache"))
    })

原理

使用由PreferencesDataStore创建的属性委托来创建 Datastore<Preferences> 实例。

override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                val applicationContext = thisRef.applicationContext

                INSTANCE = PreferenceDataStoreFactory.create(
                    corruptionHandler = corruptionHandler,
                    migrations = produceMigrations(applicationContext),
                    scope = scope
                ) {
                    applicationContext.preferencesDataStoreFile(name)
                }
            }
            INSTANCE!!
        }
    }

创建了一个文件用于将键值对写入磁盘,文件位于applicationContext.filesDir+datastore/的子目录中。文件后缀名为.preferences_pb,

public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile("$name.preferences_pb")

public fun Context.dataStoreFile(fileName: String): File =
    File(applicationContext.filesDir, "datastore/$fileName")

数据的读取直接通过dataStore.data获取,数据的写入通过dataStore.edit,实际上也是通过dataStore.updateData来写入的

public interface DataStore<T> {
   
    public val data: Flow<T>
    
    public suspend fun updateData(transform: suspend (t: T) -> T): T
}
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) }
    }
}

总结

DataStore的两种实现相比而言,Preferences DataStore相对简单一些,Proto DataStore比较复杂些。

DataStore克服了SharedPreference的许多缺点,Google也大力推荐,所以是时候跟SharedPreference说再见了,拥抱 Jetpack DataStore。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.yanghujun.com/archives/datastore

Buy me a cup of coffee ☕.