AWS AppSync
AWS AppSync Developer Guide

Android App Tutorial

This tutorial describes how to build an Android client application using AWS AppSync.

Download a Client Application

This tutorial uses the AWS AppSync Posts schema starter kit.

If you want to see the entire app without following the steps, see the whole sample.

Create an API

Before you get started, you'll need an API. For details, see Designing a GraphQL API. For this tutorial use the AWS Amplify CLI:

Run the amplify api add command and select GraphQL to create a GraphQL API using AWS AppSync. The following example uses a single object with fields. When the CLI prompts you to edit the schema.graphql file, use the following schema:

type Post @model { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type Query { singlePost(id: ID!): Post } schema { query: Query }

The schema has a single object called Post that has eight fields. The object Post is annotated with @model annotation.

~/PostsApp $ amplify api add

For service, choose GraphQL. For the authorization type, choose API Key.

Note: Alternatively, you can use your own schema. To do this, go to the root of your Android application. For example, if ~/PostsApp/ is the root of your Android application, create a directory named graphql under ~/PostsApp/app/src/main/graphql/. Now create a directory structure that mimics the package name (for example, com.amazonaws.demo.posts) for the strongly typed generated code for your schema ~/PostsApp/app/src/main/graphql/com/amazonaws/demo/posts/. Create a file named posts.graphql and add the schema definition to it.

Check the Status of the Project

~/PostsApp $ amplify status | Category | Resource name | Operation | Provider plugin | | -------- | ---------------- | --------- | ----------------- | | Api | appsyncsampleapp | Create | awscloudformation |

Create Backend Resources

Now, run amplify push to build the GraphQL API and provision the resources in the cloud.

~/PostsApp $ amplify push

If you want to do more customization of GraphQL resolvers, see the Resolver Mapping Template Reference.

Configuration for the API

The amplify push operation retrieves the GraphQL endpoint URL (ApiUrl), AWS Region (Region) and the Authentication Mode selected (AuthMode), and adds it to the AppSync section of the awsconfiguration.json file under the location you specified during amplify init. By default, this is in app/src/main/res/raw unless you overwrite it.

~/PostsApp $ ls app/src/main/res/raw awsconfiguration.json

The contents of the file will be of the following format:

~/PostsApp $ cat app/src/main/res/raw/awsconfiguration.json { "UserAgent": "aws-amplify/cli", "Version": "1.0", "IdentityManager": { "Default": {} }, "AppSync": { "Default": { "ApiUrl": "https://xxxxxYYYYYzzzzzAAAAAbbbbb.appsync-api.us-east-1.amazonaws.com/graphql", "Region": "us-east-1", "AuthMode": "API_KEY", "ApiKey": "da2-xxxxxYYYYYzzzzzAAAAAbbbbb" } } }

Code Generation for the GraphQL Operations

Run the amplify codegen to download the schema of the GraphQL API and generate the code.

Create Schema for Queries, Mutations, and Subscriptions

To interact with AWS AppSync, your client needs to define GraphQL queries and mutations.

Create a .graphql file inside the ./app/src/main/graphql folder of your app. For example, ./app/src/main/graphql/com/amazonaws/demo/posts/posts.graphql

query GetPost($id:ID!) { getPost(id:$id) { id title author content url version } } query AllPosts { listPosts { items { id title author content url version ups downs } } } mutation AddPost($input: CreatePostInput!) { createPost(input: $input) { id title author url content } } mutation UpdatePost($input: UpdatePostInput!) { updatePost(input: $input) { id author title content url version } } mutation DeletePost($input: DeletePostInput!) { deletePost(input: $input){ id title author url content } }

Download the Schema

~/PostsApp $ amplify codegen add ? Enter the file name pattern of graphql queries, mutation and subscriptions app/src/main/graphql/**/*.graphql ✔ Downloaded the schema
~/PostsApp $ tree app/src/main/graphql app/src/main/graphql ├── com │   └── amazonaws │   └── demo │   └── posts │   └── posts.graphql └── schema.json

Generate the Code for Your Schema

Now build the project. The generated source files will be available to use within the app. They won't show up in your source directory, but they're added in the build path.

Gradle Setup

Project's build.gradle

In the project's build.gradle file, add the following dependency in the build script:

classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.6.+'

Sample Project's build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { // ..other code.. dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.6.+' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }

App's build.gradle

In the app's build.gradle file, add the following plugin:

apply plugin: 'com.amazonaws.appsync'

Add the following dependency:

compile 'com.amazonaws:aws-android-sdk-appsync:2.6.+'

Sample App's build.gradle

apply plugin: 'com.android.application' apply plugin: 'com.amazonaws.appsync' android { // Typical items } dependencies { // Typical dependencies compile 'com.amazonaws:aws-android-sdk-appsync:2.6.+' }

App's AndroidManifest.xml

Add the permissions to access the network state to determine whether the device is offline. You need additional permissions for GraphQL subscriptions.

<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

Create the AWSAppSync Client

You can use the awsconfiguration.json file to supply the configuration information required to create a AWSAppSyncClient object.

The AWSConfiguration represents the configuration information present in awsconfiguration.json file. By default, the information under the Default section are used.

For authorization using the API key, update the ClientFactory.getInstance(Context) method with the following code:

public class ClientFactory { // ...other code... private static volatile AWSAppSyncClient client; public static AWSAppSyncClient getInstance(Context context) { if (client == null) { client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .build(); } return client; } }

Perform GraphQL Operations

Query the Posts

Add the PostsActivity.queryData() method and create a callback to receive the data, when available.

public class PostsActivity extends AppCompatActivity { // ...other code... private AWSAppSyncClient mAWSAppSyncClient; @Override protected void onCreate(Bundle savedInstanceState) { mAWSAppSyncClient = AWSAppSyncClient.builder() .context(getApplicationContext()) .awsConfiguration(new AWSConfiguration(getApplicationContext())) .build(); } public void queryData() { mAWSAppSyncClient.query(AllPostsQuery.builder().build()) .responseFetcher(AppSyncResponseFetchers.CACHE_AND_NETWORK) .enqueue(postsCallback); } private GraphQLCall.Callback<AllPostsQuery.Data> postsCallback = new GraphQLCall.Callback <AllPostsQuery.Data>() { @Override public void onResponse(@Nonnull final Response<AllPostsQuery.Data> response) { runOnUiThread(new Runnable() { @Override public void run() { PostsActivity.this.mAdapter.setPosts(response.data().listPosts().items()); PostsActivity.this.mSwipeRefreshLayout.setRefreshing(false); PostsActivity.this.mAdapter.notifyDataSetChanged(); } }); } @Override public void onFailure(@Nonnull ApolloException e) { Log.e(TAG, "Failed to perform AllPostsQuery", e); runOnUiThread(new Runnable() { @Override public void run() { PostsActivity.this.mSwipeRefreshLayout.setRefreshing(false); } }); } }; }

Mutate the Posts

Mutate the Posts (Add a Post)

Add the AddPostActivity.save() method and create a callback to receive confirmation.

public class AddPostActivity extends AppCompatActivity { // ...other code... private void save() { final String title = ((EditText) findViewById(R.id.updateTitle)).getText().toString(); final String author = ((EditText) findViewById(R.id.updateAuthor)).getText().toString(); final String url = ((EditText) findViewById(R.id.updateUrl)).getText().toString(); final String content = ((EditText) findViewById(R.id.updateContent)).getText().toString(); final String id = UUID.randomUUID().toString(); AddPostMutation addPostMutation = AddPostMutation.builder() .input(CreatePostInput.builder() .title(title) .author(author) .url(url) .content(content) .ups(0) .downs(0) .version(1) .build() ).build(); ClientFactory.getInstance(this).mutate(addPostMutation).enqueue(postsCallback); } private GraphQLCall.Callback<AddPostMutation.Data> postsCallback = new GraphQLCall.Callback<AddPostMutation.Data>() { @Override public void onResponse(@Nonnull final Response<AddPostMutation.Data> response) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(AddPostActivity.this, "Added post", Toast.LENGTH_SHORT).show(); AddPostActivity.this.finish(); } }); } @Override public void onFailure(@Nonnull final ApolloException e) { runOnUiThread(new Runnable() { @Override public void run() { Log.e("", "Failed to perform AddPostMutation", e); Toast.makeText(AddPostActivity.this, "Failed to add post", Toast.LENGTH_SHORT).show(); AddPostActivity.this.finish(); } }); } }; }

Mutate the Posts (Update a Post)

Add the UpdatePostActivity.save() method and create a callback to receive confirmation.

public class UpdatePostActivity extends AppCompatActivity { // ...other code... private void save() { final String title = ((EditText) findViewById(R.id.updateTitle)).getText().toString(); final String author = ((EditText) findViewById(R.id.updateAuthor)).getText().toString(); final String url = ((EditText) findViewById(R.id.updateUrl)).getText().toString(); final String content = ((EditText) findViewById(R.id.updateContent)).getText().toString(); UpdatePostMutation updatePostMutation = UpdatePostMutation.builder() .input(UpdatePostInput.builder() .id(sPost.id()) .title(title) .author(author) .url(url) .content(content) .ups(sPost.ups()) .downs(sPost.downs()) .version(sPost.version() + 1) .build()) .build(); // Make mutation call ClientFactory.getInstance(this).mutate(updatePostMutation).enqueue(postsCallback); } private GraphQLCall.Callback<UpdatePostMutation.Data> postsCallback = new GraphQLCall.Callback<UpdatePostMutation.Data>() { @Override public void onResponse(@Nonnull Response<UpdatePostMutation.Data> response) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(UpdatePostActivity.this, "Updated post", Toast.LENGTH_SHORT).show(); UpdatePostActivity.this.finish(); } }); } @Override public void onFailure(@Nonnull final ApolloException e) { runOnUiThread(new Runnable() { @Override public void run() { Log.e("", "Failed to perform UpdatePostMutation", e); Toast.makeText(UpdatePostActivity.this, "Failed to update post", Toast.LENGTH_SHORT).show(); UpdatePostActivity.this.finish(); } }); } }; }

Authentication Modes

Amazon Cognito User Pools

Follow the instructions in Set Up Email and Password Login in Your Mobile App to configure Amazon Cognito User Pools as an Identity Provider to your app.

Update the ClientFactory.getInstance() method with the following after a successful sign-in operation. The CognitoUserPoolsSignInProvider provides a method to retrieve the CognitoUserPool object from which the JWT Token can be retrieved.

public class ClientFactory { // ...other code... private static volatile AWSAppSyncClient client; CognitoUserPoolsSignInProvider cognitoUserPoolsSignInProvider = (CognitoUserPoolsSignInProvider ) IdentityManager.getDefaultIdentityManager().getCurrentIdentityProvider(); BasicCognitoUserPoolsAuthProvider basicCognitoUserPoolsAuthProvider = new BasicCognitoUserPoolsAuthProvider(cognitoUserPoolsSignInProvider.getCognitoUserPool()); public static AWSAppSyncClient getInstance(Context context) { if (client == null) { client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .cognitoUserPoolsAuthProvider(basicCognitoUserPoolsAuthProvider) .build(); } return client; } }

AWS IAM

For authorization using the Amazon IAM credentials using Amazon IAM or Amazon STS or Amazon Cognito, update the awsconfiguration.json file and code snippet as follows:

Add the following snippet to your awsconfiguration.json file.

{ "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "YOUR-COGNITO-IDENTITY-POOLID", "Region": "us-east-1" } } }, "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "AuthMode": "AWS_IAM" } } }

Add the following code to use the information in the Default section from awsconfiguration.json file.

public class ClientFactory { // ...other code... private static volatile AWSAppSyncClient client; public static AWSAppSyncClient getInstance(Context context) { if (client == null) { awsConfig = new AWSConfiguration(context); CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(context, awsConfig); AWSAppSyncClient client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(awsConfig) .credentialsProvider(credentialsProvider) .build(); } return client; } }

OpenID Connect

For authorization using any OIDC (OpenID Connect) Identity Provider, update the awsconfiguration.json file and code snippet as follows:

Add the following snippet to your awsconfiguration.json file.

{ "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "AuthMode": "OPENID_CONNECT" } } }

Add the following code to use the information in the Default section from awsconfiguration.json file.

public class ClientFactory { // ...other code... private static volatile AWSAppSyncClient client; public static AWSAppSyncClient getInstance(Context context) { if (client == null) { AWSConfiguration awsConfig = new AWSConfiguration(context); client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .oidcAuthProvider(new OidcAuthProvider() { @Override public String getLatestAuthToken() { return "jwt-token-from-oidc-provider"; } }) .build(); } return client; } }

Optimistic Updates

Update a Post

This section makes changes to the UpdatePostActivity.save() method that was created in the previous step.

For optimistic updates, create the data expected to be returned after the mutation. The optimistic updates are written to the persistent SQL store.

Note: The UpdatePostMutation class is the same class used to create the expected object and make the mutation request.

UpdatePostMutation.Data expected = new UpdatePostMutation.Data(new UpdatePostMutation.PutPost( "Post", sPost.id(), author, title, content, url, sPost.version() + 1) );

The mutation call now becomes the following, which takes in the expected data:

UpdatePostMutation.Data expected = new UpdatePostMutation.Data(new UpdatePostMutation.PutPost( "Post", sPost.id(), author, title, content, url, sPost.version() + 1) ); UpdatePostMutation updatePostMutation = UpdatePostMutation.builder() .input(UpdatePostInput.builder() .id(sPost.id()) .title(title) .author(author) .url(url) .content(content) .ups(sPost.ups()) .downs(sPost.downs()) .version(sPost.version() + 1) .build()) .build(); // Make mutation call ClientFactory.getInstance(this).mutate(updatePostMutation, expected).enqueue(postsCallback);

Add a Post

This section makes changes to the AddPostActivity.save() method that was created in the previous step.

AddPostMutation.Data expected = new AddPostMutation.Data(new AddPostMutation.CreatePost( "Post", id, title, author, url, content )); AddPostMutation addPostMutation = AddPostMutation.builder() .input(CreatePostInput.builder() .title(title) .author(author) .url(url) .content(content) .ups(0) .downs(0) .version(1) .build() ).build(); addPostOffline(expected); ClientFactory.getInstance(this).mutate(addPostMutation).enqueue(postsCallback);

This mutation affects a query, so the query structure needs to be updated to reflect the new addition.

This section adds the addPostOffline method.

private void addPostOffline(final AddPostMutation.Data addPostData) { final AWSAppSyncClient appSyncClient = ClientFactory.getInstance(this); final AllPostsQuery allPostsQuery = AllPostsQuery.builder().build(); appSyncClient.query(allPostsQuery) .responseFetcher(AppSyncResponseFetchers.CACHE_ONLY) .enqueue(new GraphQLCall.Callback<AllPostsQuery.Data>() { @Override public void onResponse(@Nonnull Response<AllPostsQuery.Data> response) { List<AllPostsQuery.Item> oldPostsList = response.data().listPosts().items(); List<AllPostsQuery.Item> newPostsList = new ArrayList<>(oldPostsList); newPostsList.add(new AllPostsQuery.Item( addPostData.createPost().__typename(), addPostData.createPost().id(), addPostData.createPost().title(), addPostData.createPost().author(), addPostData.createPost().content(), addPostData.createPost().url(), 1, 0, 0 )); AllPostsQuery.Data data = new AllPostsQuery.Data(new AllPostsQuery.ListPosts(response.data().listPosts().__typename(), newPostsList)); try { appSyncClient.getStore().write(allPostsQuery, data).execute(); } catch (ApolloException e) { Log.e(TAG, "Failed to update ListPosts query optimistically", e); } } @Override public void onFailure(@Nonnull ApolloException e) { Log.e(TAG, "Failed to update ListPosts query optimistically", e); } }); }

Offline Mutations

Offline mutations work out of the box and are available in memory, as well as through app restarts.

The callback for onResponse is received when the network is available, and the request goes through if the app wasn't closed.

For mutations that are performed after an app restart, the PersistentMutationsCallback object is called.

The PersistentMutationsCallback has information about the mutation type and identifier, which can be specified while initializing the client.

AWSAppSyncClient client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .persistentMutationsCallback(new PersistentMutationsCallback() { @Override public void onResponse(PersistentMutationsResponse response) { if (response.getMutationClassName().equals("AddPostMutation")) { // perform action here add post mutation } } @Override public void onFailure(PersistentMutationsError error) { // handle error feedback here } }) .build();

Subscriptions

Subscriptions allow information to be pushed to the client when certain events happen, such as when a new post is added.

Append to the file named ./app/src/main/graphql/com/amazonaws/demo/posts/posts.graphql as follows:

subscription OnCreatePost { onCreatePost { id author title content url } }

App's build.gradle

In the app's build.gradle file, add the following dependency:

compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0' compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'

Sample App's build.gradle

android { // Typical items } dependencies { // Typical dependencies compile 'com.amazonaws:aws-android-sdk-appsync:2.6.+' compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0' compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' }

AndroidManifest.xml

Add the following service into the application:

<service android:name="org.eclipse.paho.android.service.MqttService" />

Sample AndroidManifest

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.amazonaws.postsapp"> <!--other code--> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <service android:name="org.eclipse.paho.android.service.MqttService" /> <!--other code--> </application> </manifest>

Client Call

This section makes changes to the PostsActivity.onCreate() and PostsActivity.onPause() methods.

public class PostsActivity extends AppCompatActivity { // .. other code .. private AppSyncSubscriptionCall subscriptionWatcher; @Override protected void onCreate(Bundle savedInstanceState) { // .. other code .. subscribe(); } private void subscribe() { OnCreatePostSubscription subscription = OnCreatePostSubscription.builder().build(); subscriptionWatcher = ClientFactory.getInstance(this).subscribe(subscription); subscriptionWatcher.execute(subCallback); } private AppSyncSubscriptionCall.Callback subCallback = new AppSyncSubscriptionCall.Callback<OnCreatePostSubscription.Data>() { @Override public void onResponse(@Nonnull final Response<OnCreatePostSubscription.Data> response) { Log.d("Response", response.data().toString()); // Further code can update UI or act upon this new comment } @Override public void onFailure(@Nonnull ApolloException e) { Log.e("Error", "Subscription failure", e); } @Override public void onCompleted() { Log.d("Completed", "Completed"); } }; @Override protected void onStop() { // ..other code .. subscriptionWatcher.cancel(); } }

Complex Objects

Logical objects can be created that have more complex data, such as images or videos, as part of their structure. For example, you might create a Person type with a profile picture or a Post type that has an associated image. With AppSync, you can model these as GraphQL types.

Schema Setup

If any mutations have an input type S3ObjectInput with fields bucket, key, region, mimeType and localUri fields, the SDK will upload the file to S3.

input S3ObjectInput { bucket: String! key: String! region: String! localUri: String mimeType: String }

For example, to add a photo field in the Post type. Update Post type, add the new S3ObjectInput type and add a new mutation, putPostWithPhoto.

type Mutation { ...other mutations here... putPostWithPhoto( id: ID!, author: String!, title: String, content: String, url: String, ups: Int, downs: Int, photo: S3ObjectInput version: Int! ): Post } type S3Object { bucket: String! key: String! region: String! } input S3ObjectInput { bucket: String! key: String! region: String! localUri: String mimeType: String } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int photo: S3Object version: Int! }

Next, update the putPostWithPhoto` mutation resolver to use PutItemWithS3Object template for request mapping and Return single item for response mapping from the AppSync console.

Next, update the response mapping template for the photo field.

$util.toJson($util.dynamodb.fromS3ObjectJson($context.source.file))

Client Code

To use complex objects, you need AWS Identity and Access Management credentials for reading and writing to Amazon S3. These can be separate from the other authentication credentials used in the AWS AppSync client. Credentials for complex objects are set in the S3ObjectManagerImplementation builder parameter, which you can use like the following:

public class ClientFactory { // ...other code... private static volatile AWSAppSyncClient client; private static volatile S3ObjectManagerImplementation s3ObjectManager; public static AWSAppSyncClient getInstance(Context context) { if (client == null) { client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .s3ObjectManager(getS3ObjectManager(context)) // Here we initialize the s3 object manager. .build(); } return client; } // Copy the below two methods and add the .s3ObjectManager builder parameter // initialize and fetch the S3 Client public static final S3ObjectManagerImplementation getS3ObjectManager(final Context context) { if (s3ObjectManager == null) { AmazonS3Client s3Client = new AmazonS3Client(getCredentialsProvider(context)); s3Client.setRegion(Region.getRegion("us-east-1")); // you can set the region of bucket here s3ObjectManager = new S3ObjectManagerImplementation(s3Client); } return s3ObjectManager; } // initialize and fetch cognito credentials provider for S3 Object Manager public static final AWSCredentialsProvider getCredentialsProvider(final Context context){ final CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider( context, Constants.COGNITO_IDENTITY, // Identity pool ID Regions.fromName(Constants.COGNITO_REGION) // Region ); return credentialsProvider; } }

The SDK uploads the file found at the localUri when the bucket, key, region, localUri, and mimeType are all provided. Now, the SDK uploads any field which has S3ObjectInput type in the mutation. The only requirement from a developer is to provide the correct bucket, key, region, localUri, and mimeType.

Example:

Add the following permissions to AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Then, in your activity where you are adding a post, update the code as follows:

public class AddPostActivity extends AppCompatActivity { // ...other code... // Photo selector application code. private static int RESULT_LOAD_IMAGE = 1; private String photoPath; public void choosePhoto(View view) { Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(i, RESULT_LOAD_IMAGE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && null != data) { Uri selectedImage = data.getData(); String[] filePathColumn = {MediaStore.Images.Media.DATA}; Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumn[0]); String picturePath = cursor.getString(columnIndex); cursor.close(); // String picturePath contains the path of selected Image photoPath = picturePath; } } // Actual mutation code private void save() { final String title = ((EditText) findViewById(R.id.updateTitle)).getText().toString(); final String author = ((EditText) findViewById(R.id.updateAuthor)).getText().toString(); final String url = ((EditText) findViewById(R.id.updateUrl)).getText().toString(); final String content = ((EditText) findViewById(R.id.updateContent)).getText().toString(); S3ObjectInput s3ObjectInput = S3ObjectInput.builder() .bucket("YOUR_BUCKET_NAME") .key("public/"+ UUID.randomUUID().toString()) .region("us-east-1") .localUri(photoPath) .mimeType("image/jpg").build(); PutPostWithPhotoMutation addPostMutation = PutPostWithPhotoMutation.builder() .title(title) .author(author) .url(url) .content(content) .ups(0) .downs(0) .photo(s3ObjectInput) .expectedVersion(1) .build(); ClientFactory.getInstance(this).mutate(addPostMutation).enqueue(postsCallback); } // Mutation callback code private GraphQLCall.Callback<PutPostWithPhotoMutation.Data> postsCallback = new GraphQLCall.Callback<PutPostWithPhotoMutation.Data>() { @Override public void onResponse(@Nonnull final Response<PutPostWithPhotoMutation.Data> response) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(AddPostActivity.this, "Added post", Toast.LENGTH_SHORT).show(); AddPostActivity.this.finish(); } }); } @Override public void onFailure(@Nonnull final ApolloException e) { runOnUiThread(new Runnable() { @Override public void run() { Log.e("", "Failed to perform AddPostMutation", e); Toast.makeText(AddPostActivity.this, "Failed to add post", Toast.LENGTH_SHORT).show(); AddPostActivity.this.finish(); } }); } }; }

Conflict Resolution

When clients make a mutation, either online or offline, they can send a version number with the payload (named expectedVersion) for AWS AppSync to check before writing to DDBlong. A DDB resolver mapping template can be configured to perform conflict resolution in the cloud, which you can learn about in the Resolver Mapping Template Reference for DynamoDB. If the service determines it needs to reject the mutation, data is sent to the client and you can optionally run an additional callback to perform client-side conflict resolution.

For example, suppose you had a mutation with DynamoDB set for checking the version, and the client sent expectedVersion:0, as in the following example:

UpdatePostMutation updatePostMutation = UpdatePostMutation.builder() .input(UpdatePostInput.builder() .id("my-post-id") .title(title) .author(author) .url(url) .content(content) .ups(0) .downs(0) .expectedVersion(0) .build()) .build(); ClientFactory.getInstance(this).mutate(updatePostMutation).enqueue(postsCallback);

This would fail the version check because 0 would be lower than any of the current values. You can then define a custom callback conflict resolver. A custom conflict resolver will receive the following variables:

  • handler: the ConflictResolutionHandler which needs to be used to either update the value or let the mutation fail

  • serverState: the JSONObject representing the object state in server (DynamoDB)

  • clientState: Optional if a name of a mutation is set on a GraphQL statement

  • recordIdentifier: the unique identifier for operation

  • operationType: the string representation of operation type for switching between conflict resolving behavior

For example, you could have the following custom conflict resolver:

class ClientConflictResolver implements ConflictResolverInterface { @Override public void resolveConflict(@Nonnull ConflictResolutionHandler handler, @Nonnull JSONObject serverState, @Nonnull JSONObject clientState, @Nonnull String recordIdentifier, @Nonnull String operationType) { if (operationType.equals("UpdatePostMutation")) { try { UpdatePostMutation updatePostMutation = UpdatePostMutation.builder() .input(UpdatePostInput.builder() .id(clientState.getString("id")) .title(clientState.getString("title")) .author(clientState.getString("author")) .url(clientState.getString("url")) .content(clientState.getString("content")) .ups(0) .downs(0) .expectedVersion(serverState.getInt("version")) .build()) .build(); handler.retryMutation(updatePostMutation, recordIdentifier); } catch (JSONException je) { // in case of un-expected errors, we fail the mutation // we can also call the below method if we want server data to be accepted instead of client. handler.fail(recordIdentifier) } } } }

You also have to provide an instance of ClientConflictResolver to the AWSAppSyncClient via a builder parameter as follows:

client = AWSAppSyncClient.builder() .context(context) .awsConfiguration(new AWSConfiguration(context)) .conflictResolver(new ClientConflictResolver()) // Here we are passing in the conflict resolver. .build();