Android进阶宝典 -- 还在使用Gson吗?试下kotlin-serialization的强大功能

前言

本文首发于掘金,有意向转发的同行可私信,想在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.

需要我们在当前项目中引入插件,否则此注解将不会生效。

  1. 首先在project级别的gradle文件下,配置classpath
groovy 复制代码
plugins{
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.10'
}
  1. 在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的规则,这势必会带来一定的学习成本,总之没有最好的,只有合适的才算最好的,可以视情况选择合适的工具。

相关推荐
li_liuliu25 分钟前
Android4.4 在系统中添加自己的System Service
android
C4rpeDime3 小时前
自建MD5解密平台-续
android
鲤籽鲲4 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514778 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯8 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯8 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐9 小时前
Handle
android
m0_7482329210 小时前
Android Https和WebView
android·网络协议·https
m0_7482517211 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_7482546613 小时前
go官方日志库带色彩格式化
android·开发语言·golang