Offline-first
For a better understanding of Amplify DataStore's opinionated approach, consult these resources:
To further explore and understand the principles of building offline-first applications, we recommend reviewing the Android guide for building offline-first apps.
We understand the importance of these capabilities to your applications, and while DataStore will no longer receive new features, we want to ensure that you have the necessary resources to transition smoothly. The following sections provide high-level recommendations on alternative solutions.
Remote sync
By regularly synchronizing your local store, it can serve as the primary source for your application, allowing for read operations that can be done offline. Opting to manage offline writes will introduce additional complexity to your application, so it is crucial to consider whether you want to handle this for each individual model.
Live syncing
You can achieve real-time detection of changes (create, update, delete) to a remote repository by leveraging AWS AppSync subscriptions, as long as you are connected to the internet. The following function subscribes to remote creation, updates, and deletion and propagates those changes into your Room database. These subscriptions will need to be established each time your application reconnects to the internet.
suspend fun startSubscriptions() = supervisorScope { // Subscribe to creates apolloClient.subscription(OnCreateSubscription()).toFlow().onEach { response -> response.data?.onCreatePost?.postDetails?.let { post -> postDao.insert(post.toLocalModel()) } }.launchIn(this)
// Subscribe to updates apolloClient.subscription(OnUpdateSubscription()).toFlow().onEach { response -> response.data?.onUpdatePost?.postDetails?.let { post -> postDao.insert(post.toLocalModel()) } }.launchIn(this)
// Subscribe to deletes apolloClient.subscription(OnDeleteSubscription()).toFlow().onEach { response -> response.data?.onDeletePost?.postDetails?.let { post -> postDao.delete(post.toLocalModel()) } }.launchIn(this)}Local cache refresh
Whenever your app reconnects to the internet, whether through launching the app or network updates, perform a complete refresh of your local store to ensure that you receive all the updates that you may have missed.
suspend fun syncAllPosts() { // Sync all pages of posts until there is no nextToken var nextToken: String? = null do { val query = GetPostsQuery(nextToken = Optional.presentIfNotNull(nextToken)) val response = apolloClient.query(query).execute() response.data?.listPosts?.items?.let { posts -> val mapped = posts.mapNotNull { it?.postDetails?.toLocalModel() } postDao.insertAll(mapped) } nextToken = response.data?.listPosts?.nextToken } while (nextToken != null)}If you often sync large amounts of data, configuring AWS AppSync Delta Sync operations can reduce the amount of time it takes to refresh your local store. Read more about how to configure and implement Delta Sync functionality in the AWS AppSync Delta Sync guide.
Detect network status
You can proactively detect network status updates via Android's ConnectivityManager. This allows you to identify scenarios where your local cache needs to be synced and subscriptions need to be reestablished.
class NetworkMonitor {
enum class Status { Connected, Disconnected }
fun monitor(context: Context) = callbackFlow { val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) trySend(Status.Connected) }
override fun onLost(network: Network) { super.onLost(network) trySend(Status.Disconnected) } }
val networkRequest = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build()
val connectivityManager = context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.requestNetwork(networkRequest, networkCallback)
awaitClose { connectivityManager.unregisterNetworkCallback(networkCallback) } }}
class SyncEngine(val networkMonitor: NetworkMonitor) { suspend fun start(context: Context) { networkMonitor.monitor(context).collect { networkStatus -> when (networkStatus) { NetworkMonitor.Status.Connected -> startSync() NetworkMonitor.Status.Disconnected -> stopSync() } } }}Offline mutations
By storing any changes made locally in a separate pending mutations table and synchronizing them at a later time, you can enable your users to work offline. Your pending mutations table should contain:
- Auto incrementing primary key (not your model's normal primary key)
- Enum for type of mutation (create, update, or delete)
- The state of the model at the time of the action
As an example, a pending mutation for the sample Post type might look like the following:
enum class MutationType { Create, Update, Delete}
@Entity(tableName = "post_mutation")data class PostMutationEntity( val postId: String, val type: MutationType, val title: String?, val content: String?, val status: PostStatus?, val rating: Int?, val timestamp: String, @PrimaryKey(autoGenerate = true) val id: Int = 0)Apply pending mutations
These pending mutations can be synced to AWS AppSync when the application has connectivity. To incorporate pending mutation table entries into your app before syncing, you must aggregate them with the results of your local store queries. You can fetch both the PostEntity and PostMutationEntity instances, and conflate the two into a single display model, optimistically applying the mutation before it is processed by AWS AppSync.
fun PostMutationEntity.applyTo(post: PostEntity) = post.copy( title = title ?: post.title, content = content ?: post.content, status = status ?: post.status, rating = rating ?: post.rating)