You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中用Retrofit解析含多类型对象列表的JSON最佳实践

问题背景

API响应中rows字段是包含两种对象的列表,通过type字段值分为widget和cover类型。我搜遍相关内容没找到类似问题,疑惑为何没人碰到这种情况,现提出两个问题:

  1. 在Android Kotlin+Retrofit环境下,解析该响应的最佳实践是什么?数据类应如何设计?我目前的方案是用一个包含两种对象所有字段的数据类,根据type值区分后映射为业务对象,同时参考了Gson自定义类型适配器的方案。
  2. 单数据类方案与Gson自定义适配器方案哪个更优?

API响应示例:

{"data": {"classId": "65d8376f8d58f95dd10c4e10","name": "AI Fitness 💪","displayName": "Fitness Lifestyle 💪","rows": [{"type": "widget","width": 0.6,"name": "test","_id": "66793d57b935500b0b5090dc","headerText": "Trending Globally 🎖","maxClickCount": 1000000,"cards": [{"deepLink": "turnipgg://zaps/create?tab=cover&coverCategoryId=65d8444900677e37e4569aa9","imageUrl": "https://bulk-cover-dump.s3.ap-south-1.amazonaws.com/widgets/1_lovebirds.webp"},{"deepLink": "turnipgg://zaps/create?tab=cover&coverCategoryId=65d8444900677e37e4569aac","imageUrl": "https://bulk-cover-dump.s3.ap-south-1.amazonaws.com/widgets/2_movies.webp"}]},{"type": "cover","width": 0.6,"_id": "65f43ef179f720000717c8be","score": 18993,"name": "abcdef","url": "https://overlays-prod.s3.ap-south-1.amazonaws.com/tlFPY89T.webp","caption": "abc","anonymous": false,"minimumTagCount": 0,"attributes": {"Guessable": false,"Hintable": false,"Anonymous": false}},{"type": "cover","width": 0.6,"_id": "65f43ef179f720000717c8be","score": 18993,"name": "abcdef","url": "https://overlays-prod.s3.ap-south-1.amazonaws.com/tlFPY89T.webp","caption": "abc","anonymous": false,"minimumTagCount": 0,"attributes": {"Guessable": false,"Hintable": false,"Anonymous": false}}],"nextToken": "eyJjIjoiMTIiLCJlIjoiIiwiZiI6IjEwIiwiciI6IiJ9"}}

解答

问题1:解析最佳实践与数据类设计

针对Kotlin+Retrofit环境下的多类型列表解析,主流有两种方案,对应的数据类设计如下:

方案A:单数据类+业务层映射(你当前的方案)

设计一个包含widget和cover所有字段的数据类,非公共字段设为可空类型,示例:

data class Row(
    val type: String,
    val width: Double,
    val _id: String,
    val name: String,
    // Widget专属字段
    val headerText: String? = null,
    val maxClickCount: Int? = null,
    val cards: List<Card>? = null,
    // Cover专属字段
    val score: Int? = null,
    val url: String? = null,
    val caption: String? = null,
    val anonymous: Boolean? = null,
    val minimumTagCount: Int? = null,
    val attributes: Attributes? = null
)

data class Card(
    val deepLink: String,
    val imageUrl: String
)

data class Attributes(
    val Guessable: Boolean,
    val Hintable: Boolean,
    val Anonymous: Boolean
)

// 外层响应数据类
data class ApiResponse(
    val data: Data
)

data class Data(
    val classId: String,
    val name: String,
    val displayName: String,
    val rows: List<Row>,
    val nextToken: String
)

解析完成后,在业务层根据type字段判断,将通用Row映射为具体业务对象(如WidgetRow、CoverRow),避免在UI或业务逻辑中处理大量可空字段。

方案B:Gson自定义类型适配器+密封类(类型安全推荐方案)

用Kotlin密封类定义不同行类型,配合Gson自定义适配器自动完成解析,无需手动映射:

  1. 定义密封类及子类:
sealed class Row {
    abstract val type: String
    abstract val width: Double
    abstract val _id: String
    abstract val name: String

    data class Widget(
        override val type: String,
        override val width: Double,
        override val _id: String,
        override val name: String,
        val headerText: String,
        val maxClickCount: Int,
        val cards: List<Card>
    ) : Row()

    data class Cover(
        override val type: String,
        override val width: Double,
        override val _id: String,
        override val name: String,
        val score: Int,
        val url: String,
        val caption: String,
        val anonymous: Boolean,
        val minimumTagCount: Int,
        val attributes: Attributes
    ) : Row()
}

// Card、Attributes、ApiResponse、Data类同方案A
  1. 编写Gson类型适配器:
class RowTypeAdapter : TypeAdapter<Row>() {
    override fun write(out: JsonWriter, value: Row) {
        // 若需序列化可实现,此处省略
    }

    override fun read(`in`: JsonReader): Row {
        val jsonObject = JsonParser.parseReader(`in`).asJsonObject
        val type = jsonObject.get("type").asString

        return when (type) {
            "widget" -> Gson().fromJson(jsonObject, Row.Widget::class.java)
            "cover" -> Gson().fromJson(jsonObject, Row.Cover::class.java)
            else -> throw IllegalArgumentException("Unknown row type: $type")
        }
    }
}
  1. 在Retrofit中注册适配器:
val gson = GsonBuilder()
    .registerTypeAdapter(Row::class.java, RowTypeAdapter())
    .create()

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create(gson))
    .build()

解析后rows列表会直接返回Row.Widget和Row.Cover实例,无需手动映射,类型安全性更高。

问题2:两种方案对比

维度单数据类方案Gson自定义适配器方案
类型安全性低:需手动处理可空字段,易触发空指针高:编译时类型检查,从根源避免空安全问题
代码整洁度业务层需额外映射逻辑,代码冗余解析层处理映射,业务层直接使用具体类型
维护成本新增字段需修改单数据类,易遗漏新增类型仅需扩展密封类和适配器分支
开发速度快:无需编写适配器,直接解析稍慢:需编写并维护适配器代码
性能略优:无额外解析逻辑 overhead略差:解析时多一次JSON对象转换操作

方案选择建议

  • 若项目处于快速迭代阶段,且两种类型的字段变化频率低,可暂时采用单数据类方案快速落地;
  • 追求代码健壮性、长期维护性的项目,优先选择Gson自定义适配器+密封类方案,从根源避免类型判断错误和空指针问题。

内容的提问来源于stack exchange,提问作者Khay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 18:23:18