Android view에서 ViewModel을 어떻게 호출해 사용해 볼 수 있을까?



개인 광고 영역
읽기 전에

이 글은 정승욱 님의 액티비티 : 코드 제로라는 제목의 영상 중 ViewModel에서 Click event 처리에 대한 부분을 샘플 코드로 작성하였습니다.

함께 일하면서 익힌 코드의 일부 방향성을 글에 담고 있으며, 글 작성에 대한 동의를 받았습니다.

본문

위 영상에서 언급한 내용을 포함하고 있으나, 일부 이해하지 못할 수 있으니 그냥 간단하게 이러한 방법으로도 접근이 가능하구나 정도로만 이해하시면 좋겠습니다.

이 글에서는 View에서 ViewModel을 어떻게 호출하고 접근하는 게 좋은지를 다루기 위한 글입니다.

RxJava 또는 Flow를 활용해 Click event 처리를 stream으로 받아 처리하도록 만들어 줄 수 있습니다. 대표적인 예는 JakeWharton/RxBinding의 예가 됩니다.

RxBinding처럼 Rx 스트림을 활용하는 방법을 사용하여 작성하는 코드의 샘플입니다. 이 글에서 언급하는 코드는 Flow만을 활용하였습니다.

참고로

RxJava와 Flow를 쓴다고 해서 모두 UnitTest 가능한 형태는 아닙니다.

  • RxJava의 마지막은 subscribe를 해야 동작하지만, dispose가 리턴됩니다. 이러면 이어서 테스트할 수 없습니다.
  • Flow의 마지막 역시 launchIn을 해야 동작하는데, Job이 리턴됩니다. 이 역시 테스트가 불가능합니다.

테스트가 가능한 형태로 진화하려면 subscribe, launchIn을 외부의 다른 방법을 활용해야 하는데, 이는 위 영상에서 언급하고 있으니 참고하시면 되겠습니다.


이 글에서 설명하지 않는 부분

  • Flow, RxJava에 대해서는 설명하지 않습니다.
  • View의 캐싱에 대해서는 다루지 않습니다.
  • UnitTest 가능한 형태로의 접근을 설명하지는 않습니다.


이 글에서 알아볼 내용

  • View에서 ViewModel을 호출하는 방법이 아닌 ViewModel에서 처리하는 방법을 알아봅니다.
  • ViewModel에서 처리할 때 Flow 활용하는 방법을 알아봅니다.


일반적인 접근 방법

일반적인 View에서 ViewModel을 호출하는 방법은 2가지가 있습니다.

ViewBinding을 활용해 setOnClickListener를 호출하는 방식의 코드와

binding.buttonFirst.setOnClickListener {
  // 버튼 클릭 이벤트 처리
}

DataBinding을 활용해 onClick을 바로 불러주는 방법을 사용합니다.

android:onClick="@{() -> viewModel.nextEvent()}"

가장 일반적인 접근 방법에 해당하는데, 이 방법은 코드의 강 결합이 발생하는 단점을 가지고 있습니다.


ViewAccess로 View에 접근 시키는 방법을 정의한다.

Flow로 어떻게 접근할 수 있을까요? 코드는 매우 간단합니다. 여기서는 DataBinding이 필요하기 때문에 DataBinding을 이용해 초기화한 Fragment 코드를 활용합니다.

저는 ViewAccess라는 interface를 정의하고, ViewAccessImpl에서 ViewDataBinding을 파라메터로 가지는 인터페이스를 추가하였습니다.

interface ViewAccess {
    fun find(@IdRes resourceId: Int): View
}

class ViewAccessImpl(
    private val dataBinding: ViewDataBinding
) : ViewAccess {

    /**
     * todo findViewById를 1번 만 하는 부분은 이 코드에 담아두지 않음.
     * 별도 캐싱이 필요함.
     */
    override fun find(@IdRes resourceId: Int): View =
        dataBinding.root.findViewById(resourceId)
}

todo에 적어두었지만 findViewById를 매번 진행하게 되는 부분은 별도의 캐싱이 필요한데, DataBinding을 공통으로 활용하기 위한 코드이다 보니 위와 같이 접근하고 있습니다.

사실 이 코드에서는 view를 넘겨주고, findViewById를 활용해도 됩니다.


OnClickEvent 처리

여기서는 OnClickEvent만을 다루게 됩니다. resourceId를 전달받고, ViewAccess에서 참조해 flowOf()를 이용해 View 접근 방법을 Flow stream으로 변경합니다.

interface OnClickEventControl {

    fun onClick(@IdRes resourceId: Int): Flow<Boolean>
}

당연하게도 onClick이 일어나면 Flow를 리턴하게 됩니다. 이를 ViewModel에서 활용합니다.

setOnClickListener는 어디서 등록하나요?

Flow에 보면 callbackFlow가 존재합니다. 여기서 setOnClickListener를 등록하고, 이에 대한 클릭 이벤트를 trySendBlocking()을 이용해 처리하면 됩니다.

callbackFlow에 대해서 알지 못하신다면 이전에 작성한 글을 참고하시면 됩니다.

코드는 매우 간단합니다. viewAccess로 검색한 View를 flowOf()로 감싸고, 이를 활용해 callbackFlow에서 callback 등록을 해주는 과정만 거치면 됩니다.

class OnClickEventControlImpl(
    private val viewAccess: ViewAccess
) : OnClickEventControl {

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun onClick(@IdRes resourceId: Int): Flow<Boolean> =
        flowOf(viewAccess.find(resourceId))
            .flatMapLatest { view ->
                callbackFlow {
                    view.setOnClickListener {
                        trySendBlocking(true)
                    }

                    awaitClose {
                        view.setOnClickListener(null)
                    }
                }
            }
}


ViewModel에서 활용하기

ViewModel의 load라는 함수를 만들었습니다. 한 번만 등록하도록 만들었고, 아래와 같이 viewController에서 onClick이 발생하도록 구성합니다.

onClick의 id 값을 찾고, 그 이벤트가 발생하면 _updateCount를 하거나, _clickEvent를 처리하도록 구현하였습니다.

/**
 * 이렇게 동작할 수 있음을 보여주기 위한 코드입니다.
 * 참고만 하세요.
 */
fun load(viewController: OnClickEventControl) {
    viewController.onClick(R.id.button_second)
        .onEach {
            _clickEvent.value = true
        }
        .launchIn(viewModelScope)

    /**
     * Plus 버튼 이벤트 처리
     */
    viewController.onClick(R.id.btn_plus_count)
        .onEach {
            _updateCount.value = (_updateCount.value ?: 0) + 1
        }
        .launchIn(viewModelScope)
}

일반적인 Activity나 DataBinding에서 _clickEvent를 구현하는 것이 아닌, Flow 내부에서 click 이벤트가 발생하고, 이를 스트림으로 받아 활용할 수 있도록 작성한 부분이 차이가 있습니다.


이렇게 작성하면서 얻는 장/단점

이러한 방법은 초기 구성에 많은 고민이 필요합니다. 어떤 View의 이벤트든 다 Flow/RxJava의 형태로 변경을 해야 하고, 이왕이면 이를 좀 더 활용도 좋게 만들 필요가 있습니다.

그에 따른 고민 비용이 많이 드는 대신 이후 작업에 있어서는 매우 간결하게 View와 ViewModel의 구성만 해주면 이벤트 처리가 가능해지는 형태가 됩니다.

RxJava와 Flow를 활용하기 때문에 Error도 쉽게 접근할 수 있을 수 있습니다.

UnitTest가 가능한 형태로 만들려면 좀 더 많은 고민이 필요한데 위와 같이 load() 함수 통으로 둔다면 UnitTest에 대한 의미가 크지 않습니다.

아래와 같이 다시 Flow를 리턴 시킬 수 있도록 구현해 줘야 UnitTest도 진행해 보고, Ui에서도 활용할 수 있는 형태를 만들 수 있습니다.

fun buttonSecoundEvent(): Flow<Boolean> = ...

fun buttonPlusCountEvent(): Flow<Int> = ...


이 코드는

이 코드는 GitHub에 업로드해두었으니 참고하시면 되겠습니다.

GitHub ClickEvent 샘플


마무리

View에서 ViewModel의 함수를 직접 호출하는 게 아닌 간접적인 호출을 할 수 있도록 만들어주는 방법에 대해서 서술해 보았습니다.

이런 방법은 하나의 방법이긴 하지만 더 좋은 코드를 작성하고, 결합력을 줄이는데 의미가 있어 보입니다.

현 샘플에서는 그런 결합을 무시하고 샘플로 작성한 부분이지만, 더 발전시킬 수 있는 부분은 충분히 있습니다.

Activity/xml 등에서 setOnClickListener를 처리하는 것보단 내가 모르게 알아서 처리해 주고, 그걸 활용하는 방법이 좀 더 유용하기 합니다.

요즘같이 안드로이드 개발이 크게 재미있지 않은 시점에선 이런 방법을 함께 고민하실 분이 있다면 매우 좋은 것 같습니다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments