Building a Kotlin-esque API for Retrofit

At Coinbase we use Retrofit and Square’s RxJava 2 Adapter as our API to the wire. Retrofit makes networking a breeze, but throughout our app we found ourselves writing code like this:

Handling the result of a network call

This works, but there’s a few rough edges:

Our API isn’t declarative. To determine what state we’re in we have to null check a bunch of things and it’s easy to miss a case

We’ve inadvertently leaked our network serializer (moshi) into our application layer to deserialize error bodies

Streams get torn down when a network error occurs. This isn’t a big deal here, but if we start combining network streams with other Observables we likely don’t want network errors to terminate the resulting stream

Let’s look at how we can use Retrofit’s CallAdapter API to nerf down these edges. We’ll use Sealed Classes to represent the result of network calls and build error body deserialization into Retrofit.

Sealed Classes + Typed Error Body Deserialization — Networking Nirvana

To have Retrofit return an instance of NetworkResponse when the getTokens() API is invoked, we have to write a custom CallAdapter.Factory . The CallAdapter.Factory below says, “I know how to create instances of NetworkResponse that are emitted to RxJava streams."

When the getTokens() API is invoked, this CallAdapter.Factory :

Delegates to an adapter that knows how to make an instance of the type Observable<AccessToken> (line 46)

(line 46) Asks for a converter capable of serializing the type Error (line 54)

(line 54) Creates a KotlinRxJava2CallAdapter (line 60). This adapter deserializes error bodies and will decorate the stream of Observable<AccessToken> into a Single<NetworkResponse<AccessToken,Error>>

To use our custom CallAdapter.Factory we have to plug it into our Retrofit instance. Adapter registration order is important since we’ve written a delegating CallAdapter ; we must register our adapter before any other adapters it may delegate to. In the snippet below we delegate the creation of Observable<AccessToken> to Square’s RxJava Adapter.

val retrofit = Retrofit.Builder()

.baseUrl("https://api.coinbase.com/v2/")

.addCallAdapterFactory(KotlinRxJava2CallAdapterFactory())

.addCallAdapterFactory(RxJava2CallAdapterFactory.create())

.build()

If you’re interested in having a Kotlin-esque API to the wire that provides error body deserialization, you can find the full code for our Adapter here.

P.S. We’re hiring!

Special thanks to Jesse Wilson for catching a bug in the full code for the adapter; all error codes aren’t guaranteed to return JSON!