Skip to content
Permalink
Browse files

Add first-party Kotlin coroutine suspend support

  • Loading branch information...
JakeWharton committed Sep 11, 2018
1 parent 61dc1be commit 3486d8f5c66812b77b6d3a897986ed4b3a4a4668
27 pom.xml
@@ -46,11 +46,12 @@

<!-- Compilation -->
<java.version>1.7</java.version>
<kotlin.version>1.2.60</kotlin.version>
<kotlin.version>1.3-M2</kotlin.version>

<!-- Dependencies -->
<android.version>4.1.1.4</android.version>
<okhttp.version>3.11.0</okhttp.version>
<kotlinx.coroutines.version>0.26.0-eap13</kotlinx.coroutines.version>
<animal.sniffer.version>1.14</animal.sniffer.version>

<!-- Adapter Dependencies -->
@@ -114,6 +115,11 @@
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx.coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-annotations</artifactId>
@@ -215,6 +221,11 @@
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@@ -321,4 +332,18 @@
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>kotlin-eap</id>
<url>https://dl.bintray.com/kotlin/kotlin-eap</url>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>kotlin-eap</id>
<url>https://dl.bintray.com/kotlin/kotlin-eap</url>
</pluginRepository>
</pluginRepositories>
</project>
@@ -29,6 +29,11 @@
<artifactId>kotlin-stdlib</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.codehaus.mojo</groupId>
@@ -17,9 +17,14 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.annotation.Nullable;
import kotlin.coroutines.Continuation;
import okhttp3.Call;
import okhttp3.ResponseBody;

import static retrofit2.Utils.getRawType;
import static retrofit2.Utils.methodError;

/** Adapts an invocation of an interface method into an HTTP call. */
@@ -31,13 +36,32 @@
*/
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
CallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method);
Type responseType = callAdapter.responseType();
if (responseType == Response.class || responseType == okhttp3.Response.class) {
CallAdapter<ResponseT, ReturnT> callAdapter = null;
boolean continuationWantsResponse = false;
Type responseType;
if (requestFactory.isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type continuationType = parameterTypes[parameterTypes.length - 1];
responseType = Utils.getParameterLowerBound(0, (ParameterizedType) continuationType);
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
}
} else {
callAdapter = createCallAdapter(retrofit, method);
responseType = callAdapter.responseType();
}

if (responseType == okhttp3.Response.class) {
throw methodError(method, "'"
+ Utils.getRawType(responseType).getName()
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
// TODO support Unit for Kotlin?
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}
@@ -46,7 +70,8 @@
createResponseConverter(retrofit, method, responseType);

okhttp3.Call.Factory callFactory = retrofit.callFactory;
return new HttpServiceMethod<>(requestFactory, callFactory, callAdapter, responseConverter);
return new HttpServiceMethod<>(requestFactory, callFactory, callAdapter,
continuationWantsResponse, responseConverter);
}

private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
@@ -73,20 +98,41 @@

private final RequestFactory requestFactory;
private final okhttp3.Call.Factory callFactory;
private final CallAdapter<ResponseT, ReturnT> callAdapter;
/** Null indicates a Kotlin coroutine service method. */
private final @Nullable CallAdapter<ResponseT, ReturnT> callAdapter;
/**
* True if the coroutine continuation should receive the full Response object. Only meaningful
* when {@link #callAdapter} is null.
*/
private final boolean continuationWantsResponse;
private final Converter<ResponseBody, ResponseT> responseConverter;

private HttpServiceMethod(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
CallAdapter<ResponseT, ReturnT> callAdapter,
private HttpServiceMethod(RequestFactory requestFactory, Call.Factory callFactory,
@Nullable CallAdapter<ResponseT, ReturnT> callAdapter, boolean continuationWantsResponse,
Converter<ResponseBody, ResponseT> responseConverter) {
this.requestFactory = requestFactory;
this.callFactory = callFactory;
this.callAdapter = callAdapter;
this.continuationWantsResponse = continuationWantsResponse;
this.responseConverter = responseConverter;
}

@Override ReturnT invoke(Object[] args) {
return callAdapter.adapt(
new OkHttpCall<>(requestFactory, args, callFactory, responseConverter));
OkHttpCall<ResponseT> call =
new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);

if (callAdapter != null) {
return callAdapter.adapt(call);
}

//noinspection ConstantConditions Suspension functions always have arguments.
Object continuation = args[args.length - 1];
if (continuationWantsResponse) {
//noinspection unchecked Guaranteed by parseAnnotations above.
return (ReturnT) KotlinExtensions.awaitResponse(call,
(Continuation<Response<ResponseT>>) continuation);
}
//noinspection unchecked Guaranteed by parseAnnotations above.
return (ReturnT) KotlinExtensions.await(call, (Continuation<ResponseT>) continuation);
}
}
@@ -14,9 +14,51 @@
* limitations under the License.
*/

// Hide the class from Java consumers.
@file:JvmName("-KotlinExtensions")
@file:JvmName("KotlinExtensions")

package retrofit2

import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

inline fun <reified T> Retrofit.create(): T = create(T::class.java)

suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
// TODO handle nullability
continuation.resume(response.body()!!)
} else {
continuation.resumeWithException(HttpException(response))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

suspend fun <T : Any> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
@@ -29,6 +29,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import kotlin.coroutines.Continuation;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
@@ -75,6 +76,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
private final boolean isFormEncoded;
private final boolean isMultipart;
private final ParameterHandler<?>[] parameterHandlers;
final boolean isKotlinSuspendFunction;

RequestFactory(Builder builder) {
method = builder.method;
@@ -87,6 +89,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
isFormEncoded = builder.isFormEncoded;
isMultipart = builder.isMultipart;
parameterHandlers = builder.parameterHandlers;
isKotlinSuspendFunction = builder.isKotlinSuspendFunction;
}

okhttp3.Request create(Object[] args) throws IOException {
@@ -102,6 +105,11 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl,
headers, contentType, hasBody, isFormEncoded, isMultipart);

if (isKotlinSuspendFunction) {
// The Continuation is the last parameter and the handlers array contains null at that index.
argumentCount--;
}

List<Object> argumentList = new ArrayList<>(argumentCount);
for (int p = 0; p < argumentCount; p++) {
argumentList.add(args[p]);
@@ -147,6 +155,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
MediaType contentType;
Set<String> relativeUrlParamNames;
ParameterHandler<?>[] parameterHandlers;
boolean isKotlinSuspendFunction;

Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
@@ -178,8 +187,9 @@ RequestFactory build() {

int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
parameterHandlers[p] = parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p]);
for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
parameterHandlers[p] =
parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}

if (relativeUrl == null && !gotUrl) {
@@ -287,7 +297,7 @@ private Headers parseHeaders(String[] headers) {
}

private ParameterHandler<?> parseParameter(
int p, Type parameterType, @Nullable Annotation[] annotations) {
int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
ParameterHandler<?> result = null;
if (annotations != null) {
for (Annotation annotation : annotations) {
@@ -308,6 +318,15 @@ private Headers parseHeaders(String[] headers) {
}

if (result == null) {
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
}

@@ -348,6 +348,14 @@ static Type getParameterUpperBound(int index, ParameterizedType type) {
return paramType;
}

static Type getParameterLowerBound(int index, ParameterizedType type) {
Type paramType = type.getActualTypeArguments()[index];
if (paramType instanceof WildcardType) {
return ((WildcardType) paramType).getLowerBounds()[0];
}
return paramType;
}

static boolean hasUnresolvableType(@Nullable Type type) {
if (type instanceof Class<?>) {
return false;
@@ -16,4 +16,4 @@
-dontwarn kotlin.Unit

# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.-KotlinExtensions
-dontwarn retrofit2.KotlinExtensions
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2;

import kotlin.coroutines.Continuation;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Rule;
import org.junit.Test;
import retrofit2.http.GET;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

/**
* This code path can only be tested from Java because Kotlin does not allow you specify a raw
* Response type. Win! We still test this codepath for completeness.
*/
public final class KotlinSuspendRawTest {
@Rule public final MockWebServer server = new MockWebServer();

interface Service {
@GET("/")
Object body(Continuation<? super Response> response);
}

@Test public void raw() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(server.url("/"))
.build();
Service service = retrofit.create(Service.class);

try {
service.body(null);
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Response must include generic type (e.g., Response<String>)\n"
+ " for method Service.body");
}
}
}
Oops, something went wrong.

0 comments on commit 3486d8f

Please sign in to comment.
You can’t perform that action at this time.