前言
很久没写东西了,主要是 Compose 更新变化太快,官方文档更新都跟不上代码,写相关的内容马上就会过时。
但是在 Compose Demo 项目中不是只有 UI,数据层面逐步增加了 Room 和 DataStore,DataStore 已经迎来 beta 版本,想提前了解一下的话,现在正是时候。
DataStore 概述
DataStore 的目标是成为 SharedPreference 的上位替代,用于少量 key-value 数据的存储,存储方式是本地文件。这听起来和 SharedPreference 并没有本质区别,事实上也确实如此,因为二者的区别在 API 的封装上。SharedPreference 的实现相当简单,存取值得时候通过类型消除和类型强转保证 xml 的文件内容和 Java 的数据类型匹配,无法保证安全性。在读写数据时主要提供同步的 API,在 UI 线程读取较大内容的时候会有明显卡顿,而异步的 apply 写入又难以确认写入完成的时间。
DataStore 从设计之初就想避免上述问题,也因此变得相对复杂,提高了使用门槛,要不要换恐怕还得从业务角度考虑,现在仅做了解即可。
首先,DataStore 提供了两种不同数据结构的实现,分别是 Preferences 实现和 Proto 实现。其中 Preferences 的实现较为简单,不需要提前定义数据类型,也不能保证类型安全,这方面跟 SharedPreference 一致;Proto 实现则是使用 protobuf 预定义要保存的数据,可以保证类型正确,代价是代码更加复杂。
其次,DataStore 深度绑定 Kotlin 协程,读取数据和写入数据的函数都是 suspend 函数,读数据的返回值直接采用了 Flow,统一了单次获取和持续监听。
使用 DataStore
DataStore 的两种实现对应着不同的依赖,我觉得 Preferences 实现像是一种向易用性妥协的方案,真的想要简单好用不如还是用 SharedPreference,所以要学就一步到位,用 Proto 实现。
Proto 实现的使用方式分为以下几步:
- 引入依赖
- 编写 .proto 定义数据格式
- 编译 .proto(手动调用或者配置 gradle 插件自动生成)
- 编写数据类接入代码
- 读写数据
比起 SharedPreference 确实多了很多步骤,但流程还算清晰,真正的坑都潜伏在细节中。
一、引入依赖
implementation("androidx.datastore:datastore:1.0.0-beta02") // 截止到20210626的最新版
复制代码
二、protobuf
protobuf 的全称是 Protocal Buffers,详细信息参考官网:developers.google.com/protocol-bu…
protobuf 已经有很多应用场景了,最广为人知的应该是 gRPC。比起 xml 或者 json,protobuf 更节省空间,速度更快。
具体的语法不在本文范围内,按需学习即可。举个例子,我用来保存用户个人配置的一个 .proto 文件内容如下:
syntax = "proto3";
option java_package = "com.github.moqigit.timecount.datastore";
option java_multiple_files = true;
message UserPreferences {
int64 lastSessionTime = 1;
bool anti_anxious = 2;
bool check_update = 3;
bool cloud_storage = 4;
string primary_color = 5;
string light_color = 6;
string dark_color = 7;
}
复制代码
protobuf 的使用方法是先定义数据结构,再编译成指定语言供开发者使用。(暂不支持 Kotlin,我们用 Java 即可)
按照官方文档安装后,我们可以在终端用 protoc 命令编译 .proto 文件
Usage: protoc [OPTION] PROTO_FILES
三、kts + protobuf 插件
每次改动都敲命令或者执行脚本编译 proto 也是个比较麻烦的事,程序员应该向自动化努力。gradle 构建系统已经有 protobuf 的插件,”简单”添加一下就可以解放双手。
简单的前提,是别用 kts。
因为用 Kotlin 编写 gradle 脚本实际上还是比较冷门的,毕竟 groovy 的 DSL 相当简洁了,完全可以满足日常开发工作的需求。不过用都用了,坑是自选的,只能建议各位现阶段不要太勇,别给自己找麻烦。
过程主要是查文档,看 kts 中的函数和 groovy 的区别,逐步解决构建中的错误,不赘述了,直接看结果:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("com.google.protobuf").version("0.8.12")
}
dependencies {
implementation("androidx.datastore:datastore:1.0.0-beta02")
implementation("com.google.protobuf:protobuf-javalite:3.10.0")
}
// protobuf 插件的配置函数跟 groovy 写法上有一定的区别,千万别直接复制
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
generateProtoTasks {
all().configureEach {
builtins {
id("java") { option("lite") }
}
}
}
}
复制代码
.proto 文件放在 app/src/main/proto 文件夹中可以省略配置 android sourceSets,make 成功后可以在 generated java 文件夹下找到生成的代码,大概是这样
四、接入数据类
文档参考:developer.android.com/topic/libra…
接入数据类是一个增加模板代码的过程,分成两步,最终目的是提供一个 DataStore 对象的获取方式。
- 实现
Serializer<T>
,定义默认值 - 通过委托创建
DataStore<T>
对象供外部调用
代码如下:
object UserPreferenceSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (e: Exception) {
throw CorruptionException("Cannot read proto.", e)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
val Context.userPrefDataStore : DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb", // 保存文件名
serializer = UserPreferenceSerializer
)
复制代码
Serializer 的代码几乎是固定的,创建 DataStore 的参数也只是多需要一个文件名,如果能通过带参数的注解之类的方式自动生成对应代码就好了,希望正式版能有优化。
fileName 对应的文件保存在 /data/data/com.github.moqigit.timecount/files/datastore 里面,内容不是文本所以不能直接查看,安全性比 SharedPreference 高。protobuf 非常省空间,上面的7个键值对的测试数据只需要 40B 的空间,另一个用 SharedPreference 存的2个键值对的文件就要 188B。
五、读写 API
读写 API 就相对简单了,会协程就没什么问题。
读:
private fun printData() {
viewModelScope.launch {
val pref = this@MainActivity.userPrefDataStore.data.firstOrNull() ?: return@launch
Log.e("asdfg", "${pref.lastSessionTime}")
Log.e("asdfg", "${pref.toString()}")
}
}
复制代码
打印数据只需要一次读取,所以取 first 即可,如果需要监听数据变化,则需要改用 collect。
写:
private fun writeTestData() {
viewModelScope.launch {
pref.updateData {
it.toBuilder()
.setAntiAnxious(true)
.setCloudStorage(true)
.setCheckUpdate(true)
.setDarkColor("#333333")
.setLightColor("#262626")
.setLastSessionTime(System.currentTimeMillis())
.setPrimaryColor("#513f2d")
.build()
}
}
}
复制代码
经过事先定义的数据结构自带数据类型,读写过程就像 data class 一样顺滑,数据类型经由编译器检查也很难出错,这应该是 protobuf 的大优势。
对应的页面还没弄好,只能在 Logcat 看效果了。
首次打开,只有一个默认值有数据
写入之后,数据更新
默认值在 Serializer 的 defaultValue 中设置,如果自定义了完善的默认值数据,初次打开就不会一片空白了。
DataStore 的 Hilt 封装
DataStore 的读写依赖 Context,使用时限制较多,解决方案也是老生常谈了,可以全局持有 applicationContext,用顶层函数或者单例提供 DataStore 对象。
但 Demo 项目已经接入 Hilt 了,DataStore 也很适合通过依赖注入创建对象,随手加一下就好了:
@Module
@InstallIn(SingletonComponent::class)
object DSManager {
private val Context.userPrefDataStore : DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferenceSerializer
)
@Provides
fun provideUserPrefDS(@ApplicationContext context: Context) = context.userPrefDataStore
}
复制代码
在 Activity 的使用:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var pref: DataStore<UserPreferences>
//……
}
复制代码
在 ViewModel 的使用:
@HiltViewModel
class TimeCountViewModel @Inject constructor(
private val pref: DataStore<UserPreferences>,
) : ViewModel() {
// ……
}
复制代码
DataStore 的上手体验就是这样了,初次使用比较复杂,优势需要复杂的业务场景才能体现,个人不是很推荐急着使用,毕竟兼容历史版本是一个巨大的坑,。而且我们也不是一定要在 SharedPreference 和 DataStore 二选一,不想用 Kotlin 协程还可以试一下 MMKV。
Hilt 也比较稳定了,接下来准备写 Compose 或者 Hilt,有什么想法欢迎评论指出~