How to create a call adapter for suspending functions in Retrofit?

后端 未结 3 1370
慢半拍i
慢半拍i 2020-12-05 02:35

I need to create a retrofit call adapter which can handle such network calls:

@GET(\"user\")
suspend fun getUser(): MyResponseWrapper


        
相关标签:
3条回答
  • 2020-12-05 03:08

    Here is a working example of an adapter, which automatically wraps a response to the Result wrapper. A GitHub sample is also 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()
        ...
    }
    ...
    
    0 讨论(0)
  • 2020-12-05 03:10

    This question came up in the pull request where suspend was introduced to Retrofit.

    matejdro: From what I see, this MR completely bypasses call adapters when using suspend functions. I'm currently using custom call adapters for centralising parsing of error body (and then throwing appropriate exceptions), smilarly to the official retrofit2 sample. Any chance we get alternative to this, some kind of adapter that is injected between here?

    It turns out this is not supported (yet?).

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


    For error handling I went for something like this to invoke api calls:

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-05 03:20

    When you use Retrofit 2.6.0 with coroutines you don't need a wrapper anymore. It should look like below:

    @GET("user")
    suspend fun getUser(): User
    

    You don't need MyResponseWrapper anymore, and when you call it, it should look like

    runBlocking {
       val user: User = service.getUser()
    }
    

    To get the retrofit Response you can do the following:

    @GET("user")
    suspend fun getUser(): Response<User>
    

    You also don't need the MyWrapperAdapterFactory or the MyWrapperAdapter.

    Hope this answered your question!

    Edit CommonsWare@ has also mentioned this in the comments above

    Edit Handling error could be as follow:

    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>()
    

    Where you just need to call create with the response as ApiResponse.create(response) and it should return correct type. A more advanced scenario could be added here as well, by parsing the error if it is not just a plain string.

    0 讨论(0)
提交回复
热议问题