Android中Retrofit如何处理动态Key的JSON响应?
我在Android应用里用Retrofit和CoinGecko API交互,目标调用的端点是:https://api.coingecko.com/api/v3/simple/price?ids=01coin&vs_currencies=btc
它返回的JSON结构如下:
{ "01coin": { "btc": 1.3651e-8 } }
这里的01coin和btc都是由请求参数动态生成的,换查询币种后响应结构会跟着变。我之前调用其他接口的代码是这样的:
Retrofit接口代码
@GET("coins/list") suspend fun getCoinsList(): Response<ResponseCoinsList>
对应响应类代码
class ResponseCoinsList : ArrayList<ResponseCoinsList.ResponseCoinsListItem>() { data class ResponseCoinsListItem( @SerializedName("id") val id: String?, // 01coin @SerializedName("name") val name: String, // 01coin @SerializedName("symbol") val symbol: String? // zoc ) }
现在需要解决的问题是:怎么定义适配这种动态JSON对象的响应类?
解决方案1:直接使用嵌套Map(最简洁)
因为响应的外层Key是动态的币种ID,内层Key是动态的计价货币,直接用Map<String, Map<String, Double>>作为响应类型即可,不需要自定义数据类,Gson会自动完成解析。
调整后的Retrofit接口
@GET("simple/price") suspend fun getSimplePrice( @Query("ids") ids: String, @Query("vs_currencies") vsCurrencies: String ): Response<Map<String, Map<String, Double>>>
使用示例
val response = api.getSimplePrice("01coin", "btc") val btcPrice = response.body()?.get("01coin")?.get("btc")
如果一次查询多个币种或计价货币,比如ids=01coin,bitcoin&vs_currencies=btc,usd,也能直接通过Map的Key获取对应数据。
解决方案2:自定义数据类结合Map(兼顾类型安全与灵活性)
如果常用固定的几种计价货币(比如btc、usd),可以定义一个数据类来封装内层价格,外层依然用Map来兼容动态的币种ID:
定义内层数据类
data class CurrencyPriceDetail( @SerializedName("btc") val btcPrice: Double?, @SerializedName("usd") val usdPrice: Double? // 可根据需求添加更多常用计价货币的字段 )
调整后的Retrofit接口
@GET("simple/price") suspend fun getSimplePrice( @Query("ids") ids: String, @Query("vs_currencies") vsCurrencies: String ): Response<Map<String, CurrencyPriceDetail>>
使用示例
val response = api.getSimplePrice("01coin", "btc,usd") val coinData = response.body()?.get("01coin") val btcPrice = coinData?.btcPrice val usdPrice = coinData?.usdPrice
这种方式既可以通过属性直接访问常用价格,遇到不常用的计价货币时,还可以通过Gson的toJsonTree等方法动态获取(或者直接把CurrencyPriceDetail改成Map<String, Double>,灵活度更高)。
解决方案3:自定义TypeAdapter(复杂场景专用)
如果需要把动态Key的结构转换成列表等自定义格式,可以编写Gson的TypeAdapter来实现解析逻辑。比如把响应转成List<CurrencyPrice>,其中每个元素包含币种ID和对应的价格Map:
定义目标数据类
data class CurrencyPrice( val currencyId: String, val priceMap: Map<String, Double> )
编写TypeAdapter
class CurrencyPriceTypeAdapter : TypeAdapter<List<CurrencyPrice>>() { override fun write(out: JsonWriter, value: List<CurrencyPrice>?) { // 如果需要序列化可以实现此方法,这里只处理解析 } override fun read(`in`: JsonReader): List<CurrencyPrice> { val list = mutableListOf<CurrencyPrice>() `in`.beginObject() while (`in`.hasNext()) { val currencyId = `in`.nextName() val priceMap = mutableMapOf<String, Double>() `in`.beginObject() while (`in`.hasNext()) { val currency = `in`.nextName() val price = `in`.nextDouble() priceMap[currency] = price } `in`.endObject() list.add(CurrencyPrice(currencyId, priceMap)) } `in`.endObject() return list } }
配置Retrofit使用自定义TypeAdapter
在构建Retrofit时,给Gson添加这个TypeAdapter:
val gson = GsonBuilder() .registerTypeAdapter(object : TypeToken<List<CurrencyPrice>>() {}.type, CurrencyPriceTypeAdapter()) .create() val retrofit = Retrofit.Builder() .baseUrl("https://api.coingecko.com/api/v3/") .addConverterFactory(GsonConverterFactory.create(gson)) .build()
调整后的Retrofit接口
@GET("simple/price") suspend fun getSimplePrice( @Query("ids") ids: String, @Query("vs_currencies") vsCurrencies: String ): Response<List<CurrencyPrice>>
这种方式适合有特殊结构转换需求的场景,但步骤繁琐,一般前两种方案足够覆盖大部分使用场景。
内容的提问来源于stack exchange,提问作者Mohsen Mohebi

