At Qusion, we recently started working with GraphQL on mobile. So, we did some research on what are the best practices and we decided on using the Apollo Android library. We also wanted to use that in a Kotlin "coroutine world", so in this article I'm going to share what we learned and how we structured our code.
Anyone who stumbes into Apollo's Getting Started guide is greeted with lots of Java Callbacks in the examples. But, deeper in their github documentation there is a small section on support for RxJava and Kotlin Coroutines. And we will stricly focus on the latter. Of course, Rx is a solid way, but I've found that coroutines are very powerful and easier to organize.
The apollo library has a very clean extension function on ApolloCall that creates a Deferred object and enqueues it in the Callback. This is what we are going to use in order to convert this callback-based system to a nice coroutine world.
This gives us a way to do one-shot calls to our back-end or local database since it only returns the first response. Now we can use that in a repository:
Let's quickly unpack this code. The ApolloService is just a wrapper around the library's ApolloClient. The .await() keyword is suspending (waits for the result of the Deferred object). This is executed sequentually and that's the reason the function is a suspend function, i.e it will be called in some launched coroutine.
The flaw here is that this is a very "simplistic" approach, i.e there is no network nor business error catching. And that logic is usually shared between all the queries/mutations, so it's way nicer to abstract it in a single place. That place would be our custom ApolloService class. We need to expose an interface with generic query/mutation functions. We also need a way to encapsulate state. This is where Kotlin's sealed classes come in play.
(Strongly recommend using sealed classes for any sort of "state representation")
Now having this in mind, the ApolloServiceImpl looks like this:
We have a single place where all the errors of any query/mutation are caught. The repositories only knows how to build the actuall query data (the generated classes from apollo) and just passes it to this function. That way we don't put too much responsibility in a single layer.
The call side in the reposity looks much cleaner now:
Let's move closer to the UI and see how the ViewModels should behave. We need a scope to launch this coroutine and, when we get the result, we need some LiveData to update. We pass in an IO Dispatcher, to notify that this coroutine will be doing some "heavy" work. For the scope we use `viewModelScope`, which is tied to the lifecycle of the ViewModel, so when it gets destroyed, all the coroutines are cancelled and there are no leaks.
This is a simple usage in a ViewModel:
With that, we wrap the whole flow. What we accomplished is the following: We can do a simple "fire and forget" query (to do some heavy work off the main thread), in which all the error are caught and the response is used to properly update the UI. All of that in a nice readable way with Kotlin coroutines.
There is one "catch" in our previous approach. On mobile we sometimes need to show cached data first while we try to fetch the latest from the network. And we can't do that elegantly with our initial one-shot query approach.
Luckily, Kotlin has Channel and Flow that work perfectly in this use-case. Channel is a Kotlin primitive used to communicate between coroutines and Flow is a cold (asyncronous) stream of data that can emit values, to be later consumed.
There is of course much more to it that what I just mentioned, but it's enough to help us tackle this specific use-case. It's definetly worth knowing more about Flows and Channels tho.
The idea is to feed a Channel (offer/send data to it) wherever we get a response from an enqueued apollo callback. Then send a NetworkResult of that reponse to a Flow that is consumed somewhere in the ViewModel.
So let's create a nice ApolloCall extension function to give us the mentioned Flow.
The `flow` keyword creates a Flow (who would've guessed right?) and allows us to emit values inside the builder block function. That's where we enqueue an ApolloCallback with out Channel, i.e. the ChannelCallback class, which has a very strightforward implementation. (forwarded straight from apollo's source code).
So now our ApolloService implementation is very clean. We changed the return type to Flow and called the created extension function.
The way to consume it near the UI is still in a ViewModel, using a liveData builder. That way when the live data becomes active we create the coroutine and enqueue the call, thus we start recieveing values to our exposed Flow.
I know that the code snippets require some prior knowledge of Apollo and Coroutines to fully unpack every bit. But the goal of this article is to show (or "expose") a way to nicely use Apollo in conjunction with Kotlin coroutines. It's a single idea, a way of thinking and approaching Apollo, that I hope will be useful to the readers. I hope it inspires better and more fleshed out solutions!
I also talked in depth about this recently in a Kotlin/Everywhere event. There will be public recording soon (I hope), which I will link here.