使用 kotlinx.serialization 和DataStore 实现更简单易用的缓存

2,939 阅读5分钟

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

前言

DataStore 是Android 官方Jetpack组件库的一个组件,一个简易的数据存储解决方案,指代取代SharedPreferences,支持Koltin 协程和Flow,让应用能够以异步的方式存储和使用数据。

官方推荐两种使用方式,Preferences DataStoreProto DataStorePreferences 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的指定协议的复操作,同时也不失类型安全,使用起来方便了不少。

一个简单的框架如下: image.png

总结

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" 
}