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:
- 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. - Every method in a Retrofit interface must be annotated with both the HTTP method type (
GET,POST,PUTetc) 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. - 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
@Pathannotation to specify that this argument should fill in{repo}portion of the path. It also uses the@Headerannotation to specify that this particular call should also include anIf-Modified-Sinceheader with the annotated argument being the value that corresponds to the header. - 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
Observableinstance that emits objects wrapped in Retrofit’sResponseobject and that contains data corresponding to the typeList<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:
- The
ScalarsConverterFactory, enables Retrofit to convert JSON objects into simple Java primitive types and strings. - 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:
-
clicksis anObservable<Unit>representing clicks on the bottom FAB. -
keyChangesis anObservable<CharSequence>representing text changes to the left-mostEditTextobject. -
valueChangesis anObservable<CharSequence>representing text changes to the right-mostEditTextobject.
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:
- You’re building a new
creationObservableby chaining off of thebuttonObservableyou 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 thetakeoperator to limit the number of items emitted by thebuttonObservableto just one. - You’re then constructing a JSON object by using the
mapoperator and breaking apart thePair<CharSequence, CharSequence>you received fromButtonObservable. 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 callingmap, you’re posting the new JSON object to thejsonTextLiveDataobject so the user can immediately see the JSON they constructed. - You’re then using
flatMapto create a new Observable by using the newcreateJsonmethod you declared earlier. - The result of that last
flatMapis that you’re now operating on anObservable<String>. What you really care about is the ID of the new JSON object you created on the JSONBlob API. So you’re using amapoperator to pull out the ID portion of theURIon 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:
- Just like before, you’re using
flatMapto start streaming the events from thebuttonObservable. - You’re then using the
mapoperator to build up a new JSON object that includes the latest values in the key and valueEditTexts. After that, you’re using anothermapoperator to create aPair<String, String>by combining thebinIdvalue from thecreationObservablewith the new JSON string. - You’re then using the
flatMapCompletableoperator to take thePair<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 usingflatMapCompletablebecause the return type for theupdateJsonmethod isCompletable.
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:
- The first is that you’re using the
Singlereactive type. Once again Retrofit is amazingly flexible in what return types you can specify for your methods.Singleis a fantastic choice when using Retrofit since your network calls will almost always return a single result and then finish. Setting the return type asSingle<YourResponseObject>lets you more clearly specify the expected structure of your HTTP calls. - The second interesting piece is that you’re using Retrofit’s built-in
Responseobject. You used theResponseobject earlier when creating the JSON, but it’s worthwhile to dig in here. Retrofit always allows you to wrap your model object in itsResponseobject. TheResponseobject 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 theResponseobject, you could easily set the return type of this method to beSingle<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:
-
You could include the
Responseobject as a wrapper to your model type in the return type. That means having a return type that looks likeSingle<Response<MyObject>>. In this scenario, if your server returns a non successful (i.e., non 2xx) status code, aResponse<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 theResponseobject to figure out if the call was successful or not. You’ll see an example of this later on. -
Alternatively, if you exclude the
Responseobject and instead specify your return type to look something likeSingle<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, yourSingle<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:
- Instead of using
flatMapCompletable, you’re switching to use a normalflatMapmethod. That means the return object in yourflatMaplambda will have to be anObservableinstead of aCompletable. - You’re again calling the
JsonBinApi.updateJsonmethod, passing through the bin id and the new JSON object. - Instead of calling it quits after updating the JSON object through the API, you’re chaining a call to
JsonBinApi.getJsonafter the initial call to update the JSON object. SinceJsonBinApi.updateJsonreturns aCompletable, you can use theandThenmethod to execute another reactive type after the completable finishes. Finally, sinceflatMapexpects anObservableto be returned in its lambda, you’re using thetoObservablemethod to turn theSingle<Response<String>>object returned byJsonBinApi.getJsoninto anObservable<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
subscribeOnandobserveOnoperators to do your network operations off themainthread. - You can wrap your custom model types in the
Responseobject 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
Responseobject you’ll see fewer exceptions in yoursubscribeerror 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.