Coinbase Logo

Supercharging Retrofit with Kotlin

By Author

Product

, January 28, 2019

, 2 min read time

1*0uVlXhN1SYC1CcZNuOpE_Q.webp

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:

authApi.getTokens() .subscribe({ response: Response<AccessToken> -> when { response.isSuccessful && response.body() != null -> { // Case 1: Success. We got a response with a body. } response.errorBody() != null -> { // Case 2: Failure. We got an error from the backend, deserialize it. val error = moshi.adapter(Error::class.java).fromJson(errorBody.source()) } else -> { // Case 3: Failure. Response didn't have a body. Show a vanilla error. } } }, { t ->

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.

authApi.getTokens() .subscribe { response : NetworkResponse<AccessToken, Error> -> when (response) { is NetworkResponse.Success<AccessToken> -> { // A 2XX response that's guaranteed to have a body of type AccessToken. } is NetworkResponse.ServerError<Error> -> { // A non-2XX response that may have an Error as its error body. } is NetworkResponse.NetworkError -> { // A request that didn't result in a response from the server. } } }

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."

/** * A [CallAdapter.Factory] which allows [NetworkResponse] objects to be * returned from RxJava streams created by Retrofit. * * Adding this class to [Retrofit] allows you to write service methods like: * * fun getTokens(): Single<NetworkResponse<AccessToken,Error>> */ class KotlinRxJava2CallAdapterFactory : CallAdapter.Factory() {

override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? {

// This adapter only handles creating RxJava streams. If the caller // isn't asking for an RxJava stream, return null, this isn't the right // adapter! val rawType = getRawType(returnType) val isFlowable = rawType === Flowable::class.java val isSingle = rawType === Single::class.java val isMaybe = rawType === Maybe::class.java if (rawType !== Observable::class.java && !isFlowable && !isSingle && !isMaybe ) { return null }

// Check to see if the RxJava stream is emitting instances of NetworkResponse. // If not this isn't the right adapter, return null! val observableEmissionType = getParameterUpperBound(0, returnType) if (getRawType(observableEmissionType) != NetworkResponse::class.java) { return null }

// Ask Retrofit for an adapter that's capable of creating an instance // of Observable<AccessToken> val successBodyType = getParameterUpperBound(0, observableEmissionType) val delegateType = Types.newParameterizedType( Observable::class.java, successBodyType ) val delegateAdapter = retrofit.nextCallAdapter( this, delegateType, annotations )

// Ask Retrofit for a serializer than can serialize an instance of Error val errorBodyType = getParameterUpperBound(1, observableEmissionType) val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>( null, errorBodyType, annotations )

return KotlinRxJava2CallAdapter( successBodyType, delegateAdapter, errorBodyConverter, isFlowable, isSingle, isMaybe ) } }

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)

  • Asks for a converter capable of serializing the type

     

    Error

     

    (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!

Coinbase logo