如何在Retrofit中为挂起功能创建呼叫适配器?

har*_*min 14 java android kotlin retrofit kotlin-coroutines

我需要创建一个可处理此类网络呼叫的改造呼叫适配器:

@GET("user")
suspend fun getUser(): MyResponseWrapper<User>
Run Code Online (Sandbox Code Playgroud)

I want it to work with Kotlin Coroutines without using Deferred. I have already have a successful implementation using Deferred, which can handle methods such as:

@GET("user")
fun getUser(): Deferred<MyResponseWrapper<User>>
Run Code Online (Sandbox Code Playgroud)

But I want the ability make the function a suspending function and remove the Deferred wrapper.

With suspending functions, Retrofit works as if there is a Call wrapper around the return type, so suspend fun getUser(): User is treated as fun getUser(): Call<User>

My Implementation

I have tried to create a call adapter which tries to handle this. Here is my implementation so far:

Factory

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
    }
Run Code Online (Sandbox Code Playgroud)

Adapter

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
}
Run Code Online (Sandbox Code Playgroud)
runBlocking {
  val user: MyWrapper<User> = service.getUser()
}
Run Code Online (Sandbox Code Playgroud)

Everything works as expected using this implementation, but just before the result of the network call is delivered to the user variable, I get the following error:

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)
    ...
Run Code Online (Sandbox Code Playgroud)

From Retrofit's source, here is the piece of code at HttpServiceMethod.java:185:

@GET("user")
suspend fun getUser(): MyResponseWrapper<User>
Run Code Online (Sandbox Code Playgroud)

I'm not sure how to handle this error. Is there a way to fix?

Val*_*kov 24

这是一个适配器的工作示例,它自动将响应Result包装到包装器。还提供了GitHub 示例。

// 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'
}
Run Code Online (Sandbox Code Playgroud)
// 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()
    ...
}
...
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的解决方案!但如果响应是对象列表,例如 Result&lt;List&lt;Foo&gt;&gt;,则它将不起作用。为了完成该解决方案,我们应该将“Type”更改为 ResultAdapter 的输入,而不是“Class&lt;T&gt;”,以便 _responseType()_ 函数也返回一个 _type_。在 _MyCallAdapterFactory_ 中,将 `ResultAdapter(getRawType(resultType))` 更改为 `ResultAdapter&lt;Any&gt;(resultType)` (4认同)

Hak*_*ied 20

当您使用Retrofit 2.6.0协程时,您不再需要包装器。它应该如下所示:

@GET("user")
suspend fun getUser(): User
Run Code Online (Sandbox Code Playgroud)

你不再需要MyResponseWrapper了,当你调用它时,它应该看起来像

runBlocking {
   val user: User = service.getUser()
}
Run Code Online (Sandbox Code Playgroud)

要进行改造,Response您可以执行以下操作:

@GET("user")
suspend fun getUser(): Response<User>
Run Code Online (Sandbox Code Playgroud)

您也不需要MyWrapperAdapterFactoryMyWrapperAdapter

希望这回答了您的问题!

编辑 CommonsWare@ 在上面的评论中也提到了这一点

编辑 处理错误可能如下:

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>()
Run Code Online (Sandbox Code Playgroud)

您只需要使用响应调用 createApiResponse.create(response)并且它应该返回正确的类型。还可以在此处添加更高级的场景,通过解析错误(如果它不仅仅是一个普通字符串)。

  • 我知道我不需要 ** 包装器,但我 ** 想要 ** 包装器。我想使用的包装器提供了一种替代方法来处理网络调用中可能发生的任何错误。 (3认同)
  • `暂停乐趣 getUser(): Response&lt;User&gt;` 帮助了我。谢谢! (2认同)
  • 当使用“suspend”关键字时,“ApiResponse”不能作为返回类型,调用会抛出“无法调用接口的无参数构造函数”的错误 (2认同)

小智 5

这个问题出现在suspend引入 Retrofit 的拉取请求中。

matejdro:据我所知,这个 MR 在使用挂起函数时完全绕过了调用适配器。我目前正在使用自定义调用适配器来集中解析错误主体(然后抛出适当的异常),与官方的 Retrofit2 示例类似。我们是否有机会找到替代方案,在此处注入某种适配器?

事实证明这不受支持(还?)。

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


对于错误处理,我采用了类似的方法来调用 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)
    }
}
Run Code Online (Sandbox Code Playgroud)