MVVM패턴?
MVVM패턴이란 Model, View, View Model을 분리하여 뷰에 모델간의 의존성을 줄어주는 패턴이다.

뷰(Activity / Fragment)와 모델(Repository)이 분리되어 있고, 이 분리된 두 로직 사이에서 뷰의 이벤트에 따라 모델이 데이터를 반환/저장하도록 통신하는 뷰모델이 존재한다.
뷰 | 뷰모델 | 모델 |
이벤트를 발생시켜 데이터 요청 | ||
해당 데이터를 불러오는 모델의 메소드를 호출 | ||
뷰모델에서 요청하는 값을 반환 | ||
모델로부터 받은 값을 라이브데이터에 저장 | ||
라이브데이터를 감지해 저장된 값을 뷰에 출력 |
위와 같이 뷰와 모델은 서로 어덯게 동작하는지와 상관없이 로직을 작성할 수 있고 뷰모델을 통해 데이터를 통신할 수 있게 된다.
MVVM 사용 이유
android의 대중적인 패턴에는 MVC와 MVP도 존재한다. 하지만 구글은 왜 MVVM모델의 사용을 권장하는가?
MVVM의 장점은 다음과 같다.
- View와 Model이 독립성을 유지할 수 있다.
- 독립성을 유지하기에 효율적인 유닛테스트가 가능하다.
- View를 잘 몰라도 로직을 구현할 수 있다.
- viewModel을 재사용할 수 있다.
MVVM 적용하기
우선 ViewModel과 LiveData를 build.gradle(:app)의 dependencies안에 implientation해준다.
아래 2개는 viewModel을 간단하게 생성하기 위해 사용된다. (viewModel을 선언할 한 곳만 삽입해도 된다)
(참조 : https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko)
// 2023.04.04 기준
def lifecycle_version = "2.5.1"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
def activity_version = "1.6.1"
//noinspection GradleDependency
implementation("androidx.activity:activity-ktx:$activity_version")
def fragment_version = "1.5.5"
implementation("androidx.fragment:fragment-ktx:$fragment_version")
build.gradle(:app)의 android { } 안에 아래와 같이 넣어준다. 데이터와 뷰 바인딩을 위한 것이다.
buildFeatures{
viewBinding true
dataBinding = true
}
viewModel에서 사용하는 즉, 값이 변경될 수 있는 변수는 MutableLiveData를 사용하고 ‘_’를 접두사로 사용한다. view에서 옵저버를 통해 변화를 감지하는 값의 변수는 LiveData를 사용하고 접두사 없이 사용한다.
이처럼 두개의 변수로 나누는 이유는 view에서 데이터를 조작할 수 없게 하기 위해서이고 변수명의 스타일은 kotlin의 코드 컨벤션을 따른 것이다.
Names for backing properties If a class has two properties which are conceptually the same but one is part of a public API and another is an implementation detail, use an underscore as the prefix for the name of the private property:
백업 속성의 이름 클래스에 개념적으로 동이하지만 하나는 공용 API 의 일부이고 다른 하나는 구현 세부 정보인 두 개의 속성이 있는 경우 개인 속성 이름의 접두사로 밑줄을 사용합니다. (파파고)
// ViewModel class
class MainViewModel : ViewModel() {
private val _num = MutableLiveData<Int>()
val num: LiveData<Int> = _num
init{
_num.value = 0
}
fun incNum() {
_num.value = _num.value?.plus(1) ?: 1
}
fun decNum() {
if(_num.value == 1) return
_num.value = _num.value?.minus(1)
}
}
xml 구성. <layout> </layout>으로 코드들을 감싸준다. <data> <data>안에 바인딩할 뷰모델과 뷰를 등록해준다. XML상에서 View에 의해 전달된 이벤트를 처리하는 표현식을 사용하기 때문에 layout으로 감싸주는 형식으로 구성된다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="view"
type="com.example.mvvmsample.MainFragment" />
<variable
name="viewModel"
type="com.example.mvvmsample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainFragment">
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="40sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="40sp"
android:gravity="center"
android:onClick="@{()->viewModel.decNum()}"
android:text="ㅡ"/>
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="40sp"
android:gravity="center"
android:onClick="@{()->viewModel.incNum()}"
android:text="+"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
activity에서 fragment를 등록해준다. fragment의 경우 activity의 라이프 사이틀에 종속되어있기 때문에 viewModel의 값이 초기화되는 문제가 발생하기 때문에 if(savedInstanceState == null){}를 통해 fragment가 destroy되는 것을 막아주어야한다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// todo inflate는 무엇인가?
binding = ActivityMainBinding.inflate(layoutInflater)
// todo setContentView는 무엇인가?
setContentView(binding.root)
if(savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.contentFrame, MainFragment.newInstance())
.commit()
}
}
}
fragment를 생성하고 view와 viewModel을 바인딩 해준다. DataBindingUtil을 통해 xml을 연결시켜준다.
view에서 <variable>로 선언한 view와 viewModel에 해당하는 값을 넣어준다.
viewModel의 경우 implementation("androidx.fragment:fragment-ktx:1.5.5")를 선언해 주었기 때문에 by viewModels()를 사용하여 간단하게 선언 해줄 수 있다. (activity의 경우 implementation("androidx.activity:activity-ktx:1.6.1")을 선언해주어야 한다)
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)
binding.view = this
binding.viewModel = viewModel
viewModel.num.observe(viewLifecycleOwner) {
binding.tvNum.text = it.toString()
}
return binding.root
}
companion object {
fun newInstance() = MainFragment()
}
}
fragment의 일부인 아래의 코드는 viewModel의 num값을 observe하며 변화가 있을 때 tvNum의 text값을 변경해주는 코드이다.
viewModel.num.observe(viewLifecycleOwner) {
binding.tvNum.text = it.toString()
}
이처럼 viewModel의 값을 observe하며 view 컨트롤을 해주면 된다.
참조
viewModel에 관하여 https://jslee-tech.tistory.com/45
AAC ViewModel과 MVVM ViewModel https://wooooooak.github.io/android/2019/05/07/aac_viewmodel/
MVVM 적용방식 https://velog.io/@dddooo9/Android-MVVM-패턴을-사용하는-이유와-방법
Uploaded by N2T