DEV Community

Cover image for Android: Apollo3 and GraphQL
Eric Donovan
Eric Donovan

Posted on • Updated on

Android: Apollo3 and GraphQL

There is a pre-primer here if you're completely new to GraphQL and Apollo.

Let's run through a small GraphQL driven Android app. We'll touch on a few things which are specific to Apollo3 as we go, but most of this post applies to the previous Apollo version too. [At the time of publication, Apollo3 is still in alpha, but 3.0.0 is coming soon. You can check the maven repo to see the latest releases]

Why Upgrade

Here's why you might consider upgrading to Apollo3 soon:

  • Pure kotlin (like Ktor), which opens up the possibility of using it in a KMP project
  • True coroutine API so no need to use latches etc for unit testing
  • Apollo3's autogenerated DTOs make much better use of query fragments, so your mapping code will be DRYer (no more Node1, Node2, Node3 classes)

Why Apollo3 not just Apollo v3? for the same reason it's Retrofit2. The different name space allows your app to migrate to Apollo3 in stages. Or to migrate your app code to Apollo3 while continuing to use libraries that might still be tied to the previous Apollo.

Adding Apollo3 to your android project

Go ahead and clone the sample app, but the basic steps if you're doing it for your own app are:

add the plugin

plugins {
  ...
  id("com.apollographql.apollo3").version("3.0.0")
}
Enter fullscreen mode Exit fullscreen mode

configure the plugin

apollo {
    packageName.set("foo.bar.myapp")
    srcDir("src/main/graphql")
}
Enter fullscreen mode Exit fullscreen mode

add a dependency on apollo-runtime

implementation("com.apollographql.apollo3:apollo-runtime:3.0.0")
Enter fullscreen mode Exit fullscreen mode

download the server schema from the command line

./gradlew downloadApolloSchema \
  --endpoint="https://stuff.thing/graphql-endpoint" \
  --schema="app/src/main/graphql/schema.json"
Enter fullscreen mode Exit fullscreen mode

If you're doing this at work, you'll probably need to be on your company's VPN to download the schema and it might only be downloadable from the staging server, not from the prod server, but they should match anyway

add a graphQL request file and put it next to where the schema file has been downloaded, this one is called LaunchList.graphql

query Launches {
 launches {
  cursor
  hasMore
  launches {
    id
    site
    mission {
      name
      missionPatch(size: SMALL)
    }
    isBooked
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

You won't be able to see these files in the regular Android view, you'll need to switch to the Project view:

Android Studio's Project view shows the graphql files

Sync the project and click Build -> Make Project and you should be able to see the autogenerated Apollo DTOs from your autocomplete. Once everything is done, try typing LaunchesQuery

Make the call

The basic Apollo3 call looks like this. LaunchesQuery() is generated for you by Apollo, based on the query we added above, and apolloClient.query() is a suspend function so we need to call this from within a coroutine

apolloResponse = apolloClient.query(LaunchesQuery())
Enter fullscreen mode Exit fullscreen mode

Code like this is a little bit raw, we can get all sorts of edge cases here because a network call is being made. We need to ensure we are not just handling the ApolloResponse (which can contain success data, or error(s), or both), but we also need to handle any issues that could happen in the HTTP server or cache layer before we even get to the GraphQL bit (e.g. 500s or 404s), and also exceptions that can be thrown before we even manage to get a network connection (e.g. java.io.IOException).

I will leave that as an exercise for the reader... just kidding 😬

  • Robustness / Error Handling
  • Testing
  • Diagnostics / Logging

These are the things that separate a proof of concept app from a production quality commercial app, so let's dig in to those areas as they relate to Apollo.


Robustly handling network errors

The first thing I'd recommend, is to handle these errors in some centralised, consistent way (a lot of the error handling is common to every call we will make, so there is no point in repeating it). Here's the ErrorHandler for our sample app.

My second recommendation for your consideration, is to handle these errors in your networking or data layer or wherever it is you place the apollo query code. This makes the first recommendation easier, because errors will be caught before they bubble up to potentially many different parts of the app's UI to be handled there (or more likely not handled at all).

In the sample app, we are using a CallProcessor class (from the fore library) to wrap the Apollo calls and enforce error handling.

val result = callProcessor.processCallAwait {
  apolloClient.query(LaunchesQuery())
}
Enter fullscreen mode Exit fullscreen mode

When the calls are wrapped like this, you won't need to handle any exceptions, or networking issues (well actually you are handling them, the CallProcessor just delegates this responsibility to the ErrorHandler class and requires that you provide it - see above). The result you get back will contain an Either<Failure, SuccessResult>

A SuccessResult in this case looks like this:

data class SuccessResult<S, F>(
  val data: S,
  val partialErrors: List<F> = listOf(),
  val extensions: Map<String, Any?> = hashMapOf(),
  val executionContext: ExecutionContext = Empty
)
Enter fullscreen mode Exit fullscreen mode

(data will be the auto generated network DTOs containing your stuff)

The Failure class is something you define yourself (a kotlin data class or an enum would be ideal) and it's created by the ErrorHandler linked to above.

In a previous clean architecture sample, we had something like this:

sealed class DataError(val resolution: DomainError) {
  object Misc: DataError(RETRY_LATER)
  object Network: DataError(CHECK_NETWORK_THEN_RETRY)
  object Client: DataError(RETRY_LATER)
  object RateLimited: DataError(RETRY_LATER)
  object SessionTimedOut: DataError(LOGIN_THEN_RETRY)
  object Busy: DataError(RETRY_LATER)
}
Enter fullscreen mode Exit fullscreen mode

The next step usually involves mapping your network/data layer objects to domain objects, or whatever you like (according to your architecture).

when (result) {
  is Right -> // handle success
  is Left -> // handle failure
}
Enter fullscreen mode Exit fullscreen mode

In the absence of this kind of error handling strategy, what usually happens when a generic networking bug is raised in production in a specific part of the app's UI: it will only be addressed in that specific place, without porting that error handling code to all the other places that network requests are being made.

Partial successes

One of the interesting things about GraphQL is that it provides for partial success results that have some errors attached.

If you want to take advantage of these half-success results with the CallProcessor, then you can set the allowPartialSuccesses flag to true. In which case your SuccessResults may include a non empty partialErrors list. (if that flag is set to false, these half-success cases are considered normal Failures).


Testing

Your architecture will somewhat dictate where you can put the boundaries of your test code.

Unit Testing

If you are making your requests directly on the apolloClient instance, and this is being passed in to the class you are trying to test, that would be a convenient place to mock. (During a test, your mocked apolloClient instance can return ApolloResponse objects containing valid DTOs or throw exceptions as appropriate).

If you are wrapping the Apollo calls like we do in the sample app with a CallProcessor or similar, it's arguably a little easier. This time during a test, you will be mocking the CallProcessor to return an Either<Success> or an Either<Failure>, with no need to have your mocks throw exceptions. There's an example of this test strategy in the apollo3 sample in the main fore repo.

Both the strategies above will require you to mock the autogenerated Apollo DTOs. This is probably the worst part of Apollo Android IMO, mocking these DTOs can be fiddly for even basic APIs like the one in the sample. Once you move to larger commercial GraphQL APIs, this mocking is extremely tedious.

If you are making the Apollo requests from inside some form of data-source class as part of a clean architecture app, depending on how you arrange your module boundaries, you can switch to mocking domain objects rather than the autogenerated DTOs (which will be much easier). There is an example of this structure in the clean architecture sample app. That sample uses Ktor and a rest API rather than Apollo and a GraphQL API, but it uses a CallProcessor class in the same manner to this sample. See this class which returns Domain objects rather than Data / DTO objects, for example.

Integration Testing

This strategy probably gives us the most bang for our buck. Because Apollo3 is using OkHttp3 under the hood, we can use a regular OkHttp3 interceptor to trick Apollo into thinking it is receiving data from the network (when in fact the data has come from static test files that are read locally). Once again there is an example of that kind of test in the Apollo3 sample that comes with the main fore repo.


Logging network calls

During development, being able to see what network traffic your app is sending and receiving is crucial in understanding what's going on IMO, especially for any members of the team who are dealing with GraphQL for the first time. You can turn it off for release builds.

There is an interesting gotcha here that you might come across if you're using multiple network related libraries likes Ktor, Coil, Apollo3 etc in the same app. OkHttp3 is almost ubiquitous, but there is a difference between OkHttp3 v3.x.x (preferred by Coil and Apollo3 for instance) and OkHttp3 v4.x.x (preferred by Ktor). Specifically the v3 functions: method(), body(), code() are replaced by the fields: method, body, code in v4. This can cause problems when logging network traffic if you don't know which version you are dealing with, or if a transitive dependency changes on you.

Anyway, if you already use an OkHttp interceptor class to log your network traffic with Retrofit or Ktor, the chances are it's going to work with Apollo3 too (but if it doesn't, it's probably related to that v3 / v4 gotcha).

For the sample app, we're using the InterceptorLogging class from fore which uses reflection to choose the correct logging method and works with both OkHttp3 v3 and v4:

val apolloClient = CustomApolloBuilder.create(
  globalRequestInterceptor,
  InterceptorLogging(logger)
)
Enter fullscreen mode Exit fullscreen mode

This will give you logs that look like this:

typical logging output

Note the 5EFD7: this is a randomly generated string which is constant across a specific request and response cycle. It's a very basic way to deal with the problem of logging interleaved requests. If you see a request marked 5EFD7, and a response marked 6EDFG, then they are not related to each other (look further down in logcat to find the matching response which will also be marked 5EFD7).


Well that's all I have to share relating to Apollo3, I've used it a little (not in production yet) but it already seems to be working well. If you're interested, keep an eye on the official v3 docs to see when it hits stable.

Thanks for reading! and here's the sample android app on github.

Discussion (0)