Using the Datastore API on Android

These days, your app needs to store and sync more than just files. With the Datastore API, structured data like contacts, to-do items, and game state can be synced effortlessly. Datastores support multiple platforms, offline access, and automatic conflict resolution.

Here are the basic concepts that underlie the Datastore API:

Account manager and datastore manager
The account manager is your starting point for working with users within your app. The datastore manager allows your app to open datastores, get a list of datastores, wait for changes to multiple datastores, and so on. You can begin working with datastores right away using local datastores. Users can then link their Dropbox account at any time to sync their data.
Datastores and tables

Datastores are containers for your app's data. Each datastore contains a set of tables, and each table is a collection of records. As you'd expect, the table allows you to query existing records or insert new ones.

A datastore is cached locally once it's opened, allowing for fast access and offline operation. Datastores are also the unit of transactions; changes to one datastore are committed independently from another datastore. After modifying a datastore, call the sync method to sync those changes back to Dropbox and also apply changes that may have been made by other devices.

The unit of sharing is a single datastore, and one or more datastores may be shared between accounts. Any datastore with a shareable ID can be shared by assigning roles to principals, creating an access control list. Any Dropbox account with the correct permissions will then be able to open the shared datastore by ID.

Records

Records are how your app stores data. Each record consists of a set of fields, each with a name and a value. Values can be simple objects, like strings, integers, and booleans, or they can be lists of simple objects. A record has an ID and can have any number of fields.

Unlike in SQL, tables in datastores don't have a schema, so each record can have an arbitrary set of fields. While there's no requirement to have the same fields, it makes sense for all the records in a table to have roughly the same fields so you can query over them.

Now that you're familiar with the basics, read on to learn how to get the Datastore API running in your app.

Setting up the SDK

If you want to code along with this guide, start by visiting the SDKs page for instructions on downloading the SDK and setting up your project.

Initializing managers

To start using the Datastore API, you'll need to create a DbxAccountManager object for working with a user's account. You'll also need to create a DbxDatastoreManager for working with datastores in your app.

Each Android Activity or Fragment which interacts with Dropbox can obtain a DbxAccountManager and a DbxDatastoreManager. A good place to do so is in your onCreate() method.

Be sure to replace APP_KEY and APP_SECRET with the real values for your app, which can be found in the App Console.

private DbxAccountManager mAccountManager;
private DbxDatastoreManager mDatastoreManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set up the account manager
    mAccountManager = DbxAccountManager.getInstance(getApplicationContext(), APP_KEY, APP_SECRET);

    // Set up the datastore manager
    if (mAccountManager.hasLinkedAccount()) {
        try {
            // Use Dropbox datastores
            mDatastoreManager = DbxDatastoreManager.forAccount(mAccountManager.getLinkedAccount());
        } catch (DbxException.Unauthorized e) {
            System.out.println("Account was unlinked remotely");
        }
    }
    if (mDatastoreManager == null) {
        // Account isn't linked yet, use local datastores
        mDatastoreManager = DbxDatastoreManager.localManager(mAccountManager);
    }
}

Your app can use datastores without requiring users to login to Dropbox first. Note that we're checking if the user has linked their account yet and then either using Dropbox datatores or local datastores.

Local datastores are stored on the local device, and not synced to Dropbox. You can later migrate these datastores to a Dropbox account when the user chooses to link.

Next let's add the ability to link the user's Dropbox account. This allows you to sync the user's data between devices.

Linking accounts

You can start the linking flow in response to a user action asking to link to Dropbox. This example will add a new click handler to onCreate() for a button called link_button which will start the linking process. You'll also need to add a button with the same name to your activity or fragment's layout as well. Keep in mind, you'll need all the pieces in this section in place before it will work end-to-end.

The Datastore API will automatically store the user's account info on the device once they've linked. You should check whether an account is already linked by calling hasLinkedAccount.

static final int REQUEST_LINK_TO_DBX = 0;  // This value is up to you

private Button mLinkButton;
private DbxAccountManager mAccountManager;
private DbxDatastoreManager mDatastoreManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set up the account manager
    mAccountManager = DbxAccountManager.getInstance(getApplicationContext(), APP_KEY, APP_SECRET);

    // Button to link to Dropbox
    mLinkButton = (Button) findViewById(R.id.link_button);
    mLinkButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mAccountManager.startLink((Activity)MyActivity.this, REQUEST_LINK_TO_DBX);
        }
    });

    // Set up the datastore manager
    if (mAccountManager.hasLinkedAccount()) {
        try {
            // Use Dropbox datastores
            mDatastoreManager = DbxDatastoreManager.forAccount(mAccountManager.getLinkedAccount());
            // Hide link button
            mLinkButton.setVisibility(View.GONE);
        } catch (DbxException.Unauthorized e) {
            System.out.println("Account was unlinked remotely");
        }
    }
    if (mDatastoreManager == null) {
        // Account isn't linked yet, use local datastores
        mDatastoreManager = DbxDatastoreManager.localManager(mAccountManager);
        // Show link button
        mLinkButton.setVisibility(View.VISIBLE);
    }
}

Starting the linking process will launch an external Activity so your app will need to override onActivityResult() which will be called when linking is complete. You can specify any request code you want, it's just used to indicate the reason that onActivityResult() is being called (in this case, because the Dropbox account linking activity completed).

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_LINK_TO_DBX) {
        if (resultCode == Activity.RESULT_OK) {
            DbxAccount account = mAccountManager.getLinkedAccount();
            try {
                // Migrate any local datastores to the linked account
                mDatastoreManager.migrateToAccount(account);
                // Now use Dropbox datastores
                mDatastoreManager = DbxDatastoreManager.forAccount(account);
                // Hide link button
                mLinkButton.setVisibility(View.GONE);
            } catch (DbxException e) {
                e.printStackTrace();
            }
        } else {
            // Link failed or was cancelled by the user
        }
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

When onActivityResult() is called with the REQUEST_LINK_TO_DBX request code and RESULT_OK, you should see an info-level message in LogCat saying "Dropbox user <userid> linked." At this point, the account has been linked with the account manager.

You now have all the pieces you need to link to a user's account. Run the app and press your Link to Dropbox button. Your app should proceed through the authorization flow and return to your app with the account associated with the account manager.

Creating a datastore and your first table

For this tutorial, we'll use the default datastore. Each app has its own default datastore per user.

DbxDatastore datastore = mDatastoreManager.openDefaultDatastore();

In order to store records in a datastore, you'll need to put them in a table. Let's define a table named "tasks":

DbxTable tasksTbl = datastore.getTable("tasks");

In the future, you might choose to add more tables to store related sets of things such as a "settings" table for the app or a "people" table to keep track of people assigned to each task. For now, this app is really simple so you only need one table to hold all your tasks.

You've got a datastore manager, a datastore for your app, and a table for all the tasks you're about to make. Let's start storing some data.

Working with records

A record is a set of name and value pairs called fields, similar in concept to a map. Records in the same table can have different combinations of fields; there's no schema on the table which contains them. You can insert the record and add fields to it in a single line of code.

DbxRecord firstTask = tasksTbl.insert().set("taskname", "Buy milk").set("completed", false);

This task is now in memory, but hasn't been persisted to storage or synced to Dropbox. Thankfully, that's simple:

datastore.sync();

Sync may be a straightforward method, but it wraps some powerful functionality. Sync both saves all of your local changes and applies any remote changes as well, automatically merging and dealing with conflicts along the way. Sync even works offline; you won't apply any remote changes, but your local changes will be saved to persistent storage and synced to Dropbox when the device comes back online.

Once syncing completes, visit the datastore browser and you should see your newly created task.

Accessing data from a record is straightforward:

String taskname = firstTask.getString("taskname");

Editing tasks is just as easy. This is how you can mark the first result as completed:

firstTask.set("completed", true);
datastore.sync();

After the edit, calling sync will commit the edits locally and then sync them to Dropbox.

Finally, if you want to remove the record completely, just call deleteRecord().

firstTask.deleteRecord();
datastore.sync();

Querying records

You can query the records in a table to get a subset of records that match a set of field names and values you specify. The query method takes a set of conditions that the fields of a record must match to be returned in the result set. For each included condition, all records must have a field with that name and that field's value must be exactly equal to the specified value. For strings, this is a case-sensitive comparison (e.g. "abc" won't match "ABC").

DbxFields queryParams = new DbxFields().set("completed", false);
DbxTable.QueryResult results = tasksTbl.query(queryParams);
DbxRecord firstResult = results.iterator().next();

results provides an iterable set of DbxRecord objects.

The records that meet the specified query are not returned in any guaranteed order. The entire result set is returned so you may apply sort in memory after the request completes.

If no condition set is provided, the query will return every record in the table.

DbxTable.QueryResult results = tasksTbl.query();

Using listeners

A datastore will receive changes from other instances of your app when you call sync(). For some apps, the frequency of updates will be low; others may be rapid-fire. In either case, your app should respond as soon as those changes happen by updating the state of your app. You can do this by registering sync status listeners.

For Android, you'll use DbxDatastore.addSyncStatusListener() to register the listener. To adhere to Android's app lifecycle, it's a good idea to remove the listener when the app goes to the background. In this example, MainActivity implements the SyncStatusListener interface and uses onResume() and onPause() to add and remove itself as a listener.

public class MainActivity extends Activity implements DbxDatastore.SyncStatusListener {

    // ...

    @Override
    public void onResume() {
        super.onResume();
        mStore = mDatastoreManager.openDefaultDatastore();
        mStore.addSyncStatusListener(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        mStore.removeSyncStatusListener(this);
        mStore.close();
    }

    @Override
    public void onDatastoreStatusChange(DbxDatastore datastore) {
        if (store.getSyncStatus().hasIncoming) {
            try {
                Map<String, Set<DbxRecord>> changes = mStore.sync();
                // Handle the updated data
            } catch (DbxException e) {
                // Handle exception
            }
        }
    }

    // ...

}

The SyncStatusListener.onDatastoreStatusChange method is called whenever the state of the datastore changes, which includes downloading or uploading changes. It is also called when there are changes ready to be applied to the local state.

Checking getSyncStatus().hasIncoming in the listener lets you figure out when there are new changes that your app should respond to. If there are changes, calling sync() will apply those changes to the datastore. sync() will also return a mapping of tables to sets of records that changed as a result of the sync. Your app can update based on the set of changed records, or you can simply query the new states of the tables and update your app's views with the results.

Records and fields

The record is the smallest grouping of data in a datastore. It combines a set of fields to make a useful set of information within a table.

Record IDs

Each record has a string ID. An ID can be provided when a record is created, or one will be automatically generated and assigned if none is provided. Once a record is created, the ID cannot be changed.

Other records can refer to a given record by storing its ID. This is similar to the concept of a foreign key in SQL databases.

Field types

Records can contain a variety of field types. Earlier in this tutorial, you saw strings and booleans, but you can also specify a number of other types. Here is a complete list of all supported types:

  • String (String)
  • Boolean (boolean)
  • Integer (long) – 64 bits, signed
  • Floating point (double) – IEEE double
  • Date (Date) – POSIX-like timestamp stored with millisecond precision.
  • Bytes (byte[]) – Arbitrary data, which is treated as binary, such as thumbnail images or compressed data. Individual records can be up to 100KB, which limits the size of the blob. If you want to store larger files, you should use the Sync API and reference the paths to those files in your records.
  • List (DbxList) – A special value that can contain other values, though not other lists.

Customizing conflict resolution

Datastores automatically merge changes on a per-field basis. If, for example, a user were to edit the taskname of a task on one device and the completed status of that same task on another device, the Datastore API would merge these changes without a conflict.

Sometimes, however, there will be simultaneous changes to the same field of the same record, and this requires conflict resolution. For example, if a user were to edit the completed status of a task on two different devices while offline, it's unclear how those changes should be merged when the devices come back online. Because of this ambiguity, app developers can choose what conflict resolution rule they want to follow.

To set the conflict resolution rule, call the setResolutionRule() method on a table, and pass in the name of a field and the resolution rule you want to apply to that field.

taskTable.setResolutionRule("completed", DbxTable.ResolutionRule.LOCAL);

There are five available resolution rules that affect what happens when a remote change conflicts with a local change:

  • ResolutionRule.REMOTE – The remote value will be chosen. This is the default behavior for all fields.
  • ResolutionRule.LOCAL – The local value of the field will be chosen.
  • ResolutionRule.MAX – The greater of the two changes will be chosen.
  • ResolutionRule.MIN – The lesser of the two changes will be chosen.
  • ResolutionRule.SUM – Additions and subtractions to the value will be preserved and combined.

Note that resolution rules don't persist, so you should set any custom resolution rules after opening a datastore but before the first time you sync.

Sharing a datastore

For some applications you may want to share data between users. The Datastore API allows you to share a datastore across multiple Dropbox accounts.

To share a datastore, you'll first need to update its permissions by assigning a role to a group of users (called a principal).

// Shareable datastore
DbxDatastore datastore = mDatastoreManager.createDatastore();
datastore.setRole(DbxPrincipal.PUBLIC, DbxDatastore.Role.EDITOR);
datastore.sync();

Note that if your app is using local datastores, you can create a shareable datastore and update the permissions locally. However, the datastore can't actually be shared with others until after the user links their account to Dropbox and the data is migrated.

There are two available principals to whom you may apply a role:

  • DbxPrincipal.PUBLIC – The role will apply to all Dropbox users.
  • DbxPrincipal.TEAM – The role will apply to everyone on the user's team (only applicable for Dropbox for Business accounts).

There are four available roles:

  • DbxDatastore.Role.NONE – The principal has no access to this datastore.
  • DbxDatastore.Role.VIEWER – The principal is able to view this datastore.
  • DbxDatastore.Role.EDITOR – The principal is able to edit this datastore.
  • DbxDatastore.Role.OWNER – The principal is the owner of this datastore. This role cannot be assigned directly. The user who created a datastore is always that datastore's owner.

After assigning a role to a principal, you'll want to share the datastore ID with other users. A common way to share the datastore ID is to send a URL containing the datastore ID via email, text message, or some other mechanism within your app.

Any user who has the datastore ID and the appropriate permissions may then open the datastore:

DbxDatastore datastore = DbxDatastore.openDatastore(datastoreId);

At any time you may view the access control list for a datastore as a mapping of roles applied to principals using the listRoles() method. You can also find out the current user's role with the getEffectiveRole() method.

Example apps

See our example apps for more help getting started with the Datastore API.

  • Lists – A sample app that demonstrates many features of the Datastore API.
  • Click the Box – A simple app for storing game state using the Datastore API.