好久没写东西了,主要是 Compose 更新变化太快,官方文档更新都跟不上代码,写相关的内容立刻就会过期。java
可是在 Compose Demo 项目中不是只有 UI,数据层面逐步增长了 Room 和 DataStore,DataStore 已经迎来 beta 版本,想提早了解一下的话,如今正是时候。android
DataStore 的目标是成为 SharedPreference 的上位替代,用于少许 key-value 数据的存储,存储方式是本地文件。这听起来和 SharedPreference 并无本质区别,事实上也确实如此,由于两者的区别在 API 的封装上。SharedPreference 的实现至关简单,存取值得时候经过类型消除和类型强转保证 xml 的文件内容和 Java 的数据类型匹配,没法保证安全性。在读写数据时主要提供同步的 API,在 UI 线程读取较大内容的时候会有明显卡顿,而异步的 apply 写入又难以确认写入完成的时间。git
DataStore 从设计之初就想避免上述问题,也所以变得相对复杂,提升了使用门槛,要不要换恐怕还得从业务角度考虑,如今仅作了解便可。程序员
首先,DataStore 提供了两种不一样数据结构的实现,分别是 Preferences 实现和 Proto 实现。其中 Preferences 的实现较为简单,不须要提早定义数据类型,也不能保证类型安全,这方面跟 SharedPreference 一致;Proto 实现则是使用 protobuf 预约义要保存的数据,能够保证类型正确,代价是代码更加复杂。github
其次,DataStore 深度绑定 Kotlin 协程,读取数据和写入数据的函数都是 suspend 函数,读数据的返回值直接采用了 Flow,统一了单次获取和持续监听。web
DataStore 的两种实现对应着不一样的依赖,我以为 Preferences 实现像是一种向易用性妥协的方案,真的想要简单好用不如仍是用 SharedPreference,因此要学就一步到位,用 Proto 实现。json
Proto 实现的使用方式分为如下几步:安全
比起 SharedPreference 确实多了不少步骤,但流程还算清晰,真正的坑都潜伏在细节中。markdown
implementation("androidx.datastore:datastore:1.0.0-beta02") // 截止到20210626的最新版
复制代码
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
每次改动都敲命令或者执行脚本编译 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 就相对简单了,会协程就没什么问题。
读:
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 的读写依赖 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,有什么想法欢迎评论指出~