You are viewing documentation for a preview version of this software.

Learn about previews.

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:

Kotlin
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:

Kotlin
1// Replace
2import com.apollographql.apollo.cache.normalized.* 
3// With
4import com.apollographql.cache.normalized.*
Kotlin
1// Replace
2import com.apollographql.apollo.cache.normalized.api.MemoryCacheFactory
3// With
4import com.apollographql.cache.normalized.memory.MemoryCacheFactory

Linked directives

If you are using @typePolicy or @fieldPolicy directives, update your extra.graphqls file to link the new specification:

GraphQL
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:

Kotlin
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 cacheMissesAsException and serverErrorsAsException.

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 suspend one that accepts a publish parameter to control whether changes should be published to watchers

  • a 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.

Kotlin
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.

Kotlin
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().

Kotlin
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 a withContext block with a Dispatchers.IO context to ensure that they run on a background thread.

  • readFragment() now returns a ReadResult<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.

Kotlin
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.

Kotlin
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

  • FieldPolicyCacheResolver now supports simple list cases. If your field takes a flat list of ids, you no longer need to implement a custom CacheResolver for it and can use @fieldPolicy.

  • As for TypePolicyCacheKeyGenerator, you can pass CacheKey.Scope.SERVICE to the constructor if your ids are unique across the service:

Kotlin
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.key

  • NormalizedCache.loadRecord()

Removed / deprecated APIs

  • ApolloCacheHeaders.EVICT_AFTER_READ is removed. Manually call ApolloStore.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:

Kotlin
1val apolloClient = ApolloClient.Builder()
2    // ...
3    .cache(cacheFactory = /*...*/)
4    .build()

Optionally pass a defaultMaxAge (infinity by default) and keyScope (TYPE by default).

Feedback

Edit on GitHub

Ask Community