Skip to content

CallAdapter 마이그레이션

BuNa edited this page Sep 13, 2023 · 6 revisions

CallAdapter 마이그레이션

CallAdapter 적용에 따라, 프로젝트 반영을 위해 부나에 의해 작성되었습니다.

4가지 응답 상태

기존 handleApi { ... }를 CallAdapter로 마이그레이션 하였습니다. 그에 따라 코드를 간략하게 소개합니다.

CallAdapter로 마이그레이션하면서 총 4개상태가 생겼습니다.

// 200대 응답 코드를 받은 경우
data class Success<T : Any>(val data: T) : ApiResponse<T>()
// 200대 응답 코드가 아닌 경우, 서버와의 통신은 성공함
data class Failure(val responseCode: Int, val message: String?) : ApiResponse<Nothing>()
// 서버와의 통신조차 하지 못한 경우
object NetworkError : ApiResponse<Nothing>()
// 그 외 기타 오류
data class Unexpected(val error: Throwable?) : ApiResponse<Nothing>()

ApiResponse

위 4가지 상태는 ApiResonse를 상속받고 있습니다. ApiResponse는 ApiModel(Response) -> Data Model로 쉽게 변환할 수 있도록 map {...} 메서드를 제공합니다.

sealed class ApiResponse<out T : Any> {
    fun <R : Any> map(transform: (T) -> R): ApiResponse<R> = when (this) {
        is Success -> Success(transform(data))
        is Failure -> Failure(responseCode, message)
        is NetworkError -> NetworkError
        is Unexpected -> Unexpected(error)
    }
}

handleApi {...} 에서 CallAdapter로 마이그레이션 방법!

기존 코드를 마이그레이션 하는 방법을 소개합니다.

  1. Service interface 반환 타입 변경
// Before
interface ActivityService {
    @GET("/activities")
    suspend fun getActivities(): Response<List<ActivitiesApiModel>>
}

// After
interface ActivityService {
    @GET("/activities")
    suspend fun getActivities(): ApiResponse<List<ActivitiesApiModel>>
}
  1. Repository 반환 타입 변경 및 handleApi {...} 함수 제거
// Before
override suspend fun getActivities(): ApiResult<List<Activity>> = withContext(dispatcher) {
    handleApi(
        execute = { activityService.getActivities() },
        mapToDomain = List<ActivitiesApiModel>::toData,
    )
}

// After
override suspend fun getActivities(): ApiResponse<List<Activity>> = withContext(dispatcher) {
    activityService
        .getActivities()
        .map(List<ActivitiesApiModel>::toData)
}

handleApi에 lambda를 2개 넘겨줘서 가독성을 낮추는 문제 해결

  1. ViewModel의 분기 처리
// Before - 분기 처리 모호함
private fun fetchActivities(): Job = viewModelScope.launch {
    when (val result = activityRepository.getActivities()) {
        is ApiSuccess -> _activities.postValue(OnboardingUiState.from(activitiesResult.data))
        is ApiError, is ApiException ->
            _activities.postValue(activities.value.copy(isLoadingActivitiesFailed = true))
    }
}
// After
private fun fetchActivities(): Job = viewModelScope.launch {
    when (val result = activityRepository.getActivities()) {
        is Success -> _activities.postValue(OnboardingUiState.from(result.data))
        is NetworkError -> updateToNetworkErrorState()
        is Unexpected -> updateToUnknownErrorState()
        is Failure -> when (result.responseCode) {
            401 -> // 로그인 화면으로 이동
            else -> // updateToUnknownErrorState()
        }
    }
}

NetworkError와 Unexpected를 분리하여 인터넷 연결 상태에 대한 분기 처리를 확실히 할 수 있게 되었습니다.

+) 인터넷 연결 상태 불량과 같은 모든 ViewModel에서 공통으로 사용하는 UiState와 전이 함수는 BaseViewModel로 분리하면 지금보다도 보일러 플레이트 코드를 확실히 줄일 수 있을 것으로 예상됩니다.