안드로이드/이론

[ 안드로이드 ] Handler를 통한 Viewmodel 변수 상태 체크하기

dongx._.2 2023. 3. 21. 15:12

 

기존에 안드로이드 프로젝트에 MVVM만 적용했을 땐, ViewModel을 기능 기준으로 분류를 했었다. 화면마다 ViewModel를 만들기에는 사용하는 Repository가 너무 많이 겹쳤기 때문에 클래스간 의존성이 너무 높아져서 기능별로 ViewModel-Repository 세트를 구현한다음에, 뷰에서 필요한 ViewModel을 가져다 쓰는 방식으로 개발을 진행했었다.

 

개발하면서도 위의 문제가 거슬렸었는데, 최근 클린 아키텍쳐를 공부하면서 해결 방안을 찾은 것 같다.

 

클린 아키텍쳐에선 Repository의 기능 중 필요한 기능만 Use Case로 뽑아서 쓰고, Use Case는 Repository 인터페이스를 참조하기 때문에 의존성이 높지 않다.

 

위의 방식을 이용하면 화면마다 ViewModel을 만들어도 크게 부담스럽지 않다. 오히려 화면마다 ViewModel을 만들 수 있어서 관리가 더욱 편할 듯하다.

 

ViewModel을 공용으로 쓸 때는 여러 화면에서의 상태 변수들이 하나의 ViewModel에 모여있는 문제가 있었는데, 클린 아키텍쳐를 도입하면서 이러한 문제들이 어느정도 해결이 되었다. 

 

특정 화면의 상태 변수만 관리할 수 있게 되어서 Handler를 통한 상태 체크 방법을 기록하고자 한다.

 

 

State 클래스 생성

sealed class LoginActivityState  {
    object Init : LoginActivityState()
    data class IsLoading(val isLoading: Boolean) : LoginActivityState()
    data class ShowToast(val message: String) : LoginActivityState()
    data class SuccessLogin(val loginEntity: LoginEntity) : LoginActivityState()
    data class ErrorLogin(val rawResponse: WrappedResponse<LoginResponse>) : LoginActivityState()
}

State 클래스 내에는 총 4가지 상태를 나타내는 클래스가 있다.

  1. IsLoading 클래스
    • 로딩 중임을 나타내는 클래스, 매개변수로 로딩 플래그 변수를 전달 받는다.
  2. ShowToast 클래스
    • Toast 메세지를 출력하는 클래스, 매개변수로 출력할 메세지를 전달 받는다.
  3. SuccessLogin 클래스
    • 로그인 성공을 나타내는 클래스, 로그인 정보를 매개변수로 전달 받는다.
  4. ErrorLogin 클래스
    • 로그인 실패를 나타내는 클래스, 에러에 대한 응답을 매개변수로 전달 받는다.

 

로그인을 담당하는 LoginActivity 클래스를 예시로 작성했지만, 화면의 역할에 따라 다양한 상태 클래스를 만들어 활용하면 좋을 듯하다.

 

 

상태 변수(state) 선언

@HiltViewModel
class LoginViewModel @Inject constructor(private val loginUseCase: LoginUseCase): ViewModel() {
    private val state = MutableStateFlow<LoginActivityState>(LoginActivityState.Init)
    val mState: StateFlow<LoginActivityState> get() = state
    
    /.../
}

LoginActivity의 State를 관리할 변수를 생성한다. StateFlow로 선언하여 Observing이 가능하도록 한다. (Handler가 해당 변수를 관찰하며 상태값에 따른 처리를 할 예정)

 

    private fun setLoading(){
        state.value = LoginActivityState.IsLoading(true)
    }

    private fun hideLoading(){
        state.value = LoginActivityState.IsLoading(false)
    }

    private fun showToast(message: String){
        state.value = LoginActivityState.ShowToast(message)
    }

상태에 따른 동작을 행할 메소드를 지정해준다. 로그인 성공, 에러에 대한 처리는 login API 요청 응답을 받으면서 처리하면 되기 때문에 Loading과 Toast 메세지에 대한 메소드만 작성해준다.

 

 

요청 결과에 따른 상태 값 세팅

    fun login(loginRequest: LoginRequest){
        viewModelScope.launch {
            loginUseCase.execute(loginRequest)
                .onStart {
                    setLoading()
                }
                .catch { exception ->
                    hideLoading()
                    showToast(exception.message.toString())
                }
                .collect { baseResult ->
                    hideLoading()
                    when(baseResult){
                        is BaseResult.Error -> state.value = LoginActivityState.ErrorLogin(baseResult.rawResponse)
                        is BaseResult.Success -> state.value = LoginActivityState.SuccessLogin(baseResult.data)
                    }
                }
        }
    }

UseCase로 요청을 보냄과 동시에 로딩 상태로 전환한다.

에러가 생기면 로딩을 중지하고 토스트 메세지로 에러를 출력한다.

 

요청이 완료되면 응답 결과에 따라 상태를 지정한다.

 

ViewModel 전체 코드

더보기
@HiltViewModel
class LoginViewModel @Inject constructor(private val loginUseCase: LoginUseCase): ViewModel() {
    private val state = MutableStateFlow<LoginActivityState>(LoginActivityState.Init)
    val mState: StateFlow<LoginActivityState> get() = state


    private fun setLoading(){
        state.value = LoginActivityState.IsLoading(true)
    }

    private fun hideLoading(){
        state.value = LoginActivityState.IsLoading(false)
    }

    private fun showToast(message: String){
        state.value = LoginActivityState.ShowToast(message)
    }


    fun login(loginRequest: LoginRequest){
        viewModelScope.launch {
            loginUseCase.execute(loginRequest)
                .onStart {
                    setLoading()
                }
                .catch { exception ->
                    hideLoading()
                    showToast(exception.message.toString())
                }
                .collect { baseResult ->
                    hideLoading()
                    when(baseResult){
                        is BaseResult.Error -> state.value = LoginActivityState.ErrorLogin(baseResult.rawResponse)
                        is BaseResult.Success -> state.value = LoginActivityState.SuccessLogin(baseResult.data)
                    }
                }
        }
    }



}

sealed class LoginActivityState  {
    object Init : LoginActivityState()
    data class IsLoading(val isLoading: Boolean) : LoginActivityState()
    data class ShowToast(val message: String) : LoginActivityState()
    data class SuccessLogin(val loginEntity: LoginEntity) : LoginActivityState()
    data class ErrorLogin(val rawResponse: WrappedResponse<LoginResponse>) : LoginActivityState()
}

 

Handler 선언

State의 값에 따라 행동을 정할 Handler 메소드를 선언한다.

    private fun handleStateChange(state: LoginActivityState){
        when(state){
            is LoginActivityState.Init -> Unit
            is LoginActivityState.ErrorLogin -> handleErrorLogin(state.rawResponse)
            is LoginActivityState.SuccessLogin -> handleSuccessLogin(state.loginEntity)
            is LoginActivityState.ShowToast -> showToast(state.message)
            is LoginActivityState.IsLoading -> handleLoading(state.isLoading)
        }
    }

 

각 상태마다 실행할 메소드를 선언한다.

    private fun handleErrorLogin(response: WrappedResponse<LoginResponse>){
        showGenericAlertDialog(response.message)
    }

    private fun handleLoading(isLoading: Boolean){
        binding.loginButton.isEnabled = !isLoading
        binding.registerButton.isEnabled = !isLoading
        binding.loadingProgressBar.isIndeterminate = isLoading
        if(!isLoading){
            binding.loadingProgressBar.progress = 0
        }
    }

    private fun handleSuccessLogin(loginEntity: LoginEntity){
        sharedPrefs.saveToken(loginEntity.token)
        showToast("Welcome ${loginEntity.name}")
        goToMainActivity()
    }

 

토스트 메세지를 출력하는 showToast 메소드는 다른 화면에서도 공통으로 많이 쓰므로 Common 디렉토리에 확장 함수로 선언한다.

fun Context.showToast(message: String){
    Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}

 

Handler 옵저빙

viewModel 내의 mState 변수에 observer로 handleStateChange 메소드를 등록한다.

    private fun observe(){
        viewModel.mState
            .flowWithLifecycle(lifecycle,  Lifecycle.State.STARTED)
            .onEach { state -> handleStateChange(state) }
            .launchIn(lifecycleScope)
    }

 

이런 식으로 작성하면 상태에 따른 다양한 처리를 해줄 수 있다. Boolean 변수로 성공/실패만 체크하는 방법보다 훨씬 가독성도 좋고 편리한 듯하다.