GraphQL with Android

Giving REST a rest! 🚀

It’s safe to say that a majority of developers work by consuming REST APIs in their day-to-day tasks. And this is not at all surprising as REST is amazing architecture.

But since the development landscape and the technologies within that landscape are constantly changing, developers need to keep themselves up-to-date with the latest and greatest, especially when it comes to optimizing development time, cost, speed, and more. And to achieve this, GraphQL enters in the picture.

GraphQL: A query language for your APIs

  • It’s an alternative to REST APIs.
  • It’s not specific to a single platform and works with all types of clients including Android, iOS, or web.
  • It stands between your server and client and helps you query your data in a more optimized way.
  • It’s client-driven. GraphQL always exposes only one endpoint, hence giving clients the power to ask for exactly what they need, making it easier to evolve APIs over time, and enabling powerful developer tools.
  • It helps in avoiding multiple calls, unlike REST. The user can specify the details he/she wants in a single query, avoiding the need to perform multiple calls to various endpoints to get the desired data.
  • It’s built for top-tier and high-load applications. The fact that it’s used by Facebook says a lot. So if you need a tool to manage a large number of users, GraphQL is a perfect fit.
  • It allows you to change the design of your application without having to mess with its backend.

Now that you’re familiar with the basics of GraphQL and its advantages, you might want to know more about it 🤔. This is what we’ll be covering in this blog post; hopefully, you’ll be able to create a starter project in GraphQL after reading this post!

Just keep calm—we’re on the way! 👀

Setting Up GraphQL on Android

We’ll be using apollo-android, which is a compliant client that generates Java/Kotlin models according to your query and makes it easier to get the results from the response of the API. Apollo Android has 2 components:

  • Apollo Codegen: This component is a Gradle plugin used to generate code like ButterKnife. Apollo Codegen generates Java/Kotlin models from standard GraphQL queries at compile time.
  • Networking/Caching: the other component of Apollo-Android is the networking and caching piece, which takes care of all the network communication with your GraphQL API, parsing the response to the correct model, enabling you to pass dynamic data to your GraphQL queries, and response caching.

Here are the steps involved in the process:

1. Adding the Gradle dependencies

  • To your project’s root build.gradle file, add the following dependency:
buildscript {
..
  repositories {
    jcenter()
    google()
  }
  dependencies {
     classpath 'com.android.tools.build:gradle:3.5.3'
     classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     classpath 'com.apollographql.apollo:apollo-gradle-plugin:1.2.2'
  }
}
..
  • To your app’s build.gradle add the following on top:
apply plugin: 'com.android.application'
apply plugin: 'com.apollographql.android' //Add this

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-kapt'

apply plugin: 'kotlin-android-extensions'
...
  • Add the following dependencies to your app’s build.gradle:
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    ...
    implementation 'com.apollographql.apollo:apollo-android-support:1.2.2'
    implementation 'com.apollographql.apollo:apollo-runtime:1.2.2'
...
}

After this, sync your project.

2. Generating models from your queries

  1. Create a directory for your GraphQL files like you would for Java/Kotlin: src/main/graphql/org.aerogear.graphqlandroid/. Apollo-Android will generate models in the org.aerogear.graphqlandroid package.
  2. Put your GraphQL queries and mutation in a .graphql file. For example: src/main/graphql/org.aerogear.graphqlandroid/createTask.graphql:
mutation createTask($input: TaskInput!){
    createTask(input: $input){
        id
        version
        title
        description
        status
        creationMetadata{
            createdDate
            taskId
            createdBy{
                id
                firstName
                lastName
                title
                email
                taskId
                creationmetadataId
            }
        }
        assignedTo{
            id
            firstName
            lastName
            title
            email
            taskId
            creationmetadataId
        }
    }
}

3. We need to add a schema.json file. In order to do this, let’s run the following command:

  • In this command, you have to specify the path to your server as well as a place to save a schema.json file.
  • Add your schema.json file to the directory at src/main/graphql/com/org.aerogear.graphqlandroid/schema.json. If you don’t have a schema.json file yet, you can read more about downloading a schema file.
  • Now you can rebuild the project and Apollo will generate all the necessary classes.

3. Setting up the Apollo client

1. Creating an apollo-client

To send requests to your backend server, you need an apollo-client. You simply need to write a GraphQL query, and an apollo-client will take care of requesting and caching your data, as well as updating your UI. A simple instance of an apollo-client takes in the base server URL and an object OkHttpClient. It’s always preferable to make only one instance of the client and use it throughout the entire application to save resources and to achieve higher efficiency.

object Utils {

    //To run on emulator use http://10.0.2.2:4000/graphql
    const val BASE_URL = "http://10.0.2.2:4000/graphql"
    private var apClient: ApolloClient? = null
    private var httpClient: OkHttpClient? = null

    @JvmStatic
    fun getApolloClient(context: Context): ApolloClient? {

        //If Apollo Client is not null, return it else make a new Apollo Client.
        //Helps in singleton pattern.
        apClient?.let {
            return it
        } ?: kotlin.run {
            apClient = ApolloClient.builder()
                .okHttpClient(getOkhttpClient(context)!!)             
                .serverUrl(BASE_URL)
                .build()
        }
        return apClient
    }

    private fun getOkhttpClient(context: Context): OkHttpClient? {
        httpClient?.let {
            return it
        } ?: kotlin.run {
            httpClient = OkHttpClient.Builder()
                //Adding HttpLoggingInterceptor() to see the response body and the results.
                .addInterceptor(LoggingInterceptor())
                .build()
        }
        return httpClient
    }
  }
}

2. Running mutations from the client

Here’s the general format for how the mutation is created:

 //Create a mutation object
    val input = TaskInput.builder().title(title).version(version).description(description).status("test").build()
    val mutation = UpdateTaskMutation.builder().id(id).input(input).build()
    
    //Create an object of apolloCall
    val mutationCall = apolloClient.mutate(mutation)?.refetchQueries(apolloQueryWatcher?.operation()?.name())
     
    //Create a callback object of type ApolloCall.Callback
     val callback = object : ApolloCall.Callback<UpdateTaskMutation.Data>() {
     
           override fun onResponse(response: Response<UpdateTaskMutation.Data>) {              
                val result = response.data()?.updateTask()              
                
                //In case of conflicts data returned from the server is null.
                result?.let {
                    //Perform UI Bindings.                 
                }
            }
            
       /* Called when the request could not be executed due to cancellation, a connectivity problem or timeout.
       */      
          override fun onFailure(e: ApolloException) {              
                e.printStackTrace()
            }
     }
        
     /*Call the enqueue function on ApolloClient on the apollo mutation call and pass callback to it.
     */  
     mutationCall?.enqueue(callback)
  • Create an object of typeMutation<D, T, V> . You can create a mutation of any type according to your schema.
  • Create an object of ApolloMutationCall<Mutation<D, T, V>.Data> .
  • Create an object of type ApolloCall.Callback .
  • Enqueue the ApolloCall.Callback object to the ApolloMutationCall<Mutation<D, T, V>.Data> object.

Below is an example of creating a task (takes in the title, description, and the version) in a sample using a CreateTaskMutation mutation:

   fun createTask(title: String, description: String, version: Int) {

        val input =
            TaskInput.builder().title(title).description(description).version(version).status("test")
                .build()

        val mutation = CreateTaskMutation.builder().input(input).build()

        val mutationCall = Utils.getApolloClient(this)?.mutate(
            mutation
        )?.refetchQueries(apolloQueryWatcher?.operation()?.name())

        val callback = object : ApolloCall.Callback<CreateTaskMutation.Data>() {
            override fun onFailure(e: ApolloException) {
                Log.e("onFailure() updateTask", "${mutation.variables().valueMap()}")
                e.printStackTrace()
            }

            override fun onResponse(response: Response<CreateTaskMutation.Data>) {
                val result = response.data()?.createTask()

                //Logging the result
                result?.let {
                    Log.e(TAG, "onResponse-CreateTask- $it")
                }
            }
        }
        mutationCall?.enqueue(callback)
    }

3. Querying from the client

  fun getTasks() {

        FindAllTasksQuery.builder()?.build()?.let {
            Utils.getApolloClient(this)?.query(it)
                ?.responseFetcher(ApolloResponseFetchers.NETWORK_FIRST)
                ?.enqueue(object : ApolloCall.Callback<FindAllTasksQuery.Data>() {

                    override fun onFailure(e: ApolloException) {
                        e.printStackTrace()
                        Log.e(TAG, "getTasks ----$e ")
                    }

                    override fun onResponse(response: Response<FindAllTasksQuery.Data>) {
                      //Change the UI accordingly
                        Log.e(TAG, "on Response getTasks : Data ${response.data()}")
                        val result = response.data()?.findAllTasks()

                        result?.forEach { allTasks ->
                            val title = allTasks.title()
                            val desc = allTasks.description()
                            val id = allTasks.id()
                            var firstName = ""
                            var lastName = ""
                            var email = ""
                            var userId = ""
                            allTasks.assignedTo()?.let { query ->
                                firstName = query.firstName()
                                lastName = query.lastName()
                                email = query.email()
                                userId = query.id()
                            } ?: kotlin.run {
                                firstName = ""
                                lastName = ""
                                email = ""
                                userId = ""
                            }
                            val taskOutput = UserOutput(
                                title,
                                desc,
                                id.toInt(),
                                firstName,
                                lastName,
                                userId,
                                email
                            )
                            runOnUiThread {
                                tasksList.add(taskOutput)
                                taskAdapter.notifyDataSetChanged()
                            }
                        }
                    }
              })
        }
    }
  • Here we’ve used FindAllTasksQuery, which queries the server and returns the list of all the tasks that the user has created using CreateTaskMutation (explained below).

With the help of the apollo-client object, build the query as shown above and pass the appropriate arguments. Enqueue the result as shown above and change UI accordingly.

4. Setting up the UI

You can change the UI according to your needs. Here, in the sample (gif below), we’ve made a task app that keeps track of the different tasks, their titles, descriptions, users assigned to complete the tasks, and so on. The link to the sample app is given below.

And with this, you’ve successfully made your first Android app integrated with GraphQL! Congratulations! 👏

Here is a glimpse at how the sample looks:

Conclusion:

As you’ve seen, GraphQL not only makes the development process much simpler and more efficient, but it also avoids the problem of under fetching and over fetching resources by giving only the information required by the user.

As opposed to a REST API, GraphQL requires much less time for its implementation, which results in the more effective use of time. In addition, the way GraphQL queries your app’s backend makes that backend more reliable and stable, which is yet another reason to consider this tool. Hence, it’s an amazing query language for your APIs, and you can easily use it to make some wonderfully light apps!

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square