Android

편하게 사용하고자 만든 Extensions

쎄오SseO 2023. 10. 4. 19:34

 

 

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으로 정의하여 넘겨주기만 하면 간단하게 변경이 됩니다.

 

 


 

 

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

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