본 포스팅에서는 Retrofit의 Call Adapter를 커스텀하여, 내가 원하는 상태처리를 해보도록 하겠습니다.
Api는 Unsplash의 api를 이용하겠습니다.
Retrofit에서 받을 수 있는 데이터의 타입은 다음과 같습니다.
여기서, Call Adapter를 커스텀하면, Call 타입을 원하는 타입으로 바꾸어 받을 수 있습니다.
이 방법을 이용하여, Android 에서 흔히 사용하는 sealed class를 이용한 상태 처리를 해보도록 하겠습니다.
먼저, 아래와 같이 상태 처리를 도와주는 sealed class를 만들어 줍니다.
sealed class Result<T> {
class Success<T>(val data: T, val code: Int) : Result<T>()
class Loading<T> : Result<T>()
class ApiError<T>(val message: String, val code: Int) : Result<T>()
class NetworkError<T>(val throwable: Throwable) : Result<T>()
class NullResult<T> : Result<T>()
}
Api를 통해 받을 데이터 클래스를 만들어줍니다.
data class UnsplashPhoto(
val id: String,
val description: String?,
val urls: UnsplashPhotoUrls,
val user: UnsplashUser
)
이제 본격적으로 Retrofit의 Call Adapter를 커스텀해보도록 하겠습니다.
다음과 같은 순서를 거쳐 커스텀한 Call Adapter를 Retrofit에 적용시키겠습니다.
- Custom Call Class 만들기
- Custom Call Adapter, Call Adapter Factory 만들기
- Retrofit Builder에 추가
- Retrofit Api Interface에 suspend keyword 적용
Retrofit의 Custom Call Adpater의 초입입니다. 어렵지 않으니 코드를 보면 금방 이해하실 수 있습니다.
class ResponseCall<T> constructor(
private val callDelegate: Call<T>
) : Call<Result<T>> {
override fun enqueue(callback: Callback<Result<T>>) = callDelegate.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
response.body()?.let {
when(response.code()) {
in 200..299 -> {
callback.onResponse(this@ResponseCall, Response.success(Result.Success(it, response.code())))
}
in 400..409 -> {
callback.onResponse(this@ResponseCall, Response.success(Result.ApiError(response.message(), response.code())))
}
}
}?: callback.onResponse(this@ResponseCall, Response.success(Result.NullResult()))
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResponseCall, Response.success(Result.NetworkError(t)))
call.cancel()
}
})
override fun clone(): Call<Result<T>> = ResponseCall(callDelegate.clone())
override fun execute(): Response<Result<T>> = throw UnsupportedOperationException("ResponseCall does not support execute.")
override fun isExecuted(): Boolean = callDelegate.isExecuted
override fun cancel() = callDelegate.cancel()
override fun isCanceled(): Boolean = callDelegate.isCanceled
override fun request(): Request = callDelegate.request()
override fun timeout(): Timeout = callDelegate.timeout()
}
실질적으로 우리가 받을 데이터들을 정의하는 곳입니다.
위에서 정의한 sealed class를 토대로 입맛에 맞게 데이터를 정의하여 callback.onResponse().. 해주시면 됩니다.
class ResponseAdapter<T>(
private val successType : Type
) : CallAdapter<T, Call<Result<T>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<Result<T>> = ResponseCall(call)
}
class ResponseAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (Call::class.java != getRawType(returnType)) return null
check(returnType is ParameterizedType)
val responseType = getParameterUpperBound(0, returnType)
if (getRawType(responseType) != Result::class.java) return null
check(responseType is ParameterizedType)
val successType = getParameterUpperBound(0, responseType)
return ResponseAdapter<Any>(successType)
}
}
데이터의 타입을 구분해주는 코드입니다.
Retrofit Api Interface에서 suspend keyword를 적용해주지 않을 시, 맨 처음 returnType에서 Call Type으로 들어오지 않아 온전한 데이터가 return 되지 않습니다.
Retrofit.Builder()
.addCallAdapterFactory(ResponseAdapterFactory())
.baseUrl("https://api.unsplash.com/")
.build()
일반적인 Retrofit의 Builder 형태입니다.
입맛에 맞게 HttpClient, ConverterFactory, Interceptor... 를 추가해주시면 됩니다.
interface RetrofitApi {
@Headers("Accept-Version: v1", "Authorization: Client-ID --> Client ID 값 <--")
@GET("search/photos")
suspend fun searchPhotos(
@Query("query") query: String,
@Query("page") page: Int,
@Query("per_page") perPage: Int
): Result<UnsplashResponse>
}
suspend를 붙이는게 중요합니다. suspend keyword를 적용하지 않으면 api 호출 시, Call Type으로 받지 않아 온전한 데이터를 얻지 못 합니다.
다음과 같이 사용하면 됩니다.
val retrofit = Retrofit.Builder()....
val api = retrofit.create(RetrofitApi::class.java)
val response = api.searchPhotos(...)
when(response) {
is Result.Success ->
is Result.ApiError ->
is Result.NetworkError ->
is Result.NullResult ->
}
전체 코드 : https://github.com/TaehoonLeee/multi-module-clean-architecture
안녕하세요
작성해주신 코드를 보면서 적용해보고 있는데요
모든 코드 적용 후 실행을 했을 때 sealed class Result 에서 "Can't instantiate abstract" 오류가 발생하는데 혹시 원인과 해결 방법을 알 수 있을까요 ?