LiveData란? (간략히 정리하기)
LiveData는 관찰가능한(Observable) 데이터 홀더 클래스이다. 안드로이드 클린 아키텍처로 MVVM(Model-View-ViewModel)을 지향하고 있다. MVVM 아키텍처에서 VIewModel과 View의 데이터 이벤트 처리방식으로 LiveData를 많이 이용한다. 관찰가능한 다른 클래스와 다르게 LiveData는 액티비티, 프레그먼트, 서비스 등의 라이프사이클을 인지하여 활성화 상태(여기서 활성화만 START 또는 RESUME 상태이다) 인 경우에만 데이터를 업데이트 한다. 이전에 LiveData가 아닌 직접 구현하여 사용하였던 방식을 편하게 사용할 수 있게 되었다.
LiveData의 observe() 함수형은 아래와 같다. 첫 인자로 LifecycleOwner를 전달하고, 두 번째 인자로 Observer 인스턴스를 전달한다. LifecycleOwner는 위에서 설명한 것 처럼 액티비티, 프레그먼트, 서비스등의 라이프사이클을 인지하도록 전달하는 것이다.
또한 LiveData 객체에 전달하는 Observer 객체는 LiveData가 가지고 있는 데이터에 어떠한 변화가 일어날 경우, LiveData는 등록된 Observer 객체에 변화를 알려주고, Observer의 onChanged() 메소드가 실행되게 된다.
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer)
예제를 이용하여 동작원리 및 문제점 그리고 보완점에 대해서 정리해보려 한다. 예제는 액티비티에서 버튼 클릭 시 상태가 변화하도록 한다. 상태는 아래와 같이 선언되어 있다.
enum class State {
READY,
START,
RUNNING,
EXIT
}
1. LiveData
LiveData의 아키텍처 관점 (Presentation, Domain Layer 관련)은 다른 글에서 별도로 다루니 여기서는 LiveData에 대해 간단하게 정리만 한다.
LiveData를 사용한 예제를 통해 어떻게 ViewModel에서 Data의 Event를 처리하는지 확인해보자. LiveData의 인스턴스는 ViewModel 클래스 내에서 생성하도록 권장한다. ViewModel내에 아래와 같이 선언한다.
private val _stateEvent = MutableLiveData(State.READY)
val stateEvent: LiveData<State> = _stateEvent
전체 예제 코드는 아래와 같다.
Activity
LiveData에 Observe() 함수를 호출하여 관찰하는 Observer 인스턴스를 결합하는 코드는 컴포넌트의 onCreate() 함수 내에 위치하도록 구현하였다. onCreate() 함수 내에 구현하는 이유는 크게 2가지 이다.
- 안드로이드 생명주기인 onResume()에 LiveData를 observe() 하게되는 경우 Pause 또는 Stop에 의해서 잠시 백그라운드 상에서 inactive(비활성화)된 앱이 다시 active(활성화)가 되면서 LiveData에 observe() 코드가 중복 호출이 된다.
- 액티비티나 프래그먼트가 활성화되는 즉시 UI에 표시 할 수 있는 데이터를 가질 수 있기 때문에 해당 컴포넌트는 STARTED 상태가 되자마자 LiveData 객체로부터 가장 최신의 값을 수신해야 한다.
class Step1Activity : AppCompatActivity() {
private val viewModel:Step1ViewModel by viewModels()
private var mBinding: ActivityStep1Binding? = null
private val binding get() = mBinding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityStep1Binding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnState.setOnClickListener {
viewModel.changeNextState()
}
viewModel.stateEvent.observe(this) { state ->
Toast.makeText(this, state.toString(), Toast.LENGTH_SHORT).show()
}
}
}
ViewModel
class Step1ViewModel: ViewModel() {
private val _stateEvent = MutableLiveData(State.READY)
val stateEvent: LiveData<State> = _stateEvent
fun changeNextState() = viewModelScope.launch {
when (_stateEvent.value) {
State.READY -> _stateEvent.value = State.START
State.START -> _stateEvent.value = State.RUNNING
State.RUNNING -> _stateEvent.value = State.EXIT
State.EXIT -> _stateEvent.value = State.READY
else -> {}
}
}
}
문제점
위의 코드에서 문제점은 무엇일까? LiveData는 컴포넌트가 활성화 되는 시점에 다시 관찰을 시작하면서 마지막 데이터는 수신하게 된다. 즉 예제에서 Toast를 생성하는 경우, 디바이스를 회전시켜 화면의 회전이벤트가 발생하게 되면, Toast 메시지가 지속적으로 발생하게 되는 문제가 있다.
- 액티비티 처음 실행 시 LiveData의 observe() 수행되며 Toast 메시지 실행
- 디바이스의 화면 회전 시킴
- LiveData를 관찰하고있던 Observer가 비활성화되었다가 다시 활성화되면서 관찰 시작
- 1번의 Toast띄우라는 직전의 Event가 다시 날라옴 (READY 상태의 토스트가 동일하게 생성)
즉 이미 사용된 마지막 데이터가 지속적으로 반복 전달되는 것이다. 이를 해결하기 위해 LiveData와 Event 클래스를 사용하여 중복된 데이터를 해결하는 방법을 사용한다.
2. LiveData + Event
LiveData를 사용했을 때의 문제점을 해결하기 위해 이미 발생한 이벤트에 대한 처리를 해주는 Event 클래스를 선언하여 사용한다. LiveData를 이용하여 UI에 이벤트 전달 시 Event 클래스에 감싸서 전달하고, UI에서는 해당 이벤트가 사용되었는지 확인한 후 사용되지 않았다면 전달하고, 사용되었다면 null을 전달한다.
Event class
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
LiveData<State> 였던 선언이 LiveData<Event<State>> 로 State를 Event로 감싼 형태를 사용한다.
ViewModel
class Step2ViewModel : ViewModel() {
private val _stateEvent = MutableLiveData<Event<State>>(Event(State.READY))
var stateEvent: LiveData<Event<State>> = _stateEvent
fun changeNextState() = viewModelScope.launch {
when (_stateEvent.value?.peekContent()) {
State.READY -> _stateEvent.value = Event(State.START)
State.START -> _stateEvent.value = Event(State.RUNNING)
State.RUNNING -> _stateEvent.value = Event(State.EXIT)
State.EXIT -> _stateEvent.value = Event(State.READY)
else -> { }
}
}
}
Activity
액티비티에서 LiveData를 observe()함수 호출 시 전달하는 Observer 인스턴스의 핸들러 코드가 변경됨을 유의깊게 확인한다. Event 클래스에 감싸진 State를 얻기 위해 getContentIfNotHandled() 함수를 사용한다. 이미 해당 데이터가 호출이 되었는지를 확인하고 호출되지 않았다면 null이 아닌 정상적인 값이기에 Toast를 생성하는 let 문이 실행된다.
class Step2Activity : AppCompatActivity() {
private val viewModel:Step2ViewModel by viewModels()
private var mBinding: ActivityStep2Binding? = null
private val binding get() = mBinding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityStep2Binding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnState.setOnClickListener {
viewModel.changeNextState()
}
viewModel.stateEvent.observe(this) { it ->
it.getContentIfNotHandled()?.let { state ->
Toast.makeText(this, state.toString(), Toast.LENGTH_SHORT).show()
}
}
}
Event 클래스를 활용하여 LiveData만을 사용하였을 때의 문제점을 해결한다. 다음 글에는 위 예제들을 대체할 수 있는 SharedFlow와 StateFlow에 대해 정리해본다.
Reference
'Android > Component' 카테고리의 다른 글
[Android] Fragment 커스텀 생성자? FragmentFactory? (2) | 2022.11.25 |
---|---|
[Android] MutableFlowState의 원자성 보장 (0) | 2022.10.27 |
[Android] 하단 탭 사용하기 (BottomNavigation) (0) | 2022.08.31 |
[Android] WearOS HealthServicesClient 성능 문제 분석기 (0) | 2022.08.16 |
[Android] Google Map Key 관리 (0) | 2022.08.16 |