First query
To view this content, buy the book! 😃🙏
Or if you’ve already purchased.
First query
If you’re jumping in here,
git checkout 1_1.0.0(tag 1_1.0.0, or compare 1...2)
With the schema file, Apollo Android will be able to generate typed models from our queries. Let’s add our first query, for the list of chapters:
app/src/main/graphql/guide/graphql/toc/Chapters.graphql
query Chapters {
chapters {
id
number
title
}
}After saving the file and rebuilding, we get a ChaptersQuery.kt file (it’s in a build/generated/source/ folder—we can open it via ⌘O or Navigate > Class):
// AUTO-GENERATED FILE. DO NOT MODIFY.
//
// This class was automatically generated by Apollo GraphQL plugin from the GraphQL queries it found.
// It should not be modified by hand.
//
package guide.graphql.toc
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.OperationName
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.api.Response
import com.apollographql.apollo.api.ResponseField
import com.apollographql.apollo.api.ScalarTypeAdapters
import com.apollographql.apollo.api.ScalarTypeAdapters.Companion.DEFAULT
import com.apollographql.apollo.api.internal.OperationRequestBodyComposer
import com.apollographql.apollo.api.internal.QueryDocumentMinifier
import com.apollographql.apollo.api.internal.ResponseFieldMapper
import com.apollographql.apollo.api.internal.ResponseFieldMarshaller
import com.apollographql.apollo.api.internal.ResponseReader
import com.apollographql.apollo.api.internal.SimpleOperationResponseParser
import com.apollographql.apollo.api.internal.Throws
import kotlin.Array
import kotlin.Boolean
import kotlin.Double
import kotlin.Int
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List
import okio.Buffer
import okio.BufferedSource
import okio.ByteString
import okio.IOException
@Suppress("NAME_SHADOWING", "UNUSED_ANONYMOUS_PARAMETER", "LocalVariableName",
"RemoveExplicitTypeArguments", "NestedLambdaShadowedImplicitParameter")
class ChaptersQuery : Query<ChaptersQuery.Data, ChaptersQuery.Data, Operation.Variables> {
override fun operationId(): String = OPERATION_ID
override fun queryDocument(): String = QUERY_DOCUMENT
override fun wrapData(data: Data?): Data? = data
override fun variables(): Operation.Variables = Operation.EMPTY_VARIABLES
override fun name(): OperationName = OPERATION_NAME
override fun responseFieldMapper(): ResponseFieldMapper<Data> = ResponseFieldMapper.invoke {
Data(it)
}
@Throws(IOException::class)
override fun parse(source: BufferedSource, scalarTypeAdapters: ScalarTypeAdapters): Response<Data>
= SimpleOperationResponseParser.parse(source, this, scalarTypeAdapters)
@Throws(IOException::class)
override fun parse(byteString: ByteString, scalarTypeAdapters: ScalarTypeAdapters): Response<Data>
= parse(Buffer().write(byteString), scalarTypeAdapters)
@Throws(IOException::class)
override fun parse(source: BufferedSource): Response<Data> = parse(source, DEFAULT)
@Throws(IOException::class)
override fun parse(byteString: ByteString): Response<Data> = parse(byteString, DEFAULT)
override fun composeRequestBody(scalarTypeAdapters: ScalarTypeAdapters): ByteString =
OperationRequestBodyComposer.compose(
operation = this,
autoPersistQueries = false,
withQueryDocument = true,
scalarTypeAdapters = scalarTypeAdapters
)
override fun composeRequestBody(): ByteString = OperationRequestBodyComposer.compose(
operation = this,
autoPersistQueries = false,
withQueryDocument = true,
scalarTypeAdapters = DEFAULT
)
override fun composeRequestBody(
autoPersistQueries: Boolean,
withQueryDocument: Boolean,
scalarTypeAdapters: ScalarTypeAdapters
): ByteString = OperationRequestBodyComposer.compose(
operation = this,
autoPersistQueries = autoPersistQueries,
withQueryDocument = withQueryDocument,
scalarTypeAdapters = scalarTypeAdapters
)
/**
* extend type Subscription {
* sectionCreated: Section
* sectionUpdated: Section
* sectionRemoved: ObjID
* }
*/
data class Chapter(
val __typename: String = "Chapter",
val id: Int,
val number: Double?,
val title: String
) {
fun marshaller(): ResponseFieldMarshaller = ResponseFieldMarshaller.invoke { writer ->
writer.writeString(RESPONSE_FIELDS[0], this@Chapter.__typename)
writer.writeInt(RESPONSE_FIELDS[1], this@Chapter.id)
writer.writeDouble(RESPONSE_FIELDS[2], this@Chapter.number)
writer.writeString(RESPONSE_FIELDS[3], this@Chapter.title)
}
companion object {
private val RESPONSE_FIELDS: Array<ResponseField> = arrayOf(
ResponseField.forString("__typename", "__typename", null, false, null),
ResponseField.forInt("id", "id", null, false, null),
ResponseField.forDouble("number", "number", null, true, null),
ResponseField.forString("title", "title", null, false, null)
)
operator fun invoke(reader: ResponseReader): Chapter = reader.run {
val __typename = readString(RESPONSE_FIELDS[0])!!
val id = readInt(RESPONSE_FIELDS[1])!!
val number = readDouble(RESPONSE_FIELDS[2])
val title = readString(RESPONSE_FIELDS[3])!!
Chapter(
__typename = __typename,
id = id,
number = number,
title = title
)
}
@Suppress("FunctionName")
fun Mapper(): ResponseFieldMapper<Chapter> = ResponseFieldMapper { invoke(it) }
}
}
/**
* Data from the response after executing this GraphQL operation
*/
data class Data(
val chapters: List<Chapter>?
) : Operation.Data {
override fun marshaller(): ResponseFieldMarshaller = ResponseFieldMarshaller.invoke { writer ->
writer.writeList(RESPONSE_FIELDS[0], this@Data.chapters) { value, listItemWriter ->
value?.forEach { value ->
listItemWriter.writeObject(value.marshaller())}
}
}
companion object {
private val RESPONSE_FIELDS: Array<ResponseField> = arrayOf(
ResponseField.forList("chapters", "chapters", null, true, null)
)
operator fun invoke(reader: ResponseReader): Data = reader.run {
val chapters = readList<Chapter>(RESPONSE_FIELDS[0]) { reader ->
reader.readObject<Chapter> { reader ->
Chapter(reader)
}
}?.map { it!! }
Data(
chapters = chapters
)
}
@Suppress("FunctionName")
fun Mapper(): ResponseFieldMapper<Data> = ResponseFieldMapper { invoke(it) }
}
}
companion object {
const val OPERATION_ID: String =
"5749abd11596accd518963e92d32d4f37b4da7073cb1142b67635bfcfae7a330"
val QUERY_DOCUMENT: String = QueryDocumentMinifier.minify(
"""
|query Chapters {
| chapters {
| __typename
| id
| number
| title
| }
|}
""".trimMargin()
)
val OPERATION_NAME: OperationName = object : OperationName {
override fun name(): String = "Chapters"
}
}
}We can now import and use the ChaptersQuery and Chapter classes. The latter has typed data fields matching our query, with nullability determined from the schema:
data class Chapter(
val __typename: String = "Chapter",
val id: Int,
val number: Double?,
val title: String
) { To use the ChaptersQuery class and send our query to the server, we need a client instance. Let’s create it in the data/ folder:
app/src/main/java/guide/graphql/toc/data/Apollo.kt
package guide.graphql.toc.data
import com.apollographql.apollo.ApolloClient
object Apollo {
val client: ApolloClient by lazy {
ApolloClient.builder()
.serverUrl("https://api.graphql.guide/graphql")
.build()
}
}And in ChaptersFragment, let’s replace this list of one string:
adapter.updateChapters(listOf("Android Dev"))with the results of the query:
import androidx.lifecycle.lifecycleScope
import com.apollographql.apollo.coroutines.toDeferred
import com.apollographql.apollo.exception.ApolloException
import guide.graphql.toc.ChaptersQuery
import guide.graphql.toc.data.Apollo
class ChaptersFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launchWhenStarted {
try {
val response = Apollo.client.query(
ChaptersQuery()
).toDeferred().await()
if (response.hasErrors()) {
throw Exception(response.errors?.get(0)?.message)
}
val chapters = response.data?.chapters ?: throw Exception("Data is null")
adapter.updateChapters(chapters)
} catch (e: ApolloException) {
showErrorMessage("GraphQL request failed")
} catch (e: Exception) {
showErrorMessage(e.message.orEmpty())
}
}
}
}Our query statement (Apollo.client.query(ChaptersQuery()).toDeferred().await()) uses the client instance we created, the ChaptersQuery class, and the .toDeferred() method from the coroutines API. Since we are in a CoroutineScope, we can wait for this to complete with .await().
If the response has errors, we display the first error message. If there’s an error during query execution, for instance with the internet connection, Apollo.client.query() will throw a subclass of ApolloException.
We’re left with a type mismatch error on adapter.updateChapters(chapters):

Let’s update the type that the Adapter takes:
app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersAdapter.kt
import guide.graphql.toc.ChaptersQuery
class ChaptersAdapter(
private val context: Context,
private var chapters: List<ChaptersQuery.Chapter> = listOf(),
private val onItemClicked: ((ChaptersQuery.Chapter) -> Unit)
) : RecyclerView.Adapter<ChaptersAdapter.ViewHolder>() {
...
fun updateChapters(chapters: List<ChaptersQuery.Chapter>) {
this.chapters = chapters
notifyDataSetChanged()
}
...
}Since chapters no longer holds strings, we also need to update this part of `onBindViewHolder():
holder.binding.chapterHeader.text = chapterto:
import android.view.View
import guide.graphql.toc.R
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val chapter = chapters[position]
val header =
if (chapter.number == null) chapter.title else context.getString(
R.string.chapter_number,
chapter.number.toInt().toString()
)
holder.binding.chapterHeader.text = header
if (chapter.number == null) {
holder.binding.chapterSubheader.visibility = View.GONE
} else {
holder.binding.chapterSubheader.text = chapter.title
holder.binding.chapterSubheader.visibility = View.VISIBLE
}
holder.binding.root.setOnClickListener {
onItemClicked.invoke(chapter)
}
}We display the chapter number and title, or just the title when there is no number. At the end, we call onItemClicked, which is a function passed by the fragment, and can be updated to:
app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersFragment.kt
val adapter =
ChaptersAdapter(
requireContext()
) { chapter ->
findNavController().navigate(
ChaptersFragmentDirections.viewSections(
chapterId = chapter.id,
chapterNumber = chapter.number?.toInt() ?: -1,
chapterTitle = if (chapter.number == null) chapter.title else getString(
R.string.chapter_title,
chapter.number.toInt().toString(),
chapter.title
)
)
)
}Now when we rebuild and run the app, we see all the chapters, and when we click one, the header matches:
