🌐 Building a Compose Multiplatform GitHub GraphQL Client with Apollo Kotlin

🌐 Building a Compose Multiplatform GitHub GraphQL Client with Apollo Kotlin

Have you ever wanted to harness the power of GraphQL in a Compose Multiplatform app? In this guide, I’ll walk you through how to build a GitHub GraphQL client using Apollo Kotlin in a Compose Multiplatform setup.


🧰 Prerequisites

  • IntelliJ IDEA or Android Studio (latest)
  • Basic knowledge of Compose Multiplatform and Kotlin
  • GitHub personal access token (for GitHub GraphQL API)

⚙️ Step 1: Install Apollo GraphQL Plugin

Install the Apollo GraphQL plugin:

  1. Go to Settings → Plugins
  2. Search for “Apollo GraphQL”
  3. Install and restart the IDE

📦 Step 2: Define Apollo Dependencies

In your libs.versions.toml:

[versions]
apollo = "4.1.1"

[libraries]
apollo-runtime = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollo" }
apollo-normalized-cache = { module = "com.apollographql.apollo3:apollo-normalized-cache", version.ref = "apollo" }
apollo-normalized-cache-sqlite = { module = "com.apollographql.apollo3:apollo-normalized-cache-sqlite", version.ref = "apollo" }

🏗️ Step 3: Set Up Gradle Plugins and Source Sets

In your app/build.gradle.kts:

plugins {
    alias(libs.plugins.apollo)
}

apollo {
    service("GithubGraphQLApi") {
        // GraphQL configuration here.
        // https://www.apollographql.com/docs/kotlin/advanced/plugin-configuration/
        packageName.set("com.droidslife.graphqldemo.graphql")
        introspection {
            endpointUrl.set("https://api.github.com/graphql")
            schemaFile.set(file("src/commonMain/graphql/schema.graphqls"))
        }
    }
}

Ensure GraphQL queries are placed under src/commonMain/graphql.


🔽 Step 4: Download GitHub GraphQL Schema

Use either method:

  • Plugin: Tools → Apollo → Download Schema
  • Terminal:
./gradlew :app:downloadServiceApolloSchemaFromIntrospection \
  --endpoint="https://api.github.com/graphql" \
  --header="Authorization: Bearer YOUR_GITHUB_TOKEN"

🧠 Step 5: Create Apollo Client And interceptor

Create ApolloClientCache.kt:

class ApolloClientCache : KoinComponent {
    private val mutex = Mutex()
    private val interceptor: MyApolloInterceptor by inject()
    private var apolloClient: ApolloClient? = null

    // GitHub GraphQL API endpoint
    private val githubGraphQLEndpoint = "https://api.github.com/graphql"

    /**
     * Gets the Apollo client instance, creating it if it doesn't exist.
     * Thread-safe using a mutex to prevent multiple instances from being created.
     */
    suspend fun getApolloClient(): ApolloClient {
        if (apolloClient == null) {
            mutex.withLock {
                if (apolloClient == null) {
                    apolloClient = createApolloClient()
                }
            }
        }
        return apolloClient!!
    }

    /**
     * Creates a new Apollo client configured for the GitHub GraphQL API.
     */
    private fun createApolloClient(): ApolloClient {
        return ApolloClient.Builder()
            .networkTransport(
                HttpNetworkTransport.Builder()
                    .serverUrl(githubGraphQLEndpoint)
                    .build()
            )
            .normalizedCache(
                MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) // 10MB cache
            )
            .addInterceptor(interceptor)
            .build()
    }
}

Create Interceptor to add token MyApolloInterceptor.kt:

class MyApolloInterceptor : ApolloInterceptor, KoinComponent {
    // Use the GitHub token from BuildConfig
    // This token is set during build time from the environment variable
    private val githubToken: String = BuildConfig.GITHUB_TOKEN

    override fun <D : Operation.Data> intercept(
        request: ApolloRequest<D>,
        chain: ApolloInterceptorChain
    ): Flow<ApolloResponse<D>> = flow {
        // Create a new request with the Authorization header
        val authorizedRequest = request.newBuilder()
            .addHttpHeader("Authorization", "Bearer $githubToken")
            .build()

        // Proceed with the modified request
        emitAll(chain.proceed(authorizedRequest))
    }
}

🔍 Step 6: Define Your GraphQL Query

Create SearchRepositoriesQuery.graphql under src/commonMain/graphql:

# Query to search for repositories
query SearchRepositoriesQuery($query: String!, $first: Int = 20) {
  search(query: $query, type: REPOSITORY, first: $first) {
    repositoryCount
    edges {
      node {
        ... on Repository {
          id
          name
          nameWithOwner
          description
          url
          stargazerCount
          forkCount
          owner {
            login
            avatarUrl
          }
          primaryLanguage {
            name
            color
          }
        }
      }
    }
  }
}

🗂️ Step 7: Create a Repository to Execute Query

Now create a Repository to execute above GraphQL Query GitHubRepositoryImpl.kt:

class GitHubRepositoryImpl(
    private val apolloClientCache: ApolloClientCache
) : GitHubRepositoryInterface {
    override fun searchRepositories(query: String): Flow<Result<List<GitHubRepository>>> = flow {
        try {
            val apolloClient = apolloClientCache.getApolloClient()
            val response = apolloClient.query(SearchRepositoriesQuery(query = query)).execute()

            if (response.hasErrors()) {
                val errorMessage = response.errors?.firstOrNull()?.message ?: "Unknown error occurred"
                emit(Result.failure(Exception(errorMessage)))
            } else {
                // Map the GraphQL response to our GitHubRepository data class
                val repositories = response.data?.search?.edges?.mapNotNull { edge ->
                    edge?.node?.onRepository?.let { repo ->
                        GitHubRepository(
                            name = repo.name,
                            owner = repo.owner.login,
                            description = repo.description,
                            stars = repo.stargazerCount,
                            forks = repo.forkCount,
                            language = repo.primaryLanguage?.name,
                            release = repo.latestRelease?.releaseFragment?.toReleaseDetail()
                        )
                    }
                } ?: emptyList()

                emit(Result.success(repositories))
            }
        } catch (e: Exception) {
            emit(Result.failure(e))
        }
    }
}

🖼️ Step 8: Build the UI with Compose Multiplatform

Use Compose Multiplatform to create a simple UI that:

  • Accepts a query
  • Displays a list of repositories with stars, owner, and description

🤝 Let’s Talk!

Have you integrated GraphQL in your Compose Multiplatform project? What were your challenges and learnings?

Drop a ⭐ on the repo if it helped, and feel free to contribute!

🔗 Source Code: GitHub GraphQL Demo App