-
[번역] Retrofit과 Coroutine으로 safeApiCalls 만들기Programming/Android 2022. 1. 20. 20:06
https://dev.to/eagskunst/making-safe-api-calls-with-retrofit-and-coroutines-1121
Making safe API calls with Retrofit and Coroutines
After the 2.6.0 release, Retrofit has official support for coroutines. The migration from other adapt...
dev.to
이 포스트는 위 아티클(원문)을 번역한 글입니다.
일부 오역이 있을 수 있습니다.
2.6.0 릴리즈 이후, Retrofit은 coroutine을 공식적으로 지원합니다. 다른 어댑터( ex. RxJava)로부터 마이그레이션 하는 것이 쉬워졌습니다.
coroutine 적용 전 :
@GET("movie/{category}") fun getMovieListForCategory(@Path("category") category: String) : Single<MovieListResponse>
coroutine 적용 후 :
@GET("movie/{category}") suspend fun getMovieListForCategory(@Path("category") category: String) : MovieListResponse
번경한 interface를 CoroutineContext 내부에서 호출하면 됩니다.
obejct를 감싼 wraaper 대신에 object를 직접 받고 있습니다. 이 방법은 꽤 유용하지만, 오류를 판별하는 방법을 제공하지 않는다는 단점이 있습니다.
물론 try-catch를 써도 됩니다만, 모든 single call에 try-catch를 다는 것은 최적의 방법이 아니고, 장기적으로 보았을때 나쁜 방법입니다. 또, 어느 누가 모든 request 에 try-catch를 달길 원할까요 ?
그래서 제가 제 작업과 개인 프로젝트를 위해 만든 솔루션을 여러분들께 보여드리려고 합니다.
먼저, 우리가 가진 문제를 정의해봅시다.
1. 우리는 안전하게 호출 해야 하므로 모든 call은 try-catch 가 적용되어야합니다.
2. 우리는 모든 call이 잘 작동할 수 있는 것이 필요합니다.
3.우리는 call 이 실패할 경우를 대비하여 무엇인가 반환해야합니다.
4. 우리는 호출이 실패하면 어떻게든 오류나 메세지를 내보내야합니다.
먼저 처음 두 문제의 경우, call을 핸들링 하기 위한 static class나 abstract class를 만들 수 있습니다. 하지만 어떤 값이 리턴되는지 어떻게 알까요 ? 모든 object가 다르고, 각각에 맞는 방법을 찾아야하기 때문에 generic function를 사용할 수 있습니다.
suspend fun <T> safeApiCall() : T
이제 우리의 함수가 어떤 건지 알게 되었습니다.
우리가 직면한 문제는 모든 인터페이스에서 작동하도록 하는 방법이 무엇인지 입니다.
많은 생각을 하고 ,Retrofit 저장소와 아티클을 읽고 난 뒤, 강력한 코틀린의 고차 함수가 떠올랐습니다. 파라미터로 함수를 전달하면 그 솔루션이 모든 인터페이스에서 작동합니다.
suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T
자 이런 모습이 되었네요.
이제 try-catch로 responseFunction를 감싸고, 그것을 return 하기만 하면 됩니다. 하지만 우리는 이제 세번째 문제를 직면합니다. 이건 간단합니다. non-nullable object 대신에 nullable object를 리턴하면 됩니다.
suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T?{ return try{ responseFunction.invoke()//Or responseFunction() }catch (e: Exception){ e.printStackTrace() null } }
완성 ! 우리는 모든 call 에 안전하게 핸들링 할 수 있는 함수를 만들었어요. 이제 우리가 할 수 있는 최적화와, 에러를 다른 class들에게 알리는 방법을 살펴보겠습니다.
우리는 세 종류의 에러를 핸들링할겁니다.
HttpExceptions, SocketTimeoutException 그리고 IOException
이를 위해 핸들링하기 쉽도록 enum 클래스를 만들겁니다.
Int 형을 사용해도 상관없어요
enum class ErrorType { NETWORK, // IO TIMEOUT, // Socket UNKNOWN //Anything else }
HttpException의 경우 API에서 오류 메세지를 가져오는 함수를 만들어보겠습니다. 여기서 오류 응답을 위한 API와 오류 코드를 사용하는 JSON 객체의 종류를 확인해야합니다.
companion object { private const val MESSAGE_KEY = "message" private const val ERROR_KEY = "error" } fun getErrorMessage(responseBody: ResponseBody?): String { return try { val jsonObject = JSONObject(responseBody!!.string()) when { jsonObject.has(MESSAGE_KEY) -> jsonObject.getString(MESSAGE_KEY) jsonObject.has(ERROR_KEY) -> jsonObject.getString(ERROR_KEY) else -> "Something wrong happened" } } catch (e: Exception) { "Something wrong happened" } }
이제 우리는 오류를 알리는 역할을 하는 클래스에 interface를 implement 할거에요
interface RemoteErrorEmitter { fun onError(msg: String) fun onError(errorType: ErrorType) }
마지막으로 우리의 try-catch문을 수정해봅시다.
최적화를 위해서 reponseFunction을 호출 하고 catch block에서 context가 Dispatcher.Main으로 변경될 때 coroutine context를 Dispatcher.IO로 변경합니다
class MovieListViewModel(api: MovieListApi): ViewModel(), RemoteErrorEmitter { val movieListLiveData = liveData { val response = ApiCallsHandler.safeApiCall(this@MovieListViewModel){ api.getMovieListForCategory("popular") } emit(response) } //RemoteErrorEmitter implementation... }
. UI thread 수정시에 예외가 없도록 main dispatcher를 사용합니다.
suspend inline fun <T> safeApiCall(emitter: RemoteErrorEmitter, crossinline responseFunction: suspend () -> T): T? { return try{ val response = withContext(Dispatchers.IO){ responseFunction.invoke() } response }catch (e: Exception){ withContext(Dispatchers.Main){ e.printStackTrace() Log.e("ApiCalls", "Call error: ${e.localizedMessage}", e.cause) when(e){ is HttpException -> { val body = e.response()?.errorBody() emitter.onError(getErrorMessage(body)) } is SocketTimeoutException -> emitter.onError(ErrorType.TIMEOUT) is IOException -> emitter.onError(ErrorType.NETWORK) else -> emitter.onError(ErrorType.UNKNOWN) } } null } }
이제 진짜 끝났어요 ! 이제 coroutine을 적용한 Http call 에서 발생한 예외를 처리할 수 있는 클래스가 생겼습니다.
다음은 viewModel에 적용한 예제입니다.
class MovieListViewModel(api: MovieListApi): ViewModel(), RemoteErrorEmitter { val movieListLiveData = liveData { val response = ApiCallsHandler.safeApiCall(this@MovieListViewModel){ api.getMovieListForCategory("popular") } emit(response) } //RemoteErrorEmitter implementation... }
더 많은 예제는 이곳에서 확인할 수 있습니다.
https://github.com/eagskunst/MoviesApp
GitHub - eagskunst/MoviesApp: An application that show a list of categories, a list of movies,the details of the movie and let t
An application that show a list of categories, a list of movies,the details of the movie and let the user save it on his own list. - GitHub - eagskunst/MoviesApp: An application that show a list of...
github.com
'Programming > Android' 카테고리의 다른 글
런치박스 개발일지 : 기획 / 기술스택 / 앱 이름 선정 (0) 2022.03.22 기존 MVC 프로젝트, MVVM 패턴으로 리팩토링 해보기 (0) 2022.03.22 [Android] PhoneNumberFormattingTextWatcher 적용 안되는 이유 (0) 2021.10.22 ViewModelProvider Error 발생 (0) 2020.09.13 (TIL Android) RecyclerView에 구분선 지울려다가 RecyclerView 파고들기 (0) 2020.06.30