Jetpack Compose and GraphQL, a very merry combination!

Share on:

There are a number of ways in which Android and iOS development is “converging” at the moment but I think the most exciting area is the adoption of Declarative UI frameworks on both platforms -SwiftUI on iOS and Jetpack Compose on Android. As such, my interest is always piqued, when I see tweets like following from Thomas Ricouard, by what would be involved in creating a Jetpack Compose version of a project like this. An added interest in this case is the use of GraphQL and also saw this as an opportunity to try out the Apollo GraphQL library (and, for bonus points, it’s Kotlin Multiplatform support!). The code shown here is included as part of MortyComposeKMM project on Github.



GraphQL

So, GraphQL is described here as

a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API

In general it’s seen as an alternative to “traditional” REST APIs that gives more control over content returned and a mechanism to obtain schema of that content…from which, as we’ll see later, strongly typed client APIs can be generated. The server we’re going to interact with in this project is https://rickandmortyapi.com/graphql (BTW the web page at this url also provides a way to interactively try out different queries).

The following is GraphQL query for example for retrieving list of the show’s characters from the endpoint.

query GetCharacters($page: Int) {
	characters(page: $page) {
		info {
			pages, count, next
		}
		results {
			id, name, image,
			episode {
				id, name
			}
		}
	}
}

Apollo GraphQL

The way we’re going to interact with our GraphQL server in this case is through use of the Apollo library, described as a “strongly-typed, caching GraphQL client for the JVM, Android and Kotlin multiplatform”

Joe Birch wrote a really nice article recently on getting started wth Apollo GraphQL on Android that covers a lot of details on how to set it up so will just mention below a number of the specific steps needed for this project. Also, in our case we’re using Apollo’s Kotlin Multiplatform support and have followed some of instructions related to that.

So, having added the Apollo gradle plugin and related dependencies to our shared Kotlin Multiplatform module we can now run following to retrieve schema.json for the endpoint we’re interacting with.

./gradlew downloadApolloSchema --endpoint="https://rickandmortyapi.com/graphql"  --schema="schema.json"

We copy this file along with our .graphql query file(s) to shared/src/commonMain/graphql/ folder (see Queries.graphql in repository to see the queries we’re using…the GetCharacters one was shown earlier). Now, when we build the shared module, source code is generated which allows us to interact with the endpoint. Using this code we can now create something like MortyRepository class as shown below where we instantiate ApolloClient object and use it to make various queries.

class MortyRepository {
    private val apolloClient = ApolloClient(
        networkTransport = ApolloHttpNetworkTransport(
            serverUrl = "https://rickandmortyapi.com/graphql",
            headers = mapOf(
                "Accept" to "application/json",
                "Content-Type" to "application/json",
            )
        )
    )

    suspend fun getCharacters(page: Int): Response<GetCharactersQuery.Data> {
        return apolloClient.query(GetCharactersQuery(Input.optional(page))).execute().single()
    }

    suspend fun getEpisodes(page: Int): Response<GetEpisodesQuery.Data> {
        return apolloClient.query(GetEpisodesQuery(Input.optional(page))).execute().single()
    }
}

Compose Paging library

The above GraphQL queries support pagination and this plays very well with use of Compose’s Paging library. We can now create PagingSource such as following that invokes APIs in our MortyRepository.

class CharactersDataSource(private val repository: MortyRepository) : PagingSource<Int, CharacterDetail>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CharacterDetail> {
        val pageNumber = params.key ?: 0

        val charactersResponse = repository.getCharacters(pageNumber)
        val characters = charactersResponse?.resultsFilterNotNull()?.map { it.fragments.characterDetail }

        val prevKey = if (pageNumber > 0) pageNumber - 1 else null
        val nextKey = charactersResponse.data?.characters?.info?.next
        return LoadResult.Page(data = characters ?: emptyList(), prevKey = prevKey, nextKey = nextKey)
    }
}

Our ViewModel then returns a Flow representing the paged data.

class CharacterListsViewModel(private val repository: MortyRepository): ViewModel() {

    val characters: Flow<PagingData<CharacterDetail>> = Pager(PagingConfig(pageSize = 20)) {
        CharactersDataSource(repository)
    }.flow

}

And, finally, we can use Compose Paging Library’s collectAsLazyPagingItems() to collect values which we can show in the list (Note we’re also using Koin to inject view model dependency in our Composable function). Now as we scroll down through the list, the data is automatically retrieved from endpoint as needed.

@Composable
fun CharactersListView() {
    val characterListsViewModel = getViewModel<CharacterListsViewModel>()
    val lazyCharacterList = characterListsViewModel.characters.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyCharacterList) { character ->
            character?.let {
                CharactersListRowView(character)
            }
        }
    }
}

And this is what our UI looks like….on Android: Desktop Compose Screenshot

and on iOS (using same GraphQL queries in shared Kotlin Multiplatform code): Desktop Compose Screenshot


Featured in Kotlin Weekly Issue #230, Android Weekly Issue #446 and GraphQL Weekly Issue #218