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>
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)
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)
您也不需要MyWrapperAdapterFactory或MyWrapperAdapter。
希望这回答了您的问题!
编辑 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)并且它应该返回正确的类型。还可以在此处添加更高级的场景,通过解析错误(如果它不仅仅是一个普通字符串)。
小智 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)
| 归档时间: |
|
| 查看次数: |
1715 次 |
| 最近记录: |