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.
Before we get into the implementation, quickly check whether you added INTERNET permission to the manifest.
<uses-permission android:name="android.permission.INTERNET"/>
We can split this into three sub-tasks
- Gradle dependency setup
- Preparing the Apollo GraphQL client
- 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")
}
}
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")
}
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()
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()
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.
- We can add interceptors to the API calls to log/diagnose the API calls during development.
- Timeout/exponential backoff — everything is possbile using okHttp
- 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
}
}
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
}
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
)
// 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
}
// 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")
}
}
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
}
}
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
}
}
}
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()
)
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.
Top comments (0)