diff --git a/CHANGELOG.md b/CHANGELOG.md index 35109323809..58a17aacef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,139 @@ Change Log ========== +# Version 3.6.0 + +_2022-09-01_ + +This version brings initial support for `@defer` as well as data builders. + +## 💙️ External contributors + +Many thanks to @engdorm, @Goooler, @pt2121 and @StylianosGakis for their contributions! + +## ✨️ [new] `@defer` support + +`@defer` support is experimental in the Kotlin Client and currently a [Stage 2 GraphQL specification draft](https://github.com/graphql/graphql-spec/blob/main/CONTRIBUTING.md#stage-2-draft) to allow incremental delivery of response payloads. + +`@defer` allows you to specify a fragment as deferrable, meaning it can be omitted in the initial response and delivered as a subsequent payload. This improves latency for all fields that are not in that fragment. You can read more about `@defer` in the [RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md) and contribute/ask question in the [`@defer` working group](https://github.com/robrichard/defer-stream-wg). + +Apollo Kotlin supports `@defer` by default and will deliver the successive payloads as `Flow` items. Given the below query: + +```graphql +query GetComputer { + computer { + __typename + id + ...ComputerFields @defer + } +} + +fragment ComputerFields on Computer { + cpu + year + screen { + resolution + } +} +``` + +And the following server payloads: + +**payload 1**: +```json +{ + "data": { + "computer": { + "__typename": "Computer", + "id": "Computer1" + } + }, + "hasNext": true +} +``` + +**payload 2**: +```json +{ + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computer", + ] + } + ], + "hasNext": true +} +``` + +You can listen to payloads by using `toFlow()`: + +```kotlin +apolloClient.query(query).toFlow().collectIndexed { index, response -> + // This will be called twice + + if (index == 0) { + // First time without the fragment + assertNull(response.data?.computer?.computerFields) + } else if (index == 1) { + // Second time with the fragment + assertNotNull(response.data?.computer?.computerFields) + } +} +``` + +You can read more about it in [the documentation](https://www.apollographql.com/docs/kotlin/fetching/defer). + +As always, feedback is very welcome. Let us know what you think of the feature by +either [opening an issue on our GitHub repo](https://github.com/apollographql/apollo-android/issues) +, [joining the community](http://community.apollographql.com/new-topic?category=Help&tags=mobile,client) +or [stopping by our channel in the KotlinLang Slack](https://app.slack.com/client/T09229ZC6/C01A6KM1SBZ)(get your +invite [here](https://slack.kotl.in/)). + +## ✨️ [new] Data Builders (#4321) + +Apollo Kotlin 3.0 introduced [test builders](https://www.apollographql.com/docs/kotlin/testing/test-builders/). While they are working, they have several limitations. The main one was that being [response based](https://www.apollographql.com/docs/kotlin/advanced/response-based-codegen#the-responsebased-codegen), they could generate a lot of code. Also, they required passing custom scalars using their Json encoding, which is cumbersome. + +The [data builders](https://www.apollographql.com/docs/kotlin/testing/data-builders/) are a simpler version of the test builders that generate builders based on schema types. This means most of the generated code is shared between all your implementations except for a top level `Data {}` function in each of your operation: + +```kotlin +// Replace +val data = GetHeroQuery.Data { + hero = humanHero { + name = "Luke" + } +} + +// With +val data = GetHeroQuery.Data { + hero = buildHuman { + name = "Luke" + } +} +``` + +## ✨️ [new] Kotlin 1.7 (#4314) + +Starting with this release, Apollo Kotlin is built with Kotlin 1.7.10. This doesn't impact Android and JVM projects (the minimum supported version of Kotlin continues to be 1.5) but if you are on a project using Native, you will need to update the Kotlin version to 1.7.0+. + +## 👷‍ All changes + +* Data builders (#4359, #4338, #4331, #4330, #4328, #4323, #4321) +* Add a flag to disable fieldsCanMerge validation on disjoint types (#4342) +* Re-introduce @defer and use new payload format (#4351) +* Multiplatform: add enableCompatibilityMetadataVariant flag (#4329) +* Remove an unnecessary `file.source().buffer()` (#4326) +* Always use String for defaultValue in introspection Json (#4315) +* Update Kotlin dependency to 1.7.10 (#4314) +* Remove schema and AST from the IR (#4303) + # Version 3.5.0 _2022-08-01_ diff --git a/docs/source/config.json b/docs/source/config.json index eae5f159ea5..a1433639de0 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -38,7 +38,8 @@ "GraphQL variables": "/advanced/operation-variables", "Error handling": "/essentials/errors", "Custom scalars": "/essentials/custom-scalars", - "Fragments": "/essentials/fragments" + "Fragments": "/essentials/fragments", + "@defer support (experimental)": "/fetching/defer" }, "Caching": { "Introduction": "/caching/introduction", @@ -64,7 +65,7 @@ "Overview": "/testing/overview", "Mocking HTTP responses": "/testing/mocking-http-responses", "Mocking GraphQL responses": "/testing/mocking-graphql-responses", - "Test builders": "/testing/test-builders", + "Data builders": "/testing/data-builders", "UI Tests": "/testing/ui-tests" }, "Advanced": { diff --git a/docs/source/fetching/defer.mdx b/docs/source/fetching/defer.mdx new file mode 100644 index 00000000000..32262f69b8a --- /dev/null +++ b/docs/source/fetching/defer.mdx @@ -0,0 +1,73 @@ +--- +title: 'Using the @defer directive in Apollo Kotlin' +description: 'Fetch slower schema fields asynchronously' +--- + +> ⚠️ **The `@defer` directive is experimental in Apollo Kotlin and currently a [Stage 2 GraphQL specification draft](https://github.com/graphql/graphql-spec/blob/main/CONTRIBUTING.md#stage-2-draft).** Before it reaches Stage3 acceptance into the GraphQL specification, it could change in backward incompatible ways. If you have feedback on it, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Defer%20Support]) or in the [Kotlin Slack community](https://slack.kotl.in/). + +Beginning with version `3.6.0`, Apollo Kotlin supports [the `@defer` directive](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md), which enables your queries to receive data for specific fields asynchronously. This is helpful whenever some fields in a query take much longer to resolve than the others. + +For example, let's say we're building a social media application that can quickly fetch a user's basic profile information, but retrieving that user's friends takes longer. If we include _all_ of those fields in a single query, we want to be able to display the profile information as soon as it's available, instead of waiting for the friend fields to resolve. + +To achieve this, we can apply the `@defer` directive to an in-line fragment that contains all slow-resolving fields related to friend data: + +```graphql +query PersonQuery($personId: ID!) { + person(id: $personId) { + # Basic fields (fast) + id + firstName + lastName + + # Friends (slow) + ... on User @defer { + friends { + id + } + } + # highlight-start + # Friend fields (slower) + ... on User @defer { + friends { + id + } + } + # highlight-end + } +} +``` + +In the generated code for this query, the `onUser` field for the fragment will be nullable. That is because when the initial payload is received from the server, the fields of the fragment are not yet present. A `Person` will be emitted with only the basic fields filled in. + +When the fields of the fragment are available, a new `Person` will be emitted, this time with the `onUser` field present and filled with the fields of the fragment. + +```kotlin +apolloClient.query(PersonQuery(personId)).toFlow().collect { + println("Received: $it") + if (it.dataAssertNoErrors.person.onUser == null) { + // Initial payload: basic info only + // ... + } else { + // Subsequent payload: with friends + // ... + } +} +``` + +Will print something like this: + +``` +Received: Person(id=1, firstName=John, lastName=Doe, onUser=null)) +Received: Person(id=1, firstName=John, lastName=Doe, onUser=OnUser(friends=[Friend(id=2), Friend(id=3)])) +``` + +### Limitations/known issues +* `@defer` cannot be used with `responseBased` codegen. +* Some servers might send an empty payload to signal the end of the stream. In such a case you will receive an extra terminal emission. You can filter it out by using `distinct()`: + +```kotlin +apolloClient.query(MyQuery()).toFlow() + .distinct() // filter out duplicates + .collect { /* ... */ } +``` + diff --git a/docs/source/testing/data-builders.mdx b/docs/source/testing/data-builders.mdx new file mode 100644 index 00000000000..5fa44a8a771 --- /dev/null +++ b/docs/source/testing/data-builders.mdx @@ -0,0 +1,139 @@ +--- +title: Data builders (experimental) +--- + +> ⚠️ **Data builders are experimental and subject to change.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Test%20Builders]) or in the [Kotlin Slack community](https://slack.kotl.in/). + +Apollo Kotlin generates models for your operations and parsers that create instances of these models from your network responses. Sometimes though, during tests or in other occasions, it is useful to instantiate models manually with known values. Doing so is not as straightforward as it may seem, especially when fragments are used. Creating `operationBased` models requires instantiating every fragment as well as choosing an appropriate `__typename` for each composite type. Data builders make this easier by providing builders that match the structure of the Json document. + +__Note: previous versions of Apollo Kotlin used data builders. Data builders are a simpler version of test builders that also plays nicer with custom scalars. If you're looking for the test builders documentation, it's still available [here](./test-builders).__ + +## Enabling data builders + +To enable data, set the `generateDataBuilders` option to `true`: + +```kotlin title="build.gradle[.kts]" +apollo { + // ... + + // Enable data builder generation + generateDataBuilders.set(true) +} +``` + +This generates a builder for each composite type in your schema as well as a helper `Data {}` function for each of your operations. + +## Example usage + +Let's say we're building a test that uses a mocked result of the following query: + +```graphql +query HeroForEpisode($ep: Episode!) { + hero(episode: $ep) { + firstName + lastName + age + + ship { + model + speed + } + + friends { + firstName + lastName + } + + ... on Droid { + primaryFunction + } + + ... on Human { + height + } + } +} +``` + +Here's how we can use the corresponding data builder for that mocked result: + +```kotlin +@Test +fun test() { + val data = HeroForEpisodeQuery.Data { + // Set values for particular fields of the query + hero = buildHuman { + firstName = "John" + age = 42 + friends = listOf( + buildHuman { + firstName = "Jane" + }, + buildHuman { + lastName = "Doe" + } + ) + ship = buildStarship { + model = "X-Wing" + } + } + } + + assertEquals("John", data.hero.firstName) + assertEquals(42, data.hero.age) +} +``` + +In this example, the `hero` field is a `Human` object with specified values for `firstName` and `age`. The values for `lastName`and `height` are automatically populated with mock values. +Similarly, values for the ship's speed, the 1st friend's last name and 2nd friend's first name are automatically populated. + +> You can replace `buildHuman` above with `buildDroid` to create a `Droid` object instead. + +## Configuring default field values + +To assign default values to fields, data builders use an implementation of the `FakeResolver` interface. By default, they use an instance of `DefaultFakeResolver`. + +The `DefaultFakeResolver` gives each `String` field the field's name as its default value, and it increments a counter as it assigns default values for `Int` fields. It defines similar default behavior for other types. + +You can create your _own_ `FakeResolver` implementation (optionally delegating to `DefaultTestResolver` for a head start). You then pass this implementation as a parameter to the `Data` function, like so: + +```kotlin {6} +// A TestResolver implementation that assigns -1 to all Int fields +class MyFakeResolver : FakeResolver { + private val delegate = DefaultFakeResolver(__Schema.all) + + override fun resolveLeaf(context: FakeResolverContext): Any { + return when (context.mergedField.type.leafType().name) { + "Int" -> -1 // Always use -1 for Ints + else -> delegate.resolveLeaf(context) + } + } + + override fun resolveListSize(context: FakeResolverContext): Int { + // Delegate to the default behaviour + return delegate.resolveListSize(context) + } + + override fun resolveMaybeNull(context: FakeResolverContext): Boolean { + // Never + return false + } + + override fun resolveTypename(context: FakeResolverContext): String { + // Delegate to the default behaviour + return delegate.resolveTypename(context) + } +} + +@Test +fun test() { + val data = HeroForEpisodeQuery.Data(resolver = myTestResolver) { + hero = buildHuman { + firstName = "John" + } + } + + // Unspecified Int field is -1 + assertEquals(-1, data.hero.age) +} +``` diff --git a/docs/source/testing/test-builders.mdx b/docs/source/testing/test-builders.mdx index e65c52bc215..d5312f67c40 100644 --- a/docs/source/testing/test-builders.mdx +++ b/docs/source/testing/test-builders.mdx @@ -2,7 +2,7 @@ title: Test builders (experimental) --- -> ⚠️ **Test builders are experimental and subject to change.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-android/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md&title=[Test%20Builders]) or in the [Kotlin Slack community](https://slack.kotl.in/). +> ⚠️ Test builders are [response based](https://www.apollographql.com/docs/kotlin/advanced/response-based-codegen#the-responsebased-codegen) and may generate a lot of code. Moving forward, we recommend to use [data builders](./data-builders) instead, which are simpler to use. This page is kept as reference only. Apollo Kotlin provides **test builders** that enable you to instantiate your GraphQL model classes with default values. Test builders are especially helpful for testing models with a large number of fields or a deeply nested hierarchy. They automatically populate the `__typename` field and deduplicate merged fields whenever possible. diff --git a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt index 069e01ff623..b5c81903cd4 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt @@ -16,10 +16,13 @@ import defer.WithInlineFragmentsQuery import defer.fragment.ComputerFields import defer.fragment.ScreenFields import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class DeferTest { @@ -243,7 +246,18 @@ class DeferTest { ) mockServer.enqueueMultipart(jsonList) - val actualResponseList = apolloClient.query(query).toFlow().toList() + + apolloClient.query(query).toFlow().collectIndexed { index, response -> + // This will be called twice + + if (index == 0) { + // First time without the fragment + assertNull(response.data?.computer?.computerFields) + } else if (index == 0) { + // Second time will have the fragment + assertNotNull(response.data?.computer?.computerFields) + } + } assertResponseListEquals(expectedDataList, actualResponseList) }