4 New Year’s resolutions for your Kotlin apps
Martin Bonnin
Happy New Year everyone 🥳. I hope 2023 brings you joy, happiness and loads of type safety!
To kickstart the year, here’s a small list of tips & tricks for working with GraphQL in a Kotlin app. These tips have nothing in common besides the fact that they are easy to implement and will make your life easier.
It’s a list of things that came up in GitHub issues, conferences and/or other discussions. Very often the GraphQL setup was been done some years ago and it’s easy to miss these quality of life improvements.
It might be that you’re doing some or all of them already. If that’s the case then congrats, you’ve earned yourself a good cup of coffee ☕! If not, let’s dive in (and grab a good cup of coffee too 😃!).
1. Use SDL for your schema
Early versions of Apollo Kotlin only had support for introspection JSON. This is convenient because you can get it directly from your GraphQL endpoint. It is very verbose though and reading it is very cumbersome. To give you and idea, here’s a small sample (full JSON available here):
{
"data": {
"__schema": {
"queryType": {
"name": "Root"
},
"mutationType": null,
"subscriptionType": null,
"types": [
{
"kind": "OBJECT",
"name": "Root",
"description": null,
"fields": [
// ~500 lines skipped
]
},
// ~5000 lines skipped
],
// ...
}
}
}
All in all, the introspection JSON is 5843 lines long for ~50 actual GraphQL types. That’s way too much. Thankfully, SDL makes it a lot more concise. SDL is the part of the GraphQL language that deals with type definitions. The same schema translated to SDL looks like this (full SDL available here):
type Root {
allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection
# ~30 lines skipped
}
"""
A single film.
"""
type Film implements Node {
"""
The title of this film.
"""
title: String
# ~50 lines skipped
}
# more types
Much better, right? The good news is you can convert your existing schema.json
to a schema.graphqls
SDL file:
$ ./gradlew convertApolloSchema --from src/main/graphql/schema.json
--to src/main/graphql/schema.graphqls
$ rm src/main/graphql/schema.json
Moving forward, when updating your schema, you can ask your backend team for the SDL file or directly convert on the fly:
$ ./gradlew downloadServiceApolloSchemaFromIntrospection
If your schema has a .graphqls
extension, Apollo Kotlin will recognize it and convert it automatically to SDL.
2. Update your schema in GitHub actions
Talking about updating your schema, typing ./gradlew downloadServiceApolloSchemaFromIntrospection
is verbose and it’s easy to forget doing it. You could tweak your build to download it every time but this has two major drawbacks:
- it makes your builds longer as they now need to download a file all the time.
- it makes your builds non-reproducible as the schema will depend on when you run your builds. If you checkout an older tag, it will download your recent schema.
For these reasons, we recommend committing your schema in source control and updating it in a cron job.
We have created a GitHub action for this: update-graphql-schema. To use it, create a new GitHub workflow file named update-graphql-schema.yaml
and copy paste the following contents (update endpoint and schema):
name: Update GraphQL schema
on:
schedule:
# every night at midnight (GitHub actions time)
- cron: "0 0 * * *"
jobs:
update:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: apollographql/update-graphql-schema@4b4517cad731e2564488f203de81937dcc4ef92e #main
with:
schema: "src/main/graphql/schema.graphqls"
# With GraphOS
key: "service:fullstack-tutorial:abc123"
# With introspection
endpoint: "https://example.com/graphql"
Whenever your schema changes, a new pull request is created:
You can view in action in the Confetti repo (yaml file, example pull request). Not only does it save you from updating manually but if using SDL, it shows the changes in a nice visual way!
3. Fail on deprecated fields usages
When you update your schema, your new schema might contain newly deprecated fields. That’s one of the nice things with GraphQL. You can evolve your API progressively. When a field is deprecated, it’s good to stop using it so that the backend team can ultimately remove it.
By default Apollo Kotlin does two things:
- It displays a warning during codegen.
- It marks generated classes with Kotlin’s
@Deprecated
annotation. This way, your IDE can show it as strikethrough andkotlinc
can itself display a warning.
Warnings are easy to ignore though and if you do not edit the code that is using deprecated fields, you might continue using the field for longer than necessary.
If you want to be strict about not using deprecated GraphQL fields, set failOnWarnings
to true:
apollo {
service("service") {
packageName.set("com.example")
warnOnDeprecatedUsages.set(true)
failOnWarnings.set(true)
}
}
Whenever you’ll try to use a deprecated field your build will fail (and that’s a good thing!):
> Task :shared:generateServiceApolloSources FAILED
w: /Users/mbonnin/git/Confetti/shared/src/commonMain/graphql/Queries.graphql: (17, 9): Apollo: Use of deprecated field `name`
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':shared:generateServiceApolloSources'.
> Apollo: Warnings found and 'failOnWarnings' is true, aborting.
4. Try out the data builders
When it comes to testing, you had 2 options with early versions of Apollo Kotlin:
- enqueue a fake JSON into a mock server like OkHttp MockWebServer. This works but requires maintaining a set of
.json
Strings that are not easy to create manually. You could intercept the network traffic to get some real life samples but doing that and maintaining it is cumbersome - call the model constructors manually. This works too but for models that have a lot of properties, it’s tedious to pass them all. What’s more merge fields require to duplicate some properties and it’s easy to create inconsistent models by passing a wrong
__typename
Apollo Kotlin 3.6.0 introduced data builders. Data builders intend to solve both issues by generating builders for your schema types.
Note: contrary to the models, the data builders are based on the schema and not the operation. This means it is possible to set a value in your builders that is never going to be read by a certain operation. This is a tradeoff to avoid the exponential blowup issues that come with responseBased codegen and allows sharing builders amongst different operations.
To enable the data builders, set generateDataBuilders
to true in your Gradle files:
apollo {
service("service") {
// ...
generateDataBuilders.set(true)
}
}
That generates builders that you can then use with a Data {}
function:
val data = GetHeroQuery.Data {
hero = buildHuman {
firstName = "John"
age = 42
friends = listOf(
buildHuman {
firstName = "Jane"
},
buildHuman {
lastName = "Doe"
}
)
ship = buildStarship {
model = "X-Wing"
}
}
}
To get a Json out of it, you can use Data.toJsonString()
(JVM/Android only):
val data = GetHeroQuery.Data { ... }
mockServer.enqueue(data.toJsonString())
You can read more in the official doc
Conclusion
That’s it for now! I hope you enjoyed this little tour. As always, feedback is very welcome! You can also take a look at the ROADMAP to get a glimpse of what’s coming in 2023.