Hey Android developers! Today, I’m super excited to walk you through an advanced topic: using GraphQL with Kotlin Coroutines and Flow for efficient data fetching. This setup lets us manage asynchronous operations with ease, and we’re going to use a clean architecture that includes RestClient, DataSource, Repository, UseCase, ViewModel, and Jetpack Compose at the UI level. Let’s dive in!

Project Structure

Here’s a quick look at the structure we’ll be building:

GraphQL with Android
Project Structure
  • RestClient: Responsible for making network requests.
  • DataSource: The interface layer for the data sources (GraphQL API).
  • Repository: Contains business logic and mediates between data sources.
  • UseCase: Defines specific actions for the app, such as fetching data.
  • ViewModel: Manages the app’s state using Flow.
  • ComposeView: Presents the UI using Jetpack Compose.

Adding the Required Dependencies

To start, open up your build.gradle.kts (app-level) file and throw in these dependencies:


// In your build.gradle.kts (app-level)
dependencies {
    implementation("com.apollographql.apollo3:apollo-runtime:3.8.5")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
}

Setting Up the RestClient

The first step is to set up the RestClient for making network requests. We’ll use Apollo’s GraphQL client and OkHttp to handle the HTTP requests.


// RestClient.kt
import com.apollographql.apollo3.ApolloClient
import okhttp3.OkHttpClient

object RestClient {
    private const val BASE_URL = "https://your-graphql-api-endpoint.com/graphql"

    val apolloClient: ApolloClient by lazy {
        ApolloClient.Builder()
            .serverUrl(BASE_URL)
            .okHttpClient(OkHttpClient.Builder().build())
            .build()
    }
}

Now, we have a simple RestClient that handles the creation of the ApolloClient for GraphQL requests.

Creating the DataSource

The DataSource will interface directly with our API. It’s responsible for making the actual network calls. In this case, we’ll have a function that fetches Product data from the GraphQL API.


// ProductDataSource.kt
import com.apollographql.apollo3.api.ApolloResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import com.example.GetProductDetailsQuery

interface ProductDataSource {
    fun getProductDetails(productId: String): Flow<ApolloResponse<GetProductDetailsQuery.Data>>
}

class ProductDataSourceImpl : ProductDataSource {
    override fun getProductDetails(productId: String): Flow<ApolloResponse<GetProductDetailsQuery.Data>> {
        return flow {
            val response = RestClient.apolloClient.query(GetProductDetailsQuery(productId)).execute()
            emit(response)
        }
    }
}

Using Flow for Asynchronous Data Fetching

We’re using Flow here to emit the API response asynchronously. This allows us to easily collect the data later in our ViewModel.

Building the Repository

The Repository sits between the data sources and the business logic, ensuring data is handled correctly. Here’s how to build the repository for fetching Product data:


// ProductRepository.kt
import kotlinx.coroutines.flow.Flow
import com.apollographql.apollo3.api.ApolloResponse

class ProductRepository(private val dataSource: ProductDataSource) {
    fun getProduct(productId: String): Flow<ApolloResponse<GetProductDetailsQuery.Data>> {
        return dataSource.getProductDetails(productId)
    }
}

Our repository simply forwards the request to the DataSource and returns a Flow that emits the API responses.

Defining the UseCase

The UseCase defines a single action the app can perform, in this case, fetching product details. It helps keep our ViewModel clean by isolating business logic.


// GetProductUseCase.kt
class GetProductUseCase(private val repository: ProductRepository) {
    operator fun invoke(productId: String) = repository.getProduct(productId)
}

The invoke function allows us to call the UseCase directly as if it were a function, which makes the code a little cleaner.

Setting Up the ViewModel

The ViewModel is where we use Flow to manage the UI’s state. We collect data from the UseCase and expose it to the UI through a StateFlow.


// ProductViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

class ProductViewModel(private val getProductUseCase: GetProductUseCase) : ViewModel() {

    private val _productState = MutableStateFlow<ProductState>(ProductState.Loading)
    val productState: StateFlow<ProductState> = _productState

    fun fetchProduct(productId: String) {
        viewModelScope.launch {
            getProductUseCase(productId)
                .catch { e -> _productState.value = ProductState.Error(e.message ?: "Unknown error") }
                .collect { response -> 
                    _productState.value = ProductState.Success(response.data?.product)
                }
        }
    }
}

sealed class ProductState {
    object Loading : ProductState()
    data class Success(val product: GetProductDetailsQuery.Product?) : ProductState()
    data class Error(val message: String) : ProductState()
}

As you can see, we’re collecting the Flow emitted by the UseCase and handling success, loading, and error states accordingly using a sealed class.

Displaying Data with Jetpack Compose

Finally, we’ll show the fetched Product data in our UI using Jetpack Compose. We’ll observe the ViewModel’s productState and update the UI accordingly.


// ProductScreen.kt
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
    val productState by viewModel.productState.collectAsState()

    when (productState) {
        is ProductState.Loading -> CircularProgressIndicator()
        is ProductState.Success -> {
            val product = (productState as ProductState.Success).product
            product?.let {
                Text(text = "Name: ${it.name}")
                Text(text = "Price: ${it.price}")
                Text(text = "Description: ${it.description}")
            }
        }
        is ProductState.Error -> {
            Text(text = (productState as ProductState.Error).message)
        }
    }
}

Here, we observe the StateFlow from the ViewModel, and depending on the state (loading, success, or error), we update the UI accordingly. Easy and clean!

Wrapping It Up

So, there you have it! We’ve built a fully functional GraphQL integration using Kotlin Coroutines and Flow in a clean architecture. From RestClient to Jetpack Compose, we’ve covered every layer of the stack.

If you want more detailed guides like this, be sure to check out my post on State Management in Jetpack Compose or the one on Kotlin 2.0 Key Updates.

For official documentation on GraphQL and Apollo, head over to the Apollo GraphQL Docs, or if you want to dig deeper into Coroutines, visit the Android Coroutines Guide.

Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.

Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *