Retrofit은 HTTP API를 자바 api 형탤 사용할 수 있도록 하는 라이브러리로, kotlin에서는 코루틴과 엮여서 사용하게 된다. suspend 함수 형태로 선언하여 쉽게 사용할 수 있으나, 예외처리부분에서 약간의 귀차니즘이 생긴다. try-catch블록으로 매번 묶어버리기에는 보일러플레이트가 많아진다.
여기서 Result 클래스가 나타난다. 코틀린의 Result란 동작이 실패하든 성공하든 동작의 결과를 캡슐화 해서 나중에 처리될 수 있도록 하는 것이 목적인 클래스
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/
원형은 아래와 같다.
@JvmInline value class Result<out T> /* internal constructor */ {
val isSuccess: Boolean
val isFailure: Boolean
fun getOrNull(): T?
fun exceptionOrNull(): Throwable?
companion object {
fun <T> success(value: T): Result<T>
fun <T> failure(exception: Throwable): Result<T>
}
}
그렇다면 retrofit의 결과를 캡슐화해서 필요할 때 필요한 처리만 하면 어떨까? 라는 생각이 들게 되고, 이를 위해 retrofit CallAdapter를 지원한다.
CallAdapter 인터페이스의 정의는 아래와 같다
Adapts a Call with response type R into the type of T. Instances are created by a factory which is installed into the Retrofit instance.
Call<R>
을 T 타입으로 변환해주는 인터페이스로 CallAdapter.Factory에 의해 인스턴스가 생성된다.
다음 코드는 adapter를 구현한 예제이다
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>> {
return ResponseCall(call)
}
}
두개의 메서드를 오버라이드한것으로 볼 수 있다.
Call<ResponsSeearchMovie>
이 원래 retrofit 서비스 인터페이스의 리턴값이라고 할때 responseType 에서 반환하는 값은 ResponsSeearchMovie
가 된다Call<T>
를 받아 Call<Result<T>>
변환해 주는 역할을 하며, 위 코드에서는 ResponseCall이라는 클래스가 그 역할을 한다.다음은 Call<T>
를 Call<Result<T>>
로 변경해주는 ResponseCall의 구현이다.
class ResponseCall<T> (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)))
}
in 400..409 -> {
callback.onResponse(this@ResponseCall, Response.success(Result.failure(IllegalArgumentException("cannot find"))))
}
}
} ?: callback.onResponse(this@ResponseCall, Response.success(Result.failure(IllegalArgumentException("no body"))))
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResponseCall, Response.success(Result.failure(t)))
call.cancel()
}
})
override fun cancel() = callDelegate.cancel()
override fun clone(): Call<Result<T>> = ResponseCall(callDelegate.clone())
override fun execute(): Response<Result<T>> {
throw UnsupportedOperationException("unsupported operation")
}
override fun isCanceled(): Boolean = callDelegate.isCanceled
override fun isExecuted(): Boolean = callDelegate.isExecuted
override fun request(): Request = callDelegate.request()
override fun timeout(): Timeout = callDelegate.timeout()
}
다른 부분은 파라미터로 받는 Call객체에 위임하면 되지만, enqueue는 실제로 결과를 처리해야할 부분이기 때문에 중요하게 확인해야 한다.
enqueue 메소드는, 인자로받은 Call 객체의 enqueue를 실행시켜 그 결과를 Call<Result<T>>
로 변경하는 역할을 한다.
여기서 잘 살펴봐야 할 점이 있는것이, onFailuer 콜백이든 onResponse의 200번대가 아닌 코드에 대한 처리든 모두 Response의 success로 결과를 넘겨주고 있다.
onFailure에서 예외를 던지면, Api를 사용하는 입장에서 또 try-catch등으로 예외처리가 들어가야 할 테니, 그러지 말고 결과 및 예외를 success 로 Result로 포장해서 줄테니 알아서 사용해라 라는 의미다.
정의는 다음과 같다.
Creates CallAdapter instances based on the return type of the service interface methods.
리턴 타입에 기반해 retrofit의 서비스 메소드에서 어댑터를 생성하는 팩토리 인스턴스이다.
팩토리 클래스의 구현은 다음과 같다.
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)
}
}
중요 메서드들이다
List<? extends Runnable>
의 raw type은 List가 된다.어댑터 get 메소드의 동작은 다음과 같은 순서로 이루어진다.
여기까지 어댑터를 생성했다.
어댑터 클래스를 인터페이스 생성시 추가한다.
Retrofit.Builder().baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
}.asConverterFactory("application/json".toMediaTypeOrNull()!!))
.addCallAdapterFactory(ResponseAdapterFactory())
.client(logClient).build().create(MovieInformationInterface::class.java)
인터페이스 선언시 메소드는 항상 suspend fun으로 리턴 타입은 Result로 래핑한 타입을 선언한다
@GET("search/movie")
suspend fun searchMovieWithPage(@Query("query") queryString: String,
@Query("page") page: Int,
@Query("api_key") apiKey:String = BuildConfig.TMDB_API_KEY,
@Query("include_adult") includeAdult: Boolean = true): Result<ResponseSearchMovie>
사용시에는 아래와 같이 사용하면 된다
al searchedMovies = movieInformationService.searchMovie(URLEncoder.encode("Dark Knight", "utf-8"))
searchedMovies.onSuccess {
totalPages = it.totalPages
currentPage = it.page
}.onFailure {
L.e("Call Fail so end loading")
}
https://medium.com/shdev/retrofit%EC%97%90-calladapter%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%95-853652179b5b
https://dev-repository.tistory.com/105
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/