2023. 12. 10. 18:43ㆍ- 디자인 패턴
MVVM은 Model, View, ViewModel 로 구성되는데 한마디로 말하면,
체계적으로 앱을 만들고 관리하기 위해서 만들어진 디자인 패턴입니다.
- View : 화면을 구성하는 사용자 인터페이스
- Viewmodel : view가 요청한 데이터를 Model에 요청
- Model : 데이터를 관리하는 클래스
MVVM 사용하는 이유?
MVVM 에서 가장 중요한 것은 관심사 분리 입니다.
여러 기능들이 분리되지 않고 한쪽에 쏠려있어 유지보수가 힘들어지는 문제를 해결하기 위해 나온 패턴입니다.
아래의 다이어그램을 확인해보겠습니다.
뷰(Activity / Fragment)와 모델(Repository)이 분리되어 있고, 이 분리된 두 로직 사이에서 뷰의 이벤트에 따라 모델이 데이터를 반환/저장하도록 통신하는 뷰모델이 존재합니다.
그러니까 예를 들면 아래와 같은 구조라 할수 있습니다.
뷰 | 뷰모델 | 모델 |
1. 이벤트를 발생시켜 데이터 요청 5. 라이브데이터를 감지해 저장된 값을 뷰에 출력 |
2. 해당 데이터를 불러오는 모델의 메소드를 호출 4. 모델로부터 받은 값을 라이브데이터에 저장 |
3. 뷰모델에서 요청하는 값을 반환 |
뷰의 입력에 따른 뷰모델에게 데이터 갱신 요청 > 뷰 모델은 모델에게 데이터 갱신 요청 및 반환 > 데이터 갱신이 되면 데이터바인딩 되어 있는 뷰가 갱신
뷰는 ui업데이트만 신경쓰면됩니다. 또한 생명주기로부터 안전하여 메모리 누수 안정성이 좋아집니다.
이런 구조가 된다면 뷰는 모델이, 모델은 뷰가 어떻게 동작하는지와 상관없이 로직을 작성할 수 있고 뷰모델을 통해 데이터를 통신할 수 있게 됩니다.
안드로이드에서 주로 사용 하는 패턴으론 MVC와 MVP도 있는데 아래와 같은 문제가 있습니다.
MVC의 경우에는 안드로이드에서 적용할 때 View와 Controller가 Activity에서 모두 처리되어야하기 때문에 Activity가 커지는 문제가 있어서 관심사의 분리가 비교적 원활하지 않습니다.
MVP는 Presenter가 뷰와 1대1로 동작하기 때문에 뷰와 프레젠터의 의존성이 강해지는 문제가 발생하고 이에 따라 종종 프레젠터의 로직이 비대해지는 문제가 발생합니다.
MVVM 패턴 적용 예제
안드로이드 스튜디오에 mvvm패턴을 적용한 예제를 확인해보겠습니다.
먼저 app단위 build.gradle에 아래와 같이 추가 해주세요.
build.gradle
plugins {
...
id 'kotlin-kapt'
}
dataBinding{
enabled = true
}
dependencies {
...
kapt "com.android.databinding:compiler:3.1.4"
implementation 'androidx.fragment:fragment-ktx:1.5.5' // viewModels()
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
}
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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/addButton"
android:hint="할 일 메모하기"
android:paddingHorizontal="18dp"
android:paddingBottom="18dp"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="add"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/editText"
android:layout_marginTop="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
메인 화면입니다. dataBinding 을 사용하기 위해서는 <layout> 태그로 감싸줘야 합니다.
그 외에 더 해야할 작업들이 있는데 밑에서 진행하도록 하겠습니다.
Model
데이터 클래스를 만들어 줍니다.
Recyclerview에 들어갈 데이터 입니다.
data class Todo(
val content: String
)
ViewModel
class ViewModel: ViewModel() {
// mutable인 값이 변하는 _todoList는 private를 사용하여 외부에선 접근이 불가하게 하고
// view가 사용 하는 계속 변경되는 데이터 todoList는 get()을 사용하여 읽기만 가능하도록. view는 데이터를 보여주는 역할이기때문. 역할을 분명히 나누기 위해
// LiveData는 항상 UI업데이트 목적으로.
private var _todoList = MutableLiveData<List<Todo>>()
val todoList : LiveData<List<Todo>> get() = _todoList
private var items = mutableListOf<Todo>()
init {
items = arrayListOf(
Todo("테스트1"),
Todo("테스트2")
)
_todoList.postValue(items)
}
fun addTodo(content: String){
if(content == ""){
return
}
val todo = Todo(content)
items.add(todo)
_todoList.postValue(items)
}
}
View가 UI 를 업데이트하는데 필요한 데이터들이 ViewModel에서 관리됩니다.
addTodo는 파라미터로 String type의 content를 받아서 할 일 list에 추가해줍니다.
content는 editText의 text가 들어오게 뒤에서 연결해주겠습니다.
recycler_item2.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model2" // Todo 데이터 클래스를 이름 model2로 연결
type="com.dldmswo1209.mvvmpattern.model.Todo" />
</data>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingVertical="30dp"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="10dp"
android:text="@{model2.content}"
android:elevation="10dp"
android:background="@color/white" />
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
리사이클러뷰 아이템 레이아웃입니다.
마찬가지로 DataBinding 을 사용하기 위해서 layout 태그로 감싸줬고,
data 태그를 사용해서 model 변수를 생성해줍니다.
type 은 Model data class 가 있는 패키지 경로입니다.
android:text="@{model2.content}"
text에 model의 content를 연결해줍니다.
RecyclerAdapter
class RecyclerAdapter: ListAdapter<Todo, RecyclerAdapter.MyViewHolder>(diffUtil) {
inner class MyViewHolder(private val binding: RecyclerItem2Binding): RecyclerView.ViewHolder(binding.root){
fun bind(todo : Todo){
binding.model2 = todo
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(RecyclerItem2Binding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object{
val diffUtil = object: DiffUtil.ItemCallback<Todo>(){
override fun areItemsTheSame(oldItem: Todo, newItem: Todo): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Todo, newItem: Todo): Boolean {
return oldItem == newItem
}
}
}
}
리사이클러뷰 어답터입니다.
binding.model2 = todo 를 통해 위에서 만든 recycler_item2 <data> model2 변수에 todo 를 전달해줍니다.
이렇게 todo 를 전달해주면, 데이터바인딩을 통해 TextView text 에 model2.content가 들어가게 됩니다.
MyBindingAdapter2
object MyBindingAdapter2 {
// observe대신 사용
@BindingAdapter("items2") // xml items2속성 만듬. xml에서 items2를 사용하여 fun setItem사용
@JvmStatic
fun setItem(recyclerView: RecyclerView, todoList: List<Todo>?){
if(recyclerView.adapter == null){
val adapter = RecyclerAdapter()
recyclerView.adapter = adapter
}
todoList?.let{
val myAdapter = recyclerView.adapter as RecyclerAdapter
myAdapter.submitList(it)
myAdapter.notifyDataSetChanged()
}
}
}
Adapter설정을 MainActivity가 아니라 items2속성을 통해 진행합니다.
liveData의 관찰자(observer)를 통해서 list를 리사이클러뷰 어답터에 전달할 수 있지만, MVVM 패턴과는 거리가 있는 접근방식이므로 BindingAdapter 를 사용하겠습니다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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="viewModel2"
type="com.dldmswo1209.mvvmpattern.viewModel.ViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/addButton"
android:hint="할 일 메모하기"
android:paddingHorizontal="18dp"
android:paddingBottom="18dp"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="add"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="@{()->viewModel2.addTodo(editText.getText().toString())}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/editText"
android:layout_marginTop="10dp"
items2="@{viewModel2.todoList}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
다시 activity_main.xml 로 돌아와서 ViewModel클래스를 연결해줍니다.
<data>
<variable
name="viewModel2"
type="com.dldmswo1209.mvvmpattern.viewModel.ViewModel" />
</data>
AppCompatButton에 클릭 이벤트를 연결 합니다.
android:onClick="@{()->viewModel2.addTodo(editText.getText().toString())}"/>
MyBindingAdapter2로 만든 items2속성을 사용해 todoList 데이터를 RecyclerView에 연결합니다.
items2="@{viewModel2.todoList}"
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val viewModel : ViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel2 = viewModel
binding.lifecycleOwner = this
}
}
마지막으로 MainActivity 입니다.
viewModel 변수로 생성한 viewModel을 전달합니다.
lifeCycleOwner를 지정해줘야 viewModel 에 선언해놓은 LiveData 의 변화를 감지해 UI 를 업데이트할 수 있습니다.
Mvvm패턴을 적용해 관심사를 분배 하고 역할을 나눠 줬기 때문에 위와 같이 심플하게 코드 작성이 가능했습니다.
참고 자료
https://velog.io/@dldmswo1209/Android-MVVM-Pattern-%EC%A0%81%EC%9A%A9-%EC%98%88%EC%A0%9C