이번 포스팅에는 MVVM 디자인 패턴을 실현하기 위한 핵심인 ViewModel을 사용하는 실습을 포스팅하려고 합니다.
MVVM 패턴을 왜 하려고 하는지, 또 이전 포스팅 DataBinding을 보지 못한 분들은 보고 오는 것이
이번 포스팅을 이해하는데 더 편할 것이라고 생각합니다.
ViewModel
먼저 ViewModel 부터 알아봅시다. 우리가 ViewModel을 쓰려고 하는 이유는 크게 보자면
UI와 로직의 분리입니다. MVVM 패턴을 적용시키려는 이유가 무엇이었나요?
바로 화면에 보여지는 UI 로직과 데이터 처리 로직을 분리시켜서 유지보수와 개발 효율을 높이는것이었죠?
ViewModel을 사용하게 된다면 UI관련 데이터를 액티비티와 프래그먼트로 부터 분리시킬 수 있습니다.
그 말은 즉 액티비티와 프래그먼트는 UI를 업데이트 하는데만 주력할 수 있다는 것이고, ViewModel에 있는 데이터는
액티비티 또는 프래그먼트의 수명주기로부터 자유로워진다는 뜻입니다. (!!)
또한 ViewModel에 있는 데이터는 마치 싱글톤 객체처럼 사용이 가능해서 프래그먼트들 사이에서 ViewModel을 이용해
데이터를 쉽게 공유할 수 있습니다. 이렇게 되면 프래그먼트 사이에서 데이터 전송을 하는 중간 다리 역할인 액티비티의
역할이 줄어들겠죠?
요약하자면 다음과 같습니다.
- 액티비티와 프래그먼트로 부터 데이터가 분리된다.
- 액티비티와 프래그먼트가 데이터 관리를 할 필요가 없으므로 UI 업데이트에만 집중할 수 있다.
- ViewModel에 있는 데이터는 액티비티와 프래그먼트의 수명주기에 영향을 받지 않는다.
- 프래그먼트 사이에서 데이터 공유가 훨씬 쉬워진다.
OnCleared()
ViewModel에는 onCleared() 함수가 존재합니다. 이 함수는 액티비티의 onDestroy()가 호출된 후 실행되는 함수입니다.
이 함수에서 ViewModel에 있는 리소스를 해제하기에 적합합니다.
LiveData
LiveData는 식별 가능한 데이터 홀더 클래스입니다. 스스로 수명 주기를 인식하기 때문에 여러 이점이 존재하죠.
- UI와 데이터 상태의 일치 보장
- 메모리 누출이 없음
- 비정상 종료가 없음
- 수명주기를 자동으로 관리
- 최신의 데이터 유지
- 기기회전 등 프래그먼트나 액티비티가 재생성되어도 데이터의 변화가 없음
LiveData는 ViewModel과 함께 사용해야 그 효과가 커지게 됩니다. ViewModel 안에 있는 LiveData 객체를 DataBinding을 통해 UI에서 관찰만 할 수 있도록 만들면 액티비티나 프래그먼트에서 일일히 데이터를 갱신할 필요 없이 알아서 UI에 최신 데이터가 보이게 될 것입니다.
실습 준비
ViewModel과 LiveData, DataBinding을 사용하기 위해 Gradle세팅을 해줍니다.
apply plugin: 'kotlin-kapt'
android {
...
buildFeatures{
dataBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies{
...
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.activity:activity-ktx:1.1.0"
}
안드로이드 공식 문서에서는 현재 코틀린으로 ViewModel을 사용할 때 activity-ktx artifact에 있는 'by viewModels()' 속성을 사용하라고 명시하고 있습니다. 그래서 다음과 같이 gradle을 세팅해줍니다.
물론 apply plugin : 'kotlin-kapt' 도 반드시 설정해 주셔야 합니다.
xml 세팅
플로팅 버튼을 누를 때마다 화면 중앙에 있는 숫자가 +1 또는 -1 되는 화면을 만들어 보겠습니다.
xml은 다음과 같이 세팅해 줍니다.
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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_baseline_exposure_neg_1_24" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_baseline_exposure_plus_1_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DataBinding을 사용하기 위해서 <layout>태그로 전체를 감싸주고 <data>태그로 바인딩될 영역을 미리 설정해 줍니다.
ViewModel 만들기
LiveData가 관리될 ViewModel을 만들어줍니다. 여기서 class의 상속을 ViewModel()과 AndroidViewModel() 두 개를
사용할 수 있는데, 차이는 다음과 같습니다.
- Application의 유무가 차이점이다. AndroidViewModel은 Application을 상속받기 때문에 메모리 leak이 발생할 수 있다.
- Context 작업이 필요할 경우 AndroidViewModel을 쓴다.
AndroidDevleoper에서는 ViewModel() 사용을 권장하고 있습니다. 저는 그냥 AndroidViewModel을 써봤는데,
ViewModel로 사용하셔도 무방합니다.
MainViewModel.kt
package org.three.sampleviewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
class MainViewModel( application: Application) : AndroidViewModel(application){
// ViewModel()을 상속받을 경우
// class MainViewModel():ViewModel(){}
//LiveData
//값이 변경되는 경우 MutableLiveData로 선언한다.
var count = MutableLiveData<Int>()
init {
count.value = 0
}
fun increase(){
count.value = count.value?.plus(1)
}
fun decrease(){
count.value = count.value?.minus(1)
}
}
ViewModel안에 LiveData가 있는 모습입니다. LiveData 객체의 값이 변경될 경우에는 MutableLiveData<T>()으로 선언해줍니다.
그 다음 increase(), decrease() 함수를 선언 해 각각 값이 증가/감소하는 역할의 함수를 만들어줍니다.
DataBinding
xml에서 위에서 만든 ViewModel을 데이터바인딩 시켜줍니다.
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="viewModel"
type="org.three.sampleviewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(viewModel.count)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{()->viewModel.decrease()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_baseline_exposure_neg_1_24" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{()->viewModel.increase()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_baseline_exposure_plus_1_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
플로팅 버튼에 각각 increase()/decrease() 함수를 바인딩 시켜주고, 텍스트 뷰에는 LiveData 객체를 바인딩시켜 줍니다.
메인 코드 작성
메인 코드에서는 바인딩 연결과 ViewModel 선언만 해주면 됩니다.
MainActivity.kt
package org.three.sampleviewmodel
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import org.three.sampleviewmodel.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityMainBinding
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mBinding.lifecycleOwner = this
mBinding.viewModel = model
}
}
메인코드에서 ViewModel을 xml에 바인딩 시켜주고 lifecycleOwner를 통해 수명주기 관리를 설정해주면
알아서 UI에서 변경된 값을 관찰해 실시간으로 변동이 일어나게 됩니다.
기존에 코드와 다르게 setOnClickListener를 선언하고 변수를 일일히 액티비티에서 바꿔주는 일을 하지 않아도 되고,
알아서 UI에서 데이터를 감지하게 되니 메인 코드가 무척 깔끔해진 것을 볼 수 있습니다.
ViewModel + LiveData + DataBinding을 사용하면 좀 더 모듈간의 독립성을 보장할 수 있고, 사용자가 일일히
수명주기를 관리하지 않아도 됩니다.
이 포스팅을 보고 여러분들도 MVVM 패턴에 조금은 익숙해졌으면 하는 바람입니다.
전체 코드를 보고 싶으시면 아래 링크를 타고 들어가셔서 확인해보시면 됩니다~
https://github.com/kangmin1012/NewAndroidExercise/tree/ViewModel
'안드로이드' 카테고리의 다른 글
[Android/Kotlin] MotionLayout - 2 ( 실제로 적용해보자.) (0) | 2020.09.17 |
---|---|
[Android/Kotlin] MotionLayout (애니메이션을 쉽게 적용하기) (3) | 2020.09.06 |
[Android/Kotlin]ViewPager에 wrap_content 가능케 하기 (0) | 2020.07.06 |
[Android/Kotlin] Shared elemet transition - RecyclerView에 적용 (1) | 2020.06.25 |
[Android/Kotlin] 화면 전환 시 생동감 있게 애니메이션 적용하기 (1) | 2020.06.22 |