如何在 Retrofit 中创建用于暂停功能的调用适配器?
Posted
技术标签:
【中文标题】如何在 Retrofit 中创建用于暂停功能的调用适配器?【英文标题】:How to create a call adapter for suspending functions in Retrofit? 【发布时间】:2019-10-22 07:21:46 【问题描述】:我需要创建一个可以处理此类网络呼叫的改装呼叫适配器:
@GET("user")
suspend fun getUser(): MyResponseWrapper<User>
我希望它在不使用 Deferred
的情况下与 Kotlin Coroutines 一起工作。我已经使用Deferred
成功实现了,可以处理如下方法:
@GET("user")
fun getUser(): Deferred<MyResponseWrapper<User>>
但我希望能够使函数成为暂停函数并删除 Deferred
包装器。
使用挂起函数,Retrofit 就像在返回类型周围有一个 Call
包装器一样工作,因此 suspend fun getUser(): User
被视为 fun getUser(): Call<User>
我的实现
我已尝试创建一个尝试处理此问题的呼叫适配器。到目前为止,这是我的实现:
工厂
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
适配器
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()
使用此实现,一切都按预期工作,但就在网络调用的结果被传递到 user
变量之前,我收到以下错误:
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)
...
来自 Retrofit 的源代码,这里是 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);
我不确定如何处理此错误。有办法解决吗?
【问题讨论】:
请注意,Retrofit 2.6.0 引入了对suspend
的内置支持。
是的,我的问题是关于 Retrofit 2.6.0。
“但我希望能够使函数成为挂起函数并删除延迟包装”——然后只需添加 suspend
关键字。您不需要CallAdapter
或其工厂。 suspend fun getUser(): MyResponseWrapper<User>
将直接与 Retrofit 2.6.0 一起使用。
你是说任何类型的任意类型现在都可以通过 Retrofit 自动适应?那么现在不需要调用适配器了吗?我的问题提出了一个问题,即在暂停功能时,改造假定 Call
包装器围绕响应类型。这会导致自定义调用适配器崩溃。我想我会在 Retrofit 的存储库上提出一个问题。
"你是说任何类型的任意类型现在都可以通过 Retrofit 自动适应?" -- 我是说你不再需要suspend
的CallAdapter
(与以前使用Jake 的Deferred
相比)。您仍然需要转换器(Moshi、Gson、Jackson 等)来将您的 Web 服务负载转换为 POKO。我不知道 MyResponseWrapper
是什么,所以我无法评论你是否需要在 Retrofit 中做一些特别的事情。
【参考方案1】:
这是一个适配器的工作示例,它自动包装对Result
包装器的响应。 GitHub 示例也是 available。
// 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()
...
...
【讨论】:
谢谢你。我已经对其进行了测试,并且可以正常工作。 My issue 在 Retrofit 的 repo 上仍然保持开放,希望有一个官方样本。 感谢您的解决方案!但如果响应是对象列表,它将不起作用,例如结果>。要完成解决方案,我们应该将Type
更改为 ResultAdapter 的输入,而不是 Class<T>
,以便 responseType() 函数也返回一个 type。在 MyCallAdapterFactory 中,将 ResultAdapter(getRawType(resultType))
更改为 ResultAdapter<Any>(resultType)
感谢@aaronmarino 和 qnd-S!我已经更新了答案和示例。
很高兴它有帮助。编码愉快!
这不会与 Retrofit 2.8 一起编译,因为他们添加了 Call.timeout()。【参考方案2】:
当您将Retrofit 2.6.0
与协程一起使用时,您不再需要包装器。它应该如下所示:
@GET("user")
suspend fun getUser(): User
你不再需要MyResponseWrapper
,当你调用它时,它应该是这样的
runBlocking
val user: User = service.getUser()
要获得改造 Response
,您可以执行以下操作:
@GET("user")
suspend fun getUser(): Response<User>
您也不需要MyWrapperAdapterFactory
或MyWrapperAdapter
。
希望这回答了你的问题!
编辑 CommonsWare@ 在上面的 cmets 中也提到了这一点
编辑 处理错误可能如下:
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>()
您只需要调用 create 并将响应作为 ApiResponse.create(response)
并且它应该返回正确的类型。在这里也可以添加一个更高级的场景,如果它不仅仅是一个纯字符串,则通过解析错误。
【讨论】:
我知道我不需要包装器,但我想要包装器。我想使用的包装器提供了另一种方法来处理网络调用中可能发生的任何错误。 您可以检查响应是否成功为response.isSuccessful
,然后如果失败,您可以通过解析response.errorBody()
以不同的方式处理响应正文,如果没有则@987654333 @
suspend fun getUser(): Response<User>
帮助了我。谢谢!
我也在尝试为这种情况创建一个包装器(直接使用适配器处理错误)。有人让它工作吗?
ApiResponse
在使用 suspend
关键字时不能用作返回类型,调用会引发类似于 Unable to invoke no-args constructor for interface
的错误【参考方案3】:
这个问题出现在将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)
【讨论】:
感谢您强调该评论。看起来 Retrofit 的开发人员已经意识到了这个问题。我想我们只能等着看会发生什么。 @mikael 有什么办法可以把它变成一个扩展方法?以上是关于如何在 Retrofit 中创建用于暂停功能的调用适配器?的主要内容,如果未能解决你的问题,请参考以下文章
OKHttp Authenticator 不适用于 Retrofit 暂停乐趣