Deserialization

JSON serialization/deserialization 应该是不少 Android app 都会做的事,基本上近乎每个 Android project 都会用了一个或几个这些 library,而 Android 都有好几个选择。除了上一篇提过的 org.json 套件之外,GsonMoshiKotlin serialization 都是热门的选择。

一般的 deserialization library 都是透过 Java/Kotlin class 的 property 名和 data type 来跟 JSON 做转换,例如你在 Java class 有一个叫 createdAtString property,deserialization library 就会找 JSON object 有没有一个叫 createdAt 的 property。如果有就把它以 String 的方式读取,然後塞进去那个 Java class 的 createdAt property 入面。如果是 serialization 的话,就是把这个动作掉转来做,就是看到 Java class 的 createdAt property 是 String,然後跟 Java String 最相近的 JSON data type 是 string,那 JSON object 就会出现 {"createdAt": "2021-09-01"}

Gson

Gson 是由 Google 开发,是上面三个 library 最早出现的一个,功能亦都很强大。例如可以设定 JSON 和 Java class property 之间的命名转换规则(FieldNamingPolicy),亦都支援 streaming parsing,适合处理巨大的 JSON 文档。

Moshi

Moshi 是 Square 开发的 JSON serialization/deserialization library。特色是它们功能上没 Gson 那麽多,专注在主要的功能上,所以比 Gson 小巧。另外一大特色是它提供了 Kotlin codegen(一个 annotation processor 生成 Moshi 的 adapter),就是 Moshi 能感知你的 Kotlin class property 是不是 nullable 而决定是不是把那个 Kotlin class property 设定成那个 property 的预设值还是用 JSON document 找到的值。

Gson 是不会知道你那个 property 是不是 nullable,因为它是 Kotlin 的 language feature。所以在 Gson 做 deserialization 时的做法是:

  1. 用 Java reflection 执行 default constructor(即是没有参数的 constructor)建立那一个 object
  2. 看到那 JSON 有一个 property,就用 Java reflection 找那个 class 有没有对应的 property,如果有就把 JSON property 的值塞进去那个 Java object 入面

问题就是出现在第二步,即使你在 default constructor 入面把那些 property 塞了不是 null 的值(如果是 Kotlin data class 就是在定义 property 时提供了预设值),但是如果 JSON property 的 value 是 null 的话 Gson 仍会把 null 塞入去那个 property。如果那个是 Kotlin 的 non-null property 那就会去到调用 property 的地方突然出现 NullPointerException。同样的情况如果是用 Moshi 配合 Kotlin 支援的话,它就会在 deserialization 抛出 JsonDataException,不会把问题延到其他地方才发现。

Kotlin serialization

Kotlin Serialization 是那三个 library 最新的一个,我们将会用这个 library 来做 API JSON response 的 deserialization。这个 library 的特色是它是用 Kotlin compiler plugin 来生成 visitor 的 code(就是类似 Moshi 生成 adapter 般)、它本身的设计是支援多种格式诸如 Protobuf、Properties 等等还有是支援 Kotlin multiplatform。

Moshi 有个问题是如果 property 是 non-null 但 JSON 对应的 property 是 null 的话它就会 throw exception,但 Kotlin serialization 经过设定後可以让它设定那个 property 的预设值而不是 throw exception。

如果你有 Protobuf 的需求的话,可以看看你是在那些地方用。如果是只有 app 自己在用(例如是 DataStore 的话)用 Kotlin serialization 都是不错的选择。但如果是和其他地方(例如跟 backend 之间通讯)那就不如考虑其他的 library。因为 Kotlin serialization 的野心是如果 backend、mobile 都是用 Kotlin 写的话,那就只需要交换那个加了 Kotlin serialization annotation 的 data class 原档就可以了,不需要再交换 proto 档作中转。另外,Kotlin serialization 是用 proto2,如果要用 proto3 还是用其他 library。

Response data class

我们选好了 Kotlin serialization,那就为先前的 data class 补回 annotation。其实要补回的 annotation 只有两个:

  • @Serializable 放在 class 开头
  • @SerialName 放在 property 开头,用以指明 JSON 的 property 名
@Serializable
data class EtaResponse(
    /**
     * system status code.
     */
    @SerialName("status") val status: Int = STATUS_ERROR_OR_ALERT,
    /**
     * Alert message.
     */
    @SerialName("message") val message: String = "",
    /**
     * URL for Special Train Services Arrangement case.
     */
    @SerialName("url") val url: String = "",
    /**
     * Indicate if the train is delayed.
     */
    @SerialName("isdelay") val isDelay: String = IS_DELAY_FALSE,
    @SerialName("data") val data: Map<String, Data> = emptyMap()
) {
    @Serializable
    data class Data(
        /**
         * Indicate the destinations of the train in the specific line (up trip).
         */
        @SerialName("UP") val up: List<Eta> = emptyList(),
        /**
         * Indicate the destinations of the train in the specific line (down trip).
         */
        @SerialName("DOWN") val down: List<Eta> = emptyList()
    )

    @Serializable
    data class Eta(
        /**
         * Platform numbers for the departure / arrival train.
         */
        @SerialName("plat") val plat: String = "1",
        /**
         * Estimated arrival time (or departure time) of the train.
         */
        @SerialName("time") val time: String = EMPTY_TIMESTAMP,
        /**
         * MTR Station Code in capital letters.
         */
        @SerialName("dest") val dest: String = "",
        /**
         * The sequence of the 4 upcoming trains.
         */
        @SerialName("seq") val seq: String = "0"
    )

    companion object {
        const val EMPTY_TIMESTAMP = "-"

        const val STATUS_NORMAL = 1
        const val STATUS_ERROR_OR_ALERT = 0
        const val IS_DELAY_TRUE = "Y"
        const val IS_DELAY_FALSE = "N"
    }
}

R8 rules

不要忘记在 proguard-rules.pro 加入对应的 R8 rule。如果不加就会有问题。

-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations

# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
    *** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
    kotlinx.serialization.KSerializer serializer(...);
}

# Change here net.swiftzer.etademo
-keep,includedescriptorclasses class net.swiftzer.etademo.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class net.swiftzer.etademo.** { # <-- change package name to your app's
    *** Companion;
}
-keepclasseswithmembers class net.swiftzer.etademo.** { # <-- change package name to your app's
    kotlinx.serialization.KSerializer serializer(...);
}

<<:  Day 4 仓库 Repository

>>:  从 JavaScript 角度学 Python(18) - 档案处理

Day26【Web】TCP 安全协定:SSL/TLS

SSL 安全通讯协定 (Secure Socket Layer)和 TLS 传输层安全性协定 (Tr...

Day 11 Implement audio wave generator

Implement audio wave generator using the Mbed API ...

年龄为多少秒

为了了解javascript的变数运算,我们练习去算出一小时、一天、一年、甚至年龄有多少秒 我通常练...

Week38 - 各种安全性演算法的应用 - 概念篇 [高智能方程序系列]

本文章同时发布於: Medium iT 邦帮忙 大家好,这几天较有时间,终於可以好好的思考文章 XD...

【Day10】AddInvitationFragment(下) X DatePickerDialog

接下上集!!,我们已经完成layout,还有上传照片了。那麽接下来我们要做的就是把选取时间的日历叫...