Building a REST Client with Kotlin and Retrofit2

If you regularly work with REST endpoints you're probably used to a lot of tedium. Between carefully constructing the parameters, encoding data in just the right format, efficiently sending the requests, parsing the results and handling errors at every step along the way; there is a lot of room to make mistakes.

For simple APIs, it often makes sense to manually format the request and send it out. Checking a status code, and maybe mapping results to an object aren't that big a deal for a simple API. But what if there are 20 different endpoints? Or you need to manage authorization headers? Eventually, it makes sense to start looking for a wrapper library, or maybe making your own wrapper layer.

That's what this post is about.

For JVM developers, pairing Kotlin, Retrofit2 and Gson make it easy to bang out a type-safe client wrapper.

Let us jump right into the code:

// Create data classes for serializing inputs and outputs.
data class Results<out T>(
    val next: String?,
    val previous: String?,
    val results: List<T>
)

data class CryptoQuote (
    val ask_price: Double,
    val bid_price: Double,
    val mark_price: Double,
    val high_price: Double,
    val low_price: Double,
    val open_price: Double,
    val symbol: String,
    val id: String,
    val volume: Double
)

// Bounds, Intervals, and Spans are enum types.
data class Historical (
    val data_points: List<DataPoint>,
    val bounds: Bounds,
    val interval: Intervals,
    val span: Spans,
    val symbol: String,
    val id: String,
    val open_price: Double?,
    val open_time: Date?,
    val previous_close_price: Double?,
    val previous_close_time: Date?
) {
  // Nested data object used by Historical
  data class DataPoint(
      val begins_at: Date,
      val open_price: Double,
      val close_price: Double,
      val high_price: Double,
      val low_price: Double,
      val volume: Double,
      val session: String,
      val interpolated: Boolean
  )
}

// Define interface using Retrofit annotations and data objects.
interface RobinhoodCryptoQuotesApi {
  /**
   * @param ids comma separated list of ids
   */
  @GET(marketdata/forex/quotes/)
  fun getQuoteIds(
    @Query(ids) ids: String
  ): Call<Results<CryptoQuote>>

  /**
   * @param symbols comma separated list of symbols
   */
  @GET(marketdata/forex/quotes/)
  fun getQuoteSymbols(
    @Query(symbols) symbols: String
  ): Call<Results<CryptoQuote>>

  /**
   * @param symbol id or symbol for a pairing.
   */
  @GET(marketdata/forex/quotes/{symbol}/)
  fun getQuote(
    @Path(symbol) symbol: String
  ): Call<CryptoQuote>

  
  @GET("marketdata/forex/historicals/{id-pair}/")
  fun getHistoricals(
      @Path("id-pair") idPair: String,
      @Query("bounds") bounds: Bounds,
      @Query("spans") spans: Spans,
      @Query("interval") interval: Intervals
  ): Call<Historical>
}

This is a complete listing of the Robinhood market data endpoints. If you aren't familiar with Kotlin, this code is essentially declaring an interface and some POJOs. By sprinkling in some special annotations used by Retrofit2, it can be converted into a concrete object for accessing the endpoints. Errors are even handled by the Call objects which wrap your results.

To go from endpoint definition to concrete object we need to invoke the Retrofit2 builder. While the HttpClient doesn't really support anything besides OkHttp, there are hooks to adapt the serialization and call adapter layers. Here you can see I configure OkHttp to insert a token header, and add Gson and Enum adapters for serialization.


  // Inject the authentication header.
  private class TokenInterceptor(private val token: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
      val newRequest = chain.request().newBuilder()
          .addHeader("Authorization", "Token $token")
          .build()
      return chain.proceed(newRequest)
    }
  }

  // Build a token aware HttpClient
  val tokenHttpClient = OkHttpClient.Builder()
      .addInterceptor(TokenInterceptor("my-token"))
      .build()

  // Build the api
  val api = Retrofit.Builder()
      .addConverterFactory(GsonConverterFactory.create())
      .addConverterFactory(EnumRetrofitConverterFactory())
      .baseUrl("https://api.robinhood.com/")
      .client(tokenHttpClient)
      .build()
      .create(RobinhoodCryptoQuotesApi::class.java)

  // Make a request 
  val response = api.getHistoricals(
      idPair = "3d961844-d360-45fc-989b-f6fca761d511",
      spans = Spans.DAY,
      bounds = Bounds.TWENTY_FOUR_SEVEN,
      interval = Intervals.DAY
  ).execute()

  // Print out some data  
  for(data in response!!.body()!!.data_points) {
    println(data.high_price)
  }

And that's all! In the final code, there was some additional work to make an OAuthTokenInterceptor, but all of that complexity is constrained to the constructor. Defining the endpoints is a matter of typing out the data classes for serialization, naming your methods, and applying the Retrofit2 annotations.

All in all, Retrofit is a very concise way to define a client library in a type-safe way. The code described in this article can be found on GitHub.

Comments

  1. I came to correct point I guess.. Quick question. where did you get the GUID for BTC? also how did you find the API parameters? if you wouldn't mind? I am trying to find the GUIDs for other coins also. Appreciate your help..

    ReplyDelete

Post a Comment

Popular posts from this blog

Decommissioning the cloud: A self-hosted future

Using a JFileChooser to browse AWS S3

Taking Stuff Apart: Realistic Modulette-8