はじめに
APIのエラーレスポンスの型が統一されている場合, エラーレスポンスの型を定義し, デコードを共通化して行いたくなります.
今回はRetrofitのCallAdapter
を使用して, APIエラー時の挙動を(HttpException
ではなく) Custom Exceptionを吐くよう変更する方法をメモしておきます.
公式のサンプルにも同じ目的のCallAdapter
がありますが, coroutinesの場合必要な定義が少し変わるのでメモしておきます.
CallAdapter.Factory#getの処理
suspend関数を使う場合, CallAdapter.Factory#get
で returnType
がCall
であることを確認する必要があります.
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関数であれば,
Hoge
をCall<Hoge>
に変換する. -
Call<Hoge>
に対応できるCallAdapter.Factory
を探索する
これらから, suspend関数のreturnType
はHoge
ではなくCall<Hoge>
が与えられます. そのため, suspend関数に対応してCallAdapter
を設定したい場合は, returnType
がCall<..>
である時にインスタンスを返すように実装します.
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)