github apollographql/apollo-kotlin v3.0.0-alpha01

latest releases: v4.0.1, v4.0.0, v4.0.0-rc.2...
3 years ago

This version is the first alpha on the road to a 3.0.0 release!

Apollo Android 3 rewrites a lot of the internals to be Kotlin first and support a multiplatform cache. It also has performance improvements, new codegen options, support for multiple client directives and much more!

This document lists the most important changes. For more details, consult the preliminary docs and the migration guide.

This is a big release and the code is typically less mature than in the main branch. Please report any issue, we'll fix them urgently. You can also reach out in kotlinlang's #apollo-android slack channel for more general discussions.

Kotlin first

This version is 100% written in Kotlin, generates Kotlin models by default and exposes Kotlin first APIs.

Using Kotlin default parameters, Builders are removed:

import com.apollographql.apollo3.ApolloClient

val apolloClient = ApolloClient("https://com.example/graphql")

Queries are suspend fun, Subscriptions are Flow<Response<D>>:

val response = apolloClient.query(MyQuery())
// do something with response

apolloClient.subscribe(MySubscription()).collect { response ->
    // react to changes in your backend
}

Java codegen is not working but will be added in a future update. For details and updates, see this GitHub issue

Multiplatform

This version unifies apollo-runtime and apollo-runtime-kotlin. You can now use apollo-runtime for both the JVM and iOS/MacOS:

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("com.apollographql.apollo3:apollo-runtime:3.0.0-alpha01")
            }
        }
    }
}

The normalized cache is multiplatform too and both apollo-normalized-cache (for memory cache) and apollo-normalized-cache-sqlite (for persistent cache) can be added to the commonMain sourceSet.

The In-Memory cache uses a custom LruCache that supports maxSize and weighter options. The SQLite cache uses SQLDelight to support both JVM and native targets.

val apolloClient = ApolloClient("https://com.example/graphql")
                        .withNormalizedCache(MemoryCacheFactory(maxSize = 1024 * 1024))

val apolloRequest = ApolloRequest(GetHeroQuery()).withFetchPolicy(FetchPolicy.CacheFirst)
val apolloResponse = apolloClient.query(apolloRequest)

Current feature matrix:

JVM iOS/MacOS JS
apollo-api (models)
apollo-runtime (network, query batching, apq, ...) 🚫
apollo-normalized-cache 🚫
apollo-normalized-cache-sqlite 🚫
apollo-http-cache 🚫 🚫

Performance improvements

SQLite batching

SQLite batching batches SQL requests instead of executing them sequentially. This can speed up reading a complex query by a factor 2x+ (benchmarks). This is especially true for queries that contain lists:

{
  "data": {
    "launches": {
      "launches": [
        {
          "id": "0",
          "site": "CCAFS SLC 40"
        },
        ...
        {
          "id": "99",
          "site": "CCBGS 80"
        }
      ]
    }
  }
}

Reading the above data from the cache would take 103 SQL queries with Apollo Android 2 (1 for the root, 1 for data, 1 for launches, 1 for each launch). Apollo Android 3 uses 4 SQL queries, executing all the launches at the same time.

Streaming parser

When it's possible, the parsers create the models as they read bytes from the network. By amortizing parsing during IO, the latency is smaller leading to faster UIs. It's not always possible though. At the moment, the parsers fall back to buffering when fragments are encountered because fragments may contain merged fields that need to rewind in the json stream:

{
    hero {
        friend {
            name
        }
        ... on Droid {
            # friend is a merged field which Apollo Android needs to read twice
            friend {
                id
            }
        }
    }
}

A future version might be more granular and only fallback to buffering when merged fields are actually used.

Codegen options

The codegen has been rethought with the goal of supporting Fragments as Interfaces.

To say it turned out to be a complex feature would be a understatement. GraphQL is a very rich language and supporting all the edge cases that come with polymorphism, @include/@skip directives and more would not only be very difficult to implement but also generate very complex code (see 3144). While fragments as interfaces are not enabled by default, the new codegen allows to experiment with different options and features.

codegenModels

The codegen supports 3 modes:

  • compat (default) uses the same structure as 2.x.
  • operationBased generates simpler models that map the GraphQL operation 1:1.
  • responseBased generates more complex models that map the Sson response 1:1.

responseBased models will generate fragments as interfaces and can always use streaming parsers. They do not support @include/@skip directives on fragments and will generate sensibly more complex code.

Read Codegen.md for more details about the different codegen modes.

To change the codegen mode, add this to your build.gradle[.kts]:

apollo {
    codegenModels.set("responseBased") // or "operationBased", or "compat"
}

Types.kt

The codegen now generates a list of the different types in the schema.

You can use these to get the different __typename in a type-safe way:

val typename = Types.Human.name 
// "Human"

or find the possible types of a given interface:

val possibleTypes = Types.possibleTypes(Types.Character)
// listOf(Types.Human, Types.Droid)

apollo-graphql-ast

This version includes apollo-graphql-ast that can parse GraphQL documents into an AST. Use it to analyze/modify GraphQL files:

file.parseAsGQLDocument()
    .definitions
    .filterIsInstance<GQLOperationDefinition>
    .forEach {
        println("File $file.name contains operation '$it.name'")
    }

Client directives

@nonnull

@nonnull turns nullable GraphQL fields into non-null Kotlin properties. Use them if such a field being null is generally the result of a larger error that you want to catch in a central place (more in docs):

query GetHero {
  # data.hero will be non-null
  hero @nonnull {
    name
  }
}

@optional

Apollo Android distinguishes between:

  • nullable: whether a value is null or not
  • optional: whether a value is present or not

The GraphQL spec makes non-nullable variables optional. A query like this:

query GetHero($id: String) {
    hero(id: $id) {
        name
    }
}

will be generated with an optional id constructor parameter:

class GetHeroQuery(val id: Optional<String?> = Optional.Absent)

While this is correct, this is also cumbersome. If you added the id variable in the first place, there is a very high chance you want to use it. Apollo Android 3 makes variables non-optional by default (but possibly nullable):

class GetHeroQuery(val id: String?)

If for some reason, you need to be able to omit the variable, you can opt-in optional again:

# id will be an optional constructor parameter
query GetHero($id: String @optional) {
    hero(id: $id) {
        name
    }
}

@typePolicy

You can use @typePolicy to tell the runtime how to compute a cache key from object fields:

extend type Book @typePolicy(keyFields: "isbn")

The above will add isbn wherever a Book is queried and use "Book:$isbn" as a cache key.

Since this works at the schema level, you can either modify your source schema or add an extra extra.graphqls file next to it that will be parsed at the same time.

@fieldPolicy

Symmetrically from @typePolicy, you can use @fieldPolicy to tell the runtime how to compute a cache key from a field and query variables.

Given this schema:

type Query {
    book(isbn: String!): Book
}

you can tell the runtime to use the isbn argument as a cache key with:

extend type Query @fieldPolicy(forField: "book", keyArgs: "isbn")

Apollo Adapters

This version includes an apollo-adapters artifact that includes Adapters for common custom scalar types:

  • InstantAdapter for kotlinx.datetime.Instant ISO8601 dates
  • LocalDateAdapter for kotlinx.datetime.LocalDate ISO8601 dates
  • DateAdapter for java.util.Date ISO8601 dates
  • LongAdapter for java.lang.Long
  • BigDecimalAdapter for a MPP com.apollographql.apollo3.BigDecimal class holding big decimal values

WebSockets

This version comes with builtin graphql-ws support in addition to the default graphql-transport-ws. You can use graphql-ws for queries and mutation in addition to subscriptions if your server supports it.

// Creates a client that will use WebSockets and `graphql-ws` for all queries/mutations/subscriptions
val apolloClient = ApolloClient(
    networkTransport = WebSocketNetworkTransport(serverUrl = "wss://com.example/graphql", protocol = GraphQLWsProtocol())
)

Bugfixes

  • Input data class arguments with a default value should be generated as Input.absent() (#3194)
  • Make a high level abstraction for http client (#3119)
  • Add fine grade control on auto persisted query (#3118)
  • Http logging for ApolloHttpNetworkTransport (Kotlin Runtime) (#3107)
  • Multi-modules: simplify the dependency resolution (#3069)
  • KMM - Unable to complete packForXcode to build project containing com.apollographql. apollo3:apollo-normalized-cache-sqlite (#3051)
  • Add typesafe properties for typenames (#3048)
  • Support enums with different cases (#3035)
  • Enable support for the graphql-ws protocol in apollo-android (#3029)
  • Relocate apollo-gradle-plugin dependencies (#3011)
  • Support for modifying the executable document at runtime (#3004)
  • @optional directive for operation variables (#2948)
  • Allow passing null values in Custom JSON Scalars (#2920)
  • multiplatform watchers and store (#2904)
  • Multiplatform optimistic updates (#2903)
  • Multiplatform imperative store API (#2902)
  • change package name and group id for 3.x (#2879)
  • Multiplatform lock-free LRU memory cache (#2844)
  • Multiplatform watchers (#2843)
  • @include on inline Fragments generates non-nullable variable (#2796)
  • Multiplatform FileUpload (#2784)
  • Allow to omit non-nullable variables if they have a defaultValue (#2686)
  • Do not calculate http cache key when the http cache is not used (#2659)
  • Multiplatform normalized store/cache (#2636)
  • Custom scalars in arguments are not correctly serialized to cache keys (#2577)
  • [FileUpload] Remove reflection Component: Kotlin Multiplatform Runtime (#2570)
  • [Plugin] Robustify no-source handling (#2563)
  • [ScalarTypes] Allow tri-state fields (null, undefined, defined) for scalars such as JSON (#2562)
  • CacheKeyResolver.fromFieldArguments should have access to the type information (#2496)
  • move BigDecimal to a custom scalar adapter (#2468)
  • [Compiler] Impossible to query __typename on root objects (#2444)
  • Empty query filter to represent "match all", rather than "match nothing"? (#1961)
  • Allow queries and mutations over websocket (#1197)

Don't miss a new apollo-kotlin release

NewReleases is sending notifications on new releases.