Apollo Android GraphQL Flow Bindings
Featured in Android Weekly #444 & Kotlin Weekly #228
In this blog post, we’ll explore how the Apollo-Android library gives you the ability to use Flows when making queries. The library provides a coroutines integration module. How does the module bridge to Flows? Let’s explore in this article.
GraphQL Query with Callbacks
The first step to making a GraphQL query with Apollo-Android is to create an Apollo Client. The library provides a builder to create the client.
val client: ApolloClient = ApolloClient.builder()
.serverUrl("url")
.build()
We set a URL to the GraphQL server and retrieve an instance of our client. The second step is to supply a query to the client. Assume we have a query specified.
val testQuery = TestQuery.builder().build()
client.query(testQuery).enqueue(
object : ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
...
}
override fun onFailure(e: ApolloException) {
...
}
override fun onStatusEvent(event: ApolloCall.StatusEvent) {
...
}
})
In the code snippet above, I call the query method and enqueue a callback to get the request’s result. Each method in the callback informs us about different types of information. The onFailure
method will tell us of a thrown exception while making a request. The library will call the onStatusEvent
status changes such as scheduled, completed, and whether the data came from the cache or network. See the Apollo Call class for a list of status events.
Bridge Apollo Callback to Flow
How does the library allow you to make queries using Flows? The library uses callbackFlow provided by the Kotlin coroutines library to provide a bridge from the callback shown above to Flows.
Callback Flow
The documentation for the callbackFlow method states,
“Creates an instance of a cold Flow with elements that are sent to a SendChannel provided to the builder’s block of code via ProducerScope. It allows elements to be produced by code that is running in a different context or concurrently.”
See below how the Apollo Client uses callbackFlow
.
fun <T> ApolloCall<T>.toFlow() = callbackFlow {
clone().enqueue(
object : ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
runCatching {
offer(response)
}
}
override fun onFailure(e: ApolloException) {
close(e)
}
override fun onStatusEvent(event: ApolloCall.StatusEvent) {
if (event == ApolloCall.StatusEvent.COMPLETED) {
close()
}
}
}
)
awaitClose { this@toFlow.cancel() }
}
Source: Apollo Android’s Coroutines Extensions
The library provides the toFlow
extension above on the Apollo Client. As we did earlier, inside the method, the client enqueues an Apollo Callback. It does so inside callbackFlow. Callback Flow provides a channel to send data inside a producer coroutine scope. Anytime a method in Apollo Callback is invoked, an operation is performed on the channel.
The onResponse
method sends data to the callback flow’s channel via the channel’s offer method. The onFailure
method and completed status event close the channel. Lastly, the awaitClose
function keeps the flow running. Otherwise, invoking the callback will close the channel.
The More You Know!
In the implementation above, the channel’s offer method is used to send the returned result. The call to offer method is wrapped around a try and catch. This is due to an known issue in the Kotlin coroutines library. See SendChannel.offer should never throw. At times, calling offer causes a JobCancellationException
to surface to the client.
Usage
The toFlow
method returns a Flow upon which we could apply operators, handle retries, and errors.
val client: ApolloClient = ApolloClient.builder()
.serverUrl("url")
.build()
val query = TestQuery.builder().build())
val flow = client.query(query).toFlow()
Flow Retries
Flow has operators to handle retries and errors. The retry extension on a Flow allows you to specify the number of retries to perform and a condition to determine when to perform a retry.
client
.query(query)
.toFlow()
.retries(2) { throwable ->
}
The first parameter to the retry block is the number of retries to perform. The second parameter gives you a throwable to specify if you want to do a retry on a specific exception.
Flow Errors
If an exception occurs, the catch Flow extension can intercept it.
client
.query(query)
.toFlow()
.catch { throwable ->
}
The catch operator provides a FlowCollector that allows you to emit a wrapper type when an exception occurs.
Using the coroutines integration module in Apollo Android’s library provides you the benefit of using these operators.
How Apollo Flow Binding Evolved
The strategy to bridge from ApolloCallback to Flows has evolved in the previous releases of the library. Previously, the library provided an API that gives you a Channel or a Flow when making a query. Let’s look at the initial implementation of the toChannel
and toFlow
method in the library.
Channel Extension
An earlier version of the library provided a toChannel
method to read results.
class ChannelCallback<T>(
val channel: Channel<Response<T>>
) : ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
channel.offer(response)
}
override fun onFailure(e: ApolloException) {
channel.close(e)
}
override fun onStatusEvent(event: ApolloCall.StatusEvent) {
if (event == ApolloCall.StatusEvent.COMPLETED) {
channel.close()
}
}
}
fun <T> ApolloCall<T>.toChannel(): Channel<Response<T>> {
checkCapacity(capacity)
val channel = Channel<Response<T>>(capacity)
channel.invokeOnClose {
cancel()
}
enqueue(ChannelCallback(channel))
return channel
}
Source: Apollo Android’s Early Channel Extension
The approach is similar to using the callbackFlow method we saw earlier. The toChannel
method enqueues a callback which communicates with a channel to send data. However, in this case, the channel is created manually and given to the callback.
You could get the results of the request by using the receive method on a channel.
val channel = client
.query(query)
.toChannel()
val response = channel.receive()
Flow Extension
The initial implementation also had a variant of the toFlow
extension on the Apollo Client.
fun <T> ApolloQueryWatcher<T>.toFlow(capacity: Int) = flow {
checkCapacity(capacity)
val channel = Channel<Response<T>>(capacity)
enqueueAndWatch(ChannelCallback(channel = channel))
try {
for (item in channel) {
emit(item)
}
} finally {
cancel()
}
}
Source: Apollo Android’s Early Flow Extension
This implementation doesn’t use a callbackFlow
. It creates a Flow using the flow builder. It reads from a channel inside a loop to read the results and emit them to the flow.
As we could see, the approaches for bridging from Apollo Callback to Flow has evolved, and it has gotten straightforward as the Kotlin coroutines library has added more features. The callbackFlow
makes it easier to bride callbacks to Flows.
Summary
-
Use a callbackFlow to bridge callbacks to Flows
-
CallbackFlow method launches a coroutine on a producer scope and provides a channel to send data from the callback.
-
Converting to a Flow gives you the benefits of using its extensive operators for retries, errors, or mapping operators.