Как создать адаптер вызова для приостановки функций в Retrofit?
Мне нужно создать модернизированный адаптер вызовов, который может обрабатывать такие сетевые вызовы:
@GET("user")
suspend fun getUser(): MyResponseWrapper<User>
Я хочу, чтобы он работал с Kotlin Coroutines без использования Deferred
. У меня уже есть успешная реализация с использованием 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)
...
Из исходного кода, вот фрагмент кода в 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);
}
Я не уверен, как справиться с этой ошибкой. Есть ли способ исправить?
Ответы
Ответ 1
Вот рабочий пример адаптера, который автоматически переносит ответ на оболочку 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'
}
// 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 TestResponse(
@SerializedName("slideshow")
val slideshow: Any
)
interface Service {
@GET("json")
suspend fun test(): Result<TestResponse>
}
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([email protected], 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([email protected], Response.success(result))
}
})
override fun cloneImpl() = ResultCall(proxy.clone())
}
class ResultAdapter<T>(
private val clazz: Class<T>
): CallAdapter<T, Call<Result<T>>> {
override fun responseType() = clazz
override fun adapt(call: Call<T>): Call<Result<T>> = 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(getRawType(resultType))
}
else -> null
}
}
else -> null
}
}
suspend fun test() {
val retrofit = Retrofit.Builder()
.baseUrl("https://httpbin.org/")
.addCallAdapterFactory(MyCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(Service::class.java)
val result = service.test()
...
}
...
Ответ 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 @также упоминал об этом в комментариях выше
Ошибка редактирования обработки может быть следующей:
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)
и он должен вернуть правильный тип. Здесь также можно добавить более сложный сценарий, анализируя ошибку, если она не является простой строкой.
Ответ 3
Этот вопрос возник в запросе на извлечение, где в Retrofit было введено suspend
.
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)
}
}