Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

18. Retrofit
Written by Alex Sullivan

Throughout this book, you’ve often used the popular Retrofit library to build your apps. In this chapter, you’ll further explore how exactly Retrofit interfaces with the Rx world and how you can take advantage of all that it offers.

Getting started

For this chapter, you’ll build a JSON-viewing app. The app you’ll build will allow you to add rows to a JSON object, save that object to the JSONBlob (https://jsonblob.com/) storage API and then retrieve that saved JSON string.

While building the app, you’ll explore the different options you have when interacting with Retrofit.

Open the starter project for the chapter and run the app. You’ll see a white screen with an empty JSON object, signified with the {} text. You’ll also see two EditTexts and a FloatingActionButton (FAB) at the bottom of the screen.

That’s where you’ll add the new rows for the JSON object.

Recap of Retrofit

Before you start exploring how Retrofit interacts with RxJava, it’s worth taking a moment to recap what Retrofit is.

Retrofit is an open-source, networking library made and maintained by the Square team. It allows you to declare your networking interface via an interface. It abstracts away the tedious boilerplate of setting up HTTP connections and executing them. A typical Retrofit interface will look like this example from Chapter 8, “Transforming Operators in Practice”:

// 1
interface GitHubApi {
  // 2
  @GET("repos/ReactiveX/{repo}/events")
  // 3
  fun fetchEvents(@Path("repo") repo: String,
      @Header("If-Modified-Since") lastModified: String)
      // 4
    : Observable<Response<List<AnyDict>>>
}

Here’s a breakdown of the above code:

  1. As mentioned earlier, Retrofit requires you to declare your API in an interface. You don’t need to worry about implementing the interface, though; Retrofit provides a simple hook to create an instance for you.
  2. Every method in a Retrofit interface must be annotated with both the HTTP method type (GET, POST, PUT etc) and the relative path to the APIs endpoint. This path doesn’t declare the full API endpoint. Instead, you provide a root URL when using that Retrofit hook to create the interface. You can even make the relative path dynamic. In this example, the method expects to receive an argument that will ultimately fill in the {repo} section of the relative path.
  3. You can name your Retrofit methods whatever you want. What’s more important is how you annotate the arguments that will be passed in to the method. This example uses a dynamic path, so you need to use the @Path annotation to specify that this argument should fill in {repo} portion of the path. It also uses the @Header annotation to specify that this particular call should also include an If-Modified-Since header with the annotated argument being the value that corresponds to the header.
  4. One of the beautiful parts about Retrofit, and the piece that you’ll interact with the most for this chapter, is the fact that you can choose your return type for your API methods, and Retrofit will do it’s best to give you objects that correspond to that type. The above code is telling Retrofit to provide an Observable instance that emits objects wrapped in Retrofit’s Response object and that contains data corresponding to the type List<AnyDict>. Now that’s a super-powered library!

While the above code specifies Observable as a return type for the fetchEvents method, if you’re not using RxJava you’d typically use the Call<T> object as a return type. You could even just specify the actual model object as a return type. If, instead of Observable<Response<List<AnyDict>>> you specified List<AnyDict> as the return type, Retrofit would interpret that as a blocking network call.

Retrofit uses the OkHttp HTTP client under the hood and allows you to customize it to your heart’s desire. This chapter’s project will use a custom OkHttp instance to log all network calls.

Including Rx adapters

Open the JsonBinService class and look at the create method in the companion object. At the bottom of the method, you’re declaring an instance of the Retrofit object with the following code:

val retrofit = Retrofit.Builder()
  .baseUrl(JsonBinApi.API)
  .client(client)
  .addConverterFactory(ScalarsConverterFactory.create())
  .addConverterFactory(GsonConverterFactory.create())
  .build()

Notice the two lines calling the addConverterFactory method on the Retrofit builder.

addConverterFactory takes an instance of Converter.Factory. This converter plugin architecture is how Retrofit serializes and deserializes the types that the network returns. In this block, you’re specifying two different converters:

  1. The ScalarsConverterFactory, enables Retrofit to convert JSON objects into simple Java primitive types and strings.
  2. The GsonConverterFactory, allows you to plug Gson, a JSON serialization library, into the Retrofit converter architecture. Without this converter factory, you wouldn’t be able to tell Retrofit to return complex model types since it would have no way of deserializing its JSON representation into the objects.

In addition to these converter factories, Retrofit also lets you specify custom call adapter factories that allow you to customize the return type of your interface methods.

To specify Rx return types in your Retrofit interface methods, you’ll need one of these CallAdapter.Factory instances. Luckily, the square team provides a separate library that exposes just such a factory.

Open the build.gradle file and add the following dependency to the dependencies block:

implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version"

Open the JsonBinService class. Now, update the Retrofit.Builder to specify the new CallAdapter.Factory:

val retrofit = Retrofit.Builder()
  .baseUrl(JsonBinApi.API)
  .client(client)
  .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
  .addConverterFactory(ScalarsConverterFactory.create())
  .addConverterFactory(GsonConverterFactory.create())
  .build()

You can now specify reactive return types for your Retrofit methods.

Creating a JSON object

Now that you’ve got Retrofit properly configured, it’s time to create a JSON object and save it with the JSONBlob API.

Open the JsonViewModel class and look around. There’s three important fields to look at:

private val clicks = PublishSubject.Create<Unit>()
private val keyChanges = BehaviorSubject.create<CharSequence>()
private val valueChanges =
    BehaviorSubject.create<CharSequence>()

Each of these arguments represents some action done by the user.

In the above:

  • clicks is an Observable<Unit> representing clicks on the bottom FAB.
  • keyChanges is an Observable<CharSequence> representing text changes to the left-most EditText object.
  • valueChanges is an Observable<CharSequence> representing text changes to the right-most EditText object.

Add the following code to the init block of JsonViewModel:

val buttonObservable = clicks
  .flatMap {
    Observables.combineLatest(keyChanges, valueChanges)
  }
  .share()

You’re using flatMap to create a new Observable every time the user clicks on the floating action button. The new Observable will combine the latest values emitted by the keyChanges and valueChanges Observables, thus emitting the current text in the left key EditText and the right value EditText every time the user clicks the FAB.

To accomplish this combination, you’re using the Observables.combineLatest method exposed by the RxKotlin library. Finally, you’re using the share operator so that you can subscribe to the resulting Observable multiple times.

You’ve now got an Observable that will emit a Pair<CharSequence, CharSequence> representing the current text in the key EditText and the value EditText. You now want to use that Observable to create a new JSON object in the JSONBlob API.

Before you can wire up the creation logic in the JsonViewModel, you’ll need to add a new Retrofit method that sends a JSON object to the JSONBlob API.

Open JsonBinService and add the following below the companion object declaration:

@POST("jsonBlob")
@Headers("Content-Type:application/json")
fun createJson(@Body json: String): Observable<Response<String>>

You’re creating a new method that will POST a JSON string to the JSONBlob jsonBlob endpoint. The POST will deliver a payload of the initial JSON string. The return type of your createJson method will be Observable<Response<String>>.

Now that the Retrofit method has been created, you can update the JsonBinApi class to reference it.

Open JsonBinApi and replace the existing body of the createJson method with the following:

return service.createJson(json).map {
  it.headers().get("Location")
}

It calls the createJson method you just defined. It then inspects the headers object of the Response to find the URI of the bin where your newly created JSON is stored.

With the Retrofit method set up and your API ready to go, it’s time to actually send some JSON to the API.

Open the JsonViewModel class and add the following code below the buttonObservable declaration:

val creationObservable = buttonObservable
  // 1
  .take(1)
  // 2
  .map { "{\"${it.first}\":\"${it.second}\"}" }
  .doOnNext { jsonTextLiveData.postValue(it) }
  // 3
  .flatMap {
    JsonBinApi.createJson(it).subscribeOn(Schedulers.io())
  }
  // 4
  .map { it.substringAfterLast("/") }
  .cache()

Here’s a breakdown of the above code:

  1. You’re building a new creationObservable by chaining off of the buttonObservable you designed earlier. You only want to create a JSON object in the JSONBlob API once. After that, you’ll update the object, so you’re using the take operator to limit the number of items emitted by the buttonObservable to just one.
  2. You’re then constructing a JSON object by using the map operator and breaking apart the Pair<CharSequence, CharSequence> you received from ButtonObservable. The JSON object string may look a bit funky, but that’s just because you need to use an escaping character, \, to include quotation marks in the string. After calling map, you’re posting the new JSON object to the jsonTextLiveData object so the user can immediately see the JSON they constructed.
  3. You’re then using flatMap to create a new Observable by using the new createJson method you declared earlier.
  4. The result of that last flatMap is that you’re now operating on an Observable<String>. What you really care about is the ID of the new JSON object you created on the JSONBlob API. So you’re using a map operator to pull out the ID portion of the URI on the result.

Now you’re cooking with gas!

Add the following code at the bottom of the init method to subscribe to the creation Observable:

creationObservable
  .subscribe()
  .addTo(disposables)

You’re using the cache method so that you can reference the ID of the JSON you created at a later point.

Now, run the app. Enter some text in both of the EditTexts and click the FAB. You should see a JSON object appear on your screen.

Updating the JSON

After creating and storing a JSON object in the JSONBlob API, it’s time to update that object with new values.

Add the following code to the JsonViewModel init method right below the creationObservable declaration but before subscribing to it:

val updateObservable = creationObservable
  .flatMap { buttonObservable }
  .map {
    createNewJsonString(it.first, it.second,
      jsonTextLiveData.value!!)
  }

You’re creating a new updateObservable by calling flatMap on the cached creationObservable and returning the buttonObservable you defined earlier. You’re then using the map operator to take the latest input from the buttonObservable and creating a new JSON string from it and the current JSON string, which is stored in the jsonTextLiveData object.

By subscribing to updateObservable, you’ll ensure that the creationObservable is run and then after the initial JSON object is created you switch to just emitting new JSON strings. By using flatMap here, you’re able to chain the creation of a JSON object into the updating of that object.

All that’s left to do is to subscribe to the updateObservable. Remove the existing code that subscribes to creationObservable and replace it with the following:

updateObservable
  .subscribe {
    jsonTextLiveData.postValue(it)
  }
  .addTo(disposables)

Run the app. Add an initial key and value, and then tap the FAB. Next, try adding a different key and value, and then tap the FAB. You should see an ugly chunk of JSON.

Now, edit either the key or the value in preparation for adding another line.

Woah. Something weird is happening.

Every time you update the text, even if you don’t click the FAB, the JSON blob is being updated. That certainly shouldn’t happen. Spend a minute or so making cool pyramid designs.

After you’re done playing around with the bug, look back at the creation of the buttonObservable:

val buttonObservable = clicks
  .flatMap {
    Observables.combineLatest(keyChanges, valueChanges)
  }
  .share()

buttonObservable is supposed to emit a Pair<CharSequence, CharSequence> whenever the button is clicked. However, that’s not actually what the above code does!

Instead, buttonObservable, as it’s currently defined, emits a Pair<CharSequence, CharSequence> when the button is entered and then every time either EditText is changed. The problem lies in the combineLatest call. combineLatest will emit a pair anytime either of the EditText objects change.

To fix the bug, update append a take(1) operator to the end of combineLatest:

val buttonObservable = clicks
  .flatMap {
    Observables.combineLatest(keyChanges, valueChanges)
      .take(1)
  }
  .share()

Now only the first pair of CharSequences will be emitted whenever the button is tapped. Run the app and confirm that you’ll only see changes to the JSON on the screen when you tap the FAB.

You’re now updating the JSON that the user sees, but you’re not actually saving the updated JSON on the JSONBlob API.

Open the JsonBinService Retrofit interface and add the following method below the createJson method you added earlier:

@PUT("jsonBlob/{id}")
@Headers("Content-Type:application/json")
fun updateJson(@Path("id") binId: String, @Body json: String): Completable

In the above, updateJson takes an ID of the JSON “bin” to update and a new JSON string. It uses the PUT HTTP method to update the JSON object at the given bin. You don’t actually care about what the server returns when you hit the endpoint, so you’re making the return type Completable. Now, you’ll be able to call this method and be notified when it finishes without actually caring about any data that comes with it.

You’ll often find that using the Completable return type pairs well with a REST PUT request, since it’s entirely mutative. Retrofit’s fluent call adapter functionality allows you the flexibility to declare the types that make the most sense for your HTTP calls.

Open the JsonBinApi class and replace the body of the updateJson method with the following:

return service.updateJson(bin, json)

Now that you’ve got your API calls ready to go, it’s time to update the JsonViewModel to actually save off the new JSON. Replace the existing updateObservable declaration with the following:

val updateObservable = creationObservable
  // 1
  .flatMap { binId ->
    buttonObservable
      // 2
      .map { createNewJsonString(it.first, it.second,
        jsonTextLiveData.value!!) }
      .map { binId to it }
  }
  // 3
  .flatMapCompletable {
    JsonBinApi.updateJson(it.first, it.second)
      .subscribeOn(Schedulers.io())
  }

Here’s a breakdown of the above code:

  1. Just like before, you’re using flatMap to start streaming the events from the buttonObservable.
  2. You’re then using the map operator to build up a new JSON object that includes the latest values in the key and value EditTexts. After that, you’re using another map operator to create a Pair<String, String> by combining the binId value from the creationObservable with the new JSON string.
  3. You’re then using the flatMapCompletable operator to take the Pair<String, String> object produced earlier in the chain and sending both the bin id and the new JSON object through to the JSONBlob via the Retrofit method you implemented earlier. You’re using flatMapCompletable because the return type for the updateJson method is Completable.

To complete the JSON updating flow, replace the existing code that subscribes to updateObservable with the following:

updateObservable
  .subscribe()
  .addTo(disposables)

You’re now sending JSON updates through to the JSONBlob API.

There’s only one issue: You’re never actually printing the new JSON to the screen. Since the jsonTextLiveData object isn’t being updated, the JSON you’re generating and sending to the JSONBlob API isn’t being built up. Instead, you’re only ever sending up a JSON object with two lines: the first line when you created the object and the new values from your EditText streams.

You could simply add a doOnNext operator in the Observable chain you just wrote and emit the new JSON values. However, the JSONBlob API exposes an endpoint that allows you to fetch the current JSON object in a bin, so you can be confident that your JSON is actually saved.

That sounds like the best option moving forward.

Retrieving JSON

Open the JsonBinService class again and add the following method below the updateJson method you added earlier:

@GET("jsonBlob/{id}")
@Headers("Content-Type:application/json")
fun getJson(@Path("id") binId: String): Single<Response<String>>

This time, you’re targeting the /jsonBlob/{id} endpoint, which returns whatever JSON is stored in that bin. Just like in the updateJson method, you’re passing in a bin id that will be used to identify your JSON.

For this method, you’re setting the return type as Single<Response<String>>. There’s two interesting things at play, here:

  1. The first is that you’re using the Single reactive type. Once again Retrofit is amazingly flexible in what return types you can specify for your methods. Single is a fantastic choice when using Retrofit since your network calls will almost always return a single result and then finish. Setting the return type as Single<YourResponseObject> lets you more clearly specify the expected structure of your HTTP calls.
  2. The second interesting piece is that you’re using Retrofit’s built-in Response object. You used the Response object earlier when creating the JSON, but it’s worthwhile to dig in here. Retrofit always allows you to wrap your model object in its Response object. The Response object provides several nice to haves, such as HTTP status codes, access to header objects, and any errors that may have been encountered. If you didn’t want to use the Response object, you could easily set the return type of this method to be Single<String>.

Handling errors

This is a good time to pause for a moment and consider how error handling works in Retrofits RxJava integration. No matter what reactive return type you specify, whether its Observable, Single, Completable, or Maybe, if you do not have internet access when you attempt to make a call through Retrofit you will hit the error block of your subscriber. Not particularly surprising but good to point out.

What may be slightly more surprising is that depending on what return type you use with Retrofit, and specifically depending on whether you include the Response object in that return type, you may or may not see HTTP server errors and non successful status calls in your error blocks.

You have two options when deciding on a return type for your Retrofit methods:

  1. You could include the Response object as a wrapper to your model type in the return type. That means having a return type that looks like Single<Response<MyObject>>. In this scenario, if your server returns a non successful (i.e., non 2xx) status code, a Response<MyObject> will still be delivered to the success block of your subscriber, even though the server ultimately rejected the call. You can check the status code of the Response object to figure out if the call was successful or not. You’ll see an example of this later on.

  2. Alternatively, if you exclude the Response object and instead specify your return type to look something like Single<MyObject>, you’ll then see non successful status codes going into the error block. So if your server returned a 404, meaning the resource wasn’t found, your Single<MyObject> would report an error and you would need to make sure to handle that error.

Tying it all together

Now that you’ve got a method in your Retrofit interface to retrieve JSON, you need to update the JsonBinApi class to reference the new method. Replace the body of the getJson method with the following:

return service.getJson(bin)

Now, open the JsonViewModel class. After you call through to the JsonBinApi.updateJson method, you need to retrieve the newly updated JSON and send it through your jsonTextLiveData object.

Remove the existing flatMapCompletable block of the updateObservable and replace it with the following:

// 1
.flatMap { pair ->
  // 2
  JsonBinApi.updateJson(pair.first, pair.second)
    // 3
    .andThen(JsonBinApi.getJson(pair.first))
    .toObservable()
    .subscribeOn(Schedulers.io())
}

Here’s a breakdown:

  1. Instead of using flatMapCompletable, you’re switching to use a normal flatMap method. That means the return object in your flatMap lambda will have to be an Observable instead of a Completable.
  2. You’re again calling the JsonBinApi.updateJson method, passing through the bin id and the new JSON object.
  3. Instead of calling it quits after updating the JSON object through the API, you’re chaining a call to JsonBinApi.getJson after the initial call to update the JSON object. Since JsonBinApi.updateJson returns a Completable, you can use the andThen method to execute another reactive type after the completable finishes. Finally, since flatMap expects an Observable to be returned in its lambda, you’re using the toObservable method to turn the Single<Response<String>> object returned by JsonBinApi.getJson into an Observable<Response<String>>.

Now, update the code at the bottom of the init method that subscribes to the updateObservable and replace it with the following:

updateObservable
  .subscribe {
    if (it.isSuccessful) {
      val prettyJson = JSONObject(it.body()!!).toString(4)
      jsonTextLiveData.postValue(prettyJson)
    } else {
      errorLiveData.postValue("Whoops, we got an error!")
    }
  }
  .addTo(disposables)

In this block, you’re checking the Response object to see if the HTTP call was successful. If it was, you’re formatting your JSON to be nice and pretty and then sending the new JSON string returned by the API into the jsonTextLiveData. If it wasn’t successful you’re sending an error message through the errorLiveData.

Run the app. You should now be able to add as many rows to the JSON object as you want, and you should see a nicely formatted JSON blob.

Key points

  • In order to return any of the reactive types from a Retrofit interface, you have to make sure to include the RxJava3 call adapter library.
  • Once you do include the call adapter library, you can return any of the reactive types you’ve seen in the book. Observable, Flowable, Completable, Single, Maybe — the whole gang’s here!
  • You can use the Observables (or other reactive types) you receive from Retrofit just like any other Observable. Make sure to use the subscribeOn and observeOn operators to do your network operations off the main thread.
  • You can wrap your custom model types in the Response object to get access to HTTP status codes and errors. You can even nest those types inside your reactive types!
  • Make sure to pay extra special attention to how you handle errors when using Retrofit. If you use the Response object you’ll see fewer exceptions in your subscribe error handling code.

Where to go from here?

Retrofit is a great example of a library that makes use of RxJava in a very pragmatic way. Retrofit is a solid addition to any Android project, and even more so when coupled with RxJava.

Be sure to check out the Retrofit repository on GitHub github.com/square/retrofit if you’re interested in taking a deeper dive into the library.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.