EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
Cache migration guide
Up to Apollo Kotlin v4, the Normalized Cache used to be part of the Apollo Kotlin repository.
It is now hosted in a dedicated repository and published at its own cadence and versioning scheme.
The previous codebase will not receive new features, it is deprecated in Apollo Kotlin v5, and it will be removed in the future. New developments happen in the new repository instead.
This guide highlights the main differences between the previous version and this new library, and how to migrate to it.
Artifacts and packages
First, update the dependencies to your project:
1// build.gradle.kts
2dependencies {
3 // Replace
4 implementation("com.apollographql.apollo:apollo-normalized-cache") // Memory cache
5 implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite") // SQLite cache
6
7 // With
8 implementation("com.apollographql.cache:normalized-cache:$cacheVersion") // Memory cache
9 implementation("com.apollographql.cache:normalized-cache-sqlite:$cacheVersion") // SQLite cache
10}Note: the com.apollographql.apollo:apollo-normalized-cache-api artifact no longer exists, the code it contained has been merged into com.apollographql.cache:normalized-cache.
Then update your imports:
1// Replace
2import com.apollographql.apollo.cache.normalized.*
3// With
4import com.apollographql.cache.normalized.*1// Replace
2import com.apollographql.apollo.cache.normalized.api.MemoryCacheFactory
3// With
4import com.apollographql.cache.normalized.memory.MemoryCacheFactoryLinked directives
If you are using @typePolicy or @fieldPolicy directives, update your extra.graphqls file to link the new specification:
1# Replace
2extend schema
3@link(url: "https://specs.apollo.dev/kotlin_labs/v0.3", import: ["@typePolicy", "@fieldPolicy"])
4
5# With
6extend schema
7@link(url: "https://specs.apollo.dev/cache/v0.4", import: ["@typePolicy", "@fieldPolicy"])Compiler plugin
Configure the compiler plugin in your build.gradle.kts file:
1apollo {
2 service("service") {
3 // ...
4
5 // Add this
6 plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:$cacheVersion") {
7 pluginArgument("com.apollographql.cache.packageName", packageName.get())
8 }
9}In most cases, updating the coordinates/imports and adding the compiler plugin will be enough to migrate your project. But there were also a few renames and API breaking changes - read on for more details.
Database schema
The SQLite cache now uses a different schema.
Previously, records were stored as JSON in a text column.
Now they are stored in an equivalent binary format in a blob column.
When this library opens an existing database and finds the old schema, it will automatically delete any existing data and create the new schema.
⚠️ This is a destructive operation.
If your application relies on the data stored in the cache, you can manually transfer all the records from the an old database to a new one. See an example on how to do that here.
Make sure you thoroughly test migration scenarios before deploying to production.
ApolloStore
Partial cache reads
readOperation() now returns an ApolloResponse<D> (it previously returned a <D>). This allows for returning partial data from the cache, whereas
previously no data and a CacheMissException would be returned if any field was not found.
Now data with null fields (when possible) is returned with Errors in ApolloResponse.errors for any missing field
ApolloResponse.cacheInfo.isCacheHit will be false when any field is missing.
Partial responses and errors are stored
Previously, partial responses were not stored by default, but you could opt in with storePartialResponses(true).
Now storePartialResponses() is removed and is the default, and errors returned by the server are stored in the cache and readOperation() will return them.
By default, errors will not replace existing data in the cache. You can change this behavior with errorsReplaceCachedValues(true).
By default, partial responses are disabled so any missing or error fields are exposed as a response with no data (same behavior as previous versions). This is configurable with
cacheMissesAsExceptionandserverErrorsAsException.For more flexibility you can also implement your own fetch policy interceptor to handle partial cache reads, as shown in this example.
Publishing changes to watchers
Previously, write methods had 2 flavors:
a
suspendone that accepts apublishparameter to control whether changes should be published to watchersa non-suspend one (e.g.
writeOperationSync) that doesn't publish changes
Now only the suspend ones exist and don't publish by default. Either pass publish = true or manually call publish() to notify watchers of changes.
1// Replace
2store.writeOperation(operation, data)
3// With
4store.writeOperation(operation, data, publish = true)Passing CustomScalarAdapters to ApolloStore methods
Previously, if you configured custom scalar adapters on your client, you had to pass them to the ApolloStore methods.
Now, ApolloStore has a reference to the client's CustomScalarAdapters so individual methods no longer need an adapters argument.
1// Replace
2client.apolloStore.writeOperation(
3 operation = operation,
4 data = data,
5 customScalarAdapters = client.customScalarAdapters
6)
7
8// With
9client.apolloStore.writeOperation(
10 operation = operation,
11 data = data
12)Providing your own store
The ApolloStore interface has been renamed to CacheManager. If you provide your own implementation, change the parent interface to CacheManager.
Correspondingly, the ApolloClient.Builder.store() extension has been renamed to ApolloClient.Builder.cacheManager().
1// Replace
2val MyStore = object : ApolloStore {
3 // ...
4}
5val apolloClient = ApolloClient.Builder()
6 // ...
7 .store(MyStore)
8 .build()
9
10// With
11val MyStore = object : CacheManager {
12 // ...
13}
14val apolloClient = ApolloClient.Builder()
15 // ...
16 .cacheManager(MyStore)
17 .build()Other changes
All operations are now
suspend.Note that they may suspend or block the thread depending on the underlying cache implementation. For example, the SQL cache implementation on Android will block the thread while accessing the disk. As such, these operations must not run on the main thread. You can enclose them in awithContextblock with aDispatchers.IOcontext to ensure that they run on a background thread.readFragment()now returns aReadResult<D>(it previously returned a<D>). This allows for surfacing metadata associated to the returned data, e.g. staleness.Records are now rooted per operation type (
QUERY_ROOT,MUTATION_ROOT,SUBSCRIPTION_ROOT), when previously these were all at the same level, which could cause conflicts.
CacheResolver, CacheKeyResolver
The APIs of CacheResolver and CacheKeyResolver have been tweaked to be more future-proof. The main change is that the methods now takes a ResolverContext instead of
individual parameters.
1// Replace
2interface CacheResolver {
3 fun resolveField(
4 field: CompiledField,
5 variables: Executable.Variables,
6 parent: Map<String, @JvmSuppressWildcards Any?>,
7 parentId: String,
8 ): Any?
9}
10
11// With
12interface CacheResolver {
13 fun resolveField(context: ResolverContext): Any?
14}resolveField can also now return a ResolvedValue when metadata should be returned with the resolved value (e.g. staleness).
If you wish to use the Expiration feature, a CacheControlCacheResolver should be used.
You can call the generated cache() extension on ApolloClient.Builder which will use it by default if max ages are configured in the schema.
TypePolicyCacheKeyGenerator
You can now pass the type policies to the TypePolicyCacheKeyGenerator constructor, and it is recommended to do so.
The type policies are generated by the compiler plugin in yourpackage.cache.Cache.typePolicies.
If your entities ids are unique across the service, you can pass CacheKey.Scope.SERVICE to the TypePolicyCacheKeyGenerator constructor
to save space in the cache and improve hit rates in certain cases.
1// Replace
2val apolloClient = ApolloClient.Builder()
3 // ...
4 .normalizedCache(cacheFactory)
5 .build()
6
7// With
8val apolloClient = ApolloClient.Builder()
9 // ...
10 .normalizedCache(
11 cacheFactory,
12 cacheKeyGenerator = TypePolicyCacheKeyGenerator(
13 typePolicies = Cache.typePolicies,
14 keyScope = CacheKey.Scope.SERVICE // defaults to TYPE
15 )
16 )
17 .build()FieldPolicyCacheResolver
FieldPolicyCacheResolvernow supports simple list cases. If your field takes a flat list of ids, you no longer need to implement a customCacheResolverfor it and can use@fieldPolicy.As for
TypePolicyCacheKeyGenerator, you can passCacheKey.Scope.SERVICEto the constructor if your ids are unique across the service:
1val apolloClient = ApolloClient.Builder()
2 // ...
3 .normalizedCache(
4 cacheFactory,
5 cacheKeyGenerator = /*...*/,
6 cacheResolver = FieldPolicyCacheResolver(
7 keyScope = CacheKey.Scope.SERVICE // defaults to TYPE
8 )
9 )
10 .build()CacheKey
For consistency, the CacheKey type is now used instead of String in more APIs, e.g.:
ApolloStore.remove()Record.keyNormalizedCache.loadRecord()
Removed / deprecated APIs
ApolloCacheHeaders.EVICT_AFTER_READis removed. Manually callApolloStore.remove()when needed instead.NormalizedCache.remove(pattern: String)is removed. Please open an issue if you need this feature back.
Other changes
An ApolloClient.Builder.cache() extension function is generated by the compiler plugin, which configures the CacheKeyGenerator, MetadataGenerator, CacheResolver, and RecordMerger based
on the type policies, connection types, and max ages configured in the schema:
1val apolloClient = ApolloClient.Builder()
2 // ...
3 .cache(cacheFactory = /*...*/)
4 .build()Optionally pass a defaultMaxAge (infinity by default) and keyScope (TYPE by default).