9. Write your first mutation
In this section, you will write your first mutation to log in to the backend.
A mutation is used to change data on your server. Here the login mutation will create a session based on your 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!
Prototype your mutation in Sandbox Explorer
Open your Sandbox Explorer and click on the plus symbol to add a new tab. Next, click on the Schema icon to get back to looking at your schema, and select "Mutation" to look at your list of mutation fields:
Scroll down to take a look at the login
field:
Click the play button to the right to open that field in the Explorer tab. When it opens, click the plus sign to add field to your operation:
Notice the red error indication - this is because the type returned by the field is User
, which is not a leaf type: you need to choose which of the user's fields the mutation will 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
- 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 have that argument added:
You'll also notice that Sandbox Explorer has added a variable to your "Variables" section to match the login email.
Click the Submit Operation button to execute your mutation. You'll see that since you sent null
for the email address, you get back null
for the login:
Now, replace null
in the Query Variables section with an actual email address:
1{ "email": "me@example.com" }
Press the Submit Operation button, 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:
1mutation Login($email: String!) {
2 login(email: $email) {
3 token
4 }
5}
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:
1@Composable
2fun Login(navigateBack: () -> Unit) {
Again, it should be initialized in MainActivity
:
1composable(route = NavigationDestinations.LOGIN) {
2 Login(
3 navigateBack = {
4 navController.popBackStack()
5 }
6 )
7}
Go back to Login.kt
and create a function to execute the Login mutation:
1private suspend fun login(email: String): Boolean {
2 val response = apolloClient.mutation(LoginMutation(email = email)).execute()
3 return when {
4 response.exception != null -> {
5 Log.w("Login", "Failed to login", response.exception)
6 false
7 }
8
9 response.hasErrors() -> {
10 Log.w("Login", "Failed to login: ${response.errors?.get(0)?.message}")
11 false
12 }
13
14 response.data?.login?.token == null -> {
15 Log.w("Login", "Failed to login: no token returned by the backend")
16 false
17 }
18
19 else -> {
20 TokenRepository.setToken(response.data!!.login!!.token!!)
21 true
22 }
23 }
24}
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
:
1// Submit button
2val scope = rememberCoroutineScope()
Then, in the onClick
lambda, replace the TODO
with a call to login()
, and handle the result:
1Button(
2 modifier = Modifier
3 .padding(top = 32.dp)
4 .fillMaxWidth(),
5 onClick = {
6 scope.launch {
7 val ok = login(email)
8 if (ok) navigateBack()
9 }
10 }
11) {
12 Text(text = "Submit")
13}
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):
1// Submit button
2var loading by remember { mutableStateOf(false) }
3val scope = rememberCoroutineScope()
4Button(
5 modifier = Modifier
6 .padding(top = 32.dp)
7 .fillMaxWidth(),
8 enabled = !loading,
9 onClick = {
10 loading = true
11 scope.launch {
12 val ok = login(email)
13 loading = false
14 if (ok) navigateBack()
15 }
16 }
17) {
18 if (loading) {
19 Loading()
20 } else {
21 Text(text = "Submit")
22 }
23}
Test the login
Go to the details screen, click Book and in the Login screen, enter your email and click Submit. You now have a token that allows you to authenticate your operations.
In the next section, you will use authenticated operations to book a flight.