Docs
Launch GraphOS Studio

Programmatic cache IDs in Apollo Kotlin


In situations where declarative cache IDs don't fit your use case, you can programmatically generate cache IDs for in your normalized cache.

You can generate a given 's cache ID from one of two sources:

SourceClassDescription
From a response object's fields (e.g., Book.id)CacheKeyGeneratorThis happens after a network request and is essential to merging a query result with existing cached data. This is the most common case.
From a GraphQL operation's arguments (e.g., author(id: "au456"))CacheKeyResolverThis happens before a network request and enables you to avoid a network round trip if all requested data is in the cache already. This is an optional optimization that can avoid some cache misses.

provides a class for generating cache keys from each of these sources.

CacheKeyGenerator

The CacheKeyGenerator class enables you to generate custom cache IDs from an object's values. This basic example generates every 's cache ID from its id :

val cacheKeyGenerator = object : CacheKeyGenerator {
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
// Generate the cache ID based on the object's id field
return CacheKey(obj["id"] as String)
}
}

To use your custom CacheKeyGenerator, include it in your cache initialization code like so:

val apolloClient = ApolloClient.Builder()
.serverUrl("https://...")
.normalizedCache(
normalizedCacheFactory = cacheFactory,
cacheKeyGenerator = cacheKeyGenerator,
)
.build()

You can get the current object's typename from the context object and include it in the generated ID, like so:

val cacheKeyGenerator = object : CacheKeyGenerator {
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
val typename = obj["__typename"] as String
val id = obj["id"] as String
return CacheKey(typename, id)
}
}

You can also use the current object's typename to use different cache ID generation logic for different .

Note that for cache ID generation to work, your must return whatever your custom code relies on (such as id above). If a does not return a required , the cache ID will be inconsistent, resulting in data duplication. Also, using context.field.type.leafType().name yields the typename of the Union as opposed to the expected runtime value of Union received in the response. Instead for the __typename is safer. To make sure __typename is included in all set the adTypename gradle config:

apollo {
addTypename.set("always")
//
}

CacheKeyResolver

The CacheKeyResolver class enables you to generate custom cache IDs from an 's . This basic example generates every 's cache ID from the id if it's present:

val cacheKeyResolver = object: CacheKeyResolver() {
override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {
// [field] contains compile-time information about what type of object is being resolved.
// Even though we call leafType() here, we're guaranteed that the type is a composite type and not a list
val typename = field.type.leafType().name
// resolveArgument returns the runtime value of the "id" argument
// from either the variables or as a literal value
val id = field.resolveArgument("id", variables)
if (id is String) {
// This field has an id argument, so we can use it to compute a cache ID
return CacheKey(typename, id)
}
// Return null to use the default handling
return null
}
}

To use your custom CacheKeyResolver, include it in your cache initialization code like so:

val apolloClient = ApolloClient.Builder()
.serverUrl("https://...")
.normalizedCache(
normalizedCacheFactory = cacheFactory,
cacheKeyGenerator = cacheKeyGenerator,
cacheResolver = cacheKeyResolver
)
.build()

Note the following about using a custom CacheKeyResolver:

  • The cacheKeyForField function is called for every in your that returns a composite type, so it's important to return null if you don't want to handle a particular .
  • The function is not called for that return a list of composite types. See below.

Handling lists

Let's say we have this :

query GetBooks($ids: [String!]!) {
books(ids: $ids) {
id
title
}
}

To have the cache look up all books in the ids list, we need to override listOfCacheKeysForField in CacheKeyResolver:

override fun listOfCacheKeysForField(field: CompiledField, variables: Executable.Variables): List<CacheKey?>? {
// Note that the field *can* be a list type here
val typename = field.type.leafType().name
// resolveArgument returns the runtime value of the "id" argument
// from either the variables or as a literal value
val ids = field.resolveArgument("ids", variables)
if (ids is List<*>) {
// This field has an id argument, so we can use it to compute a cache key
return ids.map { CacheKey(typename, it as String) }
}
// Return null to use the default handling
return null
}

For the sake of simplicity, only one level of list is supported. To support more nested lists, you can implement CacheResolver. CacheResolver is a generalization of CacheKeyResolver that can return any value from the cache, even values:

val cacheResolver = object: CacheResolver {
override fun resolveField(
field: CompiledField,
variables: Executable.Variables,
parent: Map<String, @JvmSuppressWildcards Any?>,
parentId: String,
): Any? {
var type = field.type
var listDepth = 0
while (true) {
when (type) {
is CompiledNotNullType -> type = type.ofType
is CompiledListType -> {
listDepth++
type = type.ofType
}
else -> break
}
}
// Now type points to the leaf type and lestDepth is the nesting of lists required
// Return a kotlin value for this field
// No type checking is done here so it must match the expected GraphQL type
if (listDepth == 2) {
return listOf(listOf("0", "1"))
}
// CacheResolver must always call DefaultCacheResolver last or all fields will be null else
return DefaultCacheResolver.resolveField(field, variables, parent, parentId)
}
}
Previous
Declarative cache IDs
Next
Watching cached data
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company