LoginSignup
7
0

More than 3 years have passed since last update.

coroutines x Retrofitで CallAdapterを使いAPIエラーレスポンスをデコードする

Posted at

はじめに

APIのエラーレスポンスの型が統一されている場合, エラーレスポンスの型を定義し, デコードを共通化して行いたくなります.
今回はRetrofitCallAdapterを使用して, APIエラー時の挙動を(HttpExceptionではなく) Custom Exceptionを吐くよう変更する方法をメモしておきます.

公式のサンプルにも同じ目的のCallAdapterがありますが, coroutinesの場合必要な定義が少し変わるのでメモしておきます.

CallAdapter.Factory#getの処理

suspend関数を使う場合, CallAdapter.Factory#getreturnTypeCallであることを確認する必要があります.

class ErrorHandlingCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // 返り値がCall<..>である場合のみ適用する.
        // 注: 「nullを返す」 == 「このAdapterはこの関数に適用できない」を表す
        if (getRawType(returnType) != Call::class.java) {
            return null
        }
        require(returnType is ParameterizedType) { "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>" }
        ...
    }
}

coroutinesを使用する場合, 返り値の型はCallで括らず, suspend fun sampleApi(): Hoge のように直接定義すると思います.
そのため一瞬解せない気持ちになりそうですが, この時内部では以下のような処理が行われています(参考).

  • もしsuspend関数であれば, HogeCall<Hoge> に変換する.
  • Call<Hoge>に対応できるCallAdapter.Factoryを探索する

これらから, suspend関数のreturnTypeHogeではなくCall<Hoge>が与えられます. そのため, suspend関数に対応してCallAdapterを設定したい場合は, returnTypeCall<..>である時にインスタンスを返すように実装します.

CallAdapterの型

suspend関数を使う場合, Callを返すAdapterを適用します.

    class Adapter<T> : CallAdapter<T, Call<T>> {
        override fun responseType(): Type = ...
        // Call<T>を受け取り, Call<T>を返すAdapter.
        override fun adapt(call: Call<T>): Call<T> = ..
    }

前節と同様に, retrofit内部では以下のような処理がされています(参考).

  • CallAdapter#adoptの返り値としてCall<Hoge>を受け取る
  • それをContiuation (suspend関数) を介してHoge に変換して返す

そのため, 「(Call<T>を受け取り, )Call<T>を返す」型である CallAdapter<T, Call<T>>が正しい型となります.

Executor

suspend関数を使う場合, 通常callbackをメインスレッドで行う必要がないため, Retrofit#callbackExecutorは使用しません.
メインスレッド以外を設定していたり, 特別な事情がある場合は使用した方が良さそうです.

サンプル

以下がコード例です. Custom Exceptionにデコードできないケースではデフォルトと同じ挙動をさせるような実装になっています.

class ErrorHandlingCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (getRawType(returnType) != Call::class.java) {
            return null
        }
        require(returnType is ParameterizedType) { "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>" }
        return Adapter<Any>(getParameterUpperBound(0, returnType))
    }

    private class Adapter<T>(private val responseType: Type) : CallAdapter<T, Call<T>> {
        override fun responseType(): Type = responseType
        override fun adapt(call: Call<T>): Call<T> = ErrorHandlingCall(call)
    }
}



class ErrorHandlingCall<T>(
    private val delegate: Call<T>
) : Call<T> by delegate {
    override fun enqueue(callback: Callback<T>) =
        delegate.enqueue(object : Callback<T> by callback {
            override fun onResponse(call: Call<T>, response: Response<S>) {
                if (response.isSuccessful) {
                    callback.onResponse(this@ErrorHandlingCall, response)
                    return
                }
                val errorBody = response.errorBody()
                if (errorBody == null || errorBody.contentLength() == 0L) {
                    callback.onResponse(this@ErrorHandlingCall, response)
                    return
                }

                try {
                    // Custom Exceptionにデコード・変換する
                    val exception = convertToCustomException(response)
                    callback.onFailure(this@ErrorHandlingCall, exception)
                } catch (ex: Exception) {
                    callback.onResponse(this@ErrorHandlingCall, response)
                }
            }
        })
}

これをRetrofit.Builderに渡せば完成です.


interface Service {
    @GET("..sample..")
    suspend fun sample(): Sample
}

Retrofit.Builder()
    .addCallAdapterFactory(ErrorHandlingCallAdapterFactory())
    .build()
    .create(Service::class.java)

参考

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0