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 = chapter
to:
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: