web-dev-qa-db-fra.com

Comment créer un adaptateur d'appel pour suspendre des fonctions dans Retrofit?

J'ai besoin de créer un adaptateur d'appel rétrofit qui peut gérer de tels appels réseau:

@GET("user")
suspend fun getUser(): MyResponseWrapper<User>

Je veux qu'il fonctionne avec Kotlin Coroutines sans utiliser Deferred. J'ai déjà une implémentation réussie en utilisant Deferred, qui peut gérer des méthodes telles que:

@GET("user")
fun getUser(): Deferred<MyResponseWrapper<User>>

Mais je veux que la capacité fasse de la fonction une fonction de suspension et supprime le wrapper Deferred.

Avec les fonctions de suspension, Retrofit fonctionne comme s'il y avait un wrapper Call autour du type de retour, donc suspend fun getUser(): User est traité comme fun getUser(): Call<User>

Ma mise en œuvre

J'ai essayé de créer un adaptateur d'appel qui essaie de gérer cela. Voici ma mise en œuvre jusqu'à présent:

sine

class MyWrapperAdapterFactory : CallAdapter.Factory() {

    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {

        val rawType = getRawType(returnType)

        if (rawType == Call::class.Java) {

            returnType as? ParameterizedType
                ?: throw IllegalStateException("$returnType must be parameterized")

            val containerType = getParameterUpperBound(0, returnType)

            if (getRawType(containerType) != MyWrapper::class.Java) {
                return null
            }

            containerType as? ParameterizedType
                ?: throw IllegalStateException("MyWrapper must be parameterized")

            val successBodyType = getParameterUpperBound(0, containerType)
            val errorBodyType = getParameterUpperBound(1, containerType)

            val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(
                null,
                errorBodyType,
                annotations
            )

            return MyWrapperAdapter<Any, Any>(successBodyType, errorBodyConverter)
        }
        return null
    }

Adaptateur

class MyWrapperAdapter<T : Any>(
    private val successBodyType: Type
) : CallAdapter<T, MyWrapper<T>> {

    override fun adapt(call: Call<T>): MyWrapper<T> {
        return try {
            call.execute().toMyWrapper<T>()
        } catch (e: IOException) {
            e.toNetworkErrorWrapper()
        }
    }

    override fun responseType(): Type = successBodyType
}
runBlocking {
  val user: MyWrapper<User> = service.getUser()
}

Tout fonctionne comme prévu en utilisant cette implémentation, mais juste avant que le résultat de l'appel réseau ne soit livré à la variable user, j'obtiens l'erreur suivante:

Java.lang.ClassCastException: com.myproject.MyWrapper cannot be cast to retrofit2.Call

    at retrofit2.HttpServiceMethod$SuspendForBody.adapt(HttpServiceMethod.Java:185)
    at retrofit2.HttpServiceMethod.invoke(HttpServiceMethod.Java:132)
    at retrofit2.Retrofit$1.invoke(Retrofit.Java:149)
    at com.Sun.proxy.$Proxy6.getText(Unknown Source)
    ...

De la source de Retrofit, voici le morceau de code à HttpServiceMethod.Java:185:

    @Override protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call); // ERROR OCCURS HERE

      //noinspection unchecked Checked by reflection inside RequestFactory.
      Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
      return isNullable
          ? KotlinExtensions.awaitNullable(call, continuation)
          : KotlinExtensions.await(call, continuation);
    }

Je ne sais pas comment gérer cette erreur. Y a-t-il un moyen de réparer?

19
harold_admin

Voici un exemple de travail d'un adaptateur, qui encapsule automatiquement une réponse à l'encapsuleur Result. Un exemple GitHub est également disponible .

// build.gradle

...
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.6.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
    implementation 'com.google.code.gson:gson:2.8.5'
}
// test.kt

...
sealed class Result<out T> {
    data class Success<T>(val data: T?) : Result<T>()
    data class Failure(val statusCode: Int?) : Result<Nothing>()
    object NetworkError : Result<Nothing>()
}

data class Bar(
    @SerializedName("foo")
    val foo: String
)

interface Service {
    @GET("bar")
    suspend fun getBar(): Result<Bar>

    @GET("bars")
    suspend fun getBars(): Result<List<Bar>>
}

abstract class CallDelegate<TIn, TOut>(
    protected val proxy: Call<TIn>
) : Call<TOut> {
    override fun execute(): Response<TOut> = throw NotImplementedError()
    override final fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)
    override final fun clone(): Call<TOut> = cloneImpl()

    override fun cancel() = proxy.cancel()
    override fun request(): Request = proxy.request()
    override fun isExecuted() = proxy.isExecuted
    override fun isCanceled() = proxy.isCanceled

    abstract fun enqueueImpl(callback: Callback<TOut>)
    abstract fun cloneImpl(): Call<TOut>
}

class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Result<T>>(proxy) {
    override fun enqueueImpl(callback: Callback<Result<T>>) = proxy.enqueue(object: Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            val code = response.code()
            val result = if (code in 200 until 300) {
                val body = response.body()
                Result.Success(body)
            } else {
                Result.Failure(code)
            }

            callback.onResponse(this@ResultCall, Response.success(result))
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            val result = if (t is IOException) {
                Result.NetworkError
            } else {
                Result.Failure(null)
            }

            callback.onResponse(this@ResultCall, Response.success(result))
        }
    })

    override fun cloneImpl() = ResultCall(proxy.clone())
}

class ResultAdapter(
    private val type: Type
): CallAdapter<Type, Call<Result<Type>>> {
    override fun responseType() = type
    override fun adapt(call: Call<Type>): Call<Result<Type>> = ResultCall(call)
}

class MyCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ) = when (getRawType(returnType)) {
        Call::class.Java -> {
            val callType = getParameterUpperBound(0, returnType as ParameterizedType)
            when (getRawType(callType)) {
                Result::class.Java -> {
                    val resultType = getParameterUpperBound(0, callType as ParameterizedType)
                    ResultAdapter(resultType)
                }
                else -> null
            }
        }
        else -> null
    }
}

/**
 * A Mock interceptor that returns a test data
 */
class MockInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        val response = when (chain.request().url().encodedPath()) {
            "/bar" -> """{"foo":"baz"}"""
            "/bars" -> """[{"foo":"baz1"},{"foo":"baz2"}]"""
            else -> throw Error("unknown request")
        }

        val mediaType = MediaType.parse("application/json")
        val responseBody = ResponseBody.create(mediaType, response)

        return okhttp3.Response.Builder()
            .protocol(Protocol.HTTP_1_0)
            .request(chain.request())
            .code(200)
            .message("")
            .body(responseBody)
            .build()
    }
}

suspend fun test() {
    val mockInterceptor = MockInterceptor()
    val mockClient = OkHttpClient.Builder()
        .addInterceptor(mockInterceptor)
        .build()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://mock.com/")
        .client(mockClient)
        .addCallAdapterFactory(MyCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val service = retrofit.create(Service::class.Java)
    val bar = service.getBar()
    val bars = service.getBars()
    ...
}
...
9
Valeriy Katkov

Lorsque vous utilisez Retrofit 2.6.0 Avec des coroutines, vous n'avez plus besoin d'un wrapper. Il devrait ressembler à ci-dessous:

@GET("user")
suspend fun getUser(): User

Vous n'avez plus besoin de MyResponseWrapper, et lorsque vous l'appelez, cela devrait ressembler à

runBlocking {
   val user: User = service.getUser()
}

Pour obtenir la modification Response, vous pouvez procéder comme suit:

@GET("user")
suspend fun getUser(): Response<User>

Vous n'avez pas non plus besoin du MyWrapperAdapterFactory ou du MyWrapperAdapter.

J'espère que cela a répondu à votre question!

Edit CommonsWare @ l'a également mentionné dans les commentaires ci-dessus

Modifier L'erreur de gestion peut être la suivante:

sealed class ApiResponse<T> {
    companion object {
        fun <T> create(response: Response<T>): ApiResponse<T> {
            return if(response.isSuccessful) {
                val body = response.body()
                // Empty body
                if (body == null || response.code() == 204) {
                    ApiSuccessEmptyResponse()
                } else {
                    ApiSuccessResponse(body)
                }
            } else {
                val msg = response.errorBody()?.string()
                val errorMessage = if(msg.isNullOrEmpty()) {
                    response.message()
                } else {
                    msg
                }
                ApiErrorResponse(errorMessage ?: "Unknown error")
            }
        }
    }
}

class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()

Où vous avez juste besoin d'appeler create avec la réponse en tant que ApiResponse.create(response) et il doit retourner le type correct. Un scénario plus avancé pourrait également être ajouté ici, en analysant l'erreur s'il ne s'agit pas simplement d'une chaîne simple.

6
Hakem Zaied

Cette question est apparue dans la demande d'extraction où suspend a été introduit dans Retrofit.

matejdro: D'après ce que je vois, ce MR contourne complètement les adaptateurs d'appel lors de l'utilisation des fonctions de suspension. J'utilise actuellement des adaptateurs d'appels personnalisés pour centraliser l'analyse du corps de l'erreur (puis lever les exceptions appropriées), de manière similaire à l'exemple officiel de retrofit2. Toute chance que nous obtenions une alternative à cela, une sorte d'adaptateur qui est injecté entre ici?

Il s'avère que ce n'est pas encore pris en charge?.

Source: https://github.com/square/retrofit/pull/2886#issuecomment-438936312


Pour la gestion des erreurs, j'ai opté pour quelque chose comme ça pour appeler les appels api:

suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>): MyWrapper<T> {
    return try {
        val response = call.invoke()
        when (response.code()) {
            // return MyWrapper based on response code
            // MyWrapper is sealed class with subclasses Success and Failure
        }
    } catch (error: Throwable) {
        Failure(error)
    }
}
2
Mikael