Grusie 안드로이드 개발 기술 블로그

[Android] UiState, EventState 관리하기(State, Shared Flow) 본문

안드로이드 개발/코틀린

[Android] UiState, EventState 관리하기(State, Shared Flow)

grusie 2024. 3. 29. 15:04
728x90
반응형
SMALL

안드로이드 상태를 관리하는 것은 Ui와 Event로 나눌 수 있다.

필자는 기존에 UiState로만 상태 관리를 하였으나, StateFlow나 LiveData 같은 경우 동일한 State를 방출했을 때, 변화를 감지하지 않기 때문에 문제가 발생하게 되었다.(같은 이벤트 처리)

어떻게 해결하면 좋을지 찾아보던 중, Shared Flow를 사용해 EventState를 분리하여 관리하게 되면 문제가 해결 된다는 것을 알게 되었다.

 

기존 처리 방식

- LiveData 혹은 SateFlow사용하여 데이터가 변경되는 값을 감지하고 그에 맞게 뷰를 변경해준다.

 

LiveData를 사용하지 않는 이유

- LiveData는 안드로이드 라이프사이클에 맞게 상태를 관찰 할 수 있다는 장점이 있으나, 클린아키텍처 구조와 같이 라이프사이클에 의존하지 않는 구조로 가게 되었을 경우 사용하지 못한다는 단점이 있다.

그렇기에, 코틀린의 Flow를 활용해서 데이터를 처리해야 한다.

 

SharedFlow 특징

StateFlow와 다르게 Event는 기본값이 없고, 여러 번 같은 이벤트가 발생해도 계속 발생해야하기 때문에 SharedFlow를 사용 하는 것이 좋다.

 

1. 소비자의 구독 이후 발생한 이벤트만 전달해준다.

2. 중복된 값이 방출되어도 모두 collect 한다.

 

StateFlow는 새로운 상태가 입력된 경우에만 소비자가 수집 할 수 있다. 하지만, SharedFlow의 경우 중복된 이벤트가 발생해도 모두 Collect한다.

이러한 특징 때문에 UiState는 StateFlow가, EventState는 SharedFlow가 관리하는 것이 좋다.

 

State분리

sealed class SignUpUiState{
    object Empty : SignUpUiState()
    object Loading : SignUpUiState()
    object SuccessSendEmail : SignUpUiState()
    object SuccessSignUp : SignUpUiState()
    data class Alert(val alert: Int) : SignUpUiState()
    data class Error(val error: Exception) : SignUpUiState()
}

기존 회원가입 화면의 UiState이다.

이 부분에서 Alert와 Error 부분을 EventState로 이동하려고 한다.

sealed class SignUpEventState {
    object Empty : SignUpEventState()
    data class Alert(val alert: Int) : SignUpEventState()
    data class Error(val error: Exception) : SignUpEventState()
}

초기값을 위한 Empty object와, 다른 이벤트들에 해당하는 Alert, Error상태(데이터클래스)를 만들어 주었다.

 

SignUpViewModel

private val _signUpUiState = MutableStateFlow<SignUpUiState>(SignUpUiState.Empty)
val signUpUiState: StateFlow<SignUpUiState> = _signUpUiState

private val _signUpEventState = MutableSharedFlow<SignUpEventState>()
val signUpEventState: SharedFlow<SignUpEventState> = _signUpEventState

...
fun signUpEmail(){
	...
	_verifyChecked.value != true -> {   //이메일 인증이 되지 않았을 때
		setSignUpEventState(SignUpEventState.Alert(Constant.ERROR_EMAIL_UNVERIFIED))
	}
	...
}

uiState와 EventState 각각 변수를 만들어주고, 이벤트가 발생하면 emit해준다.

 

SignUpScreen

var alertCode: Int? by remember { mutableStateOf(null) }
var errorCode: Exception? by remember { mutableStateOf(null) }

LaunchedEffect(Unit) {
    viewModel.signUpEventState.collect { signUpEventStateValue ->
        when (signUpEventStateValue) {
            is SignUpEventState.Alert -> {
                alertCode = signUpEventStateValue.alert
            }

            is SignUpEventState.Error -> {
                errorCode = signUpEventStateValue.error
            }

            else -> {

            }
        }
    }
}

뷰에서는 SharedFlow를 Collect하여 사용한다.

컴포즈에서는 변경값이 달라지지 않으면 리컴포즈하지 않기 때문에, LaunchedEffect에서 Collect를 해주며, 원하는 코드가 null인지 아닌지에 따라서 뷰를 띄워주도록 진행하기로 했다.

if (errorCode != null) {
    SingleAlertDialog(
        confirm = false,
        title = stringResource(id = R.string.str_title_error),
        content = TextUtils.getErrorMsg(context, error = errorCode!!),
        onDismissRequest = { errorCode = null }
    )
}
if (alertCode != null) {
    SingleAlertDialog(
        title = stringResource(id = R.string.str_title_error),
        content = TextUtils.getAlertMsg(context, alertCode!!),
        onDismissRequest = { alertCode = null }
    )
}

각각에 해당하는 코드가 null이 아닐 경우, 그에 해당하는 동작을 하도록 구현해뒀다.

 

uiState, eventState 처리

 

이제 같은 에러를 여러번 띄우는 게 가능해졌다. 

 

 

후기

기존 xml 방식의 View에서는 이벤트를 받아서 show hide를 할 수 있었으나, compose에서는 같은 이벤트를 받았을 때 recomposable을 하지 않는다는 것을 간과하여 시간이 오래 걸렸던 것 같다.

더 좋은 방법이 있을 수 있으나, 생각 나는 방법은 이것 뿐이였다.

아무튼 SharedFlow를 활용한 이벤트 처리라는 것을 배워서 좋다. 사내 프로젝트에 적용해 보아야겠다.

반응형
LIST