반응형

개요

MVVM 아키텍처는 마이크로소프트에서 제안한 패턴으로, Model-View-ViewModel 의 약자를 딴 아키텍처입니다.

Model 은 데이터와 비즈니스 로직을 포함하고, View 는 UI 를 나타내며 ViewModel 은 Model 과 View 사이에서 상호작용을 담당합니다.

그러면 MVVM 패턴에서 이벤트 처리는 어디서 할까요? 바로 ViewModel 입니다.

 

안드로이드에서 MVVM 패턴으로 구현하기 위해서는 Jetpack 에서 제공하는 데이터 바인딩을 사용해야 하는데요. 지금부터 AAC ViewModel 과 LiveData, 그리고 DataBinding 을 사용하여 이벤트 처리를 어떻게 다룰 수 있는지 살펴보도록 하겠습니다.

 

아래 설명에서 사용되는 예제는 드로이드 나이츠 2020 에서 참고하여 작성한 코드입니다.

 

 

Event 클래스 정의하기

MVVM 에서 이벤트를 처리할 때 가장 고민되는 부분은 "Context 에 대한 의존이 필요한 경우에 어떻게 처리해야 하나?"입니다.

디벨로퍼 사이트에 따르면 Jetpack 에서 제공하는 AAC ViewModel 에서는 Context 이나 View 에 대한 의존을 갖고 있지 말라고 권장하고 있기 때문인데요. 

 

이를 해결하기 위해 Event 처리를 담당할 Event 클래스를 정의해보겠습니다. 이번에 정의하는 Event 클래스는 LiveData 에서 사용할 예정입니다.

 

class Event<out T>(private val content: T) {

    private var hasBeenHandled = false

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
}

 

Event 클래스에서 선언된 hasBeenHandled 변수는 하나의 이벤트 처리에 대해 한 번만 처리하기 위해서 사용됩니다.

 

DataBinding +  AAC ViewModel + LiveData

이제 Event 클래스를 사용하여 예제를 작성해보도록 하겠습니다.

버튼을 누르면 EditText 에서 작성된 텍스트가 다음 액티비티에서 표시되도록 하는 샘플 앱을 작성해보겠습니다.

 

MainViewModel

class MainViewModel : ViewModel() {
    private val _openEvent = MutableLiveData<Event<String>>()
    val openEvent: LiveData<Event<String>> get() = _openEvent

    val sampleText: MutableLiveData<String> = MutableLiveData()

    fun onClickEvent(text: String) {
        _openEvent.value = Event(text)
    }
}

inline fun <T> LiveData<Event<T>>.eventObserve(
    owner: LifecycleOwner,
    crossinline onChanged: (T) -> Unit
): Observer<Event<T>> {
    val wrappedObserver = Observer<Event<T>> { t ->
        t.getContentIfNotHandled()?.let {
            onChanged.invoke(it)
        }
    }
    observe(owner, wrappedObserver)
    return wrappedObserver
}

 

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

 

그러면 이제 이 MainViewModel 을 사용해서 UI 를 작성해보겠습니다.

 

activity_main.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="vm"
            type="com.ready.blog.samples.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={vm.sampleText}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <Button
            android:id="@+id/event_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="SEND"
            android:onClick="@{() -> vm.onClickEvent(vm.sampleText)}"
            app:layout_constraintTop_toBottomOf="@id/edit_text"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

보시면 two-way binding 을 사용하여 EditText 와 vm.sampleText 를 연결해주었고, Button 을 클릭하면 MainViewModel 에서 정의한 onClickEvent() 함수에 해당 텍스트를 넘기면서 호출하는 방식으로 UI 를 구성하였습니다.

 

 

MainActivity

class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
            lifecycleOwner = this@MainActivity
            vm = viewModel
        }

        initObserve()
    }

    private fun initObserve() {
        viewModel.openEvent.eventObserve(this) { sampleText ->
            val intent = Intent(this, NextActivity::class.java)
            intent.putExtra("sample", sampleText)
            startActivity(intent)
        }
    }
}

 

Activity 코드를 살펴보시면, DataBinding을 통해 activity_main.xml 을 연결해주고 난 뒤 initObserve() 함수를 호출하여 Event 객체로 들어온 Text 를 인텐트에 담아 NextActivity 를 실행하는 코드입니다.

 

activity_next.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NextActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

NextActivity

class NextActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_next)

        sample_text.text = intent.getStringExtra("sample")
    }
}

 

편의상 NextActivity 는 DataBinding 을 사용하지 않았습니다.

 

실행 화면


위 예제에서 사용된 전체 코드는 Github 에서 확인하실 수 있습니다.

반응형
반응형