在 Android 开发中,我们一般用 Retrofit + Gson 来请求网络 API 并解析返回的 Json,一般来说,我们给 Retrofit 配置一个默认的 Gson 实例就行了,如下所示:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
但有时候我们会遇到一些非常规的 Json,比如以下两种情况:
遇到这两种情况,默认的 Gson 实例就不够用了,我们必须实现自定义的 Gson TypeAdapter 来解析某些特定类型,然后把这个 adapter 注册给 Gson 实例,告诉它,以后遇到这种类型的数据就教给我来处理,你就别管了。
我们先来看第一种情况,就以 Feed 流为例,一般情况下我们得到的是一个 Json 数组,数组中包含多种类型的对象。我们会和服务端约定,这些对象,至少会有一个相同的属性,比如 type,由这个属性来标志这个对象的类别。所以在解析时,我们先取出这个 type 属性的值,根据它的值来解析整个对象。
假设我们得到的 Json 是这样的:
[
{
"type": "text",
"text": "work hard, enjoy life"
},
{
"type": "image",
"desc": "beautiful!",
"url" : "https://unsplash.it/200/200?random"
},
{
"type": "link",
"comment": "my github",
"link": "https://github.com/baurine"
}
]
这三种 model 假设分别为 TextModel, ImageModel, LinkModel,那么它们应该有一个共同的基类 BaseModel:
public class BaseModel {
public String type;
}
public class TextModel extends BaseModel {
public String text;
}
public class ImageModel extends BaseModel {
public String desc;
public String url;
}
public class LinkModel extends BaseModel {
public String comment;
public String link;
}
我们定义 getFeeds API,注意返回值的 List 中的元素必须是 BaseModel 类型:
@GET("feeds")
Call<List<BaseModel>> getFeeds(@Query("page") int page);
然后实现我们自定义的 Json 解析器 BaseModelAdapter:
public static class BaesModelAdapter implements JsonDeserializer<BaseModel> {
@Override
public BaseModel deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObj = json.getAsJsonObject();
String type = jsonObj.get("type").getAsString();
if (type.equals("text")) {
return new Gson().fromJson(json, TextModel.class);
} else if (type.equals("image")) {
return new Gson().fromJson(json, ImageModel.class);
} else if (type.equals("link")) {
return new Gson().fromJson(json, LinkModel.class);
}
return null;
}
}
把这个解析器注册到 Gson 实例中,告诉它用这个解析器来解析 BaseModel 类型的数据:
Gson gson = new GsonBuilder()
.setDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'")
.registerTypeAdapter(BaseModel.class, new BaseModelAdapter())
.create();
用新的 Gson 实例替代默认的 Gson 实例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
比如有一个获取城市列表的 API,我们期待的 Json 是这样的:
{
"cities": [
{
"id": 1,
"name": "Beijing"
},
{
"id": 2,
"name": "Shanghai"
}
]
}
结果得到的 Json 是这样的:
{
"cities": {
"1": {
"name": "Beijing"
},
"2": {
"name": "Shanghai"
}
}
}
又比如有一个天气预报的 API,我们期待的 Json 是这样的:
{
"forecasts": [
{
"date": "2017-02-08T19:00:00+08:00",
"temperature": 13
},
{
"date": "2017-02-09T19:00:00+08:00",
"temperature": 14
},
]
}
结果我们得到的 Json 是这样的:
{
"forecasts": {
"2017-02-08T19:00:00+08:00": {
"temperature": 13
},
"2017-02-09T19:00:00+08:00": {
"temperature": 14
}
}
}
由于历史原因,你可能无法要求服务器修改返回结构。所以我们必须自己在客户端手动转换,把实际得到的 Json 转成期待的 Json。
以天气预报的 API 为例,首先,我们来定义 Model,我们要把 forecasts 从对象转换成 List:
public class Weather {
public Forecasts forecasts;
//////////////////////////
public static class Forecasts {
public List<Forecast> forecastList;
public Forecasts(List<Forecast> forecastList) {
this.forecastList = forecastList;
}
}
public static class Forecast {
public Date date;
public int temperature;
}
}
因为我们要手动把 forecasts 从对象转换成 List,所以必须定义它的 Gson TypeAdapter。
我们来思考一下,怎么才能把上面实际得到的 Json 转换成期待的 Json 呢,很简单,遍历 forecasts 的每一个子对象,把作为 key 的日期值,放到它自身的对象中,增加一个 date 字段来存储它的值。
我们按这种思路来实现这个 adapter:
public static class ForecastsAdapter
implements JsonDeserializer<Forecasts> {
@Override
public Forecasts deserialize(
JsonElement json,
Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
Gson gson = new Gson();
List<Forecast> forecastList = new ArrayList<>();
Set<Map.Entry<String, JsonElement>> entries = json.getAsJsonObject().entrySet();
for (Map.Entry<String, JsonElement> entry : entries) {
String key = entry.getKey();
JsonElement value = entry.getValue();
value.getAsJsonObject().addProperty("date", key);
forecastList.add(gson.fromJson(value, Forecast.class));
}
return new Forecasts(forecastList);
}
}
然后把这个 adapter 注册到 Gson 实例以及把新的 Gson 实例配置到 Retrofit 的步骤就和上面一样了。
什么?你说要是 key 是动态的,又有多种类型,那怎么办,Apple-Pen 呗 ("I have a pen, I have an apple ..."),把上面的代码组合起来就行了。不过遇上那样的后端代码,我只能表示呵呵了。