본문 바로가기

코딩/안드로이드

Android Kotlin MVVM패턴으로 간단한 검색 앱 만들기 - 4. Livedata를 통한 데이터 바인딩

이번에는 저번 글에 이어서 Livedata를 이용하여 View 와 ViewModel간의 데이터 바인딩에 대해 글을 써보겠습니다.



저번 글에서도 설명한것처럼, View는 ViewModel 객체를 멤버로 가지고 있지만, ViewModel은 View의 객체를 가지고 있지 않습니다.


그럼, ViewModel에서 View의 함수를 호출하거나 View의 내용을 변경하거나, 혹은 Context나 Activity 객체의 함수를 호출해야 할 때는 어떻게 해야 할까요?


물론 Context를 이용하고 싶으면 AndroidViewModel을 상속하면 되지만, 지금은 넘어가도록 합시다.



해답은 바로 View가 ViewModel의 특정한 데이터를 Observing하고 있다가, 그 데이터가 변경될 때 View의 로직을 수행하면 됩니다.




예를들자면,


View는 ViewModel의 멤버중 하나인 'number'를 Observing하고 있다고 칩시다.


그 뒤 ViewModel은 number = 3과 같이 number를 변경하게 되면


View는 ViewModel.number의 변경사항을 알아채고 미리 설정해둔 로직을 수행하게 됩니다.



방금 든 예를 코드로 간략히 써보자면,


class MyViewModel:ViewModel{
val number = MutableLiveData<Int>()

fun changeNumber(num:Int){
number.postValue(num)
}
}

class MyView:View{
val myViewModel = MyViewModel()

init {
myViewModel.number.observe(this, Observer {
my_text_view.text = "number is $it"
})
}

fun changeViewModelNumber(){
myViewModel.changeNumber(num)
}
}


이렇게 구현할 수 있겠군요.


이렇게 하면, MyView.changeViewModelNumber()를 호출하게 되면, ViewModel의 number가 postValue를 통해 바뀌게 되고,


이를 myViewModel.number.observe(this, Observer{})를 통해 Observing하고 있던 뷰가 감지하여 my_text_view의 text를 변경하게 됩니다.


한가지 말씀드리자면, LiveData의 값을 변경하기 위해서는 MutableLiveData로 선언해야하고, 값의 변경은 postValue나 setValue로 변경할 수 있습니다.


이 둘의 차이는 밑에서 말씀드리겠습니다.




이런식으로 View와 ViewModel은 Livedata를 통해서 정보를 교환하게 됩니다.


그런데 Rxjava에도 Observing이 있는데 왜 하필 Livedata를 쓸까요?


그건 바로 Livedata에 여러 장점이 있기 때문입니다.




Livedata




일단 한글로 된 공식 문서가 있으니 먼저 소개해드리겠습니다.


https://developer.android.com/jetpack/arch/livedata?hl=ko




읽어보시면 이런 부분이 나옵니다.



한번 적당히 짧게 요약해보겠습니다.


1. UI와 데이터 상태의 일치를 보장해줌 

-> 위에 예시를 든 것처럼 observing을 하게 되기 때문에 일치한다. 뭐 그런 말입니다.


2. 메모리 누출 없음

-> RxJava를 쓸 때에는 addDisposable이나 compositeDisposable.clear() 등을 통해 메모리 관리를 해주어야 되는데 그럴 필요가 없다.. 뭐 그런 뜻입니다.


3. 중지된 활동으로 인한 비정상 종료 없음

-> 간혹 안드로이드 코딩을 할 때, 뷰가 살아있을 때만 뷰의 업데이트를 수행하기 위해 여러가지 플래그를 둔다거나.. 하는 코딩을 하게 됩니다. 이제 그럴 필요 없다는 뜻입니다.


4. 수명 주기를 더 이상 수동으로 처리하지 않음

-> Observing을 수행하면서 onDestroy등의 수명 주기를 인식한다는 뜻같은데.. 저도 이 부분은 조금 스터디가 필요할 것 같습니다 ㅎ...


5. 최신 데이터 유지

-> onStop등의 상태로 자고있다가 onStart등으로 다시 깨어나면 최신 데이터를 다시 받아서 뿌려줍니다. 이 기능은 정말 어마어마게 좋습니다. 6번을 보고 이어서 서술하겠습니다.


6. 적절한 구성 변경

-> 여기서 전 어마어마한 감명을 받았습니다. 보통 화면을 회전하거나, 아니면 화면 분할을 수행하면 액티비티는 onDestroy를 수행하고 다시 onCreate를 통해 뷰를 그립니다. 이러한 과정에서 뷰의 여러가지 구성요소에 할당한 데이터들, 이를테면 텍스트뷰에 저장되어 있는 이름이라던가, 리사이클러뷰에 있는 어뎁터라던가.. 하는 것들이 사라지게 됩니다. 그런데 Livedata를 쓰면 이러한 상황이 되었을 때 알아서 다시 최신 데이터를 뿌려줍니다. 그러면 액티비티에서는 그러한 데이터들을 잃어버릴 일이 없습니다.


예를들자면,


override fun onActivityResult( ... ){
my_text_view.text = intent.getStringExtra("NAME_KEY")
}

와 같은 함수가 있다고 칩시다.


이렇게 구현을 하면 화면을 회전하였을 때 my_text_view에 있는 텍스트는 날아가버립니다! 


여태 저희는 이를 방지하기 위해 Shared preference에 저장한다던가 하는 쓸대없는 구현을 하였죠


그런데 만약,


viewModel.name.observe(this, Observer{
my_text_view.text = it
})

과 같이 옵저빙을 수행하고,


override fun onActivityResult( ... ){
viewModel.changeName(intent.getStringExtra("NAME_KEY"))
}



와 같이, onActivityResult에서는 뷰모델의 함수를 호출해 준 다음, 뷰모델에서는


val nameLiveData = MutableLiveData<String>()

fun changeName(name:String){
nameLiveData.postValue(name)
}

이렇게 구현했다고 칩시다.


그러면 나중에 만약 화면이 회전되어서 onDestroy -> onCreate순으로 호출 되었다고 하더라도,


viewModel.name.observe(this, Observer{}) 에서 기존의 데이터를 다시 받아와서 my_text_view를 바꿔줍니다.


이전과 같이 다른곳에 별도로 저장한다거나 할 필요가 없어지게 되었죠.







자 그럼 실제 코드를 보며 어떻게 쓰나 한번 살펴보도록 합시다.









ViewModel



https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/viewmodel/MainViewModel.kt



class MainViewModel(private val model: DataModel) : BaseKotlinViewModel() {

private val TAG = "MainViewModel"

private val _imageSearchResponseLiveData = MutableLiveData<ImageSearchResponse>()
val imageSearchResponseLiveData:LiveData<ImageSearchResponse>
get() = _imageSearchResponseLiveData

fun getImageSearch(query: String, page:Int, size:Int) {
addDisposable(model.getData(query, KakaoSearchSortEnum.Accuracy, page, size)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.run {
if (documents.size > 0) {
Log.d(TAG, "documents : $documents")
_imageSearchResponseLiveData.postValue(this)
}
Log.d(TAG, "meta : $meta")
}
}, {
Log.d(TAG, "response error, message : ${it.message}")
}))
}
}


이전 글에서도 나왔던 코드입니다. 이번에는 이전 글에서 말하지 않았던 _imageSearchResponseLiveData.postValue() 에 대해 설명해보겠습니다.



일단 그전에, 클래스의 멤버들을 한번 보죠.


여태까지 제가 말씀드린 것과는 다르게 언더바로 시작하는 변수와 그렇지 않은 변수가 서로 쌍으로 붙어있습니다.


이것은 외부에서는 Livedata를 변경하지 못하게 하고, 내부에서는 변경이 가능하게 하기 위한 구현입니다


저런식으로 구현을 하게 되면, ViewModel에서는 _image.....LiveData.postValue()로 데이터를 변경할 수 있게 되고,


image....LiveData로 언더바가 없는 변수를 외부에서 참조함으로서 postValue는 수행하지 못하고 observe만 가능하게 한 것이죠.



그럼 이제 아시겠지만, 이 코드는 결국 네트워크로 데이터를 받아서 LiveData의 값을 postValue로 바꿔주고 있는 것입니다.


그럼 이 LiveData를 View에서는 Observing함으로써 값의 변경을 알아차릴수 있게 되는것이죠.



이제 아까 앞에서 넘어갔던 setValue와 postValue의 차이점을 서술해보겠습니다.


만약 setValue나 postValue를 호출하는 당사자가 UI스레드일경우, 둘은 차이가 없습니다.


그런데 만약 UI스레드가 아닐경우, setValue로 세팅한 값은 UI를 변경하지 못합니다.


대신 postValue로 세팅한 값은 해당 값을 UI스레드로 post해주기 때문에 UI스레드가 아니라도 UI를 변경할 수 있게 됩니다.




자 이제 ViewModel의 Livedata 설명이 끝났습니다.


View의 코드를 간략히 설명하고 이번 글도 끝마치도록 하겠습니다.






View


https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/view/MainActivity.kt


class MainActivity : BaseKotlinActivity<ActivityMainBinding, MainViewModel>() {
override val layoutResourceId: Int
get() = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()

private val mainSearchRecyclerViewAdapter: MainSearchRecyclerViewAdapter by inject()

override fun initStartView() {
main_activity_search_recycler_view.run {
adapter = mainSearchRecyclerViewAdapter
layoutManager = StaggeredGridLayoutManager(3, 1).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
orientation = StaggeredGridLayoutManager.VERTICAL
}
setHasFixedSize(true)
}
}

override fun initDataBinding() {
viewModel.imageSearchResponseLiveData.observe(this, Observer {
it.documents.forEach {document ->
mainSearchRecyclerViewAdapter.addImageItem(document.image_url, document.doc_url)
}
mainSearchRecyclerViewAdapter.notifyDataSetChanged()
})
}

override fun initAfterBinding() {
main_activity_search_button.setOnClickListener {
viewModel.getImageSearch(main_activity_search_text_view.text.toString(), 1, 80)
}
}

}


여기서 눈여겨 보셔야 할 부분은 initDataBinding()입니다.


BaseKotinActivity에서 알수있다시피, 해당 함수는 onCreate에서 호출됩니다.


그래서 데이터 바인딩을 수행하게 되는데, 보시면


viewModel의 imageSearchResponseLiveData에 observe를 호출하고 있습니다.


만약 imageSearchResponseLiveData의 값이 바뀌게 되면 observe의 인자로 주어진 Observe{}를 호출하게 되죠.


지금은 간단하게 adapter에 document의 image_url과 doc_url을 추가하고 있습니다.


그리고 마지막으로 adpater에 notifyDataSetChanged()를 호출함으로써 리사이클러 뷰의 모습을 갱신하고 있죠.







이로써 대부분의 코드를 거의 다 설명한것 같습니다.


아직 남은 코드는 SingleLiveData와 SnackbarMessage가 남아있네요. 이것에 대해서는 다음 글에 이어서 설명하겠습니다.




여기까지 읽어주셔서 감사합니다.