Overview
We're familiar with queries, but when we want to actually change, insert, or delete data, we need to reach for a new tool: GraphQL mutations.
In this lesson, we will:
- Implement a mutation operation to log in with an email
- Enable our app's login view and execute the mutation
Building a mutation
A mutation is an operation we use to change data on the server. In our application, the login mutation will create a session based on our email address.
Note: The way you log in to this particular server might differ from the way you log in with your own server. Login is often handled by middleware, or a layer totally separate from GraphQL, like OAuth.
Also note that a typical authentication flow should require a password—but for this tutorial, anyone is allowed to book flights with a valid email address!
Let's take a look at the mutation fields that exist in our app's schema.
Open up Sandbox, or expand the embedded Sandbox below.
Let's return to the Schema tab. Just below the Query tab, we'll find Mutation. Let's click on this type and scroll down to take a look at the login
mutation field:
Click the play button on the right to open that mutation in the Explorer tab. When it opens, click the plus sign next to login
to add the operation:
Adding this field prefills the Operation panel with the following syntax.
mutation Login {login {}}
Notice the red error indicator that shows up in Explorer? This is because the type returned by the mutation is User
, which is is an object type: this means we need to choose at least one of the User
type's fields for the mutation to return. For our purposes, we only need the token
field, so add it by clicking the plus sign next to it.
You'll also notice that email
wasn't automatically added as an argument even though it doesn't seem to have a default value: that's because email
is of type String
—without an exclamation point (!
)—which remember, in GraphQL, means that it's not required (although obviously you won't get too far without it).
Click the plus sign next to the email
argument to add that argument.
We'll also notice that Explorer has added a variable to your Variables section to match the login email.
Click the Submit Operation button to run the mutation. Because we sent null
for the email address, we'll get back null
for the login:
Now, replace null
in the Variables section with an actual email address:
{"email": "me@example.com"}
Submit the operation, and this time you'll get an actual response:
Next, copy the operation, either manually or using the three-dot menu's Copy operation option.
Add the mutation to the project
Now that your mutation is working, add it to your project. Create a file named Login.graphql
next to schema.graphqls
and your other GraphQL files and paste the contents of the mutation:
mutation Login($email: String!) {login(email: $email) {token}}
Note: We've also marked the email
variable as non-nullable by adding !
to the end of the type, since we always want to pass a value for it.
Build your project to generate the LoginMutation
class.
Connect the Submit button to your mutation
To navigate back to the previous screen after logging in, add a navigateBack
lambda parameter to the Login
composable:
@Composablefun Login(navigateBack: () -> Unit) {
Again, it should be initialized in MainActivity
:
composable(route = NavigationDestinations.LOGIN) {Login(navigateBack = {navController.popBackStack()})}
Go back to Login.kt
and create a new function to execute the Login
GraphQL mutation:
private suspend fun login(email: String): Boolean {val response = apolloClient.mutation(LoginMutation(email = email)).execute()return when {response.exception != null -> {Log.w("Login", "Failed to login", response.exception)false}response.hasErrors() -> {Log.w("Login", "Failed to login: ${response.errors?.get(0)?.message}")false}response.data?.login?.token == null -> {Log.w("Login", "Failed to login: no token returned by the backend")false}else -> {val token = response.data!!.login!!.token!!Log.w("Login", "Setting token: $token")TokenRepository.setToken(token)true}}}
The possible error cases are handled and a boolean is returned to indicate if the login was successful or not. If it was, the token is saved in the TokenRepository
.
Note that the function is marked as suspend
and so will need to be called from a coroutine. To do that, declare a scope in Login
:
// Submit buttonval scope = rememberCoroutineScope()Button(
Then, in the onClick
lambda, replace the TODO
with a call to login()
, and handle the result:
Button(modifier = Modifier.padding(top = 32.dp).fillMaxWidth(),onClick = {scope.launch {val ok = login(email)if (ok) navigateBack()}}) {Text(text = "Submit")}
To improve the UX, add a loading indicator that will be shown while the login is in progress (let's also disable the button to avoid multiple clicks):
// Submit buttonvar loading by remember { mutableStateOf(false) }val scope = rememberCoroutineScope()Button(modifier = Modifier.padding(top = 32.dp).fillMaxWidth(),enabled = !loading,onClick = {loading = truescope.launch {val ok = login(email)loading = falseif (ok) navigateBack()}}) {if (loading) {Loading()} else {Text(text = "Submit")}}
Test the login flow
Go to the details screen, tap Book, which takes you to the Login screen. Enter your email and tap Submit. You now have a token that allows you to authenticate your operations.
We're not doing anything with this token yet, so to verify that everything's working as expected, open logcat and look for a success message:
Up next
We've implemented our mechanism to "log in", but there's more work to do: in the next lesson, we'll learn how to authenticate operations with our login token.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.