Exploring Apollo GraphQL for Android - Real-world examples

Exploring Apollo GraphQL for Android - Real-world examples

This is Part II of the Apollo GraphQL series that I’m writing for Android. If you are new to Apollo GraphQL then I recommend that you read my first blog post of this series.

In the previous part, I covered Introduction and setting up Apollo GraphQL for Android and we also finished hitting our first GraphQL query. This means we received the data from the server. Now what about updating the data on the server? This resembles the POST request for REST API’s. But in GraphQL world it's called Mutation, using which we can update the data on the server.

Mutation

Now let’s create the similar .graphql file for our Mutation.

mutation UpdateUser(
    $userId: String!
    $name: String
    $phone: String
    $email: String
    $age: Int
) {
    updateUserDetails(
        userId: $userId
        name: $name
        phone: $phone
        email: $email
        age: $age
    ) {
        userId
    }
}

The above mutation is straightforward and can be performed with similar method as query. But usually the User entity will have many more fields and it will be defined in a Custom Data type. So our mutation might look like:

mutation UpdateUser($user: UserInput!) {
    updateUserDetails(
        user: $user
    ) {
        user {
            _id
            name
        }
    }
}

You might be wondering on how to pass objects with custom data types (like UserInput in this case) in the parameters. Well, as the Apollo generate classes for you, it also provides a builder() function to let you instantiate those input classes with data.

val userInput = UserInput.builder()
    .userId(“user-id-string”)
    .name(“John Doe”)
    .phone(“987654321”)
    .email(“hello@world.com”)
    .age(21)
    .build()

Now you can pass this object in our mutation parameter.

apolloClient.mutate(
    UpdateUserMutation.builder()
        .user(userInput)
        .build()
)
    .enqueue(object : ApolloCall.Callback<UpdateUserMutation.Data>() {

        override fun onResponse(response: Response<UpdateUserMutation.Data>) {
            if (!response.hasErrors()) {
                // Response successful
                Log.d(TAG, "Response: ${response.data()}")
            } else {
                // Request was successful but contains errors
                Log.d(TAG, "Response has errors: ${response.errors()}")
            }
        }

        override fun onFailure(e: ApolloException) {
            // Request Failed
            e.printStackTrace()
        }
    })

And there you go, we have our first mutation done.

Custom Type Adapter

There are times when data types at server side and client side won’t be compatible. We can make use of Custom Type Adapters to tackle that issue. Below is the example of server accepting Date as a ‘String’ in UTC Format. The CustomTypeAdapter will have encode() and decode() functions for you to override and add your own parsing logic.

class CustomDateAdapter : CustomTypeAdapter<Date> {

    companion object {
        const val DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
    }

    override fun encode(value: Date): CustomTypeValue<*> {
        // Parse Date in UTC TimeZone
        val calendar = Calendar.getInstance()
        calendar.timeZone = TimeZone.getTimeZone("UTC")
        calendar.time = value
        val time = calendar.time

        // Parse Date in UTC Format
        val sdf = SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH)
        return CustomTypeValue.GraphQLString(sdf.format(time))
    }

    override fun decode(value: CustomTypeValue<*>): Date {
        // Parse UTC formatted Date String in Date
        val sdf = SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH)
        sdf.timeZone = TimeZone.getTimeZone("UTC")
        val date = sdf.parse(value.value.toString())

        // Change timezone to default
        val calendar = Calendar.getInstance()
        calendar.timeZone = TimeZone.getDefault()
        calendar.time = date

        return calendar.time
    }
}

Now to actually make the CustomTypeAdapter work, add the CustomTypeAdapter into our ApolloClient instantiation.

ApolloClient.builder()
    .serverUrl("http://localhost:8080/graphql")
    .okHttpClient(okHttpClient)
    .addCustomTypeAdapter(CustomType.DATE, CustomDateAdapter())
    .build()

And then specify on which data type you have mapped the CustomTypeAdapter in your app level build.gradle file

android {
    …
    ...
    apollo {
        customTypeMapping = ["Date": "java.util.Date"]
    }
    ...
}

File Upload Support

Since version 1.0.1, Apollo has added native support for uploading file on Android. It is based on this specification for backend GraphQL server.

To start with File uploading, you need to add customTypeMapping to app level build.gradle file, similar to CustomTypeAdapter:

apollo {
  customTypeMapping = [
    "Upload" : "com.apollographql.apollo.api.FileUpload"
  ]
}

GraphQL schema uses custom scalar type named Upload for FileUpload.

mutation UploadFile(
    $file: Upload!
    $type: Int!
) {
    uploadFile(
        file: $file
        type: $type
    )
}

and to call the mutation with file upload:

val file: File = ...
// Get the MIME Type for file or else return image/png as default
val mediaType = MediaType.parse(file.getMimeType() ?: "image/png")

val apolloCall = UploadFileMutation.builder()
    .file(FileUpload(mediaType.toString(), file))
    .type(2)
    .build()

// Enqueue the call however you like it

GraphQL also allows you to upload multile files in single mutation inside an array or separate input fields, if your API accepts it.

Rx Support

It's pretty straight forward to convert Callbacks into RxJava's Reactive streams. All you have to do is add Rx2 extension dependency:

implementation 'com.apollographql.apollo:apollo-rx2-support:x.y.z'

and then wrap the query into Rx2Apollo wrapper:

val observable = Rx2Apollo.from(
    apolloClient.query(
        GetAttendeeDetailsQuery.builder()
            .userId(userId)
            .eventId(eventId)
            .build()
    )
)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())

Note: Apollo is soon dropping the support for RxJava1

For more information on how to use the Observables you can refer many other blog posts like this and this.

Coroutines Support

Apollo supports Coroutines with simple extension functions in Kotlin. You can check out them here.

When it comes to query it's recommended to use Coroutine Channels as they emit multiple responses, (usually one from cache and other one from network).

GlobalScope.launch {
    val attendeeQuery = apolloClient.query(
        GetAttendeesQuery.builder()
            .eventId(eventId)
            .build()
        )
            .httpCachePolicy(HttpCachePolicy.CACHE_FIRST)

    val channel = attendeeQuery.toChannel()
    channel.consumeEach {
        // it.data() contains the response
    }
}

// invoke channel.cancel() in Activity/Fragment/ViewModel's onDestroy callbacks
// to avoid any memory leaks

In case of Mutations there aren't any multiple response callbacks, so using .toDeferred() is the way to go.

GlobalScope.launch {
    val mutation =  apolloClient.mutate(
        UpdateUserMutation.builder()
            .user(userInput)
            .build()
    )

    val deferred = mutation.toDeferred()
    val response = deferred.await()
}

The .toDeferred() docs reads, "Converts an ApolloCall to an Deferred. This is a convenience method that will only return the first value emitted. If the more than one response is required, for an example to retrieve cached and network response, use toChannel() instead."

Error handling in Coroutines is done using try-catch block.

GlobalScope is only used for demonstration purpose. Please use appropriate Coroutine Scopes or suspend functions in your project.

Synchronous requests with Coroutines

At times you'll need to make a synchronous request, which Apollo used to support out of the box. But has dropped the support due to many API call's returning multiple callbacks due to caching.

The .toDeferred() method can act as Synchronous requests as the .await() function will suspend until we get the response.

There's one more implementation provided by one of the community member. (ref. #606).

To use it, add this Extension function:

suspend fun <T> ApolloCall<T>.execute() = suspendCoroutine<Response<T>> { cont ->
    enqueue(object : ApolloCall.Callback<T>() {
        override fun onResponse(response: Response<T>) {
            cont.resume(response)
        }

        override fun onFailure(e: ApolloException) {
            cont.resumeWithException(e)
        }
    })
}

Now you can make synchronous requests by just calling .execute() on Apollo Call's.

GlobalScope.launch {
    try {
        val response = client.mutate(
            UpdateUserMutation.builder()
                .user(userInput)
                .build()
        ).execute()

        if (!response.hasErrors()) {
            // Response successful
            // get data in response.data()
        } else {
            // Response has errors
        }
    } catch (e: Exception) {
        // Request failed
        // It's best practice to catch various exception's in separate catch blocks
    }
}

You can use runBlocking { ... } instead of GlobalScope to return any values from inside the coroutine. This method is really useful in WorkManager's.