这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战
前言
DataStore 是Android 官方Jetpack组件库的一个组件,一个简易的数据存储解决方案,指代取代SharedPreferences,支持Koltin 协程和Flow,让应用能够以异步的方式存储和使用数据。
官方推荐两种使用方式,Preferences DataStore和Proto DataStore。Preferences DataStore使用比较简单,不需要预先定义,但是不支持类型安全。Proto DataStore使用起来比较复杂,需要预先使用protocol buffers定义数据,但是类型安全。所以可不可以既不需要实现定义又能保证类型安全呢。kotlinx.serialization 给我们提供了一种解决方案。
缓存
现在有这样一个场景,如果一个页面需要加快显示,我们就需要对这个页面的数据做缓存,缓存方案有许多选择,选择缓存什么数据也是个问题。我们以前的方案是一般对接口的响应数据做缓存,这样子缓存符合直觉,在页面展示时重新parse 一遍缓存数据再做展示,暂时忽略性能问题的话,这样子缓存也有两个比较明显的问题,一是需要比较多的样板代码,而接口响应并不是完整的数据,我们往往需要从多个接口compose或者 combine App 中现有的数据才能得到最终显示的数据,这部分状态数据是当前缓存中没有,或者需要等待其他缓存数据,基于此,我们考虑不是可以直接缓存UI显示需要的数据,这样子就省略了一个步骤,加速页面显示呢?
使用kotlinx.serialization 序列化数据
响应式编程大行其道,在使用Kotlin开发时,我们经常把状态封装到数据类中,业务层处理状态问题,比如在MVVM中,我们在Repository 和 ViewModel 中处理业务问题,共同维护这些状态的组装,派发。然后由这些状态驱动View 显示,View 拿到数据类就可以直接显示了,比较理想的是View里不考虑和处理任何业务逻辑。一般的,我们将状态封装成可枚举的Sealed class,每个子类都表示一类状态:
sealed class PageState {
object Loading : PageState()
data class Error(val cause: Throwable) : PageState()
data class RenderData(
val modules: List<ModuleModel>
) : PageState()
}
在开发中,一般使用data class 数据类表示一类和数据相关的状态,这类状态的数据是可变的。为了简化数据类的序列化和反序列化处理,Koltin 官方推出了kotlinx.serialization来简化这些流程:
kotlinx.serialization 是kotlin官方的序列化库,可以基于@serializable注解,编译器在编译阶段就生成了序列化代码从而避免运行时的反射开销。
为此考虑到kotlinx.serialization 可以方便的对数据类进行序列化和反序列化,所以考虑是不是可以使用这个技术简化缓存过程呢?
kotlinx.serialization使用起来也很简单,最简单的使用方法,我们只要在目标数据类上加上
@Serializable注解即可。
@Serializable
data class RenderData(
val modules: List<ModuleModel>
) : PageState()
序列化使用起来非常简单
val bytes = Json.encodeToString(renderData).encodeToByteArray()
反序列化用起来代码也很少
val renderData = Json.decodeFromString<PageState.RenderData>(inputStream.readBytes().decodeToString())
数据的存储格式解决了,解决了如何将数据序列化的问题,现在需要考虑如何存储的问题。
使用DataStore 缓存数据
但是仅仅序列化和反序列化还不够,存储和读取也是一推样本代码,尤其是有较多的文件IO处理逻辑,模板代码和异常处理都是比较扰人的,Android Jetpack 新推出的DataStore可以帮我们简化这个过程,其提供了良好的接口,让我们仅仅关注与数据与类型的映射关系,不用考虑内部的存储过程就可以快速的实现一个缓存数据的框架。
除了官方推荐的类SharedPreferences的使用方式,一般的在使用DataStore时我们仅仅需要定义一个Serializer 来将我们的数据类型和DataStore 关联起来:
private object RenderDataSerializer :
androidx.datastore.core.Serializer<PageState.RenderData> {
override val defaultValue: PageState.RenderData
get() = RENDER_DATA_NULL
override suspend fun readFrom(input: InputStream): PageState.RenderData {
return try {
Json.decodeFromString(input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
defaultValue
}
}
override suspend fun writeTo(t: PageState.RenderData, output: OutputStream) {
try {
withContext(Dispatchers.IO) {
output.write(Json.encodeToString(t).encodeToByteArray())
}
} catch (ex: Exception) {
}
}
}
DataStore 既不关心存储数据类型,也不关注存储的数据内容。在存储数据时DataStore 会调用writeTo方法,我们将缓存数据写入;在读取时DataStore提供了一个输入流,我们将数据读出交给kotlinx.serialization 反序列化为我们需要的类型,
结合DataStore 和 kotlinx.serialization 实现的缓存框架 ,跳过Protobuf的指定协议的复操作,同时也不失类型安全,使用起来方便了不少。
一个简单的框架如下:
总结
kotlinx.serialization 让我们可以忽略序列化和反序列化的细节,DataStore帮我们实现了缓存的存储和使用。同时我们缓存的是最终数据,数据是带状态的,是完整的不依赖其他缓存的个体
Happy ending.
附录
使用 kotlinx.serialization 需要引入相关依赖。
- Project 的buidld.gradle 引入相关插件配置
buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
- Module 中的build.gradle引入插件和依赖
plugins {
id 'kotlinx-serialization'
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
}