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으로 정의하여 넘겨주기만 하면 간단하게 변경이 됩니다.
다른 새로운 확장 함수들을 작성하게 되면 덧붙여 작성하도록 하겠습니다.
더 편하게 사용할 방법, 다른 좋은 방법이 있다면 언제든 알려주시면 감사하겠습니다.
'Android' 카테고리의 다른 글
Compose에서 Naver Map 적용하기 - 2) PolyLine으로 내 경로 그리기 (0) | 2024.02.25 |
---|---|
Compose에서 Naver Map 적용하기 - 1) 위치 가져오기, 카메라 이동 (0) | 2024.02.25 |
안드로이드 서비스(Service) 알아보기 (0) | 2023.06.19 |
Paging, 안드로이드에 맞는 커서 페이징 방식으로 구현하기 (0) | 2023.06.06 |
CoroutineDispatcher.Main.immediate 알아보기 (0) | 2023.05.31 |