In a classic client-server architecture, an Android application can serve as a frontend client that relies on data from a backend service. Commonly, developers rely on REST and GraphQL web specifications to facilitate communication in this architecture.
In this article, we will talk about GraphQL and where it shines over REST, and finally, we will write some code to connect an Android application to a GraphQL server.
Jump ahead:
GraphQL is a query language for fetching and mutating data from a backend service. When interacting with a GraphQL service, there is only one entry point (URL) for retrieving and mutating data. You may ask, how is that possible? Well, with GraphQL queries, you can do simply anything.
Before I show you a sample query, let’s talk about the three GraphQL operators and their usage. GraphQL operators are used to perform CRUD requests on the server and listen for events. There are three main operators in GraphQL:Query
, Mutation
, and Subscription
:
Query
: This is useful for fetching data from the serverMutation
: This operator is used to create, edit, and delete data on the serverSubscription
: This operator is not one that surfaces in common use cases. However, it comes in handy when implementing real-time communication between your client and serverPrimarily, GraphQL addresses the issues of over-fetching and under-fetching. GraphQL shines over other web specifications like REST because the client can fetch only the data it needs.
If things still sound hazy, don’t worry — you’ll have an epiphany as we write some code 😊.
To make things more interesting, we will refactor a GitHub project using GitHub GraphQL API.
Here are a few things to note about the project we’ll be refactoring:
Go ahead and clone this GitHub Lagos Project sample application like so:
git clone https://github.com/Lamouresparus/GithubLagosProject.git
If the cloning is successful, open the project in Android Studio and run it. If all is well, you will have the same thing as in the screenshot below:
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0") implementation("com.apollographql.apollo3:apollo-runtime:3.6.2")
The Logging
interceptor is needed to aid the printing of network traffic to the Logcat
window. The second dependency, Apollo, is a library for working with GraphQL APIs on Android. Apollo is for GraphQL in the same way that Retrofit is for REST.
Migration can be a daunting task especially if you are new to a technology and if there are many touch points in the undertaking. However, in cases where the codebase is well structured and modularised, migration becomes less complex and risky. You can safely locate and update components without affecting the entire system.
In the next couple of steps, I will show you how to migrate our project to use the GitHub GraphQL API.
ApolloClient
In order to use ApolloClient
to communicate with GitHub GraphQL API, we will need to migrate. First, locate NetworkModule
in com.githublagos.di
and update the file with this code:
@Module @InstallIn(SingletonComponent::class) class NetworkModule { @Provides @Singleton fun okhttpClient(): OkHttpClient { val logging = HttpLoggingInterceptor() .apply { level = HttpLoggingInterceptor.Level.BODY } return OkHttpClient.Builder() .addInterceptor(AuthInterceptor()) .addInterceptor(logging) .build() } @Provides @Singleton fun provideApollo(client: OkHttpClient) : ApolloClient { return ApolloClient.Builder() .serverUrl("https://api.github.com/graphql") .okHttpClient(client).build() } }
From the code block above, there are two object types: OkHttpClient
and ApolloClient
. These are declared as dependencies that will be provided at runtime through dependency injection with the Hilt.
OkHttpClient
is an HTTP engine used by ApolloClient
for making a network request, while ApolloClient
itself is the library for initiating network requests to a GraphQL server.
Take note of how I customized OkHttpClient
with a customer interceptor (AuthInterceptor
). I included the interceptor to modify the HTTP request to include an authorization header.
GitHub GraphQL server requires every request to be authenticated with the right scopes. See the self-explanatory code block below:
class AuthInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .addHeader("Authorization", "token put_your_personal_access_token_here") return chain.proceed(request.build()) } }
Queries
The GraphQL schema defines all object types supported by GitHub’s API. Object types from this schema are used to build queries for making a network request. Follow the following steps to include the schema in the project:
graphql
folder under app/src/main
Now, we will build our query
to specify which data we want the server to return to us. In the same GraphQL
folder, create a GetUsers.graphql
file and paste it into it the code block below:
query GetUsers($query: String!, $pageSize:Int!, $after: String) { search(query: $query, type: USER, first: $pageSize, after: $after) { nodes { ... on User { avatarUrl login url } } pageInfo { endCursor hasNextPage } } }
In a nutshell, the code block above is a query that will be used to request a list of users from GitHub GraphQL server. We will examine each component of this query below.
GetUsers
is the query that accepts three parameters:
Query String
: This can be queries like location
, language
, and more to help narrow the search results to our specific needsPage Size
: This is the number of users we want to be returned by the server on each search queryAfter String
: This string helps to achieve pagination when working with the GitHub GraphQL server. The value can be the cursor ID of the last user object. Usually, for the initial search query, we set it to null
nodes
is basically a list of search items and are defined as [SearchResultItem]
in our GitHub schema. SearchResultItem
is a union type, and it is of several other types defined in the schema. In our case, we are interested in the User
type. The trailing syntax on the User
object is how we specify which type of SearchResultItem
we are interested in.
The pageInfo
requests the basic page information. We will use the data returned to implement pagination. endCursor
is the cursor ID of the last User
object for the current page. hasNextPage
is a Boolean that evaluates if there’s another page of users.
Now, we have defined a GraphQL file, but we cannot use it directly in our Kotlin code. We have to rebuild the project for code generation to kick in and create a Kotlin representation of the file. If the Gradle build is successful, you will find GetUsersQuery
created in your build directory:
In the previous steps, we created GetUsersQuery
and a singleton instance of ApolloClient
. These objects will be used to perform a search on GitHub GraphQL server. Copy the code below into your IDE:
class GithubApi @Inject constructor( private val apolloClient: ApolloClient ) { // const val QUERY_LOCATION = "location:lagos" suspend fun getUsers( location: String = QUERY_LOCATION, endCursor: String?, pageSize: Int ): UsersResponseModel { val nextCursor = Optional.presentIfNotNull(endCursor) val query = GetUsersQuery(location, pageSize, nextCursor) val response = apolloClient.query(query).execute() val cursor = response.data?.search?.pageInfo?.endCursor val users = response.data?.search?.nodes?.map { val userName = it?.onUser?.login.orEmpty() val avatar = it?.onUser?.avatarUrl.orEmpty() val htmlUrl = it?.onUser?.url.orEmpty() UserRemote(userName, avatar, htmlUrl) }?.filter { it.login.isNotEmpty() }.orEmpty() return UsersResponseModel(cursor, users) } }
In the code block above, I have refactored GithubApi
from an interface to a class type that accepts ApolloClient
as an argument. This object is responsible for executing queries
, mutations
, and subscriptions
.
The method
signature for getUsers
accepts a nullable endCursor
argument, making it optional. In Apollo GraphQL, we use an Optional
type to represent an optional value. This object is a Kotlin-sealed class that can either be Present
or Absent
.
With one line of code, we can execute a GraphQL query like so: apolloClient.query(query).execute()
.
This does all the heavy lifting, returning a response containing a list of GitHub users alongside the page information.
GithubUserSource
If you made it this far, kudos to you! This is the last piece of the refactoring. We want the pagination source to depend on Apollo GraphQL to execute multiple queries as per the scroll request.
First, locate GithubUserSource
and fix the breaking changes with the code block below:
class GithubUserSource @Inject constructor(private val api: GithubApi) : PagingSource<String, UserDomain>() { override fun getRefreshKey(state: PagingState<String, UserDomain>): String? { return null } override suspend fun load(params: LoadParams<String>): LoadResult<String, UserDomain> { return try { val previousKey = params.key val result = api.getUsers(pageSize = PAGE_SIZE, endCursor = previousKey ) val nextKey = result.endCursor val userList = result.items.map { it.mapToDomain() } LoadResult.Page( data = userList, prevKey = previousKey, nextKey = nextKey ) } catch (exception: IOException) { return LoadResult.Error(exception) } catch (exception: HttpException) { return LoadResult.Error(exception) } catch (exception :Exception){ return LoadResult.Error(exception) } } }
The major difference between the former and the new implementation is in the key
type. Because we are using the cursor of the last user object to facilitate our pagination, we have defined the PagingSource
to have a string
key and not an integer
.
The load()
method is invoked when there is a request for more content to be loaded into the listview
. Whenever this invocation occurs, we fetch the users from the server, specifying the page size
and end cursor
as parameters.
On the first invocation, previousKey
will be null
because, at that instance, there is no nextKey
cached within the paging source. If the operation is successful, we will get a response that contains the user’s information and the end cursor
. This response is mapped into a LoadResult.Page
object and fed into the paging source.
Voila! We have refactored a REST-based Android project to GraphQL using ApolloClient
. Now, you should be able to build and run the project successfully. There will be no visible change on the user’s screen because we only changed the data source implementation.
From my perspective as a mobile developer, GraphQL is easy to use on Android. The only learning curve I can think of would be the syntax of the query language. Other than that, it’s a breeze! The GraphQL documentation is a great place to learn more about it.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.