Retrofit advance: Multi-Converter

Jintin
ProAndroidDev
Published in
3 min readJan 8, 2018

--

Photo by Jason Leung on Unsplash

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.

  1. Different Retrofit instance.
  2. Add ConverterFactory multiple times.
  3. Add wrapper ConverterFactory and dispatch to real instance in runtime.

Reference

--

--

Android/iOS developer, husband and dad. Love to build interesting things to make life easier.