Mutations
Queries are useful to fetch data from a server, but client-server communication may also require sending data to the server. This is where Mutations become handy. Just like REST, any request might end up causing some side-effects on the server, but by convention it's suggested that one doesn't use GET requests to modify data. GraphQL is similar - technically any query could be implemented to cause a data write. However, it's useful to establish a convention that any operations that cause writes should be sent explicitly via a mutation.
Apollo Android handles GraphQL mutations. Mutations are similar to queries in syntax, the only difference being that you use the keyword mutation
instead of query
to indicate that the root fields on this query are going to be performing writes to the backend.
1mutation UpvotePost($postId: Int!) {
2 upvotePost(postId: $postId) {
3 votes
4 }
5}
GraphQL mutations represent two things in one query string:
The mutation field name with arguments,
upvotePost
, which represents the actual operation to be done on the serverThe fields you want back from the result of the mutation to update the client:
{ votes }
The above mutation will upvote a post on the server. The result might be:
1{
2 "data": {
3 "upvotePost": {
4 "id": "123",
5 "votes": 5
6 }
7 }
8}
Similar to queries, mutations are represented by instances of generated classes, conforming to the ApolloMutationCall
interface. Constructor arguments are used to define mutation variables. You pass a mutation object to ApolloClient#perform(mutation)
to send the mutation to the server, execute it, and receive typed results:
1val upvotePostMutation = UpvotePostMutation(votes = 3)
2
3apolloClient
4 .mutate(upvotePostMutation)
5 .enqueue(object: ApolloCall.Callback<UpvotePost.Data>() {
6 override fun onResponse(response: Response<UpvotePost.Data>) {
7 Log.i(TAG, response.toString());
8 }
9
10 override fun onFailure(e: ApolloException) {
11 Log.e(TAG, e.getMessage(), e);
12 }
13 }
14 )
1UpvotePostMutation upvotePostMutation = UpvotePostMutation.builder()
2 .votes(3)
3 .build();
4
5apolloClient
6 .mutate(upvotePostMutation)
7 .enqueue(
8 new ApolloCallback<>(new ApolloCall.Callback<UpvotePost.Data>() {
9 @Override public void onResponse(@NotNull Response<UpvotePost.Data> response) {
10 Log.i(TAG, response.toString());
11 }
12
13 @Override public void onFailure(@NotNull ApolloException e) {
14 Log.e(TAG, e.getMessage(), e);
15 }
16 });
17 );
Using fragments in mutation results
In many cases, you'll want to use mutation results to update your UI. Fragments can be a great way of sharing result handling between queries and mutations:
1mutation UpvotePost($postId: Int!) {
2 upvotePost(postId: $postId) {
3 ...PostDetails
4 }
5}
1apolloClient
2 .mutate(upvotePostMutation)
3 .enqueue(object: ApolloCall.Callback<UpvotePost.Data>() {
4 override fun onFailure(e: ApolloException) {
5 Log.e(TAG, e.getMessage(), e);
6 }
7
8 override fun onResponse(response: Response<UpvotePost.Data>) {
9 Log.i(TAG, response.data.upvotePost.fragments.postDetails);
10 }
11 }
12 )
1apolloClient
2 .mutate(upvotePostMutation)
3 .enqueue(
4 new ApolloCallback<>(new ApolloCall.Callback<UpvotePost.Data>() {
5 @Override public void onResponse(@NotNull Response<UpvotePost.Data> response) {
6 Log.i(TAG, response.data.upvotePost.fragments.postDetails);
7 }
8
9 @Override public void onFailure(@NotNull ApolloException e) {
10 Log.e(TAG, e.getMessage(), e);
11 }
12 })
13 );
Passing input objects
The GraphQL type system includes input objects as a way to pass complex values to fields. Input objects are often defined as mutation variables, because they give you a compact way to pass in objects to be created:
1mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
2 createReview(episode: $episode, review: $review) {
3 stars
4 commentary
5 }
6}
1val reviewInput = ReviewInput(stars = 5, commentary = "This is a great movie!")
2
3apolloClient.mutate(CreateReviewForEpisodeMutation(episode = Episode.NEWHOPE, review = reviewInput))
1ReviewInput reviewInput = ReviewInput.Builder()
2 .stars(5)
3 .commentary("This is a great movie!")
4 .build()
5
6apolloClient.mutate(CreateReviewForEpisodeMutation(Episode.NEWHOPE, reviewInput))
Designing mutation results
In GraphQL, mutations can return any type, and that type can be queried just like a regular GraphQL query. So the question is - what type should a particular mutation return?
In most cases, the data available from a mutation result should be the server developer's best guess of the data a client would need to understand what happened on the server. For example, a mutation that creates a new comment on a blog post might return the comment itself. A mutation that reorders an array might need to return the whole array.
Uploading files
Apollo Android supports file uploading over graphql-multipart-request-spec.
You need to define this mapping in your build.gradle file.
1apollo {
2 customTypeMapping = [
3 "Upload" : "com.apollographql.apollo.api.FileUpload"
4 ]
5}
Note1 You don't need to register custom type adapter for FileUpload
.
Note2 While Apollo Android doesn't use reflection for regular operations, it does for file upload. If you're using Proguard/R8, you need to keep everything inheriting from InputType
:
1-keep class * implements com.apollographql.apollo.api.InputType { *; }
In this example, the GraphQL schema uses custom scalar type named Upload
for file upload.
Change it to match your GraphQL schema.
Create graphql mutation.
1mutation SingleUpload($file: Upload!) {
2 singleUpload(file: $file) {
3 id
4 path
5 filename
6 mimetype
7 }
8}
Call your mutation with mimetype and a valid file path.
1 mutationSingle = SingleUploadMutation(file = FileUpload.create("image/jpeg", "/my/image.jpg"))
1 mutationSingle = SingleUploadMutation.builder()
2 .file(FileuploadKt.create(FileUpload.Companion, "image/jpeg", "/my/image.jpg"))
3 .build();
If you don't have a File
, you can also subclass FileUpload
:
1 object upload : FileUpload(mimetype) {
2 override fun contentLength(): Long {
3 TODO("return contentLength here")
4 }
5 override fun fileName(): String? {
6 TODO("return fileName to use in the multipart request here")
7 }
8 override fun writeTo(sink: BufferedSink) {
9 TODO("write the data here")
10 }
11 }
1FileUpload upload = new FileUpload(mimetype) {
2 @Override
3 public long contentLength() {
4 // return contentLength here
5 }
6
7 @Override
8 public String fileName() {
9 // return fileName to use in the multipart request here
10 }
11
12 @Override
13 public void writeTo(@NonNull BufferedSink sink) {
14 // write the data here
15 }
16};