Как создать адаптер вызова для приостановки функций в Retrofit? - PullRequest
7 голосов
/ 06 июня 2019

Мне нужно создать адаптер вызовов для модернизации, который может обрабатывать такие сетевые вызовы:

@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)
    ...

Из исходного кода 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);
    }

Я не уверен, как справиться с этой ошибкой.Есть ли способ исправить?

Ответы [ 2 ]

2 голосов
/ 09 июня 2019

Когда вы используете 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), и он должен вернуть правильный тип. Здесь также можно добавить более сложный сценарий, анализируя ошибку, если она не является простой строкой.

0 голосов
/ 19 июня 2019

Этот вопрос возник в запросе на получение, где в 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)
    }
}
...