이번 포스팅에서는 LiveData와 Observer Pattern에 대해 알아보도록 하겠습니다.
LiveData는 간단히 말하면 값의 변경을 감지할 수 있는 데이터 홀더입니다. 예를들어 정수 1을 담고있는 LiveData의 값이 정수 2로 변하는 순간을 시스템이 감지할 수 있다는 말이죠.
이 특징은 ViewModel과 결합할 때 시너지 효과를 얻을 수 있습니다. 이전 강의에서 UI에 표시할 데이터는 ViewModel에서 가져오고, 값이 변경되면 ViewModel을 확인하여 UI를 변경하는 구조를 채택하면 Activity 재생성에도 UI의 값을 유지할 수 있다는 것을 보였습니다.
이 때 데이터 홀더로 LiveData를 사용하면 값의 변경을 감지하여 UI에 그 변화를 자동으로 반영되게 할 수 있습니다. 이것이 MVVM 구조에서 구현해야 하는 데이터바인딩의 개념입니다.
Subject의 상태 변화를 관찰하는 Observer들을 객체와 연결하고, Subject의 상태 변화를 초래하는 Event가 발생하면 객체가 그 Event를 직접 Observer에게 통지하는 구조가 디자인 패턴의 한 종류인 Observer Pattern입니다.
유튜브를 예로 들면 채널 운영자는 Subject이고 구독자는 Observer입니다. 채널 운영자가 새로운 동영상을 등록하면 자동으로 구독자에게 알림 Event가 전달되어 구독자가 알게되는 것이 Observer Pattern이라고 할 수 있는것이죠. LiveData를 사용하는 것은 이 Observer Pattern을 구현하는 것과 같습니다.

Observer에 의해 값의 변경을 감시할 수 있는 안드로이드의 데이터 홀더로는 Observable과 LiveData가 있습니다.
Observable에는 ObservableBoolean, ObservableByte, ObservableChar 등 기본형의 Observable은 이미 준비되어 있는데, 만약 특수한 타입을 사용하고 싶다면 ObservableField를 써서 직접 구현할 수도 있습니다. Observable은 다음과 같이 사용합니다.
private val observableString = ObservableField<String>("Default value")
observableString.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
//Do something
}
})
반면 LiveData의 사용법은 다음과 같습니다.
private val observableString = MutableLiveData<String>("Default value")
observableString.observe(lifecycleOwner, Observer {
//Do something
})
구현을 보면 ObservableField는 콜백을 등록하는 과정이 있고, LiveData는 lifecycleOwner를 전달하는 과정이 있습니다. 이것이 ObservableField와 LiveData를 구분하는 가장 큰 차이점입니다.
Observable은 lifecycle을 알 수 없으므로 등록한 콜백이 상시 작동되어야 하며, 작동이 필요없어지면 removeOnPropertyChangedCallback을 호출하여 콜백을 수동으로 직접 제거해야 하는 불편함이 있습니다.
그러나 LiveData는 lifecycle이 STARTED 혹은 RESUMED 로 활성화 상태일 때만 observe를 수행하고 나머지 상태에서는 자동으로 비활성화합니다. 다음 그림을 보면 STARTED와 RESUMED는 결국 onStart와 onStop의 사이라는 것을 알 수 있습니다.
LiveData는 Observable을 lifecycle과 연동하게 하면서 다음과 같은 수많은 이점을 갖게 되었습니다.
UI와 데이터 상태의 일치 보장
메모리 누수 없음
중지된 활동으로 인한 비정상 종료 없음
수명 주기를 더 이상 수동으로 처리하지 않음
최신 데이터 유지
적절한 구성 변경
리소스 공유
그럼 이전에 만들었던 ViewModel 프로젝트에 LiveData를 적용해 보겠습니다. 우선은 프로젝트에 LiveData dependency를 추가합니다.
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
ViewModel의 counter는 liveCounter로 대체합니다. 값을 변경해야 할 필요가 있는데 LiveData는 변경불가한 타입이므로 MutableLiveData를 사용하겠습니다.
class MyViewModel(_counter: Int) : ViewModel() {
- var counter = _counter
+ val liveCounter : MutableLiveData<Int> = MutableLiveData(_counter)
}
그리고 메인액티비티에서 LiveData에 대해 Observer를 등록한 뒤 UI 업데이트 로직을 옮겨줍니다. 그럼 UI를 표시하는 로직을 더 이상 버튼의 클릭리스너 안에 둘 필요가 없게 됩니다.
binding.button.setOnClickListener {
...
myViewModel.liveCounter.value = myViewModel.liveCounter.value?.plus(1)
}
myViewModel.liveCounter.observe(this) { counter ->
binding.textView.text = counter.toString()
}
LiveData는 값을 변경할 수 없는 타입입니다. 하지만 데이터를 다루다보면 LiveData의 값을 변경해야 할 일이 생기게 됩니다. 그런데 ViewModel이 데이터 원본을 조작하도록 하면 ViewModel과 데이터의 결합관계가 강해지는 문제가 생기게 됩니다.
이런 경우엔 Transformations를 사용합니다. Transformations는 전달받은 LiveData에 변경이 일어났을 때 람다함수를 실행시킨 뒤 다시 LiveData를 반환합니다. 따라서 원본 데이터의 변경없이 새로운 LiveData를 만들어서 사용할 수 있습니다.
Transformations에는 map과 switchMap 두 개의 메소드가 있습니다. map의 사용법은 다음과 같습니다. userLiveData를 관찰하다 변화가 일어났을 때 user.firstName + user.lastName인 LiveData를 반환합니다.
val userLiveData = ...
val userNameLiveData: LiveData<String> = Transformations.map(userLiveData) { user ->
user.firstName + user.lastName
}
switchMap은 전달받은 LiveData의 값이 변경되면 mapping한 값도 따라서 변하는 특징을 갖고 있습니다.
val nameQueryLiveData = ...
val switchedNameQueryLiveData: LiveData<User> = Transformations.switchMap(nameQueryLiveData) { name ->
myDataSource.getUsersWithNameLiveData(name)
}
fun setNameQuery(name: String) {
nameQueryLiveData.setValue(name);
}
그럼 ViewModel에 적용해볼까요. counter에 "입니다"를 붙인 modifiedCounter를 새로 만들어 표시하도록 해 보겠습니다.
// MyViewModel.kt
val modifiedCounter : LiveData<String> = Transformations.map(liveCounter) { counter ->
"$counter 입니다"
}
// MainActivity.kt
myViewModel.modifiedCounter.observe(this) { counter ->
binding.textView.text = counter
}
이렇게 해서 LiveData와 Observer Pattern에 대해 알아보았습니다.