Band-aiding legacy networking libraries with Retrofit and OkHttp

(This article was cross-posted from Uncommon’s blog)

When Uncommon was brought in to refactor a big legacy app last year, we found ourselves in a strange situation. Our client was maintaining a common library for handling networking — HTTP, FCM, MQTT — across all apps under their brand. This library (let’s call it SadLibrary) had an API that was badly designed and dated, but not using it was not an option — a dedicated team was working on it and their manager started showing signs of protest.

Most modern Android apps use Retrofit to access REST-style APIs in an elegant and simple way. We wanted to see if we could make Retrofit work with SadLibrary and decided to give it a shot. Our initial idea was to fork Retrofit as RetroUnfit (pun count: 1) and plug-in SadLibrary instead of OkHttp. Thankfully, this spectacularly bad idea was shot down in no time. After going through Retrofit’s code, learning how Retrofit delegates calls to OkHttp and, how OkHttp further delegates them to interceptors, we realized that the correct solution was far simpler

OkHttp and the Magical Interceptors

For those unaware, OkHttp uses interceptors for not only letting clients hook into calls and modify their request/response bodies, but also for finally running the call on the network. This is done inside the very last interceptor and can be seen in action here: okhttp3.RealCall#194.

So to make Retrofit work with SadLibrary, all we needed to do was add an interceptor that redirects all network requests from OkHttp and forwards them to SadLibrary. Here’s an example:

class MrStealYoNetworkCalls implements okhttp3.Interceptor {

  @Override
  public Response intercept(Chain chain) throws IOException {
    okhttp3.Request okHttpRequest = chain.request();

    CustomRequest request = toCustomRequest(okHttpRequest);
    CustomResponse response = executeCall(request);
    okhttp3.Response okHttpResponse = toOkHttpResponse(response, okHttpRequest);
        
    // OkHttp never actually receives this call.

    return okHttpResponse;
  }
}

Using this interceptor is no different from using any other OkHttp interceptor:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
  // It's important to add this interceptor last because 
  // any downstream interceptors won't get called.
  .addInterceptor(MrStealYoNetworkCalls.create())
  .build();

Retrofit retrofit = new Retrofit.Builder()
  .client(okHttpClient)
  .build();

That’s all! Retrofit could now be used for making network calls without dealing with SadLibrary and its sad API!

Making Retrofit work for you

Using Retrofit in this manner is very similar to what Nick Butcher does in his beautiful Dribbble client, Plaid. Since Dribbble does not provide a search API, Plaid manually downloads the search page as HTML and parses it using Jsoup. But, instead of leaking this ugliness to the rest of the app, this functionality is exposed through Retrofit as if it were an actual Dribbble API.

/**
 *  Fake-API for searching dribbble
 */
public interface DribbbleSearchService {

  @GET("search")
  Call<List<Shot>> search(@Query("q") String query, ...);
}

The response body is parsed inside a Retrofit Converter (Github link):

/**
 * Dribbble API does not have a search endpoint 
 * so we have to do gross things :(
 */
public class DribbbleSearchConverter implements Converter<> {

  @Override
  public List<Shot> convert(ResponseBody searchResponse) {
    String html = searchResponse.string();
    Document document = Jsoup.parse(html, "https://dribbble.com");
    return parseDribbbleShots(document);
  }
}

And used like a boss (Github link):

DribbbleSearchService searchApi = new Retrofit.Builder()
    .addConverterFactory(new DribbbleSearchConverter.Factory())
    .build()
    .create(DribbbleSearchService.class);

You can watch Jake Wharton explain this in more detail in his talk, Making Retrofit Work For You (GDG Cincinnati).

Cover photo by Bryan Colosky.