Front-End Web & Mobile

Using Amazon DynamoDB Document API with the AWS Mobile SDK for Android – Part 2

In the first part of this blog, we introduced you to the Amazon DynamoDB Document API. This API is a mechanism for accessing data in DynamoDB that doesn’t require you to map models to the data. Instead, you access the data through standard accessor methods on a standard Document object. In this blog post, we show how to integrate this functionality into a real application. The application for this demonstration is an Android memo app.

The mobile backend architecture for the app is shown in the following diagram.

The services used are:

This infrastructure can be easily set up using AWS Mobile Hub.  Follow these instructions:

  1. Click Create a new Project in the AWS Mobile Hub console.  Provide a name for your project.
  2. Click NoSQL Database.
  3. Click Enable NoSQL.
  4. Click Add a new table.
  5. Click Start with an example schema.
  6. Select Notes as the example schema.
  7. Select Public for the permissions (we won’t add sign-in code to this app).
  8. Click Create Table, and then click Create Table in the dialog box.

Even though the table you created has a userID, the data is stored unauthenticated in this example. If you were to use this app in production, use Amazon Cognito to sign the user in to your app, and then use the userID to store the authenticated data.

In the left navigation pane, click Resources. AWS Mobile Hub created an Amazon Cognito identity pool, an IAM role, and a DynamoDB database for your project. Mobile Hub also linked the three resources according to the permissions you selected when you created the table. For this demo, you need the following information:

  • The Amazon Cognito identity pool ID (for example, us-east-1:f9d582af-51f9-4db3-8e36-7bdf25f4ee07)
  • The name of the Notes table (for example, androidmemoapp-mobilehub-1932532734-Notes)

These are stored in the application code and used when connecting to the database.

Now that you have a mobile app backend, it’s time to look at the frontend. First, add the required libraries to the dependencies section of the application build.gradle file:

compile 'com.amazonaws:aws-android-sdk-core:2.4.4'
compile 'com.amazonaws:aws-android-sdk-ddb:2.4.4'
compile 'com.amazonaws:aws-android-sdk-ddb-document:2.4.4'

Add the INTERNET, ACCESS_NETWORK_STATE, and ACCESS_WIFI_STATE permissions to AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

Set up the connection to the Amazon DynamoDB table by creating an Amazon Cognito credentials provider (for appropriate access permissions), and then creating a DynamoDbClient object. Finally, create a table reference:

// Create a new credentials provider
credentialsProvider = new CognitoCachingCredentialsProvider(
        context, COGNITO_POOL_ID, COGNITO_REGION);

// Create a connection to DynamoDB
dbClient = new AmazonDynamoDBClient(credentialsProvider);

// Create a table reference
dbTable = Table.loadTable(dbClient, DYNAMODB_TABLE);

You can now perform CRUD (Create, Read, Update, Delete) operations on the table:

/**
 * create a new memo in the database
 * @param memo the memo to create
 */
public void create(Document memo) {
    if (!memo.containsKey("userId")) {
        memo.put("userId", credentialsProvider.getCachedIdentityId());
    }
    if (!memo.containsKey("noteId")) {
        memo.put("noteId", UUID.randomUUID().toString());
    }
    if (!memo.containsKey("creationDate")) {
        memo.put("creationDate", System.currentTimeMillis());
    }
    dbTable.putItem(memo);
}

/**
 * Update an existing memo in the database
 * @param memo the memo to save
 */
public void update(Document memo) {
    Document document = dbTable.updateItem(memo, new UpdateItemOperationConfig().withReturnValues(ReturnValue.ALL_NEW));
}

/**
 * Delete an existing memo in the database
 * @param memo the memo to delete
 */
public void delete(Document memo) {
    dbTable.deleteItem(
        memo.get("userId").asPrimitive(),   // The Partition Key
        memo.get("noteId").asPrimitive());  // The Hash Key
}

/**
 * Retrieve a memo by noteId from the database
 * @param noteId the ID of the note
 * @return the related document
 */
public Document getMemoById(String noteId) {
    return dbTable.getItem(new Primitive(credentialsProvider.getCachedIdentityId()), new Primitive(noteId));
}

/**
 * Retrieve all the memos from the database
 * @return the list of memos
 */
public List<Document> getAllMemos() {
    return dbTable.query(new Primitive(credentialsProvider.getCachedIdentityId())).getAllResults();
}

There are two mechanisms for searching the dataset: scan and query. The query() method uses indexed fields within the DynamoDB table to rapidly retrieve the appropriate information. The scan() method is more flexible. It allows you to search on every field, but it can run into performance issues when searching large amounts of data. This results in a worse experience for your users because data retrieval will be slower. For the best experience, index fields that you intend to search often and use the query() method.

The Notes schema in DynamoDB usually segments data on a per-user basis. The app works with both authenticated and unauthenticated users by using the .getCachedIdentityId() method. This method stores the current user identity with every new note that is created.

Android does not allow you to perform network requests on the main UI thread. You must wrap each operation in an AsyncTask. For example:

/**
 * Async Task to create a new memo into the DynamoDB table
 */
private class CreateItemAsyncTask extends AsyncTask<Document, Void, Void> {
    @Override
    protected Void doInBackground(Document... documents) {
        DatabaseAccess databaseAccess = DatabaseAccess.getInstance(EditActivity.this);
        databaseAccess.create(documents[0]);
        return null;
    }
}

You can initiate a save operation by instantiating the appropriate AsyncTask and then calling .execute():

/**
 * Event Handler called when the Save button is clicked
 * @param view the initiating view
 */
public void onSaveClicked(View view) {
    if (memo == null) {
        Document newMemo = new Document();
        newMemo.put("content", etText.getText().toString());
        CreateItemAsyncTask task = new CreateItemAsyncTask();
        task.execute(newMemo);
    } else {
        memo.put("content", etText.getText().toString());
        UpdateItemAsyncTask task = new UpdateItemAsyncTask();
        task.execute(memo);
    }
    // Finish this activity and return to the prior activity
    this.finish();
}

Similarly, you can retrieve a list of memos on an AsyncTask and pass the memos back to a method in the MainActivity to populate the UI:

/**
 * Async Task for handling the network retrieval of all the memos in DynamoDB
 */
private class GetAllItemsAsyncTask extends AsyncTask<Void, Void, List<Document>> {
    @Override
    protected List<Document> doInBackground(Void... params) {
        DatabaseAccess databaseAccess = DatabaseAccess.getInstance(MainActivity.this);
        return databaseAccess.getAllMemos();
    }

    @Override
    protected void onPostExecute(List<Document> documents) {
        if (documents != null) {
            populateMemoList(documents);
        }
    }
}

The Document API is currently in beta. We would like to hear about the use cases you want us to support. You can leave your feedback and issues on our forums and GitHub.