본문 바로가기
Android

편하게 사용하고자 만든 Extensions

by 쎄오SseO 2023. 10. 4.

 

 

Android 프로젝트 중,

Clean Architecture, Flow를 사용하며

편리하게 사용하고자 만든 확장 함수들에 대해서 소개드리려고 합니다.

 

 

 

1. ResultType 확장 함수

 

sealed class ResultType<out T> {
    object Uninitialized : ResultType<Nothing>()

    object Loading : ResultType<Nothing>()

    object Empty : ResultType<Nothing>()

    data class Success<T>(val data: T) : ResultType<T>()

    data class Fail<T>(val data: T) : ResultType<T>()

    data class Error(val exception: Throwable) : ResultType<Nothing>() {
        val isNetworkError = exception is IOException
    }

}

API Response에 따른 응답을 성공, 실패, 에러를 관리해주기 위해 ResultType을 만들어서 사용했습니다.

 

 

viewModelScope.launch(Dispatchers.IO) {
      joinUseCase(user).collectLatest {
           when (it) {
               is ResultType.Success -> {
                   _joinEventFlow.emit(Event.Success("회원가입 완료"))
               }

               is ResultType.Fail -> {
                   _joinEventFlow.emit(Event.Fail(it.data.message))
               }

               is ResultType.Error -> {
                   Log.d("joinError", "${it.exception.message} ")
               }
               else -> {

              }
          }
     }
}

하지만, 위의 코드처럼 항상 ViewModel에서 Api 요청을 할 때마다 응답 값을 분기 처리 해주기 위해 when을 사용하여 작성해주어야 했습니다.

 

 

Extension Code

inline fun <T> ResultType<T>.onSuccess(
    action: (value: T) -> Unit
): ResultType<T> {
    if(this is ResultType.Success) action(this.data)
    return this
}

inline fun <T> ResultType<T>.onFailure(
    action: (value: T) -> Unit
): ResultType<T> {
    if(this is ResultType.Fail) action(this.data)
    return this
}

inline fun <T> ResultType<T>.onError(
    action: (value: Throwable) -> Unit
) {
    if(this is ResultType.Error) action(this.exception)
    return
}

이를 간결하게 작성하기 위해 위와 같은 Extension을 사용했습니다.

 

 

viewModelScope.launch(Dispatchers.IO) {
       joinUseCase(user).collectLatest {
           it.onSuccess {
               _joinEventFlow.emit(Event.Success("회원가입 완료"))
           }.onFailure {
               _joinEventFlow.emit(Event.Fail(it.message))
           }.onError {
               Log.d("joinError", "${it.message} ")
           }

       }
}

변경된 코드

 

 

 

2. Flow emit Extensions

 

override fun join(user: User) Flow<ResultType<String>> = flow {
    emit(ResultType.Loading)

    userRemoteDataSource.join(user).collect {
        // 로직 성공 시
        emit(ResultType.Success(it))

        // 로직 실패 시
        emit(ResultType.Fail(it))
    }

}.catch {
        emit(ResultType.Error(it.cause!!))
}

Api 작성 시 RepositoryImpl에서는 항상 이와 같은 구조로 사용을 했습니다.

이를 더 간결하게 작성하고자 고민하다가 만든 확장 함수가 아래 코드입니다.

 

 

Extension Code

suspend fun <T> FlowCollector<ResultType<T>>.emitResultTypeError(throwable: Throwable) {
    this.emit(ResultType.Error(throwable))
}

suspend fun FlowCollector<ResultType.Loading>.emitResultTypeLoading() {
    this.emit(ResultType.Loading)
}

suspend fun <T> FlowCollector<ResultType<T>>.emitResultTypeSuccess(data: T) {
    this.emit(ResultType.Success(data))
}

suspend fun <T> FlowCollector<ResultType<T>>.emitResultTypeFail(data: T) {
    this.emit(ResultType.Fail(data))
}

emit(ResultType … )을 더 간결하게 만들어주고자 했습니다.

 

 

 

override fun join(user: User) Flow<ResultType<String>> = flow {
    emitResultTypeLoading()

    userRemoteDataSource.join(user).collect {
        // 로직 성공 시
        emitResultTypeSuccess(it)

        // 로직 실패 시
        emitResultTypeFail(it)
    }

}.catch {
        emitResultTypeError(it.cause!!)
}

변경된 코드

 

 

 

3. Flow collect asResult ***

 

2번에서 작성한 emit 확장 함수가 조금이나마 더 편하게 만들어주었지만 완전하게 보일러 플레이트를 제거하고 크게 생산성이 향상되지는 않았습니다.

이 구조를 더 간편하게 만들어주고 싶어서 좀 더 고민을 했고 asResult() 함수를 만들게 되었습니다.

 

 

Extension Code

inline fun <T> Flow<T>.asResult(crossinline action: suspend (value: T) -> ResultType<T>): Flow<ResultType<T>> 
= this.transform {
    emit(action(it))
}.onStart {
    emitResultTypeLoading()
}.catch {
    emitResultTypeError(it)
}

기존에 위와 같이 flow 빌더를 만들어 준 뒤 내부에서 emit(Loading)을 해주고, collect를 하고, catch를 통해 emit(Error)를 하는 이 구조를 더 간결하게 만들어주고자 했고 위와 같이 만들게 되었습니다.

 

 

override fun join(user: User) Flow<ResultType<String>> = flow {
    emitResultTypeLoading()

    userRemoteDataSource.join(user).collect {
        // 로직 성공 시
        if(!it.isEmpty(){
            emitResultTypeSuccess(it)
        }
        // 로직 실패 시
        else {
            emitResultTypeFail(it)
        }
    }

}.catch {
        emitResultTypeError(it.cause!!)
}

기존 코드

 

 

override fun join(user: User) Flow<ResultType<String>> = 
    userRemoteDataSource.join(user).asResult {
            it(!it.isEmpty(){
                ResultType.Success(it)
            } else {
                ResultType.Fail("")
            }
    }

변경된 코드

 

 

asResult에 넘길 람다 함수에서 return 값을 ResultType으로 정의하여 넘겨주기만 하면 간단하게 변경이 됩니다.

 

 


 

 

다른 새로운 확장 함수들을 작성하게 되면 덧붙여 작성하도록 하겠습니다.

더 편하게 사용할 방법, 다른 좋은 방법이 있다면 언제든 알려주시면 감사하겠습니다.