Retrofit2에서 suspend func 사용시 CustomCallAdapter을 이용해 응답을 처리하는 방법
proandroiddev(medium) 자세한 설명은 여기서
github link는 맨 아래
이 글에서 NetworkResponse~.kt 파일들의 코드를 거의 저 위의 블로그에서 복붙했다고 볼 수 있다.
돌아가는 것을 보기 위해 github-api를 이용하고 아주 간략하게 data class를 만들어 적용해 봤다.
data class Git(
val name:String,
val full_name:String,
val owner: Owner
)
data class Owner(
val login:String? = null,
val url:String? = null
)
아직도 NetworkResponse~.kt들을 전부 이해하진 못했다. 그러나 실습 해보면서 제대로 작동하는 코드를 만들어가는 경험은 유익하다.
1. Service 인터페이스
그냥 suspend 만 붙이는 경우
@GET("users/{user}/repos")
suspend fun getUser(@Path("user") user:String): Call<List<Git>>
이렇게 할 경우 서버와 통신중 에러가 나면 Exception이 난다. 그래서 try-catch로 응답에 대응해야 한다.
Call -> NetworkResponse 변경
interface Service{
@GET("users/{user}/repos")
suspend fun getUser(@Path("user") user:String): NetworkResponse<List<Git>, Error>
companion object{
val service = Retrofit.Builder().baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.build()
}
}
2. NetworkResponse sealed 클래스
sealed class NetworkResponse <out T:Any,out U:Any>{
/**
* Success response with body
* */
data class Success<T:Any>(val body:T): NetworkResponse<T, Nothing>()
/**
* Failure response with body
* non-2xx response, contains error body
*/
data class ApiError<U:Any>(val body :U, val code:Int): NetworkResponse<Nothing, U>()
/**
* Network Error, such as no internet-connection
* */
data class NetworkError(val error:IOException): NetworkResponse<Nothing, Nothing>()
/**
* For example, json parsing error
* */
data class UnknownError(val error: Throwable?): NetworkResponse<Nothing, Nothing>()
}
class Nothing 은 절대 존재하지 않는 instance라고 한다. 만약 return 타입이 Nothing이면
- throws an Exception을 하거나
- 리턴값이 없을 경우
두가지만 존재한다.
sealed class 간략 설명
- sealed modifier 을 이용해 클래스의 계층을 제한할 때 쓰인다.
- enum과 유사하다.
- 여러 객체를 가질수 있다.(enum은 object;static 객체 하나만 존재)
- 상태값(value)을 넣고 사용할 수 있다.
- when 사용시 편하다.
enum class Animal{
CAT,DOG,BIRD
}
위의 enum class와 아래의 sealed class는 같은 기능을 한다.
sealed class에서 전부 object만 이용할 거면 enum을 쓰는게 kotlin개발자들의 의도이지 않을까 싶다.
sealed class Animal
object CAT:Animal()
object DOG:Animal()
object BIRD:Animal()
그런데 위의 방식과 아래의 방식에서 object들은 다른 object이다.
sealed class Animal{
object CAT
object DOG
object BIRD
}
kotlin doc example
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// the `else` clause is not required because we've covered all the cases
}
in(반공변성), out(공변성) 간략 설명
- in, out 은 제네릭을 사용할 때 쓰인다.
- <in T> 와 <out T>는 반대 기능을 한다고 생각하자.(당연하지만)
- <in T> 은 T의 상위 클래스를 가질 수 있다.
- 마치
Min is T
라고 보면 될 것 같다.
- 마치
- <out T> 은 T의 하위 클래스를 가질 수 있다.
- 마치
Max is T
라고 보면 될 것 같다.
- 마치
class Home<in T>
open class Parent
class Child():Parent()
fun main(){
val parentHome: Home<Parent> = Home<Child>() << compile error
val childHome: Home<Child> = Home<Parent>() << OK
}
class Home<out T>
open class Parent
class Child():Parent()
fun main(){
val parentHome: Home<Parent> = Home<Child>() << OK
val childHome: Home<Child> = Home<Parent>() << compile error
}
3. NetworkResponseCall 클래스
// generic S는 response data class type이 되고
// E는 error type이 된다.
// NetworkResponseCall, 이 클래스는 Call의 Wrapper Class임을 염두해두자
internal class NetworkResponseCall<S:Any,E:Any>(
private val delegate: Call<S>,
private val errorConverter: Converter<ResponseBody,E>
) :Call<NetworkResponse<S, E>>{
override fun clone(): Call<NetworkResponse<S, E>> =
NetworkResponseCall(delegate.clone(), errorConverter)
override fun execute(): Response<NetworkResponse<S, E>> =
throw UnsupportedOperationException("NetworkResponseCall doesn't support execute")
override fun enqueue(callback: Callback<NetworkResponse<S, E>>) {
return delegate.enqueue(object : Callback<S>{
override fun onResponse(call: Call<S>, response: Response<S>) {
val body = response.body()
val code = response.code()
val error = response.errorBody()
//Callback<T> 파일 주석을 보면
//response가 404 or 500 에러가 있을수 있다고 하니 이를 거르기 위해
//if(response.isSuccessful)분기 처리 해줌.
//void boolean isSuccessful(){
// return code >= 200 && code < 300
//}
if(response.isSuccessful){
if(body != null){
callback.onResponse(this@NetworkResponseCall,Response.success(
NetworkResponse.Success(body)
))
}else{
// response is successful but the body is null
callback.onResponse(this@NetworkResponseCall,Response.success(
NetworkResponse.UnknownError(null)
))
}
}else{
val errorBody = when{
error == null -> null
error.contentLength() == 0L -> null
else ->try{
errorConverter.convert(error)
}catch (e:Exception) {
null
}
}
if(errorBody != null){
callback.onResponse(this@NetworkResponseCall, Response.success(
NetworkResponse.ApiError(errorBody, code)
))
}else{
callback.onResponse(this@NetworkResponseCall, Response.success(
NetworkResponse.UnknownError(null)
))
}
}
}
override fun onFailure(call: Call<S>, t: Throwable) {
val networkResponse = when(t){
is IOException -> NetworkResponse.NetworkError(t)
else-> NetworkResponse.UnknownError(t)
}
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
}
})
}
override fun isExecuted(): Boolean =delegate.isExecuted
override fun cancel(): Unit =delegate.cancel()
override fun isCanceled(): Boolean =delegate.isCanceled
override fun request(): Request =delegate.request()
override fun timeout(): Timeout =delegate.timeout()
}
4. NetworkResponseAdapter 클래스
// `Type` 주석을 보면
// `common superinterface for all type in the Java'라고 나와 있다.
// 그냥 모든 타입을 포함할 수 있다고 보면 되겠다.
//CallAdapter 주석을 보면
//public interface CallAdapter<R, T>에서
//adapt a Call with response type R into the type of T라고 나와있다.
//그러니까 R을 T로 바꾼다라고 보면 된다.
//지금 이렇게 NetworkResponse~들을 만드는 이유가 요놈(NetworkResponseAdapter)을 suspend fun에서 써먹기 위함이다.
class NetworkResponseAdapter <S:Any,E:Any>(
private val successType:Type,
private val errorBodyConverter:Converter<ResponseBody,E>
):CallAdapter<S, Call<NetworkResponse<S, E>>>{
override fun responseType(): Type = successType
override fun adapt(call: Call<S>): Call<NetworkResponse<S, E>> =
NetworkResponseCall(call,errorBodyConverter)
}
5. NetworkResponseAdapterFactory 클래스
class NetworkResponseAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// suspend functions wrap the response type in `Call`
// retrofit이 suspend func을 처리할때 내부적으로 Call을 처리한다고 한다.
// 여기서도 그 처리를 해야하기 때문에 returnType이 Call인지 체크한다.
if (Call::class.java != getRawType(returnType)) {
return null
}
// check first that the return type is `ParameterizedType`
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
}
// get the response type inside the `Call` type
val responseType = getParameterUpperBound(0, returnType)
// if the response type is not Service then we can't handle this type, so we return null
if (getRawType(responseType) != NetworkResponse::class.java) {
return null
}
// the response type is Service and should be parameterized
check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }
val successBodyType = getParameterUpperBound(0, responseType)
val errorBodyType = getParameterUpperBound(1, responseType)
val errorBodyConverter =
retrofit.nextResponseBodyConverter<Any>(null, errorBodyType, annotations)
return NetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
}
}
구현해보면 각 반응에 알맞는 response가 오는것을 확인할 수 있다.
난이도가 있는 구현(이라고 느끼는)인데 이정도를 충분히 할 수 있을 정도가 되야겠다.
테스트해보세요 » github link
Leave a comment