Optional: Local caching
To guarantee quick and continuous access for customers, Amplify DataStore employs a local cached version of remote data. During your transition from Amplify DataStore, you should choose a local storage solution that aligns with your models to avoid unnecessary complexity.
DataStore directly uses Android's built-in SQLite database APIs, but the option you choose may depend on your caching requirements. See the helpful resources page for library suggestions.
Apollo normalized cache
Apollo Kotlin includes the option to use SQLite as a normalized cache. The normalized cache is straightforward to set up and allows Apollo to replay previous queries from the cache to improve latency, reduce bandwidth consumption, and replay queries while offline.
For setup instructions on the normalized cache, see the Migrate DataStore to Apollo — ObserveQuery section which covers adding the dependency and configuring the cache.
Custom caching with Room
For more advanced use cases where you want richer access to cached data, you can use your own caching layer instead of, or in addition to, Apollo's provided cache. Two popular libraries for this approach are SQLDelight and Room. The following examples use Room.
Local models
To cache models in your own cache, create a separate entity for local storage that corresponds to your Apollo generated model. In Room, the entity corresponding to the PostDetails fragment might look like this:
@Entity(tableName = "post")data class PostEntity( @PrimaryKey val id: String, val updatedAt: String, val createdAt: String, val title: String, val content: String, val status: PostStatus, val rating: Int)You can then store, query, and mutate these entities with a DAO:
@Daointerface PostDao { @Query("SELECT * FROM post") suspend fun getAll(): List<PostEntity>
@Query("SELECT * FROM post WHERE id = :id") suspend fun get(id: String): PostEntity
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg posts: PostEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(posts: List<PostEntity>)
@Delete suspend fun delete(post: PostEntity)}Model mapping
Extension functions let you map one model type to the other:
fun PostDetails.toLocalModel() = PostEntity( id = id, updatedAt = updatedAt, createdAt = createdAt, title = title, content = content, status = status, rating = rating)
fun PostEntity.toRemoteModel() = PostDetails( id = id, updatedAt = updatedAt, createdAt = createdAt, title = title, content = content, status = status, rating = rating)When changing your schema version, DataStore will discard your data to prevent any model incompatibilities. In Room, you can instead use a migration to keep your cached data intact.
Populate the cache
Once your Room database has been created you can populate the data by writing the results of Apollo queries into the cache. The following code saves all Post objects in the local database:
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)}Observability
To support a responsive app, pick a local store that supports observing data changes. When using Room, you can react to changes in the database by changing your DAO to return a Kotlin Flow. The updated observable DAO looks like this:
@Daointerface PostDao { @Query("SELECT * FROM post") fun getAll(): Flow<List<PostEntity>>
@Query("SELECT * FROM post WHERE id = :id") fun get(id: String): Flow<PostEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg posts: PostEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(posts: List<PostEntity>)
@Delete suspend fun delete(post: PostEntity)}