[Android/Kotlin] mvvm databinding으로 이벤트처리를 위한 Event Wrapper

Falco·2022년 5월 17일
0

Android

목록 보기
12/55
post-custom-banner

Problem

지도에서 마커를 선택했을 때 각각 마커마다 어떻게 다른 이벤트를 연결 할 수 있을까??

EX)
ㅇㅇ병원에서 이름 클릭 : ㅇㅇ병원의 정보창으로 이동


ViewModel 구조

현재 마커를 클릭했을 때 뜨는 Info창을 따로 관리하는 ViewModel을 하나 더 생성하여 사용 중이다.

MapViewModel에서 사용한 변수

뷰모델이 이름,전화번호, 개업일, .... 좌표와 거리등의 정보를 가지고 있고
마커가 클릭 될 때 이를 업데이트 시켜주어 뷰에서 이 값들이 보이게 연동하였다.

하지만
전화, 길찾기 아이콘을 눌렀을 때 이 각각의 뷰모델 변수를 사용하여 이벤트를 처리하는데서 난황을 겪었다.

Solution

MVVM, DataBinding, Event 태그로 검색하여 여러 예제 및 샘플을 봤지만 모두 동일한 Event Wrapper 클래스를 생성하여 사용하고 있었다.

유투브 설명

Event Wrapper Class

// out T는 읽기, 리턴만 할 수 있다.
open class MapEvent<out T>(private val content: T) {
    var hasBeenHandled = false
        private set

    fun getContentIfNotHandled(): T? {
    	// 이벤트가 이미 처리 되었다면
        return if (hasBeenHandled) { 
            null // null을 반환하고,
        } else { // 그렇지 않다면
            hasBeenHandled = true // 이벤트가 처리되었다고 표시한 후에
            content // 값을 반환합니다.
        }
    }

    // 이벤트의 처리 여부에 상관 없이 값을 반환하고 싶을 때 사용
    fun peekContent(): T = content
}

MapEvent라는 클래스로 사용
하나의 이벤트에 대한 liveData를 생성 및 이 이벤트를 observing하는 View가 이벤트를 중복으로 처리하지 못하게 함

이벤트 발생시 마다 Event 객체를 만들어 LiveData에 넣어서 중복실행을 방지

ViewModel

private val _telEvent = MutableLiveData<MapEvent<String>>()
val telEvent : LiveData<MapEvent<String>> get() = _telEvent
fun onTelEvent(text : String){
	_telEvent.value = MapEvent(text)
}

Activity

mapViewModel.telEvent.observe(this) { event ->
	Log.d("MapEvent Before : ",event.hasBeenHandled.toString())
	event.getContentIfNotHandled()?.let{ telno ->
		Log.d("MapEvent Doing : ", "이벤트 진행 중")
		// startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + telno.replace("-", ""))))}
	Log.d("MapEvent After : ",event.hasBeenHandled.toString())
}

MapEvent.eventObserve 정의

@MainThread
inline fun <T> LiveData<MapEvent<T>>.eventObserve(
    owner: LifecycleOwner,
    // crossinline으로 함수 내부에서 함수형태로 오는 함수 파라미터를 쓸 수 있다.
    crossinline onChanged: (T) -> Unit
): Observer<MapEvent<T>> {
    val wrappedObserver = Observer<MapEvent<T>> { t ->
        // t.getContent.. 가 null 이 아닐때 실행됨
        t.getContentIfNotHandled()?.let {
            // 이름 없이 호출되는 함수 invoke
            onChanged.invoke(it)
        }
    }
    observe(owner, wrappedObserver)
    return wrappedObserver
}

LiveData 를 수신 객체로 하는 eventObserve() 확장 함수를 정의하여 사용이 가능하다.

확장 함수를 정의해서 사용하는 이유는 Event 클래스에서 정의한 getContentIfNotHandled() 함수를 통해서 하나의 이벤트 당 한 번의 처리를 하기 위해서이다.

Event wrapper 방식은 Event 자체가 이를 제어하기 때문에 여러개의 옵저버를 등록해도 모두 값의 변경을 받을 수 있다.


With an Event wrapper, you can add multiple observers to a single-use event

단 getContentIfNotHandled() 메서드는 하나의 옵저버에서만 사용할 수 있고, 나머지는 peekContent()로 값을 받아야 한다. -> Critical Section을 공유하는 문제와 동일

MapViewModel

private val _telEvent = MutableLiveData<MapEvent<String>>()
val telEvent : LiveData<MapEvent<String>> get() = _telEvent
fun onTelEvent(text : String){
	_telEvent.value = MapEvent(text)
}

MapActivity

// 전화 열기
mapViewModel.telEvent.eventObserve(this) { it ->
	// it == String MapEvent의 Content
	startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.replace("-", ""))))
}

In XML

클릭이 되었을때 ViewModel의 이벤트가 실행되도록 설정

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> mapViewModel.onTelEvent(mapViewModel.telno)}"
android:fontFamily="@font/font_hannaair"
android:text="@{mapViewModel.telno}"
android:textColor="@color/dpblue_80"
android:textSize="20sp"
android:textStyle="bold" 
/>

참고 사이트

GitHub Link

profile
강단있는 개발자가 되기위하여
post-custom-banner

0개의 댓글