September 23, 2020

Add GraphQL to Your Jetpack Compose Apps

Martin Bonnin
FrontendHow-to

Jetpack Compose is a declarative UI framework for building Android UIs written in Kotlin. Apollo Android is a GraphQL client that generates typesafe Kotlin models to hold data to be displayed. GraphQL and Jetpack Compose: they were meant to meet.

In this post, we will show how to query a GraphQL endpoint with Apollo Android and connect the response to a Compose UI. We will also explore how the Apollo normalized cache enables reactive scenarios that play nicely with Compose unidirectional data flow.

The code for this post is available in the compose branch of the Apollo Android Tutorial.

Note: Jetpack Compose just reached the alpha stage, and the APIs will most likely change. This post assumes some familiarity with Compose and Apollo Android. If you are new to either of them, check out their respective tutorials:

The GraphQL query

In this example, we’re going to use the full-stack tutorial GraphQL endpoint. It’s an endpoint that lists all the SpaceX missions as well as additional data concerning their launches. As an excellent addition, it also allows you to book a trip to space! You can explore and test the API using the built-in playground. For starters, we’ll want to get a list of launches to display in our UI:

query LaunchList {
 launchConnection: launches {
   launches {
     id
     site
     mission {
       name
       missionPatch(size: SMALL)
     }
   }
 }
}

This code sample queries the list of launches and data concerning the mission: the name and mission patch, the mission’s logo; it will generate the following Kotlin type-safe models (details omitted for clarity):

data class LaunchListQuery {

  data class Data(
    val id: String,
    val launches: List<Launch>,
  }

  data class Launch(
    val id: String,
    val site: String?
    val mission: List<Mission>
  )

  data class Mission(
    val name: String?,
    val missionPatch: String?
  )
} 

The Composable

Once we have our query, we can write our Compose UI. Compose works by composing elementary functions, producing a tree of composable functions (Composables), renderable on demand. As an example, to display a list of launches, you can use a ScrollableColumn:

@Composable
fun LaunchList(launchList: List<LaunchListQuery.Launch>) {
   ScrollableColumn {
       launchList.forEach { launch ->
           LaunchItem(
               modifier = Modifier.fillMaxWidth(),
               launch = launch
           )
       }
   }
}

For this post’s purpose, we’ll leave LaunchItem out of the equation and assume that LaunchItem is a composable that displays a Launch. If you’re curious, you can check the implementation in the Github repository. The LaunchList Composable is said to be stateless. The way it’s displayed only depends on its inputs. It doesn’t contain any logic besides rendering.

Connecting GraphQL and Compose

On the other hand, executing a GraphQL operation is a long-running operation, and we’ll need state to execute it. We’ll also need to close the network connection when it’s not needed anymore. For this, we will introduce a new Composable on top of LaunchList that we’ll name ApolloLaunchList. Contrary to the stateless LaunchList, ApolloLaunchList is stateful. It contains some state and logic. It is said to “hoist” the state of LaunchList.

@Composable
fun ApolloLaunchList {

  // We need to handle the state here, stay tuned

  when (val value = state.value) {
     is UiState.Success -> LaunchList(launchList = value.launchList)
     is UiState.Loading -> Loading()
     is UiState.Error -> Error()
  }
}

Where UiState is a sealed class representing the different possible states:

sealed class UiState {
   object Loading : UiState()
   object Error : UiState()
   class Success(val launchList: List<LaunchListQuery.Launch>) : UiState()
}

To manage state, Compose comes with remember and mutableStateOf functions (you can learn more about them in the Compose state codelabs). Going into the details of these two functions is beyond the scope of this post. Overall, I like to think of them as:

  • remember pins data in the composition tree so that it can be looked-up during recompositions.
  • mutableStateOf tells the compiler to monitor changes to this data and trigger a recomposition if it changes.

On top of these APIs, Compose comes with helpers to interact with Kotlin coroutines like launchInComposition.

  • launchInComposition binds a coroutine scope to the lifecycle of a composable. The coroutine gets canceled when launchInComposition leaves the composition.

With these tools at hand, we can execute our GraphQL query in the ApolloLaunchList Composable:

// tell Compose to remember our state across recompositions
val state = remember { 
   // our state will be of type UiState and default to the Loading state
   mutableStateOf<UiState>(UiState.Loading) 
}

// launch a coroutine and cancel it when needed
launchInComposition {
   try {
       // execute the GraphQL query with ApolloClient 
       val launchList = apolloClient(context).query(LaunchListQuery()).toDeferred().await()
           .data
           .launchConnection
           .launches
       // and update the state
       state.value = UiState.Success(launchList)
   } catch (e: ApolloException) {
       // if there's an error, display an error
       state.value = UiState.Error
   }
}

// Display the UiState
when (val value = state.value) {
   is UiState.Success -> LaunchList(launchList = value.launchList)
   ...

With this, the Compose UI gets data from the GraphQL endpoint and displays the list of launches:

image.png

And that’s it! With just a few lines, you can execute a GraphQL query and get a typesafe Kotlin model to feed your Compose UI. But it gets better. With the Apollo Normalized Cache, you can have your Compose UI react and update automatically on mutations. We’ll see that in the next paragraph.

Making everything reactive

Apollo Android comes with a normalized cache. Cache normalization is a powerful concept; it minimizes data redundancy while simultaneously notifying cache watchers about changes. You can learn more about cache normalization in this post that demystifies cache normalization. To subscribe to cache modifications, Apollo Android uses watchers. Watchers run a query and re-fetch it whenever the cache changes, and the UI can be updated accordingly.

Diagram-for-Martin-9.2-v2.png

Let’s define a BookTrip mutation to book our trip:

mutation BookTrip($id:ID!) {
 bookTrips(launchIds: [$id]) {
   launches {
     # The id is required here to lookup the correct entry in cache
     id
     # This will be written in cache and trigger an update in any watcher that is subscribed to this launch
     isBooked
   }
 }
}

Add a button in the list to book a trip that will trigger the mutation and update the cache. You can find the code in the Github repo.

image.png

Finally, use watchers to react to cache changes. For this we can use Coroutines Flows and the Compose collectAsState helper function. Replace the previous code:

val state = remember {
   apolloClient(context).query(LaunchListQuery()).watcher().toFlow()
       .map {
           val launchList = it
               .data
               ?.launchConnection
               ?.launches
               ?.filterNotNull()
           if (launchList == null) {
               // There were some error
               // TODO: do something with response.errors
               UiState.Error
           } else {
               UiState.Success(launchList)
           }
       }
       .catch { e ->
           emit(UiState.Error)
       }
}
// collectAsState will turn our flow into State that can be consumed by Composables
.collectAsState(initial = UiState.Loading)

// Display the UiState as usual
when (val value = state.value) {
   is UiState.Success -> LaunchList(launchList = value.launchList)
   ...

What’s next

These are still the very early days of compose, but the experience is already great. Having a unidirectional data flow that reacts to cache changes makes it very easy to write robust UIs that always show the latest available data. I’m looking forward to seeing even more of what we can build with Compose!

Things will undoubtedly adjust, and soon, best practices will start to emerge. For example, in this post, we made the queries from the Composables, but we can also accomplish this from ViewModels or Repositories; this would account for better testing and separation of concerns.

To see this post’s code, including some Coil-Composables and ConstraintLayout integrations, check out the compose branch of the Apollo Android tutorial. Finally, if you have ideas about how to best integrate your GraphQL queries with Jetpack Compose, please reach out on Github; we’re looking forward to making the experience as smooth as possible. Happy composing!

Written by

Martin Bonnin

Stay in our orbit!

Become an Apollo insider and get first access to new features, best practices, and community events. Oh, and no junk mail. Ever.

Similar posts

September 11, 2020

Announcing the GraphQL at Enterprise Scale Guide [Free Ebook]

by Michael Watson

Company