본문 바로가기

안드로이드/안드로이드 개인공부

[안드로이드 개인공부] LiveData를 통한 데이터 바인딩 MVVM적용하기

참고하여 공부한 링크 : https://deque.tistory.com/112?category=984011 

 

MVVM구조는 저번 글에서 설명한 것 처럼, 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_test_view의 값을 변경하게 된다.

 

LiveData의 값을 변경하기 위해서는 MutableLiveData로 선언해야하고, 값의 변경은 postValue or setValue로 변경이 가능하다.

이둘의 차이는 밑에서 알아보자.

 

근데 왜? Rx가 있는데 LiveData를 사용할까?

그건 LiveData의 여러가지의 장점이 있기 때문이다.

 

먼저 공식 문서다.

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

첫째, UI와 데이터 상태의 일치를 보장해준다.

-> 옵저빙하기 때문에 일치된다.

 

둘째, 메모리 누출이 없다.

-> RxJava를 사용할 경우, addDisposable이나, compositeDisposable.clear()를 사용하여 메모리 관리를 해주어야하는데 그럴 필요가 없다.

 

셋째, 중지된 활동으로 인한 비정상 종료가 없다.

-> 간혹 코딩할 경우, 뷰가살아 있을 때만 뷰의 업데이트를 수행하기 위해 여러가지 플래그를 둔다거나, 하는 코딩을 하게 된다.

그럴 필요가 없다.

 

넷째, 최신 데이터를 유지한다.

-> onStop으로 자고 있다가, onStart로 다시 깨어나면, 최신 데이터를 받아서 뿌려준다.

 

다섯째, 적절한 구성 변경

-> 적절한 화면 구성변경, 보통 화면을 회전하건, 화면 분할을 수행하면 액티비티는 onDestroy수행하고 다시 onCreate를 통해 뷰를 그린다. 이런 과정에서 뷰의 여러가지 구성요소에 할당한 데이터들, 이를테면 텍스트뷰에 저장되어 있는 이름이라던가, 리사이클러뷰에 있는 어뎁터라던가 하는 것들이 사라지게 된다. 그런데 LiveData를 사용하면, 다시 최신 데이터를 뿌려주게 된다. 그러면 엑티비티는 데이터를 잃어버릴 일이 없다.

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

예를 들어 이런 메소드가 있다고 하면

화면을 회전할 경우,  my_text_view에 있는 텍스트는 날아가 버린다.

그래서 이를 방지하기 위해,  SharedPreference에 저장한다던가 하는 코드를 구현했습니다.

그런데, 만약에 

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

이와 같이 옵저빙을 하고,

override fun onActivityResult( ... ){
viewModel.changeName(intent.getStringExtra("NAME_KEY"))
}
val nameLiveData = MutableLiveData<String>()

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

onActivityResult에서 뷰 모델의 함수를 호출해 준다면, 화면이 회전될 때

onDestroy -> onCreate순으로 호출되었다 하더라도, 

viewModel.name.observe(this, Observe{})에서 기존의 데이터를 받아와서  my_text_view를 바꿔주게 된다.

즉, 이전과 같이 다른 곳에 별도로 저장할 필요가 없어지게 된것이다.

 

예제를 통해 공부해 봅시다.

참고한 깃허브 주소 : https://github.com/5seunghoon/Kotlin-MVVM-Sample/blob/master/app/src/main/java/com/tistory/deque/kotlinmvvmsample/viewmodel/MainViewModel.kt

ViewModel

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와, imageSearchResponseLiveData가 있다.

_image...LiveData는 LiveData를  외부에서 변경할 수 없게 지정하기 위해서 사용되는 변수이고, 

image..LiveData는 내부에서 변경할때 사용하기 위한 변수이다.

 

즉, 위와 같이 구현하면, 네트워크로부터 데이터를 받아서  LiveData의 값을  postValue를 통해 바꿔주고 있는 것이다.

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

 

 <setValue와 postValue의 차이점>

 setValue와 postValue를 호출하는 당사자가  UI  스레드일 경우, 둘의 차이는 없다.

하지만 UI 스레드가 아닌 경우, setValue로 세팅한 값은 UI에 적용되지 않는다.

대신  postValue는 UI스레드로  post해주기 때문에, UI스레드가 아니라도 UI를 변경할 수 있게 된다.

 

View

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입니다.

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

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

 

위의 코드에서는 간단하게 adapter에 document의 image_url과 doc_url을 추가하고 있다.

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