Name:
interface
Value:
Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated May 1, 2026

LegacyYou are viewing Gen 1 documentation. Switch to the latest Gen 2 docs →

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:

@Dao
interface 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:

@Dao
interface 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)
}