Retrofit advance: Multi-Converter

When deal with HTTP requests, Retrofit probably is your best choice. It’s simple, elegant and especially flexible for different data formats. You can find almost every converter you need no matter the data type is json, xml or protobuf. You can then pass your converter into Retrofit and it will handle all the rest for you.
Here is the problem. What if we want to support json and xml/protobuf type of response at the same time?
If you’re not familiar with Retrofit, you can read my 3-min introduction first — Retrofit 2
Solution 1
The easiest way is create another Retrofit instance for different purpose. It works but if you don’t want to hold multiple Retrofit instance you can keep reading.
Solution 2
We need to dig much deeper to see how Retrofit work with converters. When we create the Retrofit builder, we pass converterFactory by addConverterFactory()
.
public Builder addConverterFactory(Converter.Factory factory) {
converterFactories.add(checkNotNull(factory, "factory == null"));
return this;
}
The source code tell us the converterFactories
is actually a List type, it give us a clue that there are chance we can support multiple converters. Next step we’ll find out how Retrofit works and how the converterFactories
is used.
Here is a brief version about how Retrofit works. Every method in your Retrofit api file will associate with a serviceMethod
object which hold all the information you provide in the method. And serviceMethod.toResponse
will be called to parse response when it get the raw result.
R toResponse(ResponseBody body) throws IOException {
return responseConverter.convert(body);
}
The responseConverter
is come from the createResponseConverter()
in ServiceMethod.Builder
private Converter<ResponseBody, T> createResponseConverter() {
Annotation[] annotations = method.getAnnotations();
try {
return retrofit.responseBodyConverter(responseType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(e, "Unable to create converter for %s", responseType);
}
}
It will call responseBodyConverter
inside Retrofit. And we finally found the converterFactories
in this method.
public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
return nextResponseBodyConverter(null, type, annotations);
}
public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
//......
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
return (Converter<ResponseBody, T>) converter;
}
}
//......
}
As you can see, the method will iterate all the converterFactories
and return the first type matched converter. So here comes the solution 2, call addConverterFactory
multiple times:
new Retrofit.Builder()
.addConverterFactory(converterFactory1)
...
.addConverterFactory(converterFactoryN)
.build();
Noted the order is important, make sure there shouldn’t be any conflict between converterFactories from top to down. If any converterFactory mistaken consumed the response first you won’t parse correctly or you can keep reading the solution 3.
Solution 3
As we just walk through the source code, we knew the converter is geted from the responseBodyConverter
method in converterFactory
.
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
return (Converter<ResponseBody, T>) converter;
}
The responseBodyConverter
method will receive all the annotations and then decide how to resolve the data.
public Converter<ResponseBody, ?> responseBodyConverter(
final Type type,
final Annotation[] annotations,
final Retrofit retrofit) {
return null;
}
As converterFactory
is passed by our own, we may create customize version of ConverterFactory
and dispatch by our rule associate the annotations pass into the function.
We can create a mapping inside our converterFactory
for which annotation map with which ConverterFactory
.
public class AnnotatedConverter extends Converter.Factory {
private final Map<Class<?>, Converter.Factory> factoryMap;
public AnnotatedConverter(final Map<Class<?>, Converter.Factory> factoryMap) {
this.factoryMap = new LinkedHashMap<>(factoryMap);
} //......
}
And in the responseBodyConverter
we can iterate the annotations and return the associate Converter if found.
public Converter<ResponseBody, ?> responseBodyConverter(
final Type type,
final Annotation[] annotations,
final Retrofit retrofit) {
for (final Annotation annotation : annotations) {
final Converter.Factory factory =
factoryMap.get(annotation.annotationType());
if (factory != null) {
return factory.responseBodyConverter(type, annotations, retrofit);
}
}
return null;
}
Make sure there are some other methods need to be implemented like requestBodyConverter
.
Then we can create the AnnotatedConverter
by several ConverterFactory
.
HashMap<Class<?>, Converter.Factory> map = new HashMap<>();
map.put(Gson.class, gsonConverterFactory);
map.put(Proto.class, protoConverterFactory);new Retrofit.Builder()
.addConverterFactory(AnnotatedConverter(map))
.build();
And here is our custom annotation example, remember to add RUNTIME
policy to reserve the annotation in runtime.
@Retention(RetentionPolicy.RUNTIME)
public @interface Gson {
}
Recap
We have three solution now.
- Different Retrofit instance.
- Add ConverterFactory multiple times.
- Add wrapper ConverterFactory and dispatch to real instance in runtime.