TL;DR:

Any DAO methods become suspend functions, including transactions .

functions, . No need for a Retrofit adapter anymore, and any method in the interfaces used with Retrofit also becomes a suspend function.

function. Any Room or Retrofit calls can then safely be made from a ViewModel within a coroutine launched from the viewModelScope extension.

within a coroutine launched from the extension. Error handling is… a little tricky?

Let's get started

We'll work with a simple app that fetches posts from the JSONPlaceholder API, stores them in a database and shows them in a list where users are able to pull to refresh.

This is the repository:

The master branch has everything working with RxJava, and the coroutines branch switches everything from RxJava to coroutines. You can go ahead and check all the changes in the single commit there, but we'll go through each one of them here.

Dependencies

These are the dependencies that we'll be moving away from:

We'll basically remove all of those and replace with these:

Yup, two less dependencies! The changes here are pretty straightforward:

We obviously need coroutines-core , and you can read more about that coroutines-android here.

, and you can read more about that here. Room's coroutine support has been around for a few months starting with Room 2.1, and it's now part of the room-ktx artifact. More info here.

artifact. More info here. Retrofit's coroutine adapter has also been around for a while, but now Retrofit has built-in suspend support, so we don't need a separate adapter dependency anymore!

Room

The changes in the Room code are also pretty straightforward: we can basically have all our DAO methods as suspend functions, so we can let Room take care of the thread they'll run on.

First let's start with what we currently have:

The first function is exposing observable data from the database. A true RxJava implementation would return a Flowable or an Observable , but instead I'm returning a LiveData . I don't want to explore converting that to coroutines because, as Christina Lee says in her talk, coroutines don't really fit great here — and that's something that didn't change with time. There's still no Room support for Channels or Flows (they're getting close to a stable API!), but this is already officially part of the next steps for Room, so soon things will be more interesting here.

Edit: Room 2.2.0 now fully supports Kotlin Flow, and Lifecycle 2.0.0 also started supporting Coroutines and LiveData integration!

LiveData is already a pretty good RxJava alternative there (and it actually plays well with coroutines), so I decided to start right away with it (going from RxJava to LiveData would be out of scope for this post). So let's just ignore posts() and look at the rest:

The first two functions are very simple: clear() removes all the data from the table, and insert() adds data to it. Both return Completable . The idea behind clearAndInsert() is to just execute both in a transaction. Those blockingAwait() calls look very odd, though, right? This is actually how I tried to write that initially:

It looks super cute and it also returns a Completable now. However, this is the error we get with this:

Method annotated with @Transaction must not return deferred/async return type io.reactivex.Completable. Since transactions are thread confined and Room cannot guarantee that all queries in the method implementation are performed on the same thread, only synchronous @Transaction implemented methods are allowed. If a transaction is started and a change of thread is done and waited upon then a database deadlock can occur if the additional thread attempts to perform a query. This restrictions prevents such situation from occurring.

And that's why we end up with those blockingAwait() calls. We could just remove the Completable from the other two methods to simplify clearAndInsert() , but then we'd affect any clients directly calling the other two methods, which is something we probably don't want to.

This is a limitation that goes away with coroutines:

So with coroutines we can simply have this:

No more Completable s and everything is a suspend function now, including the transaction. It looks beautiful and it's exactly what I'd expect from the library ❤️.

There's also a pretty useful database.withTransaction extension function that might be useful in some other cases. You can read about it here or check the I/O video here.

Retrofit

This is how we're creating our Retrofit instance:

As we said before, Retrofit now has built-in suspend support, so we don't need to add an adapter when constructing our instance anymore, and we can simply remove that line #8:

The changes we need in our API interface are similar to the changes we made in our DAO class: we can simply get rid of any RxJava and make all our methods suspend functions. So we go from this:

To this:

ViewModel

To keep things simple, there's no repository layer and the ViewModel is the consumer of both the PostDao and PostApi . It exposes data in the form of a PostViewState through a LiveData that can be observed by the activity.

We're interested in the refresh() method that fetches data from the API and persists it in the database:

Because we're passing Schedulers.io() when creating our RxJava2CallAdapterFactory , there's no need to subscribeOn() calls.

when creating our , there's no need to calls. This method also updates the view state with whether there's data being loaded or if an error happened. Those state.update() calls look fancy but they're just extensions updating the current value held by a custom MediatorLiveData which is ultimately exposed to the activity as a LiveData .

calls look fancy but they're just extensions updating the current value held by a custom which is ultimately exposed to the activity as a . By default, we need to be in the main thread in order to update the value of a LiveData . That's how that update() extension was written. However, we could instead use postValue() so we wouldn't need to worry about in which thread we are when updating the value in the LiveData . I'd rather not mix up threading too much, though, but this would simplify the chain a little, so I guess that's a valid alternative:

How does this look like with coroutines? Well, let's take a look:

The best thing about this is that there's no threading concerns in the code. Any regular calls there will be executed on the main thread, like state.update() , for instance. But whenever we call a suspend function from Room or Retrofit, it'll properly be executed in another thread.

The viewModelScope is a nice ViewModel property extension that gives us a CoroutineScope that follows the lifecycle of the ViewModel . What that means is that in most cases we can just use it and avoid having to manually take care of releasing resources — it's kinda like having AutoDisposable out of the box.

And finally, it's really nice to see what really matters without too much distractions around. Lines #4 to #6 carry all the logic we're worried about there, and even though I personally like that line #5, we can easily make it less dense by introducing some local val s.

But let's talk about error handling. That runCatching is a disguised try-catch block catching all exceptions, which we all know isn’t a great practice. But if we really want to translate what we had before with RxJava, that's what we'd do since RxJava would give us any thrown exception within its onError() callback.

There's a catch here, though. Let's look at that handleFailure() function:

You see that nasty first line? When coroutines are cancelled, they do so by propagating an exception. If we catch all exceptions, we'll catch that one too. Because we're using viewModelScope , the coroutine will only be canceled when the ViewModel is destroyed (when onCleared() is called). In that case nothing would happen. But if at some point we decide to cancel the job in another moment and we're not handling that, we risk catching that exception and showing a misleading error message to the user.

I feel like error handling with coroutines isn't a very clear topic yet. Many blogposts you'll read will tell you to avoid catching all exceptions, but won't tell you exactly what to do instead. We could just worry about Retrofit's HttpException like it's suggested here, but what happens when there's no connection and an IOException is thrown? Because we still don't have multi catch in Kotlin, things start to get ugly pretty quickly:

We could also consider wrapping Retrofit's call with a Response to help us with that, or get creative with interceptors to handle errors there instead (like this). I'm honestly not sure what's the best approach, but hopefully we'll have that more figured out at some point.

Edit: turns out there's a more idiomatic way to achieve what we want here, and it's called CoroutineExceptionHandler (thanks to Bradyn Poulsen for bringing it up). That's basically the proper way of having a generic catch block that will automatically ignore CancellationException . This is what it looks like:

And we can create a neat extension function to make things look prettier: