Page updated Jan 16, 2024

Syncing data to cloud

Once you're happy with your application, you can start syncing with the cloud by provisioning a backend from your project. DataStore can connect to remote backend and automatically sync all locally saved data using GraphQL as a data protocol.

Best practice: it is recommended to develop without cloud synchronization enabled initially so you can change the schema as your application takes shape without the impact of having to update the provisioned backend. Once you are satisfied with the stability of your data schema, setup cloud synchronization as described below and the data saved locally will be synchronized to the cloud automatically.

Setup cloud sync

Synchronization between offline and online data can be tricky. DataStore's goal is to remove that burden from the application code and handle all data consistency and reconciliation between local and remote behind the scenes, while developers focus on their application logic. Up to this point the focus was to setup a local data store that works offline and has all the capabilities you would expect from a data persistence framework.

The next step is to make sure the locally saved data is synchronized with a cloud backend powered by AWS AppSync.

Note: Syncing data between the cloud and the local device starts automatically whenever you run any DataStore operation after your app is set up.

Add the API plugin

Although DataStore presents a distinct API, its cloud synchronization functionality relies on the underlying API category. Therefore, you will still be required to incorporate the API plugin when working with DataStore.

Make sure that you declare a dependency on the API plugin in your app-level build.gradle:

1dependencies {
2 // Add this line.
3 implementation 'com.amplifyframework:aws-api:ANDROID_VERSION'
4}

Next, add the plugin in your Amplify initialization code alongside with the previously added AWSDataStorePlugin.

1Amplify.addPlugin(new AWSDataStorePlugin());
2// Add this line.
3Amplify.addPlugin(new AWSApiPlugin());
4Amplify.configure(getApplicationContext());
1Amplify.addPlugin(AWSDataStorePlugin())
2// Add this line.
3Amplify.addPlugin(AWSApiPlugin())
4Amplify.configure(applicationContext)

Push the backend to the cloud

By now you should have a backend created with conflict detection enabled, as described in the Getting started guide.

Check the status of the backend to verify if it is already provisioned in the cloud.

1amplify status

You should see a table similar to this one.

1| Category | Resource name | Operation | Provider plugin |
2| -------- | ----------------- | --------- | ----------------- |
3| Api | amplifyDatasource | No Change | awscloudformation |

Troubleshooting: if amplify status gives you an error saying "You are not working inside a valid Amplify project", make sure you run amplify init before the next step.

In case Operation says Create or Update you need to push the backend to the cloud.

1amplify push

AWS credentials needed. At this point an AWS account is required. If you have never run amplify configure before, do it so and follow the steps to configure Amplify with your AWS account. Details can be found in the Configure the Amplify CLI guide.

Existing backend

DataStore can connect to an existing AWS AppSync backend that has been deployed from another project, no matter the platform it was originally created in. In these workflows it is best to work with the CLI directly by running an amplify pull command from your terminal and then generating models afterwards, using the process described in the Getting started guide.

For more information on this workflow please see the Multiple Frontends documentation.

Distributed data

When working with distributed data, it is important to be mindful about the state of the local and the remote systems. DataStore tries to make that as simple as possible for you; however, some scenarios might require some consideration.

For instance, when updating or deleting data, one has to consider that the state of the local data might be out-of-sync with the backend. This scenario can affect how conditions should be implemented.

Update and delete with predicate

For such scenarios both the save() and the delete() APIs support an optional predicate which will be sent to the backend and executed against the remote state.

1Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"),
2 update -> Log.i("MyAmplifyApp", "Post updated successfully!"),
3 failure -> Log.e("MyAmplifyApp", "Could not update post, maybe the title has been changed?", failure)
4);
1Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"),
2 { Log.i("MyAmplifyApp", "Post updated successfully!") },
3 { Log.e("MyAmplifyApp", "Could not update post, maybe the title has been changed?", it) }
4)
1try {
2 Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"))
3 Log.i("MyAmplifyApp", "Post updated successfully!")
4} catch (error: DataStoreException) {
5 Log.e("MyAmplifyApp", "Could not update post, maybe the title has been changed?", error)
6}
1RxAmplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"))
2 .subscribe(
3 update -> Log.i("MyAmplifyApp", "Post updated successfully!"),
4 failure -> Log.e("MyAmplifyApp", "Could not update post, maybe the title has been changed?", failure)
5 );

There's a difference between the traditional local condition check using if/else constructs and the predicate in the save() and delete() APIs as you can see in the example below.

1// Tests only against the local state
2if (post.getTitle().startsWith("[Amplify]")) {
3 Amplify.DataStore.save(post,
4 update -> { /* handle result */ },
5 failure -> { /* handle failure */}
6 );
7}
8
9// Only applies the update if the data in the remote backend satisfies the criteria
10Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"),
11 update -> { /* handle result */ },
12 failure -> { /* handle failure */ }
13);
1// Tests only against the local state
2if (post.title.startsWith("[Amplify]")) {
3 Amplify.DataStore.save(post,
4 { /* handle result */ },
5 { /* handle failure */}
6 )
7}
8
9// Only applies the update if the data in the remote backend satisfies the criteria
10Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"),
11 { /* handle result */ },
12 { /* handle failure */ }
13)
1// Tests only against the local state
2if (post.title.starts("[Amplify]")) {
3 try {
4 Amplify.DataStore.save(post)
5 } catch (error: DataStoreException) {
6 // handle error
7 }
8}
9
10// Only applies the update if the data in the remote backend satisfies the criteria
11Amplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"),
12 { /* handle result */ },
13 { /* handle failure */ }
14)
1// Tests only against the local state
2if (post.getTitle().startsWith("[Amplify]")) {
3 RxAmplify.DataStore.save(post)
4 .subscribe(
5 update -> { /* handle result */ },
6 failure -> { /* handle failure */}
7 );
8}
9
10// Only applies the update if the data in the remote backend satisfies the criteria
11RxAmplify.DataStore.save(post, Post.TITLE.beginsWith("[Amplify]"))
12 .subscribe(
13 update -> { /* handle result */ },
14 failure -> { /* handle failure */ }
15 );

Conflict detection and resolution

When concurrently updating the data in multiple places, it is likely that some conflict might happen. For most of the cases the default Auto-merge algorithm should be able to resolve conflicts. However, there are scenarios where the algorithm won't be able to be resolved, and in these cases, a more advanced option is available and will be described in detail in the conflict resolution section.

Clear local data

Amplify.DataStore.clear() provides a way for you to clear all local data if needed. This is a destructive operation but the remote data will remain intact. When the next sync happens, data will be pulled into the local storage again and reconstruct the local data.

One common use for clear() is to manage different users sharing the same device or even as a development-time utility.

Note: In case multiple users share the same device and your schema defines user-specific data, make sure you call Amplify.DataStore.clear() when switching users. Visit Auth events for all authentication related events.

1// Listen for sign out events.
2final String signedOutEventName = AuthChannelEventName.SIGNED_OUT.toString();
3
4Amplify.Hub.subscribe(HubChannel.AUTH,
5 anyAuthEvent -> signedOutEventName.equals(anyAuthEvent.getName()),
6 // When one arrives, clear the DataStore.
7 signedOutEvent -> Amplify.DataStore.clear(
8 () -> Log.i("MyAmplifyApp", "DataStore is cleared."),
9 failure -> Log.e("MyAmplifyApp", "Failed to clear DataStore.")
10 )
11);
1Amplify.Hub.subscribe(HubChannel.AUTH,
2 {
3 // Listen for sign out events.
4 it.name.equals(AuthChannelEventName.SIGNED_OUT.toString())
5 },
6 {
7 // When one arrives, clear the DataStore.
8 Amplify.DataStore.clear(
9 { Log.i("MyAmplifyApp", "DataStore is cleared") },
10 { Log.e("MyAmplifyApp", "Failed to clear DataStore") }
11 )
12 }
13)
1Amplify.Hub.subscribe(HubChannel.AUTH)
2 { it.name == AuthChannelEventName.SIGNED_OUT.toString() }
3 // When sign out event arrives, clear the DataStore.
4 .onEach { Amplify.DataStore.clear() }
5 .catch { Log.e("MyAmplifyApp", "Failed to clear DataStore.", it) }
6 .collect { Log.i("MyAmplifyApp", "DataStore is cleared.") }
1// Listen for sign out events.
2final String signedOutEventName = AuthChannelEventName.SIGNED_OUT.toString();
3
4RxAmplify.Hub.on(HubChannel.AUTH)
5 .filter(event -> signedOutEventName.equals(event.getName()))
6 .flatMapObservable(RxAmplify.DataStore::clear)
7 .subscribe(
8 () -> Log.i("MyAmplifyApp", "DataStore is cleared."),
9 failure -> Log.e("MyAmplifyApp", "Failed to clear DataStore.")
10 );

This is a simple yet effective example. However, in a real scenario you might want to only call clear() when a different user is signedIn in order to avoid clearing the database for a repeated sign-in of the same user.

Selectively syncing a subset of your data

By default, DataStore fetches all the records that you’re authorized to access from your cloud data source to your local device. The maximum number of records that will be stored locally is configurable here.

You can utilize selective sync to persist a subset of your data instead.

Selective sync works by applying predicates to the base and delta sync queries, as well as to incoming subscriptions.

Note that selective sync is applied on top of authorization rules you’ve defined on your schema with the @auth directive. For more information see the Setup authorization rules section.

1Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(Post.class, () -> Post.RATING.gt(5))
4 .syncExpression(Comment.class, () -> Comment.STATUS.eq("active"))
5 .build())
6 .build());
1Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(Post::class.java) { Post.RATING.gt(5) }
4 .syncExpression(Comment::class.java) { Comment.STATUS.eq("active") }
5 .build())
6 .build())
1RxAmplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(Post.class, () -> Post.RATING.gt(5))
4 .syncExpression(Comment.class, () -> Comment.STATUS.eq("active"))
5 .build())
6 .build());

When DataStore starts syncing, only Posts with rating > 5 and Comments with status equal to active will be synced down to the user's local store.

Developers should only specify a single syncExpression per model. Any subsequent expressions for the same model will be ignored.

Reevaluate expressions at runtime

Sync expressions get evaluated whenever DataStore starts. In order to have your expressions reevaluated, you can execute Amplify.DataStore.clear() or Amplify.DataStore.stop() followed by Amplify.DataStore.start().

If you have the following expression and you want to change the filter that gets applied at runtime, you can do the following:

1public Integer rating = 5;
2
3public void initialize() {
4 Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post.class, () -> Post.RATING.gt(rating))
7 .build())
8 .build());
9}
10
11public void changeSync() {
12 rating = 1;
13 Amplify.DataStore.stop(
14 () -> Amplify.DataStore.start(
15 () -> Log.i("MyAmplifyApp", "DataStore started"),
16 error -> Log.e("MyAmplifyApp", "Error starting DataStore: ", error)
17 ),
18 error -> Log.e("MyAmplifyApp", "Error stopping DataStore: ", error)
19 );
20}
1var rating: Int = 5;
2
3fun initialize() {
4 Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post::class.java) { Post.RATING.gt(rating) }
7 .build())
8 .build())
9}
10
11fun changeSync() {
12 rating = 1;
13 Amplify.DataStore.stop(
14 {
15 Amplify.DataStore.start(
16 { Log.i("MyAmplifyApp", "DataStore started") },
17 { Log.e("MyAmplifyApp", "Error starting DataStore", it) }
18 )
19 },
20 { Log.e("MyAmplifyApp", "Error stopping DataStore", it) }
21 )
22}
1var rating = 5
2
3fun initialize() {
4 Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post::class.java) { Post.RATING.gt(rating) }
7 .build())
8 .build())
9}
10
11suspend fun changeSync() {
12 rating = 1
13 try {
14 Amplify.DataStore.stop()
15 Amplify.DataStore.start()
16 Log.i("MyAmplifyApp", "DataStore started")
17 } catch (error: DataStoreException) {
18 Log.w("MyAmplifyApp", "Failed to restart DataStore", error)
19 }
20}
1public Integer rating = 5;
2
3public void initialize() {
4 RxAmplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post.class, () -> Post.RATING.gt(rating))
7 .build())
8 .build());
9}
10
11public void changeSync() {
12 rating = 1;
13 RxAmplify.DataStore.stop()
14 .andThen(RxAmplify.DataStore.start())
15 .subscribe(
16 () -> Log.i("MyAmplifyApp", "DataStore restarted"),
17 error -> Log.e("MyAmplifyApp", "Error restarting DataStore: ", error)
18 );
19}

Each time DataStore starts (via start or any other operation: query, save, delete, or observe), DataStore will reevaluate the syncExpressions.

In the above case, the predicate will contain the value 1, so all Posts with rating > 1 will get synced down.

Keep in mind: Amplify.DataStore.stop() will retain the local store's existing content. Run Amplify.DataStore.clear() to clear the locally-stored contents.

When applying a more restrictive filter, clear the local records first by running DataStore.clear() instead:

1public void changeSync() {
2 rating = 8;
3 Amplify.DataStore.clear(
4 () -> Amplify.DataStore.start(
5 () -> Log.i("MyAmplifyApp", "DataStore started"),
6 error -> Log.e("MyAmplifyApp", "Error starting DataStore: ", error)
7 ),
8 error -> Log.e("MyAmplifyApp", "Error clearing DataStore: ", error)
9 );
10}
1fun changeSync() {
2 rating = 8;
3 Amplify.DataStore.clear(
4 {
5 Amplify.DataStore.start(
6 { Log.i("MyAmplifyApp", "DataStore started") },
7 { Log.e("MyAmplifyApp", "Error starting DataStore", it) }
8 )
9 },
10 { Log.e("MyAmplifyApp", "Error clearing DataStore", it) }
11 )
12}
1suspend fun changeSync() {
2 rating = 8
3 try {
4 Amplify.DataStore.clear()
5 Amplify.DataStore.start()
6 Log.i("MyAmplifyApp", "DataStore started")
7 } catch (error: DataStoreException) {
8 Log.w("MyAmplifyApp", "Error clearing/starting DataStore", error)
9 }
10}
1public void changeSync() {
2 rating = 8;
3 RxAmplify.DataStore.clear()
4 .andThen(RxAmplify.DataStore.start()
5 .subscribe(
6 () -> Log.i("MyAmplifyApp", "DataStore cleared and restarted"),
7 error -> Log.e("MyAmplifyApp", "Error clearing or restarting DataStore: ", error)
8 );
9}

This will clear the contents of your local store, reevaluate your sync expressions and re-sync the data from the cloud, applying all of the specified predicates to the sync queries.

You can also have your sync expression return QueryPredicates.all() in order to remove any filtering for that model. This will have the same effect as the default sync behavior.

1public Integer rating = null;
2
3public void initialize() {
4 Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post.class, () -> {
7 if (rating != null) {
8 return () -> Post.RATING.gt(rating);
9 }
10 return QueryPredicates.all();
11 })
12 .build())
13 .build());
14}
1var rating = null
2
3fun initialize() {
4 Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post::class.java) {
7 if (rating != null) {
8 Post.RATING.gt(rating)
9 }
10 QueryPredicates.all()
11 }
12 .build())
13 .build())
14}
1public Integer rating = null;
2
3public void initialize() {
4 RxAmplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
5 DataStoreConfiguration.builder()
6 .syncExpression(Post.class, () -> {
7 if (rating != null) {
8 return () -> Post.RATING.gt(rating);
9 }
10 return QueryPredicates.all();
11 })
12 .build())
13 .build());
14}

DataStore.configure() should only by called once.

Advanced use case - Query instead of Scan

You can configure selective sync to retrieve items from DynamoDB with a query operation against a GSI. By default, the base sync will perform a scan. Query operations enable a highly efficient and cost-effective data retrieval for customers running DynamoDB at scale. Learn about creating GSIs with the @index directive here.

In order to do that, your syncExpression should return a predicate that maps to a query expression.

For example, for the following schema:

1type User @model {
2 id: ID!
3 firstName: String!
4 lastName: String! @index(name: "byLastName", sortKeyFields: ["createdAt"])
5 createdAt: AWSDateTime!
6}

To construct a query expression, return a predicate with the primary key of the GSI. You can only use the eq operator with this predicate.

For the schema defined above User.LAST_NAME.eq("Doe") is a valid query expression.

Optionally, you can also chain the sort key to this expression, using any of the following operators: eq | ne | le | lt | ge | gt | beginsWith | between.

E.g., User.LAST_NAME.eq("Doe").and(User.CREATED_AT.gt("2020-10-10").

Both of these sync expressions will result in AWS AppSync retrieving records from Amazon DynamoDB via a query operation:

1Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(User.class, () -> User.LAST_NAME.eq("Doe"))
4 .build())
5 .build());
6
7// OR
8
9Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
10 DataStoreConfiguration.builder()
11 .syncExpression(User.class, () -> User.LAST_NAME.eq("Doe").and(User.CREATED_AT.gt("2020-10-10")))
12 .build())
13 .build());
1Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(User::class.java) { User.LAST_NAME.eq("Doe") }
4 .build())
5 .build())
6
7// OR
8
9Amplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
10 DataStoreConfiguration.builder()
11 .syncExpression(User::class.java) { User.LAST_NAME.eq("Doe").and(User.CREATED_AT.gt("2020-10-10")) }
12 .build())
13 .build())
1RxAmplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
2 DataStoreConfiguration.builder()
3 .syncExpression(User.class, () -> User.LAST_NAME.eq("Doe"))
4 .build())
5 .build());
6
7// OR
8
9RxAmplify.addPlugin(AWSDataStorePlugin.builder().dataStoreConfiguration(
10 DataStoreConfiguration.builder()
11 .syncExpression(User.class, () -> User.LAST_NAME.eq("Doe").and(User.CREATED_AT.gt("2020-10-10")))
12 .build())
13 .build());