前言
本文首发于掘金,有意向转发的同行可私信,想在Android领域有所进步的伙伴,可微信搜索个人的公众号「Android技术集中营」或扫码添加,有不定时福利等大家来拿。
1 传统序列化方案
目前在实际的业务开发中,服务端一般定义的数据结构为json数据,端上接收到服务端响应数据后,通过解析json生成对应的java bean类,做相应的页面数据呈现,我们在使用OkHttp和Retrofit网络库的时候,其实也是通过添加转换工厂GsonConvertFactory来实现。
这是传统的,也是很经典的数据转换形式,但是很多时候,服务端传递过来的数据我们并不一定如我们愿,看个场景:
kotlin
val jsonStr = "{\n" +
""name":"小明",\n" +
""age":12,\n" +
""sex":"男",\n" +
""school":"实验小学"\n" +
"}"
val person = Gson().fromJson(jsonStr, Person::class.java)
Log.d("TAG", "onCreate: $person")
如果服务端返回的数据正常,那么我们在反序列化之后,可以安全地使用每个字段数据进行UI的展示,那么如果服务端返回的数据异常,例如school
字段没有返回值,或者字段缺失,那么就会出现下面这种情况。
Kotlin
val jsonStr = "{\n" +
""name":"小明",\n" +
""age":12,\n" +
""sex":"男"" +
"}"
val person = Gson().fromJson(jsonStr, Person::class.java)
Log.d("TAG", "onCreate: $person")
最终序列化的结果:Person(name=小明, age=12, sex=男, school=null)
这个时候,我们取school
字段的数据,拿到的就是null,此时就会抛出空指针异常,因此我们在使用Kotlin的时候,通常会在实体类中对某些字段加空安全的处理,例如:
Kotlin
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String?
)
此时在使用school
这个字段的时候,就会提示此字段可能为空,需要做判空处理。一些伙伴想:能不能加默认值,其实是没用的,当json数据被反序列化之后,默认值也将被覆盖。
所以针对传统Gson存在的问题,自然有方案去解决这个问题,在Android进阶宝典 -- App线上网络问题优化策略这篇文章中,我介绍过如何在GsonConvertFactory中做类型的适配,但是如果针对每个数据类型都做一层适配,显然不太合适,于是Kotlin官网推出了针对Kotlin序列化的工具kotlin-serialization,看它具备的优势有哪些。
2 kotlin-serialization
kotlin-serialization是Kotlin提供的支持序列化的工具,支持JSON、Protobuf等常见的数据结构,旨在解决Gson在Kotlin中使用的限制。
2.1 基本使用
在Android项目中使用kotlin-serialization,首先需要引入依赖:
groovy
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
此时如果想要给一个类赋予序列化的能力,需要使用@Serializable
注解,
Kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String
)
到这儿并没有结束,此时编辑器对此注解曝黄,并提示:
kotlinx.serialization compiler plugin is not applied to the module, so this annotation would not be processed. Make sure that you've setup your buildscript correctly and re-import project.
需要我们在当前项目中引入插件,否则此注解将不会生效。
- 首先在project级别的gradle文件下,配置classpath
groovy
plugins{
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.10'
}
- 在app级别的gradle文件中,引入serialization插件
groovy
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
此时可以通过Json
的扩展方法decodeFromString
实现对json数据的反序列化。
Kotlin
val jsonStr = "{\n" +
""name":"小明",\n" +
""age":12,\n" +
""sex":"男"" +
",\n" +
""school":"实验小学"\n" +
"}"
val person = Json.decodeFromString<Person>(jsonStr)
//序列化
val personStr = Json.encodeToString(person)
如果想要对一个对象序列化,则可以使用encodeToString
方法实现,但是需要记住使用kotlin-serialization的前提是需要使用@Serializable
注解修饰实体类,否则会直接抛出异常。
2.2 只有backing fields支持序列化
什么是backing fields,其实很多伙伴看到之后也是一脸懵。其实这是Kotlin中特有的一个属性,例如我们自定义一个变量,那么Kotlin默认给生成了get/set方法。
kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String
) {
val teacher: String = "李老师"
var classmate: String = "小张"
get() = field
set(value) {
field = value
}
}
例如classmate
属性,Kotlin默认生成了get/set方法,其中get方法获取到的就是field,其实就是classmate
的值,而set方法则是给filed赋值,所以只要是有backing filed的变量均可被序列化。
而像teacher
属性,只有get方法,不能被赋值,此类属性不可被序列化,从最终的JSON数据中看到是没有teacher
这个字段的。
Kotlin
val person1 = Person("小明",12,"男","实验中学").apply {
classmate = "Alice"
}
val personStr = Json.encodeToString(person1)
Log.d("TAG", "onCreate: $personStr")
//输出json数据:
{"name":"小明","age":12,"sex":"男","school":"实验中学","classmate":"Alice"}
2.3 数据验证
例如我们从服务拿到用户的数据,其中name
字段是必须要有的,如果没有那么后续的业务流程无法执行,因此在实体类中,可以进行数据的强校验,例如:
kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String
) {
init {
require(name.isEmpty()) {
"name 不能为空"
}
}
}
如果此时返回的name数据为空字符串,那么在反序列的时候就会直接抛出异常,此时业务上层可直接根据抛出的异常进行异常处理。
2.4 数据解析空安全
回到文章开头的case,当从服务端返回的json数据中,缺失了关键字段之后,使用Gson解析会导致当前缺失字段为null
,即便是设置了初始值,也依然存在空指针的风险,那么在使用kotlin-serialization时,我们是有办法规避这个风险的。
kotlin
val jsonStr = "{\n" +
""name":"小明",\n" +
""age":12,\n" +
""sex":"男"" +
"}"
val person = Json.decodeFromString<Person>(jsonStr)
Log.d("TAG", "onCreate: ${person.school}")
显然,json数据中缺失了school
字段,那么直接取反序列化数据会直接抛异常:
kotlin
kotlinx.serialization.MissingFieldException: Field 'school' is required for type with serial name 'com.example.nowinandroid.data.Person', but it was missing
那么解决这个问题的方案就是,给school
属性赋初始值。
kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String = "实验中学"
)
此时我们拿到的school
值就是默认值实验中学
,这里与Gson不同的就是,kotlin-serialization在反序列化之后,如果原始数据字段缺失,不会覆盖实体类的初始值。
这只是一种情况,假如school
字段服务端返回了,但是值为null,此时依然还是会有空指针的风险,那么此时需要使用默认值,在Json
构造函数中,开启coerceInputValues
,如果school
值为空,那么还是会取默认值。
kotlin
val jsonStr = "{\n" +
""name":"小明",\n" +
""age":12,\n" +
""sex":"男"" +
",\n" +
""school":null\n" +
"}"
val jsonClient = Json {
coerceInputValues = true
}
val person = jsonClient.decodeFromString<Person>(jsonStr)
所以相较于在实体类中设置空安全,导致业务逻辑中一堆?
,这种方式显然更友好一些。
2.5 Transient属性排除
如果实体类中,某个属性不需要参与序列化,可以给其赋默认值,所以我们需要记住一点,在data class中定义的属性是默认支持序列化的,如果不想参与序列化,一种方式就是设置默认值。
kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String = "实验中学"
) {
val teacher: String = "李老师"
var classmate: String = "小张"
get() = field
set(value) {
field = value
}
}
像school
定义了默认值,那么就会参与序列化,如果是普通class中的属性,例如classmate
不想参与序列化,那么就可以使用@Transient
注解修饰。
kotlin
@Transient
var classmate: String = "小张"
get() = field
set(value) {
field = value
}
在使用@Transient
注解修饰属性时,该属性必须要有一个初始值。
2.6 默认值参数不参与序列化
在2.5 小节中,我们提到了如果一个属性有默认值,那么是不会参与序列化的,那么如果要默认值也参与序列化,那么可以使用@EncodeDefault
注解修饰。
2.7 引用对象类型
如果在一个类中,引用了另外一个类,这个类想要参与序列化,那么也需要使用@Serializable
注解修饰。
kotlin
@Serializable
data class Person(
val name: String,
val age: Int,
val sex: String,
val school: String = "实验中学",
val family: Family
) {
val teacher: String = "李老师"
@Transient
var classmate: String = "小张"
get() = field
set(value) {
field = value
}
}
@Serializable
data class Family(
val parentName:String
)
这个规则适用于任何一个序列化/反序列化框架。
2.8 泛型类的支持
现在有一个泛型类ClassA,通过@Serializable
修饰支持序列化,另外一个类ClassContainer,支持两种泛型类。
kotlin
@Serializable
data class ClassA<T>(val t: T)
@Serializable
data class ClassContainer(
val member: ClassA<Int>,
val member2: ClassA<String>
)
那么通过序列化,我们可以看到json数据中展示了两种不同的数据类型。
kotlin
val classContainer = ClassContainer(
ClassA(1), ClassA("泛型支持")
)
val str = Json.encodeToString(classContainer)
//结果:
{"member":{"t":1},"member2":{"t":"泛型支持"}}
当然,像Gson一样,kotlin-serialization同样也支持在Retrofit中使用,可以使用kotlin-serialization自带的数据转换工厂替换GsonConvertFactory,那么后续的数据转换要完全使用kotlin-serialization的规则,这势必会带来一定的学习成本,总之没有最好的,只有合适的才算最好的,可以视情况选择合适的工具。