且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何在Retrofit 2中使用gson TypeAdapter?

更新时间:2023-01-15 16:26:44

您的代码中存在一些问题,其中一些问题可以修复或重新设计以简化很多事情.首先,让我们看一下服务(名称不同).对于具有查询参数而不是@Path的请求,改造接口必须使用@Query.此外,您的服务方法必须返回Call,其中的T声明实际结果类型,以提供有关该方法返回的内容的一些信息.您提到的服务可以返回单个元素或不返回任何元素(但出于某种原因绝不返回HTTP 404),也可以返回列表,具体取决于端点URL.假设单值和无值都不是1或0大小的列表:

There are some issues in your code, and some of them can be either fixed or redesigned in order to simplify a lot of things. First, let's take a look at the service (the names differ). Retrofit interface must use @Query for requests with query parameters, not @Path. Also, your service methods must return a Call where its T declare the actual result type giving some information on what's returned by that method. The service you mentioned can return a single element or none (but never HTTP 404 for some reason), or a list depending on endpoint URLs. Let's assume, single values and none values are not meant to be 1- or 0-sized lists:

interface IService {

    @GET("country/get/all")
    Call<List<Country>> getCountries();

    @GET("country/get/iso2code/{code}")
    Call<Country> getCountryByIso2Code(
            @Path("code") String code
    );

    @GET("country/get/iso3code/{code}")
    Call<Country> getCountryByIso3Code(
            @Path("code") String code
    );

    @GET("country/search")
    Call<List<Country>> searchCountries(
            @Query("text") String query
    );

}

接下来,Country类可能如下所示:

Next, the Country class may look as follows:

final class Country {

    @SerializedName("name")
    final String name = null;

    @SerializedName("alpha2_code")
    final String code2 = null;

    @SerializedName("alpha3_code")
    final String code3 = null;

    @Override
    public String toString() {
        return name + '(' + code2 + ',' + code3 + ')';
    }

}

请注意,@SerializedName可以将外部名称映射到本地字段名称,并且这些字段已被finalnull修饰-Gson可以自行处理它们.接下来,我假设您实际上并不关心响应结构,只需要分别修改的$.Response.result即可. TypeAdapterFactory是您所需要的,并且可以与任何响应配合使用,不一定适用于某些国家/地区:

Note that @SerializedName can map an external name to a local field name, and the fields are final and nullified - Gson can deal with them itself. Next, I'm assuming that you don't really care the response structure, and only need $.Response.result that should be adapted respectively. TypeAdapterFactory is what you need and can work with any response, not necessarily for countries:

final class ResponseExtractorTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Gson gson;

    private ResponseExtractorTypeAdapterFactory(final Gson gson) {
        this.gson = gson;
    }

    static TypeAdapterFactory getResponseExtractorTypeAdapterFactory(final Gson gson) {
        return new ResponseExtractorTypeAdapterFactory(gson);
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson responseGson, final TypeToken<T> typeToken) {
        // Using responseGson would result in infinite recursion since this type adapter factory overrides any type
        return new ResponseExtractorTypeAdapter<>(gson, typeToken.getType());
    }

    private static final class ResponseExtractorTypeAdapter<T>
            extends TypeAdapter<T> {

        private final Gson gson;
        private final Type type;

        private ResponseExtractorTypeAdapter(final Gson gson, final Type type) {
            this.gson = gson;
            this.type = type;
        }

        @Override
        public void write(final JsonWriter out, final T value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public T read(final JsonReader in)
                throws IOException {
            T result = null;
            // Strip the top most enclosing { for $
            in.beginObject();
            final String name1 = in.nextName();
            switch ( name1 ) {
            case "RestResponse":
                // RestResponse { for $.Response
                in.beginObject();
                while ( in.hasNext() ) {
                    final String name2 = in.nextName();
                    switch ( name2 ) {
                    case "messages":
                        // If it's just $.Response.message, then skip it
                        in.skipValue();
                        break;
                    case "result":
                        // If it's $.Response.result, then delegate it to "real" Gson
                        result = gson.fromJson(in, type);
                        break;
                    default:
                        throw new MalformedJsonException("Unexpected at $.RestResponse: " + name2);
                    }
                }
                // RestResponse } for $.Response
                in.endObject();
                break;
            default:
                throw new MalformedJsonException("Unexpected at $: " + name1);
            }
            // Strip the top most enclosing } for $
            in.endObject();
            return result;
        }

    }

}

将它们放在一起:

public static void main(final String... args) {
    final Gson gson = new GsonBuilder()
            // configure whatever you like
            .create();
    final Gson responseGson = new GsonBuilder()
            .registerTypeAdapterFactory(getResponseExtractorTypeAdapterFactory(gson))
            .create();
    final Retrofit retrofit = new Builder()
            .baseUrl("http://services.groupkt.com/")
            .addConverterFactory(GsonConverterFactory.create(responseGson))
            .build();
    final IService apiInterface = retrofit.create(IService.class);
    apiInterface.getCountries().enqueue(callback("getCountries()")); // http://services.groupkt.com/country/get/all
    apiInterface.getCountryByIso2Code("UA").enqueue(callback("getCountryByIso2Code()")); // http://services.groupkt.com/country/get/iso2code/UA
    apiInterface.getCountryByIso3Code("UKR").enqueue(callback("getCountryByIso3Code()")); // http://services.groupkt.com/country/get/iso3code/UKR
    apiInterface.searchCountries("land").enqueue(callback("searchCountries()")); // http://services.groupkt.com/country/search?text=land
}

private static <T> Callback<T> callback(final String name) {
    return new Callback<T>() {
        @Override
        public void onResponse(final Call<T> call, final Response<T> response) {
            // Just make sure the output is not written in middle
            synchronized ( System.out ) {
                System.out.print(name);
                System.out.print(": ");
                System.out.println(response.body());
                System.out.println();
            }
        }

        @Override
        public void onFailure(final Call<T> call, final Throwable ex) {
            ex.printStackTrace(System.err);
        }
    };
}

结果(假设响应是依次接收和处理的,再加上长响应被删减):

And the result (assuming the responses are received and processed sequentally + the long responses are cut):

getCountries():[阿富汗(AF,AFG),阿兰群岛(AX,ALA),阿尔巴尼亚(AL,ALB),...,也门(YE,YEM),赞比亚(ZM,ZMB),津巴布韦( ZW,ZWE)]

getCountries(): [Afghanistan(AF,AFG), Åland Islands(AX,ALA), Albania(AL,ALB), ..., Yemen(YE,YEM), Zambia(ZM,ZMB), Zimbabwe(ZW,ZWE)]

getCountryByIso2Code():乌克兰(UA,UKR)

getCountryByIso2Code(): Ukraine(UA,UKR)

getCountryByIso3Code():乌克兰(UA,UKR)

getCountryByIso3Code(): Ukraine(UA,UKR)

searchCountries():[奥兰群岛(AX,ALA),布韦岛(BV,BVT),开曼群岛(KY,CYM),...,美国小外岛(UM,UMI),维尔京群岛(英国)(VG,VGB),维尔京群岛(美国)(VI,VIR)]

searchCountries(): [Åland Islands(AX,ALA), Bouvet Island(BV,BVT), Cayman Islands(KY,CYM), ..., United States Minor Outlying Islands(UM,UMI), Virgin Islands (British)(VG,VGB), Virgin Islands (U.S.)(VI,VIR)]