[안드로이드]이벤트 처리 결과로 UI 변경하기

Lee Yongin·2022년 3월 21일
1

안드로이드

목록 보기
5/23
post-custom-banner

1.서론

로그인 인증이나 아이디&닉네임 중복확인에 실패했음을 시각적으로 알려주기 위한 기능을 만드려고 한다.

ViewModel에서 로그인이나 중복확인에 실패 처리를 할 때, 실패의 결과로 UI에서 사용자에게 알림을 줘야 한다는 뜻이다.

2.본론

1.먼저 생각해 본 것

1.LiveData가 필요하다

MVVM 구조에서 View인 Activity는 ViewModel을 알지만, ViewModel은 View를 알면 안 된다.
ViewModel에서 인증결과 및 여부를 LiveData를 통해 Activity가 UI를 바꾸도록 하면 된다.

2.MutableLiveData 값 업데이트는 어떤 메소드로 해야할까

LiveData를 보고 있다가 UI 업데이트를 하는 거니까 메인스레드를 쓸 건데, 그러면 MutableLiveData 값 업데이트는 어떤 방식을 써야 괜찮을까..쓸데없이 디테일한가 싶기도 하지만 고민해보았다.
MutipleLiveData의 값을 바꿀 때 setValue(), postValue()를 사용하는데...이 둘은 스레드 부분에서 차이가 있다.

setValue()

Sets the value. If there are active observers, the value will be dispatched to them.T his method must be called from the main thread. If you need set a value from a background thread, you can use postValue

setValue()로 값을 업데이트하면, 이 메소드 자체가 메인스레드에서 호출이 되기 때문에 바로 반영된다.

postValue()

liveData.postValue("a");
liveData.setValue("b");
The value "b" would be set at first and later the main thread would override it with the value "a".
If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.

하지만 postValue()는 I/O 스케쥴러를 활용하고 마지막에 메인스레드에서 값을 전달하기 때문에 값의 반영이 약간 느리다.
Android Developers의 예시를 보면, liveData의 값은 먼저 b가 되었다가 a로 업데이트가 되는 것이다.(아무튼 두 가지 메소드 모두 메인스레드를 사용한다.)

로그인 인증과 이메일&닉네임 중복확인은 모두 서버api를 사용하는 네트워크 액세스가 필요하다. 따라서 메인스레드에서 처리를 맡기기에는 ANR(Android Not Responding)에러 가능성이 있을 것 같아서 postValue()를 사용하기로 했다. (버퍼링 아이콘을 달 정도로 오래 걸리진 않겠지...?)

3.Event Wrapper를 사용하자

단순하게 View가 ViewModel의 Livedata를 옵저빙하는 방식으로는 에러가 발생한다고 한다.
이미 서버로부터 이메일 중복확인에 실패해서 경고 UI를 받았는데, 잠깐 앱을 나갔다 돌아온 상황을 가정해보자.
위의 그림을 참고하면, 뷰모델 생명주기는 유지되지만 액티비티가 onStop()상태가 된다. 그 액티비티에 연결된 라이프사이클을 인식하는 구성요소가 ON_STOP이벤트를 수신한다. 그 중엔 Observer도 있기 때문에 사용자가 다시 앱을 재개하면 또 LiveData를 옵저빙하고 경고 UI를 보여줘야 하는 불필요한 경우가 생길 수 있다. (Observer 객체는 비활성에서 활성으로 변할 때 최근 값을 수신하는 특성이 있기 때문인 것 같다.)
결론은 생명주기에 오락가락하지 않는 1회성 이벤트 전달용 처리과정, Event Wrapper가 필요하다

내부에 이벤트 처리를 표시하는 hasBeenHandled가 1회성 이벤트의 핵심기능이다. 액티비티가 활성->비활성->활성->비활성 으로 변할 때마다 LiveData가 최근데이터를 수신하려고 해도, LieveData의 값이 Event Wrapper 안에 있으므로 한 번 이벤트 처리가 되면 null을 반환한다. 따라서 값 변화가 없는 옛날 데이터로 위장(?)하고 불필요한 UI업데이트를 반복하지 않는다.
Event.kt

class Event<out T>(private val content:T) {
    var hasBeenHandled = false

    fun getContentIfNotHandled():T?{
        return if(hasBeenHandled){ //이벤트가 이미 처리된 상태
            null
        } else {
            hasBeenHandled = true //이벤트 처리 표시하기
            content //값 반환
        }
    }
    /**
     * 이벤트의 처리 여부에 상관 없이 값을 반환
     */
    fun peekContent():T = content
}

3.결론

구현하려는 기능이 총 3가지였지만 결은 비슷했다.
로그인 인증 성공&실패 -> UI 업데이트
이메일, 닉네임 중복확인 성공&실패 -> UI업데이트

모두 위와 같은 패턴으로 동일해서 이메일 중복확인 기능만 보여주고 싶다. 밑의 코드를 보면 repository의 데이터콜백 인터페이스를 통해 이메일중복확인에 실패했는지, 성공했는지(성공했다면 중복되는지 안되는지)를 구현했다. 중복확인에 성공한 경우 MutableLiveData의 postValue() 메소드로 Event Wrapper를 사용해 중복여부를 표시해서 View의 Observer가 관찰가능하게 했다.

SignupViewModel.kt

fun checkUserEmail(emailText:String){
        repo.checkUserEmail(emailText,object :SgRepository.GetDataCallback<Boolean>{
            override fun onSuccess(data: Boolean?) {
                if (data != null) {
                    //LiveData로 액티비티에 성공신호 제공
                        if(data == true) _emailOk.postValue(Event("emailOk"))
                    else if(data == false) _emailOk.postValue(Event("emailFail"))
                }
            }

            override fun onFailure(throwable: Throwable) {
                //실패
                Log.d("SignupVM.checkUserEmail","onFailure")
            }
        })
    }

ViewModel은 MutableLiveData의 값을 변경했지만 View는 LiveData를 통해 읽기전용으로 관찰 가능했었다. Event Wrapper로 전달되는 최신데이터의 값이 "emailOk"인가, "emailFail"인가에 따라 다른 UI업데이트를 실행하도록 구현되었다.
SignupActivity.kt

//이메일 중복확인 결과
        viewmodel.emailOk.observe(this@SignupActivity, androidx.lifecycle.Observer {
            it.getContentIfNotHandled()?.let {
                if(it == "emailOk"){
                    //성공
                    binding.apply {
                        emailCheckbtn.setBackgroundResource(R.drawable.signup_confirmbtn2)
                        emailCheckbtn.isEnabled = false
                        emailOk.visibility = View.VISIBLE
                        emailFail.visibility = View.INVISIBLE
                    }
                }
                else if(it == "emailFail"){
                    //실패
                    binding.apply {
                        emailCheckbtn.setBackgroundResource(R.drawable.signup_confirmbtn)
                        emailCheckbtn.isEnabled = true
                        emailOk.visibility = View.INVISIBLE
                        emailFail.visibility = View.VISIBLE
                    }
                }
            }
        })

그 외 코드들은 밑의 깃허브에서 작업하고 있습니다. 학부생이라 부족한 점이 많아서 틀린 부분 댓글 달아주세요!
https://github.com/LeeYongIn0517/Bangu_android

그림 및 자료 출처
LiveData 관련
https://thdev.tech/android/2021/02/01/LiveData-Intro/
Event Wrapper관련
https://seunghyun.in/android/6/
https://leveloper.tistory.com/200
뷰모델, 액티비티 생명주기 관련
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko

profile
⚡개발자할거야 응애 안드로이드 개발자 ⚡p.s.기록만이 살길이다!
post-custom-banner

0개의 댓글