GraphQL vs REST for Android
Why GraphQL?
REST endpoints return fixed data structures. Over-fetching sends more data than needed. Under-fetching requires multiple calls. GraphQL solves both: you request exactly the fields you need in a single call.
# REST: Two calls needed
GET /users/1 -> returns full user object
GET /users/1/posts -> returns full post objects
# GraphQL: One call, exact fields
query {
user(id: 1) {
name
posts {
title
createdAt
}
}
}
Core Concepts
- Schema: Defines all types and operations the API supports
- Query: Read data (analogous to GET)
- Mutation: Write data (analogous to POST/PUT/DELETE)
- Subscription: Real-time data via WebSocket
- Selection Set: The fields you want returned
Schema Example
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
user(id: ID!): User
users(limit: Int): [User!]!
}
type Mutation {
createPost(title: String!, body: String!): Post!
}
The ! means non-null. [Post!]! means a non-null list of non-null Post objects.
Apollo Kotlin Setup
Adding Apollo to Your Project
Apollo Kotlin is the standard GraphQL client for Android. It generates type-safe Kotlin code from your schema and queries.
// build.gradle.kts (app)
plugins {
id("com.apollographql.apollo3")
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime:3.8.4")
implementation("com.apollographql.apollo3:apollo-cache-memory:3.8.4")
implementation("com.apollographql.apollo3:apollo-normalized-cache:3.8.4")
}
Place your .graphql files in src/main/graphql/. Apollo generates Kotlin classes from them at build time.
Defining a Query
# src/main/graphql/GetUser.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
This generates a GetUserQuery class with a GetUserQuery.Data response type.
Making the Call
// Create the Apollo client
val apolloClient = ApolloClient.Builder()
.serverUrl("https://api.yourapp.com/graphql")
.build()
// Execute the query
viewModelScope.launch {
val response = apolloClient
.query(GetUserQuery(id = "1"))
.execute()
when {
response.exception != null -> {
// Network or parsing error
_uiState.value = UiState.Error(response.exception!!.message)
}
response.hasErrors() -> {
// GraphQL errors in response.errors
val errorMsg = response.errors?.firstOrNull()?.message
_uiState.value = UiState.Error(errorMsg ?: "Unknown error")
}
else -> {
val user = response.data?.user
_uiState.value = UiState.Success(user)
}
}
}
GraphQL errors do not always result in HTTP errors. A 200 response can contain errors in the errors array alongside partial data. Always check both exception and errors.
Mutations, Subscriptions, and Caching
Mutations
Mutations send data to the server and return the modified object:
# CreatePost.graphql
mutation CreatePost($title: String!, $body: String!) {
createPost(title: $title, body: $body) {
id
title
body
author {
id
name
}
}
}
val response = apolloClient
.mutation(CreatePostMutation(title = "New Post", body = "Content here"))
.execute()
if (!response.hasErrors()) {
val newPost = response.data?.createPost
// Update UI with the created post
}
Normalized Caching
Apollo Kotlin uses a normalized cache. Instead of storing responses by query, it stores each object by its id or __typename + id. When any query updates a User with id: 1, all queries referencing that user reflect the change.
val apolloClient = ApolloClient.Builder()
.serverUrl("https://api.yourapp.com/graphql")
.normalizedCache(
MemoryNormalizedCacheFactory()
)
.build()
Cache Policies
// Always fetch from network
apolloClient.query(GetUserQuery(id = "1"))
.fetchPolicy(FetchPolicy.NetworkOnly)
.execute()
// Use cache if available, otherwise fetch from network
apolloClient.query(GetUserQuery(id = "1"))
.fetchPolicy(FetchPolicy.CacheFirst)
.execute()
// Write to cache then read from network
apolloClient.query(GetUserQuery(id = "1"))
.fetchPolicy(FetchPolicy.NetworkFirst)
.execute()
Optimistic Updates
For mutations that update the UI immediately (before server confirms):
apolloClient
.mutation(CreatePostMutation(title = "New", body = "Content"))
.optimisticUpdate(CreatePostMutation.Data(
createPost = CreatePostMutation.CreatePost(
id = "temp-id",
title = "New",
body = "Content",
author = CreatePostMutation.Author(id = "1", name = "You")
)
))
.execute()
The cache shows the optimistic result immediately. When the server responds, Apollo replaces it with the real data.
Subscriptions
For real-time data via WebSocket:
apolloClient.subscription(OnNewPostSubscription())
.toFlow()
.collect { response ->
val newPost = response.data?.onNewPost
// Update UI with real-time post
}
Subscriptions use WebSocket connections. On Android, manage the connection lifecycle carefully to avoid battery drain when the app is in the background.
Quiz
1. What problem does GraphQL solve compared to REST?
2. What does Apollo Kotlin's normalized cache do?
3. Can a GraphQL response return HTTP 200 but still contain errors?
4. What is an optimistic update in Apollo?
Flashcards
Question
What is the difference between a query and a mutation in GraphQL?
Click to reveal answer
Answer
A query reads data without side effects. A mutation writes data and may trigger side effects. Both can return data, but mutations are executed sequentially by the server.
Question
What does the ! mean in a GraphQL schema?
Click to reveal answer
Answer
It marks a field as non-null. String! means the field is always present. [Post!]! means a non-null list of non-null Post objects.
Question
How do you handle real-time data in Apollo Kotlin?
Click to reveal answer
Answer
Use subscriptions, which maintain a WebSocket connection to the server. Collect responses as a Kotlin Flow. Manage the connection lifecycle to avoid battery drain on Android.
Question
What is the FetchPolicy.NetworkFirst strategy?
Click to reveal answer
Answer
Apollo tries the network first. If the network call fails, it falls back to cached data. This provides the freshest data while still offering offline resilience.
Revision Notes
Key Takeaways
- 1. GraphQL eliminates over-fetching and under-fetching by letting clients specify exact fields
- 2. Apollo Kotlin generates type-safe code from .graphql files at build time
- 3. Normalized caching ensures all queries see updates when any object changes
- 4. Always check both response.exception and response.errors for complete error handling
Interview Tips
- • Explain when GraphQL is better than REST (multiple related resources, mobile bandwidth)
- • Discuss the tradeoffs of normalized caching vs simple caching
- • Describe how you would handle offline support with Apollo
- • Know why GraphQL returns 200 even with errors and how to handle it
Cheat Sheet
GraphQL Cheat Sheet
Core Operations:
- Query: Read data (GET equivalent)
- Mutation: Write data (POST/PUT/DELETE equivalent)
- Subscription: Real-time data via WebSocket
Apollo Kotlin Setup:
- Place .graphql files in src/main/graphql/
- Generate type-safe Kotlin classes at build time
- Use ApolloClient.Builder().serverUrl().build()
Caching:
- Normalized cache stores objects by type + ID
- FetchPolicy: CacheFirst, NetworkFirst, NetworkOnly
- Optimistic updates for instant UI feedback
- All queries referencing an object update when it changes
Error Handling:
- Check response.exception for network errors
- Check response.errors for GraphQL errors
- A 200 response can still contain errors