diff --git a/README.md b/README.md index 7b0f39ceef..e28c4c834d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This library is designed primarily with Android in mind, but you can use it in a * Normalized cache * File uploads * Custom scalar types -* Support for RxJava2, RxJava3, Coroutines and Reactor +* Reactive bindings for: RxJava2, RxJava3, Coroutines, Reactor and Mutiny ## Getting started @@ -67,6 +67,8 @@ dependencies { implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:x.y.z") // optional: for coroutines support implementation("com.apollographql.apollo:apollo-coroutines-support:x.y.z") + // optional: for Mutiny support + implementation("com.apollographql.apollo:apollo-mutiny-support:x.y.z") // optional: for Reactor support implementation("com.apollographql.apollo:apollo-reactor-support:x.y.z") // optional: for RxJava3 support diff --git a/apollo-mutiny-support/api.txt b/apollo-mutiny-support/api.txt new file mode 100644 index 0000000000..14533f50df --- /dev/null +++ b/apollo-mutiny-support/api.txt @@ -0,0 +1,17 @@ +// Signature format: 4.0 +package com.apollographql.apollo.mutiny { + + public final class KotlinExtensions { + } + + public class MutinyApollo { + method public static io.smallrye.mutiny.Uni!> from(com.apollographql.apollo.ApolloQueryWatcher); + method public static io.smallrye.mutiny.Uni!> from(com.apollographql.apollo.ApolloCall); + method public static io.smallrye.mutiny.Uni from(com.apollographql.apollo.ApolloPrefetch); + method public static io.smallrye.mutiny.Multi!> from(com.apollographql.apollo.ApolloSubscriptionCall); + method public static io.smallrye.mutiny.Multi!> from(com.apollographql.apollo.ApolloSubscriptionCall, io.smallrye.mutiny.subscription.BackPressureStrategy); + method public static io.smallrye.mutiny.Uni from(com.apollographql.apollo.cache.normalized.ApolloStoreOperation); + } + +} + diff --git a/apollo-mutiny-support/build.gradle.kts b/apollo-mutiny-support/build.gradle.kts new file mode 100644 index 0000000000..2348e72df0 --- /dev/null +++ b/apollo-mutiny-support/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + kotlin("jvm") +} + +dependencies { + add("implementation", project(":apollo-api")) + add("api", groovy.util.Eval.x(project, "x.dep.mutiny")) + add("api", project(":apollo-runtime")) +} + +tasks.withType { + options.encoding = "UTF-8" +} + diff --git a/apollo-mutiny-support/gradle.properties b/apollo-mutiny-support/gradle.properties new file mode 100644 index 0000000000..dddc8e52ba --- /dev/null +++ b/apollo-mutiny-support/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=apollo-mutiny-support +POM_NAME=Apollo GraphQL Mutiny Support +POM_DESCRIPTION=Apollo GraphQL Mutiny bindings +POM_PACKAGING=jar diff --git a/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyApollo.java b/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyApollo.java new file mode 100644 index 0000000000..3fcd969bb3 --- /dev/null +++ b/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyApollo.java @@ -0,0 +1,181 @@ +package com.apollographql.apollo.mutiny; + +import com.apollographql.apollo.ApolloCall; +import com.apollographql.apollo.ApolloPrefetch; +import com.apollographql.apollo.ApolloQueryWatcher; +import com.apollographql.apollo.ApolloSubscriptionCall; +import com.apollographql.apollo.api.Response; +import com.apollographql.apollo.cache.normalized.ApolloStoreOperation; +import com.apollographql.apollo.exception.ApolloException; +import com.apollographql.apollo.internal.subscription.ApolloSubscriptionTerminatedException; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.BackPressureStrategy; +import org.jetbrains.annotations.NotNull; + +import static com.apollographql.apollo.api.internal.Utils.checkNotNull; + +/** + * The MutinyApollo class provides methods for converting ApolloCall, ApolloPrefetch and ApolloWatcher types to Mutiny sources. + */ +public class MutinyApollo { + + private MutinyApollo() { + throw new AssertionError("This class cannot be instantiated"); + } + + /** + * Converts an {@link ApolloQueryWatcher} to an asynchronous Uni. + * + * @param watcher the ApolloQueryWatcher to convert. + * @param the value type + * @return the converted Uni + * @throws NullPointerException if watcher == null + */ + @NotNull + public static Uni> from(@NotNull final ApolloQueryWatcher watcher) { + checkNotNull(watcher, "watcher == null"); + return Uni.createFrom().emitter(emitter -> { + ApolloQueryWatcher clone = watcher.clone(); + emitter.onTermination(clone::cancel); + clone.enqueueAndWatch(new ApolloCall.Callback() { + @Override public void onResponse(@NotNull Response response) { + emitter.complete(response); + } + + @Override public void onFailure(@NotNull ApolloException e) { + emitter.fail(e); + } + }); + }); + } + + /** + * Converts an {@link ApolloCall} to an {@link Uni}. The number of emissions this Uni will have is based on the {@link + * com.apollographql.apollo.fetcher.ResponseFetcher} used with the call. + * + * @param call the ApolloCall to convert + * @param the value type. + * @return the converted Uni + * @throws NullPointerException if originalCall == null + */ + @NotNull + public static Uni> from(@NotNull final ApolloCall call) { + checkNotNull(call, "call == null"); + return Uni.createFrom().emitter(emitter -> { + ApolloCall clone = call.toBuilder().build(); + emitter.onTermination(clone::cancel); + clone.enqueue(new ApolloCall.Callback() { + @Override public void onResponse(@NotNull Response response) { + emitter.complete(response); + } + + @Override public void onFailure(@NotNull ApolloException e) { + emitter.fail(e); + } + + @Override public void onStatusEvent(@NotNull ApolloCall.StatusEvent event) { + if (event == ApolloCall.StatusEvent.COMPLETED) { + emitter.complete(null); + } + } + } + ); + } + ); + } + + /** + * Converts an {@link ApolloPrefetch} to a synchronous Uni + * + * @param prefetch the ApolloPrefetch to convert + * @return the converted Uni + * @throws NullPointerException if prefetch == null + */ + @NotNull + public static Uni from(@NotNull final ApolloPrefetch prefetch) { + checkNotNull(prefetch, "prefetch == null"); + return Uni.createFrom().emitter(emitter -> { + ApolloPrefetch clone = prefetch.clone(); + emitter.onTermination(clone::cancel); + clone.enqueue(new ApolloPrefetch.Callback() { + @Override public void onSuccess() { + emitter.complete(null); + } + + @Override public void onFailure(@NotNull ApolloException e) { + emitter.fail(e); + } + } + ); + }); + } + + @NotNull + public static Multi> from(@NotNull ApolloSubscriptionCall call) { + return from(call, BackPressureStrategy.LATEST); + } + + @NotNull + public static Multi> from(@NotNull final ApolloSubscriptionCall call, + @NotNull BackPressureStrategy backpressureStrategy) { + checkNotNull(call, "originalCall == null"); + checkNotNull(backpressureStrategy, "backpressureStrategy == null"); + return Multi.createFrom().emitter(emitter -> { + ApolloSubscriptionCall clone = call.clone(); + emitter.onTermination(clone::cancel); + clone.execute( + new ApolloSubscriptionCall.Callback() { + @Override public void onResponse(@NotNull Response response) { + if (!emitter.isCancelled()) { + emitter.emit(response); + } + } + + @Override public void onFailure(@NotNull ApolloException e) { + if (!emitter.isCancelled()) { + emitter.fail(e); + } + } + + @Override public void onCompleted() { + if (!emitter.isCancelled()) { + emitter.complete(); + } + } + + @Override public void onTerminated() { + onFailure(new ApolloSubscriptionTerminatedException("Subscription server unexpectedly terminated connection")); + } + + @Override public void onConnected() { + //Do nothing when GraphQL subscription server connection is opened + } + } + ); + }, backpressureStrategy); + } + + /** + * Converts an {@link ApolloStoreOperation} to a Uni. + * + * @param operation the ApolloStoreOperation to convert + * @param the value type + * @return the converted Uni + */ + @NotNull + public static Uni from(@NotNull final ApolloStoreOperation operation) { + checkNotNull(operation, "operation == null"); + return Uni.createFrom().emitter(emitter -> operation.enqueue(new ApolloStoreOperation.Callback() { + @Override + public void onSuccess(T result) { + emitter.complete(result); + } + + @Override + public void onFailure(@NotNull Throwable t) { + emitter.fail(t); + } + })); + } +} diff --git a/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyExtensions.kt b/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyExtensions.kt new file mode 100644 index 0000000000..3e6331fb11 --- /dev/null +++ b/apollo-mutiny-support/src/main/java/com/apollographql/apollo/mutiny/MutinyExtensions.kt @@ -0,0 +1,97 @@ +@file:Suppress("NOTHING_TO_INLINE") +@file:JvmName("KotlinExtensions") + +package com.apollographql.apollo.mutiny + +import com.apollographql.apollo.ApolloCall +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.ApolloMutationCall +import com.apollographql.apollo.ApolloPrefetch +import com.apollographql.apollo.ApolloQueryCall +import com.apollographql.apollo.ApolloQueryWatcher +import com.apollographql.apollo.ApolloSubscriptionCall +import com.apollographql.apollo.api.Mutation +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.Query +import com.apollographql.apollo.api.Response +import com.apollographql.apollo.api.Subscription +import com.apollographql.apollo.cache.normalized.ApolloStoreOperation + +import io.smallrye.mutiny.Multi +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.subscription.BackPressureStrategy + +@JvmSynthetic +inline fun ApolloPrefetch.mutiny(): Uni = + MutinyApollo.from(this) + +@JvmSynthetic +inline fun ApolloStoreOperation.mutiny(): Uni = + MutinyApollo.from(this) + +@JvmSynthetic +inline fun ApolloQueryWatcher.mutiny(): Uni> = + MutinyApollo.from(this) + +@JvmSynthetic +inline fun ApolloCall.mutiny(): Uni> = + MutinyApollo.from(this) + +@JvmSynthetic +inline fun ApolloSubscriptionCall.mutiny( + backpressureStrategy: BackPressureStrategy = BackPressureStrategy.LATEST +): Multi> = MutinyApollo.from(this, backpressureStrategy) + +/** + * Creates a new [ApolloQueryCall] call and then converts it to an [Uni]. + * + * The number of emissions this Uni will have is based on the + * [com.apollographql.apollo.fetcher.ResponseFetcher] used with the call. + */ +@JvmSynthetic +inline fun ApolloClient.mutinyQuery( + query: Query, + configure: ApolloQueryCall.() -> ApolloQueryCall = { this } +): Uni> = query(query).configure().mutiny() + +/** + * Creates a new [ApolloMutationCall] call and then converts it to a [Uni]. + */ +@JvmSynthetic +inline fun ApolloClient.mutinyMutate( + mutation: Mutation, + configure: ApolloMutationCall.() -> ApolloMutationCall = { this } +): Uni> = mutate(mutation).configure().mutiny() + +/** + * Creates a new [ApolloMutationCall] call and then converts it to a [Uni]. + * + * Provided optimistic updates will be stored in [com.apollographql.apollo.cache.normalized.ApolloStore] + * immediately before mutation execution. Any [ApolloQueryWatcher] dependent on the changed cache records will + * be re-fetched. + */ +@JvmSynthetic +inline fun ApolloClient.mutinyMutate( + mutation: Mutation, + withOptimisticUpdates: D, + configure: ApolloMutationCall.() -> ApolloMutationCall = { this } +): Uni> = mutate(mutation, withOptimisticUpdates).configure().mutiny() + +/** + * Creates the [ApolloPrefetch] by wrapping the operation object inside and then converts it to a [Uni]. + */ +@JvmSynthetic +inline fun ApolloClient.mutinyPrefetch( + operation: Operation +): Uni = prefetch(operation).mutiny() + +/** + * Creates a new [ApolloSubscriptionCall] call and then converts it to a [Multi]. + * + * Back-pressure strategy can be provided via [backpressureStrategy] parameter. The default value is [BackPressureStrategy.LATEST] + */ +@JvmSynthetic +inline fun ApolloClient.mutinySubscribe( + subscription: Subscription, + backpressureStrategy: BackPressureStrategy = BackPressureStrategy.LATEST +): Multi> = subscribe(subscription).mutiny(backpressureStrategy) diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index fe570e2f6f..5558faf144 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -46,7 +46,8 @@ module.exports = { 'advanced/coroutines', 'advanced/rxjava2', 'advanced/rxjava3', - 'advanced/reactor' + 'advanced/reactor', + 'advanced/mutiny' ], Reference: [ 'essentials/plugin-configuration', diff --git a/docs/source/advanced/mutiny.mdx b/docs/source/advanced/mutiny.mdx new file mode 100644 index 0000000000..bcd6229419 --- /dev/null +++ b/docs/source/advanced/mutiny.mdx @@ -0,0 +1,87 @@ +--- +title: Mutiny +--- + +Apollo Android includes support for Mutiny. + +Apollo types can be converted to Mutiny `Uni` *types* using wrapper functions `MutinyApollo.from(...)` in Java or using extension functions in Kotlin. + +Conversion is done according to the following table: + +| Apollo type | Mutiny type| +| :--- | :--- | +| `ApolloCall` | `Uni>` | +| `ApolloSubscriptionCall` | `Multi>` | +| `ApolloQueryWatcher` | `Uni>` | +| `ApolloStoreOperation` | `Uni` | +| `ApolloPrefetch` | `Uni` | + +## Prerequisites + +See Mutiny [documentation](https://smallrye.io/smallrye-mutiny/getting-started/download) + +## Including in your project + +Add the following `dependency`: + +```groovy +// Mutiny support +implementation 'com.apollographql.apollo:apollo-mutiny-support:x.y.z' +``` + +## Usage examples + +### Converting `ApolloCall` to an `Uni` + +Java: +```java +// Create a query object +EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); + +// Create an ApolloCall object +ApolloCall apolloCall = apolloClient.query(query); + +// Mutiny Uni +Uni> uni = MutinyApollo.from(apolloCall); +``` + +Kotlin: +```kotlin +// Create a query object +val query = EpisodeHeroNameQuery(episode = Episode.EMPIRE.toInput()) + +// Directly create Uni with Kotlin extension +val uni = apolloClient.mutinyQuery(query) +``` + + +### Converting `ApolloPrefetch` to a `Uni` + +Java: +```java +//Create a query object +EpisodeHeroName query = EpisodeHeroName.builder().episode(Episode.EMPIRE).build(); + +//Create an ApolloPrefetch object +ApolloPrefetch apolloPrefetch = apolloClient.prefetch(query); + +//Mutiny Uni +Uni uni = MutinyApollo.from(apolloPrefetch); +``` + +Kotlin: +```kotlin +// Create a query object +val query = EpisodeHeroNameQuery(episode = Episode.EMPIRE.toInput()) + +// Create Uni for prefetch with Kotlin extension +val uni = apolloClient.mutinyPrefetch(query) +``` + +Also, don't forget to cancel of your Observer/Subscriber when you are finished: +```java +Cancellable cancellable = ReactorApollo.from(query).subscribe().with(item -> log("👍 " + item)) + +//Cancel of your Observer when you are done with your work +cancellable.cancel(); +``` \ No newline at end of file diff --git a/docs/source/advanced/reactor.mdx b/docs/source/advanced/reactor.mdx index 57138c4b43..bd0ba2c77e 100644 --- a/docs/source/advanced/reactor.mdx +++ b/docs/source/advanced/reactor.mdx @@ -92,7 +92,4 @@ disposables.add(ReactorApollo.from(call).subscribe()); // Dispose of all collected Disposables at once disposables.dispose(); -``` - - -For a concrete example of using reactor wrappers for apollo types, checkout the sample app in the [`samples`](https://github.com/apollographql/apollo-android/tree/main/samples) module. +``` \ No newline at end of file diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index dea12075cb..11792a6787 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -16,6 +16,7 @@ def versions = [ kotlinPoet : '1.6.0', mockito : '1.9.5', moshi : '1.12.0', + mutiny : '0.18.0', okHttp4 : '4.9.0', okHttp : '3.12.11', // Keep this to keep supporting older Android devices okio : '2.9.0', @@ -74,6 +75,7 @@ ext.dep = [ kotlinCodegen: "com.squareup.moshi:moshi-kotlin-codegen:$versions.moshi", moshi : "com.squareup.moshi:moshi:$versions.moshi", ], + mutiny : "io.smallrye.reactive:mutiny:$versions.mutiny", okHttp : [ mockWebServer: "com.squareup.okhttp3:mockwebserver:$versions.okHttp", mockWebServer4: "com.squareup.okhttp3:mockwebserver:$versions.okHttp4", diff --git a/settings.gradle.kts b/settings.gradle.kts index 2bc1040679..2d85447b6a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ include("apollo-api") include("apollo-rx2-support") include("apollo-rx3-support") include("apollo-reactor-support") +include("apollo-mutiny-support") include("apollo-coroutines-support") include("apollo-http-cache") include("apollo-http-cache-api")