The Apollo team welcomes contributions of all kinds, including bug reports, documentation, test cases, bug fixes, and features.
If you want to discuss the project or just say hi, stop by the kotlinlang Slack channel (get your invite here).
You will need:
- A Java17+ JDK installed locally on your machine.
- A recent version of IntelliJ IDEA community. Android Studio might work too, but we find out the experience for multiplatform code to be better with IntelliJ IDEA.
- MacOS and the Xcode developer tools for iOS/MacOS targets.
- Simulators for iOS/watchOS tests.
This repository contains several Gradle builds:
- root build: the main libraries
build-logic: the shared Gradle logictests: integration testsbenchmarks: Android micro and macro benchmarks
We recommend opening the tests folder in IntelliJ. It's a composite build that includes the main build and
integration-tests, so it's easy to add GraphQL and test the codegen end-to-end. If you only want to do small changes,
you can open the root project to save some sync times.
To test your changes in a local repo, you can publish a local version of apollo-gradle-plugin and other dependencies
with:
./gradlew publishToMavenLocal
All dependencies will be published to your ~/.m2/repository folder. You can then use them in other projects by
adding mavenLocal()
to your repositories in your build scripts:
// build.gradle.kts
repositories {
mavenLocal()
mavenCentral()
// other repositories...
}
// settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
mavenCentral()
// other repositories...
}
}This will require that you call ./gradlew publishToMavenLocal after every change you make in Apollo Kotlin but it's
the
easiest way to try your changes. For tighter integration, you can also use Apollo Kotlin as
an included build
like it is done for the integration-tests.
- Follow our coding style
- Add labels to your issues and pull requests (at least one label for each of Status/Type/Priority).
- Give priority to the current style of the project or file you're changing, even if it diverges from the general guidelines.
- Include tests when adding new features. When fixing bugs, start with adding a test that highlights how the current behavior is broken.
- Keep the discussions focused. When a new or related topic comes up, it's often better to create a new issue than to side track the discussion.
- Send PRs for style changes.
- Surprise us with big pull requests. Instead, file an issue and start a discussion so we can agree on a direction before you invest a large amount of time.
- Commit code that you didn't write. If you find code that you think is a good fit, file an issue and start a discussion before proceeding.
- Submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it.
The coding style employed here is fairly conventional Kotlin - indentations are 2 spaces, class names are PascalCased, identifiers and methods are camelCased.
Builders/Constructors
We usually favor Builders for reasons outlined in this issue unless there is only one optional parameter.
- Use primary constructors with
@JvmOverloadswhen there is at most one optional parameter. - For classes, nest the builder directly under the class
- For interfaces that are meant to be extended by the user but that also have a builtin implementation, you can use
the
Default${Interface}naming pattern (see DefaultUpload) - If there are several builtin implementations, use a descriptive name (like AppSyncWsProtocol, ...)
- Avoid top level constructor functions like
fun CoroutineScope(){}because they are awkward to use in Java - For expect/actual, it's sometime convenient to expose an interface even if it's not intended to be subclassed by
callers like
MockServerInterface. In that case, it's ok to useFooInterfacefor the interface andFoo()for the implementation to avoid having "DefaultFoo" everywhere when there's only one "Foo". - Builders may open the door to bad combination of arguments. This is ok. In that case, they should be detected at runtime and fail fast by throwing an exception.
Java interop
- In general, it's best to avoid extension functions when possible because they are awkward to use in Java.
- The exception to the above rule is when adding function in other modules.
ApolloClient.Builderextensions are a good example of that. - If you have to use extension functions, tweak the
@file:JvmName()annotation to make the Java callsite nicer - Avoid Interface default functions as they generate
DefaultImplbytecode (and-Xjvm-default=enableis not ready for showtime yet).- Use abstract classes when possible.
- Else use extension functions.
- Functions with optional parameters are nice. Use
@JvmOverloadsfor better Java interop. Prefertop levelvalto top level singletonobjects. For an example,Adapters.StringAdapterreads better in java thanStringAdapter.INSTANCE- If some extensions do not make sense in Java, mark them with
@JvmName("-$methodName")to hide them from Java
Logging & Error messages
- Apollo Kotlin must not log anything to System.out or System.err
- Error messages are passed to the user through
Exception.message - For debugging logs, APIs are provided to get diagnostics (like CacheMissException, HttpInfo, ...). APIs are better defined and allow more fine-grained diagnostics. See https://publicobject.com/2022/05/01/eventlisteners-are-good/
- There is one exception for the Gradle plugin. It is allowed to log information though the lifecycle() methods.
- Messages should contain "Apollo: " when it's not immediately clear that the message comes from Apollo.
Gradle APIs
Gradle is a bit peculiar because it can be used from both Kotlin and Groovy, has lazy and eager APIs and can sometimes be used as a DSL and sometimes imperatively. The rules we landed on are:
- Lazy properties use names. Example:
packageName.set("com.example") - Methods with one or more parameters use verbs. Example:
mapScalar("ID", "kotlin.Long") - Except when there is only one parameter that is of
Action<T>type. Example:introspection {}
Misc
- Parameters using milliseconds should have the "Millis" suffix.
- Else use [kotlin.time.Duration]
ExperimentalContractsis ok to use. Since kotlin-stdlib does it, we can too. See https://github.com/Kotlin/KEEP/blob/master/proposals/kotlin-contracts.md#compatibility-notice
We love GitHub issues! Before working on any new features, please open an issue so that we can agree on the direction, and hopefully avoid investing a lot of time on a feature that might need reworking.
Small pull requests for things like typos, bugfixes, etc are always welcome.
Please note that we will not accept pull requests for style changes.
Apollo Kotlin observes semantic versioning. No breaking change should be introduced in minor or patch releases. See our evolution policy for more details.
The public API is tracked thanks to the Binary compatibility validator plugin.
Any change to the public API will fail the build. If that happens, you will need to run ./gradlew apiDump and check for any incompatible changes before committing these
files.
When deprecating an API, also mark it with ApolloDeprecatedSince so we can keep track of when it has been deprecated.
In general, when an existing API must be deprecated, use the WARNING level. Use the replaceWith parameter to guide the developer to an alternative API to use. This can happen in a minor release (not a breaking change).
The API can then be removed in the next major release (breaking change).
stateDiagram-v2
direction LR
NotDeprecated: Not deprecated
NotDeprecated --> Deprecated(WARNING): Minor release
Deprecated(WARNING) --> Removed: Major release
Using Kotlin's (or other dependencies') experimental or internal APIs, such as the ones marked
with @ExperimentalCoroutinesApi should be avoided as much as possible.
We have historically made exceptions to that rule for JS/native (UnsafeNumber) but no new opt-in to experimental APIs must be made.
We also have the @ApolloExperimental annotation which can be used to mark APIs as experimental, for instance when
feedback is wanted from the community on new APIs. This can also be used as a warning that APIs are using experimental
features of Kotlin/Coroutines/etc. and therefore may break in certain situations.
Releasing is done using Github Actions. The CI contains credentials to upload artifacts to Maven Central, snapshots and Google Cloud preview repos.
The version in source control always ends with -SNAPSHOT. When a tag is built, the -SNAPSHOT suffix is dropped.
To create a version:
- Go to https://github.com/apollographql/apollo-kotlin/releases/new
- Enter the new tag name in the "Choose a tag" dropdown. That tag name must match the current version without
-SNAPSHOTand be prefixed byv:vX.Y.Z. - Enter the changelog.
- Hit "publish release".
- The release job will be triggered and the artifacts will be uploaded to Maven Central when done (might take several minutes to a couple of hours).
- Update docs and prepare next release by calling
./scripts/update-repo --prepare-next-version - if it's a significant release, tweet about it 🐦
- relax 🍹
If you need to bump the version more than one minor:
- call
./scripts/update-repo --set-version x.y.z-SNAPSHOT. This makes sure that the version is updated everywhere it needs be but doesn't change the docs. The version must always end with-SNAPSHOT.
- Add a section with the version, date, and a quick summary of what the release contains.
- Optionally add a few sections to zoom in on changes you want to highlight.
- No need to highlight deprecations, as warnings in the code are enough.
- Mention and thank external contributors if any.
- Add an "All changes" section that should list all commits since last release (can use
git log --pretty=oneline <previous-tag>..main). Commits on the documentation can be omitted.
You can run tests with ./gradlew build.
Because the Apollo Kotlin compiler runs from a Gradle Plugin, a lot of integration tests are in the tests composite build.
You can run integration tests with ./gradlew -p tests build
Gradle tests are slow and not always easy to debug in the IDE. Most of the times,
an integration test from the tests composite build is faster. Keep Gradle tests for the cases
where:
- We need to test a specific version of Gradle and/or KGP, AGP or another buildscript dependency.
- We need to tweak the Gradle environment, for an example, a server needs to run in the background.
- We need to test up-to-date checks, build cache or other things that require instrumenting the build outcome.