Android - API calls to Graphql server

mahendranv

Mahendran

Posted on May 14, 2021

Android - API calls to Graphql server

This is the final article in the series. In the previous ones I covered backend setup for GraphQL and setting up IDE & gradle plugin. This one covers the API call part in client end.

image-20210514125103832

Before we get into the implementation, quickly check whether you added INTERNET permission to the manifest.

<uses-permission android:name="android.permission.INTERNET"/>
Enter fullscreen mode Exit fullscreen mode

We can split this into three sub-tasks

  1. Gradle dependency setup
  2. Preparing the Apollo GraphQL client
  3. API call

Gradle dependency setup

Gradle dependencies are required for code-gen and http client capabilities. I'm repeating the gradle setup for codegen here to give consolidated changes needed for the app.

Add apollo gradle plugin for codegen capabilities at compile time.

project root/build.gradle

buildscript {
    ext {
        ...
        apollo_version = '2.5.6'
    }

    dependencies {
        ...
        classpath("com.apollographql.apollo:apollo-gradle-plugin:$apollo_version")
    }
}
Enter fullscreen mode Exit fullscreen mode

In the app/build file, make following changes. Inlined comments on why each line needed.

app/build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id  'com.apollographql.apollo' 
    // At compile time, this gradle plugin 
    // will identify graphql schema & queries 
    // and generate classes.
}

// This is configuration input for apollo plugin.
// Apart from the codegen, few additional setup 
// can be done for safe-typing on generated classes
apollo {
    generateKotlinModels.set(true)
}

dependencies {
   // Apollo runtime dependency bundles HttpClient 
   // and necessary tools to convert queries/mutations 
   // to POST calls.
   implementation("com.apollographql.apollo:apollo-runtime:$apollo_version")
   // Coroutine support for api calls. 
   // Apollo has support for RxJava2, RxJava3 as well.
   implementation("com.apollographql.apollo:apollo-coroutines-support:$apollo_version")
}
Enter fullscreen mode Exit fullscreen mode

Make the project/module and ensure Query/Mutation classes are generated to use in our application. Next part is to setup the Apollo Client instance.


Preparing the Apollo GraphQL client

Apollo GraphQL client uses a okHttp client under the hood to make API calls. If you're using a public GraphQL endpoint which doesn't need any header, the setup is as simple as below.

private val apolloClient = ApolloClient
        .builder()
        .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql")
        .build()
Enter fullscreen mode Exit fullscreen mode

In case, your GraphQL endpoint needs additional headers to authorize your API calls. You'll have to provide an okHttp instance to the builder where you add header for each call.

  private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val original = chain.request()
            val updated = original.newBuilder()
                .addHeader(
                    "x-hasura-access-key",
                    "my-admin-key"
                )
                .build()
            chain.proceed(updated)
        }.build()

    private val apolloClient = ApolloClient
        .builder()
        .serverUrl("https://hasuraproject-16.hasura.app/v1/graphql")
        .okHttpClient(okHttpClient) // Add client here
        .build()
Enter fullscreen mode Exit fullscreen mode

Since we're making API calls to our hasura backend, we need x-hasura-access-key header in our calls. So, we go with the second one. There are few advantages providing okHttpClient manually. We can use everything that okHttp has to offer.

  1. We can add interceptors to the API calls to log/diagnose the API calls during development.
  2. Timeout/exponential backoff — everything is possbile using okHttp
  3. Having only one okHttp client is recommended in community as each okHttp client will have it's own set of thread pool / connection pools. Let's say you're already using an okHttp instance with Retrofit in some other part of the project. Reuse the same instance inside apollo client for better performance.

Off to the api call...


API call

In apollo client, the API calls are made in the form of generated input/data/query/mutation classes. The only thing we own is a schema and some graphql files where we define our required fields.

I'm starting with a mutation to demonstrate an API call since it has both input and output in it. Following would be our query.

mutation CreateExpense($e: expenses_insert_input!) {
    insert_expenses_one(object: $e) {
        id
        amount
        remarks
        is_income
    }
}
Enter fullscreen mode Exit fullscreen mode

I covered how and what classes generated for a given query here.

The above mutation query can be made like this inside a suspend function.

val e = Expenses_insert_input(
  amount = Input.optional(100),
  remarks = Input.optional("DSA book"),
  spent_on = Input.optional("2021-05-13T04:00:49.815194+00:00")
)

suspend fun createExpense(newExpense: Expenses_insert_input): CreateExpenseMutation.Insert_expenses_one? {
  val response = apolloClient.mutate(
    CreateExpenseMutation(e = newExpense)
  ).await()

  return response.data?.insert_expenses_one
}   
Enter fullscreen mode Exit fullscreen mode

This is rather an over simplified version of the API call. We create a Input object generated by Apollo. Make mutation call and get the data from response, which is again a generated class.

...

We have the response now. That's it?

Yes.. and no. For a pet project, above would do fine. For any serious work read below.

Bear in mind 🧸 that, these classes defined at the server end and it might not look so good in your codebase. And any change in server end (even a spelling mistake fix in field) will blow your codebase if you allow these generated classes escape to your viewmodel - ui layer.

So, I converted each input/output class to mitigate the potential ripple from the backend.

// Domain class to be used in UI
data class HExpense(
    val id: Long,
    val amount: Int,
    val remarks: String,
    val isIncome: Boolean,
    val spentOn: Any
)
Enter fullscreen mode Exit fullscreen mode
// Common interface scheme for mapping classes. This is for future usage where we can build a pool of adapters
interface DomainEntityMapper<E, D> {

    fun toEntity(d: D): E

    fun toDomain(e: E): D

}
Enter fullscreen mode Exit fullscreen mode
// Implementation of input class
object ExpenseInputAdapter : DomainEntityMapper<HExpense, Expenses_insert_input> {

    override fun toDomain(e: HExpense): Expenses_insert_input {
        return Expenses_insert_input(
            amount = Input.optional(e.amount),
            remarks = Input.optional(e.remarks),
            is_income = Input.optional(e.isIncome),
            spent_on = Input.optional("---")
        )
    }

    override fun toEntity(d: Expenses_insert_input): HExpense {
        TODO("Not yet implemented")
    }
}
Enter fullscreen mode Exit fullscreen mode

With the adapter safety net in place, the generated classes are stopped in API class. The modified version of the same API call is shown below.

suspend fun createExpense2(newExpense: HExpense): HExpense? {
        val response = apolloClient.mutate(
            CreateExpenseMutation(e = ExpenseInputAdapter.toDomain(newExpense))
        ).await()

        return if (response.data?.insert_expenses_one != null) {
            NewExpenseAdapter.toEntity(response.data!!.insert_expenses_one!!)
        } else {
            null
        }
}
Enter fullscreen mode Exit fullscreen mode

Complete API class

// This should be further break down into API call network module classes, this provides cover-up on aforementioned steps.
object ExpenseSdk {

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val original = chain.request()
            val updated = original.newBuilder()
                .addHeader(
                    "x-hasura-access-key",
                    BuildConfig.hasuraSecret
                )
                .build()
            chain.proceed(updated)
        }.build()

    private val apolloClient = ApolloClient
        .builder()
        .serverUrl(BuildConfig.expensesGQL)
        .okHttpClient(okHttpClient)
        .build()

    suspend fun fetchExpenses(): List<HExpense> {
        val response = apolloClient
            .query(
                GetAllExpensesQuery()
            ).await()

        return if (response.data != null) {
            response.data!!.expenses.map {
                ExpenseEntityAdapter.toDomain(it)
            }
        } else {
            emptyList()
        }
    }

    suspend fun createExpense(newExpense: HExpense): HExpense? {
        val response = apolloClient.mutate(
            CreateExpenseMutation(e = ExpenseInputAdapter.toDomain(newExpense))
        ).await()

        return if (response.data?.insert_expenses_one != null) {
            NewExpenseAdapter.toEntity(response.data!!.insert_expenses_one!!)
        } else {
            null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the viewmodel which makes the suspend call. I leave the UI implementation to you.

class ExpenseListViewModel : ViewModel() {

    var pageState: PageState by mutableStateOf(PageState(isLoading = true))
        private set

    fun loadContent() {
        pageState = PageState(isLoading = true)
        viewModelScope.launch {
            val list = ExpenseSdk.fetchExpenses()
            pageState = PageState(isLoading = false, data = list)
        }
    }
}

data class PageState(

    val isLoading: Boolean = false,

    val data: List<HExpense> = emptyList()

)
Enter fullscreen mode Exit fullscreen mode

Endnote

There is more to GraphQL, we can customize code-gen to add some type-adapters of our own and even do realtime updates with Subscriptions. To scope the article to cover API call, I left them out and UI.

Use your imagination and explore the queries. With the server we setup at first two articles in the series, we can easily build a dashboard for your expenses.

💖 💪 🙅 🚩
mahendranv
Mahendran

Posted on May 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related