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 →

Migrate DataStore to Apollo

This page covers how to replace every Amplify.DataStore.* call with the equivalent Apollo iOS operation. The examples below use the schema and GraphQL operations created in the Schema and GraphQL operations page.

Create, update, and delete (save/delete)

In Apollo iOS, you create, update, and delete data using the mutations defined in your schema.

// Create — include all required fields from your schema
let createPostInput = CreatePostInput(
title: "post",
status: .some(.case(.active)),
rating: .some(5),
content: .some("content"))
apolloClient.perform(
mutation: CreatePostMutation(input: createPostInput)
) { result in
guard let data = try? result.get().data else { return }
print("Post: \(String(describing: data.createPost?.fragments.postDetails))")
}
// Update — use .some() for fields you want to change
let updatePostInput = UpdatePostInput(
id: "post1",
status: .some(.case(.inactive)))
apolloClient.perform(
mutation: UpdatePostMutation(input: updatePostInput)
) { result in
guard let data = try? result.get().data else { return }
print("Post updated: \(String(describing: data.updatePost?.fragments.postDetails))")
}
// Delete
let deletePostInput = DeletePostInput(id: "post1")
apolloClient.perform(
mutation: DeletePostMutation(input: deletePostInput)
) { result in
guard let data = try? result.get().data else { return }
print("Post deleted: \(String(describing: data.deletePost?.id))")
}

GraphQLNullable: Apollo iOS uses a GraphQLNullable<T> type for optional input fields. This type distinguishes between "not provided" (.none), "explicitly null" (.null), and "has a value" (.some(value)). When setting optional fields with runtime values, you must use .some(value) explicitly — for example, .some(.case(.active)) for enums or .some(5) for integers.

Include all required fields in creates. If your schema defines content: String!, you must pass content in CreatePostInput — Apollo code generation makes it a required constructor parameter and the mutation will fail without it.

_version is required for updates and deletes. If your AppSync API uses DataStore conflict resolution, you must pass the _version field when updating or deleting items. The _version is returned from every query and mutation response (via the PostDetails fragment). Always store the latest _version and pass it in subsequent mutations. If you omit _version, the mutation will fail with a ConflictUnhandled error.

Query

In Apollo iOS, you query data using the queries defined in your schema.

// Single Item
let query = GetPostQuery(id: "post1")
apolloClient.fetch(query: query) { result in
guard let data = try? result.get().data else { return }
print("Query results: \(String(describing: data.getPost?.fragments.postDetails))")
}
// List Items
let query = GetPostsQuery()
apolloClient.fetch(query: query) { result in
guard let data = try? result.get().data else { return }
print("List results: \(String(describing: data.listPosts?.items))")
}

Soft-deleted items: If your AppSync API uses DataStore conflict resolution, listPosts will return soft-deleted items with _deleted: true. DataStore filtered these out automatically, but with Apollo you must filter them yourself when displaying results:

val posts = response.data?.listPosts?.items
?.mapNotNull { it?.postDetails }
?.filter { it._deleted != true }
?: emptyList()

Failing to filter _deleted items will show deleted posts in your UI.

Filtering

DataStore supported predicate queries (for example, Post.RATING.gt(3)) which were executed locally. With Apollo, you have two options:

  1. Server-side filtering using AppSync's ModelPostFilterInput. Update your GetPosts query to accept a filter:
query GetPosts($filter: ModelPostFilterInput, $nextToken: String) {
listPosts(filter: $filter, nextToken: $nextToken) {
items { ... PostDetails }
nextToken
}
}

Then pass the filter in Kotlin:

val filter = ModelPostFilterInput(
rating = Optional.present(ModelIntInput(gt = Optional.present(3)))
)
val response = apolloClient.query(
GetPostsQuery(filter = Optional.present(filter))
).execute()
  1. Client-side filtering using Kotlin collection operations on the query results:
val allPosts = response.data?.listPosts?.items
?.mapNotNull { it?.postDetails }
?: emptyList()
val filtered = allPosts.filter { (it.rating ?: 0) > 3 }

Sorting

DataStore's QuerySortBy does not have a direct Apollo equivalent for the default listPosts query. Apply sorting client-side:

val sorted = posts.sortedByDescending { it.createdAt?.toString() ?: "" }

Pagination

DataStore supported offset-based pagination (Page.startingAt(n).withLimit(5)). AppSync uses cursor-based pagination with nextToken. You can either paginate through all results using nextToken or apply client-side offset pagination with .drop() and .take():

val page = 0
val pageSize = 5
val pagedPosts = allPosts.drop(page * pageSize).take(pageSize)

Observe

In Apollo Kotlin, you observe data using the subscriptions defined in your schema. Apollo Kotlin exposes subscriptions as a Kotlin Flow via the built-in toFlow() method:

apolloClient.subscription(OnCreateSubscription()).toFlow().collect { response ->
// Handle created post
}

Unlike DataStore's single observe() which emits create, update, and delete events through one stream, Apollo requires separate subscriptions for each event type (onCreatePost, onUpdatePost, onDeletePost). You need to launch each subscription flow independently.

ObserveQuery

DataStore's observeQuery allows you to submit a query and then receive updates when the results change. To replicate this behavior with Apollo Kotlin, you use a normalized cache and the watch function.

To set this up, you need to:

  1. Add the normalized cache dependency to your build.gradle.kts:
implementation("com.apollographql.apollo:apollo-normalized-cache:4.1.0")
// For SQLite-backed persistent cache:
implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.1.0")
  1. Configure the cache when building your Apollo client:
val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)
// Or for SQLite: SqlNormalizedCacheFactory(context, "apollo_cache.db")
val apolloClient = ApolloClient.Builder()
.serverUrl(endpoint.serverUrl.toString())
.addHttpInterceptor(AppSyncInterceptor(authorizer))
.normalizedCache(cacheFactory)
.build()
  1. Watch the query to receive updates:
val query = GetPostQuery(id = id)
apolloClient.query(query).watch().collect { post ->
// This re-emits whenever the cached data for this query changes
}

The normalized cache tracks data by unique IDs, so mutations and subscription updates that write to the cache will automatically trigger watch() to re-emit with updated data.

No isSynced equivalent: DataStore's observeQuery() provided an isSynced boolean indicating whether the local store was fully synced with the cloud. Apollo's watch() does not provide an equivalent sync status indicator. If your app relies on showing sync status, you need to track this manually (for example, by setting a flag after your initial query completes and subscriptions are established).

Quick reference table

DataStore MethodApollo Kotlin EquivalentKey Difference
Amplify.DataStore.save() (create)apolloClient.mutation(CreatePostMutation(input)).execute()No _version needed for creates
Amplify.DataStore.save() (update)apolloClient.mutation(UpdatePostMutation(input)).execute()Must pass _version from last query
Amplify.DataStore.delete()apolloClient.mutation(DeletePostMutation(input)).execute()Must pass _version from last query
Amplify.DataStore.query() (single)apolloClient.query(GetPostQuery(id)).execute()Returns null if not found
Amplify.DataStore.query() (list)apolloClient.query(GetPostsQuery()).execute()Must filter _deleted records
Amplify.DataStore.observe()apolloClient.subscription(...).toFlow().collect {}Separate subscription per event type
Amplify.DataStore.observeQuery()apolloClient.query(...).watch().collect {}Requires normalized cache setup
Amplify.DataStore.clear()No longer neededNo local DataStore to clear