在第三章的最后,我们介绍了自定义Converter,是根据GsonConverterFactory改的,但其实Gson并不支持kotlin空安全特性,可以概括为:即使在kotlin class中定义非空字段,Gson解析null字段时会绕过空安全检查,最终将null值赋值给非空字段。
示例:
data class User(
val name: String,
val avatar: String,
val age: Int
)
定义如下Json
{
"name": "张三",
"age":0
}
示例中,Json并不包含avatar字段,但最终结果是正常解析,不会报错。但是由于没有avatar字段,所以avatar为null。这就导致我们只能在调用avatar时必须判断空值情况,即使kotlin提示你此字段不可空。
原因是什么?
data class中的字段没有默认值,那么就不会生成空参构造。在Gson内部解析的时候没有调用空参构造(因为没有生成)。涉及到Gson内部解析细节,简单说明就是Gson解析会有如下优先级 1.newDefaultConstructor
2.newDefaultImplementationConstructor(这个方法里面都是一些集合类相关对象的逻辑,直接跳过。)
3.newUnsafeAllocator
由于没有空参构造,所以1不会走,2原因如上,所以会走3。是个不安全的操作,并不建议,具体上网查询答案。
处理方案?
从业务逻辑出发
既然avatar可空,那我就设置为可空。
data class User(
val name: String,
val avatar: String?,
val age: Int
)
这种写法当然可以,但有缺陷。
如果我有这样的需求呢:当Json中不包含avatar,那么Gson解析结果中avatar将采用默认值。
data class User(
val name: String,
val avatar: String? = "http:.....",
val age: Int
)
这个需求就满足不了,原因之前讲过,没有空参构造,执行不安全的初始化对象逻辑,最终Json中缺省的avatar字段依旧是null。
从源码分析出发
既然没有空参构造会导致Gson内部执行不安全的初始化对象操作,那我们给出空参构造方法不就行了。
空参构造方法有很多种,这里我只介绍一种
给所有字段添加默认值
data class User(
val name: String = "",
val avatar: String = "",
val age: Int = 11
)
这样就会生成空参构造,同时满足默认值的要求,就是写起来麻烦,不过可以借助插件实现。
但如果我们解析如下数据时
{
"name": "张三",
"age":0,
"avatar":null
}
最终还是会将null赋值给avatar字段,即使将avatar设置为可空,那么之前提到的Json字段缺省即采用默认值的需求还是无法解决。
那么,该怎么办呢?
不怎么办,要么忍受,要么就用其他解析库吧。比如moshi和官方出品的kotlinx.serialization。
moshi
我们先来看看如何使用,以及对kotlin的支持如何
具体的可以查看官方文档: github.com/square/mosh…
moshi有两种解析方式,一个依靠反射,一个不依靠反射,我们采用的是不依靠反射的方式。
val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
val user = jsonAdapter.fromJson(json)
println(user)
写法相比与Gson要麻烦一点,而且解析的结果user也是可空类型,如果我们要想使用这个user就必须判空。
不过我们先来看一下对kotlin空安全的支持如何,比如我们定义如下的json
{
"name": "haha",
"age": 0,
"avatar":null
}
User定义如下
data class User(
val name: String,
val avatar: String,
val age: Int
)
此时会发生什么呢?会直接抛出JsonDataException: Non-null value 'avatar' was null
如果我将avatar设置一个默认值
data class User(
val name: String,
val avatar: String = "https...",
val age: Int
)
json数据中不给出avatar字段时会发生什么呢?
{
"name": "haha",
"age": 0
}
答案是保留了默认值,这一点也与Gson不一样,可见在支持kotlin非空字段上moshi做得比Gson要好一点。
不过当我再去搜索一些文章的时候发现一些对moshi的吐槽。
-
无法支持kotlin中泛型场景的空安全
测试一下
data class User<T>( val age: Int = 0, // 0 val avatar: String = "https...", val name: String, // 张三 val data:T )
json如下
{ "name": "张三", "age": 0, "data": null }
有泛型的情况下,需要这样写
val moshi: Moshi = Moshi.Builder().build() val listOfCardsType: Type = Types.newParameterizedType( User::class.java, UserData::class.java ) val jsonAdapter: JsonAdapter<User<UserData>> = moshi.adapter(listOfCardsType)
结果是抛出了异常
JsonDataException: Non-null value 'data_' (JSON name 'data') was null
,符合预期如果Json是这样的呢?没有data字段
{ "name": "张三", "age": 0 }
会抛出
JsonDataException: Required value 'data_' (JSON name 'data') missing
如果是List呢?
json如下
[ null, null, null ]
代码如下
val moshi: Moshi = Moshi.Builder().build() val listOfCardsType: Type = Types.newParameterizedType( List::class.java, UserData::class.java ) val jsonAdapter: JsonAdapter<List<UserData>> = moshi.adapter(listOfCardsType) val result = jsonAdapter.fromJson(json) Log.i(TAG, "result: " + result?.get(0))
结果是正常解析,并且log打印出result: null,还是将null赋值给了非null的UserData,这是不符合预期的,应该抛出异常才对。
-
对ArrayList和List的支持
定义类
data class User( val age: Int = 0, // 0 val data: List<Data> = listOf(), val name: String = "" // 张三 ) { data class Data( val key: Int = 0 // 1 ) }
代码
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
结果是正常解析,但如果将data的类型改为ArrayList,就会报错
No JsonAdapter for java.util.ArrayList<User$Data>, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter
要么使用List或者MutableList,要么自定义一个JsonAdapter解析ArrayList。
小结:
moshi使用稍微麻烦一点,对ArrayList不支持,对List中T的空值不支持。除此之外,对kotlin的空安全支持还是比Gson好的。
kotlinx.serialization
配置请看官方....
val user = Json.decodeFromString<User>(json)
代码比较简单,就一行代码,而且返回的User也是非空类型,这一点比moshi要好一点。
关于kotlinx.serialization的测试,就不详细演示了。moshi支持的kotlinx.serialization都支持,moshi不支持的,kotlinx.serialization也支持,比如:
定义如下json和User
{
"name": "张三",
"age": 0,
"data": [
null,
null
]
}
data class User(
val age: Int = 0, // 0
val data: List<Data> = listOf(),
val name: String = "" // 张三
) {
data class Data(
val key: Int = 0 // 1
)
}
结果符合预期,解析报错
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 45: Expected start of the object '{', but had ' ' instead
也支持ArrayList。
小结:
kotlinx.serialization使用起来比moshi方便很多,moshi不支持的解析kotlinx.serialization都支持,对kotlin空安全支持是目前最好的。
说了这么多,其实还没有写到我想说的部分,这篇主要是来展示一下Retrofit中自定义Converter,用moshi或者kotlinx.serialization来替代GsonConverterFactory的。
Moshi自定义解析
先来看看如何在Retrofit中使用moshi。
首先是造好的轮子:com.squareup.retrofit2:converter-moshi。只需要依赖最新版本即可。
private val moshi = Moshi.Builder()
.build()
Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))
MoshiConverterFactory.create需要传入一个moshi对象,这里直接初始化即可,这就是最基本的使用。
不过,还记得我们第三章业务需求吗?这里我再贴一下
请求结果是
{
"data": [...],
"errorCode": 0,
"errorMsg": ""
}
我们要达到的效果
lifecycleScope.launch {
val result = service.banner()
//这个BizSuccess就包含了body.errorCode == 0
if (result is ApiResult.BizSuccess) {
tvResult.setText(result.data.toString())
}
}
我们需要做的是拿到请求结果后,对Json进行手动解析,如果满足业务成功条件(errorCode=0)就返回ApiResult.BizSuccess,否则就是ApiResult.BizError等,现在我们来看下moshi的自定义解析。
private val moshi = Moshi.Builder()
//增加自定义解析即可
.add(MoshiApiResultConverterFactory())
.build()
Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))
我们增加了一个MoshiApiResultConverterFactory用于处理ApiResult的自定义解析,直接贴代码了
class MoshiApiResultConverterFactory : JsonAdapter.Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
//判断是否为ApiResult<T>
val rawType = type.rawType
if (rawType != ApiResult::class.java) return null
//获取 ApiResult 的泛型参数,比如 Banner
val dataType: Type = (type as? ParameterizedType)
?.actualTypeArguments?.firstOrNull()
?: return null
//代码1 : 获取 Banner 的 JsonAdapter
val dataTypeAdapter = moshi.nextAdapter<Any>(
this, dataType, annotations
)
return ApiResultTypeAdapter(rawType, dataTypeAdapter)
}
class ApiResultTypeAdapter<T>(
private val outerType: Type,
private val dataTypeAdapter: JsonAdapter<T>
) : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
//开始读取数据
reader.beginObject()
var code: Int? = null
var msg: String? = null
var data: Any? = null
//可空的stringAdapter
val nullableStringAdapter: JsonAdapter<String?> = Moshi.Builder().build().adapter(String::class.java,
emptySet(), "message")
//同样是分段读取
while (reader.hasNext()) {
when (reader.nextName()) {
"code" -> code = reader.nextString().toIntOrNull()
"message" -> msg = nullableStringAdapter.fromJson(reader)
"data" -> data = dataTypeAdapter.fromJson(reader)
else -> reader.skipValue()
}
}
reader.endObject()
return if (code != 0)
ApiResult.BizError(
code ?: -1,
msg ?: "N/A"
) as T
else ApiResult.BizSuccess(
code,
msg ?: "N/A",
data
) as T?
}
// 不需要序列化的逻辑
override fun toJson(writer: JsonWriter, value: T?): Unit = TODO("Not yet implemented")
}
}
代码也很好理解,直接看注释即可。其中代码1
val dataTypeAdapter = moshi.nextAdapter<Any>(
this, dataType, annotations
)
因为每一个解析类,比如我们这里是Banner都需要添加一个@JsonClass(generateAdapter = true)
注解用来生成对应的adapter解析json。这里的nextAdapter()方法的设计倒是很像Retrofit。总之,我们拿到了ApiResult中的T对应的adapter,在接下来解析数据的时候,用获取到的adapter解析data。
注意的是在第三章中写好的ApiResultCallAdapter中的responseType()传什么。
应该是ApiResult,而不是T。否则无法匹配到我们自定义的MoshiApiResultConverterFactory
kotlinx.serialization自定义解析
先说结论吧,kotlinx.serialization是支持自定义解析的,但是由于当前最新版本kotlinx-serialization-json:1.3.1对带有泛型类的自定义解析存在问题,所以我们自定义解析ApiResult编译不通过。
相关问题链接如下
这是我遇到的问题:github.com/Kotlin/kotl…
这个指出会不会是kotlin-kapt的问题:github.com/Kotlin/kotl…
另外对于sealed修饰的ApiResult在自定义解析时也会报错。
抛开这些待解决的问题,回到业务来,其实也有供我们使用的kotlinx-serialization-converter,来自于jakewharton巨佬,地址:github.com/JakeWharton…
普通使用很简单,如下所示:
val contentType = "application/json".toMediaType()
Retrofit.Builder().addConverterFactory(Json.asConverterFactory(contentType))
那如何进行自定义解析ApiResult呢,因为代码并没有跑起来,无法验证,而且我用的是Banner< T >来代替的ApiResult< T >,所以我只能说不带泛型的Banner的自定义解析是成功的,但是带有泛型的Banner< T >和ApiResult< T >只能看伪代码。
给出如下Json,这也是wanAndroid的Banner接口结果
{
"data": [
{
"desc": "一起来做个App吧",
"id": 10,
"imagePath": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"isVisible": 1,
"order": 1,
"title": "一起来做个App吧",
"type": 0,
"url": "https://www.wanandroid.com/blog/show/2"
}
],
"errorCode": 0,
"errorMsg": ""
}
定义如下Bean
//CustomBannerSerializer即指定了自定义解析的类
@Serializable(with = CustomBannerSerializer::class)
data class Banner<T>(
//这里我定义一个List<T>泛型,用来演示,虽然我们知道这个T就是指的Banner.Data
val data: List<T>? = listOf(),
val errorCode: Int = 0, // 0
val errorMsg: String = ""
) {
data class Data(
val desc: String = "", // 一起来做个App吧
val id: Int = 0, // 10
val imagePath: String = "", // https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png
val isVisible: Int = 0, // 1
val order: Int = 0, // 1
val title: String = "", // 一起来做个App吧
val type: Int = 0, // 0
val url: String = "" // https://www.wanandroid.com/blog/show/2
)
}
自定义的CustomBannerSerializer,用于解析Banner
class CustomBannerSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Banner<T>> {
override fun serialize(encoder: Encoder, value: Banner<T>) {}
override fun deserialize(decoder: Decoder): Banner<T> {
return decoder.decodeStructure(descriptor) {
var data:List<T>? = null
var errorCode: Int = 0
var errorMsg: String = ""
while (true) {
when (val index = decodeElementIndex(descriptor)) {
//因为在Banner中索引为0的是data
0 -> data = decoder.decodeSerializableValue(ListSerializer<T>(dataSerializer))
1 -> errorCode = decodeIntElement(descriptor, 1)
2 -> errorMsg = decodeStringElement(descriptor, 2)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
Banner(data,errorCode,errorMsg).also {
println(it)
}
}
}
//有待确认
override val descriptor: SerialDescriptor = dataSerializer.descriptor
}
我们将原来的ApiResult变成Banner,但是逻辑是一样的(分段解析),只不过ApiResult是sealed的(sealed修饰的类自定义解析时需要注意,kotlinx.serialization不支持)。
至此,kotlinx.serialization的自定义解析也分析完成了,有兴趣的同学可以自己试试或者kotlinx-serialization新版本更新后说不定就编译通过了。
代码已上传到Github:github.com/lt19931203/…
参考链接: