※ 헤이딜러 안드로이드 팀의 글을 참조했다.
https://medium.com/prnd/mvvm%EC%9D%98-viewmodel%EC%97%90%EC%84%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-6%EA%B0%80%EC%A7%80-31bb183a88ce
LiveData를 observe해서 값이 바뀔 때 이벤트를 처리하는 방법
A activity에서 observe로 토스트를 띄우는 이벤트를 처리했는데 B activity로 갔다 돌아오면 A activity로 다시 돌아왔을 때 이전에 실행된 토스트가 한번 더 띄워지는 문제가 발생한다.
왜냐하면 LiveData를 observe 하고 있는 observer는 inactive상태에서 active가 될 때 항상 마지막 값을 emit한기 때문이다.
따라서 한번 발생한 이벤트가 다시 발생하지 않도록 Event Wrapper를 사용하는 방법이 있다.
아래의 코드 처럼 한번 발생한 이벤트는 다시 발생하지 않도록 처리해줄 수 있다.
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
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
}
ViewModel 에서 LiveData를 사용하게되면 상단에 import android.xxx 와 같이 안드로이드 프레임 워크에 종속될 수 밖에 없는데 이런 종속성을 해결하기 위해 StateFlow, SharedFlow로 대체할 수 있다.
LiveData -> StateFlow
Event Wrapped LiveData -> SharedFlow
observe -> collect
처리해야할 이벤트가 여러개일 때 이벤트 만큼 SharedFlow를 만들수 있지만 그에 따라 각각의 SharedFlow를 collect해주어야하는 번거로움이 있다.
따라서 이벤트를 전파하는 하나의 flow만 만들고 여러개의 이벤트를 Seald class 형태로 만들어서 분기하여 처리해줄 수 있다.
// ViewModel
@HiltViewModel
class Step4ViewModel @Inject constructor() : ViewModel() {
private val _eventFlow = MutableSharedFlow<Event>()
val eventFlow = _eventFlow.asSharedFlow()
fun showToast() {
event(Event.ShowToast("토스트"))
}
fun aaa() {
event(Event.Aaa("aaa"))
}
fun bbb() {
event(Event.Bbb(36))
}
private fun event(event: Event) {
viewModelScope.launch {
_eventFlow.emit(event)
}
}
sealed class Event {
data class ShowToast(val text: String) : Event()
data class Aaa(val value: String) : Event()
data class Bbb(val value: Int) : Event()
}
}
// UI에서는 한개의 eventFlow만 collect
lifecycleScope.launch {
viewModel.eventFlow.collect { event -> handleEvent(event) }
}
...
private fun handleEvent(event: Event) = when (event) {
is Event.ShowToast -> // TODO
is Event.Aaa -> // TODO
is Event.Bbb -> // TODO
}
Lifecycle에서 repeatOnLifecycle이라는 함수가 추가되었는데 lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 이상 버전부터 사용 가능하다
따라서 생명주기에 맞추어 OnStart나 OnStop에서 코드를 작성해줄 필요가 없게 된다.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a new coroutine from the lifecycleScope
// since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// Suspend the coroutine until the lifecycle is DESTROYED.
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locations when the lifecycle is STARTED
// and stop collecting when the lifecycle is STOPPED
someLocationProvider.locations.collect {
// New location! Update the map
}
}
// Note: at this point, the lifecycle is DESTROYED!
}
}
}
이벤트가 발생하여 처리하기 전에 홈화면을 눌러 앱이 백그라운드로 내려가게 되면 해당 이벤트는 유실되어 다시 홈화면에서 앱으로 돌아올 때 이벤트가 발생했던 것을 알 수 없게 된다.
따라서 이벤트가 발생할 때 이를 캐시하고 있다가 이벤트의 consume 여부에 따라서 새로운 observer가있을 때 이벤트를 발생시킬 지 여부를 결정해주면 된다.
interface EventFlow<out T> : Flow<T> {
companion object {
const val DEFAULT_REPLAY: Int = 3
}
}
interface MutableEventFlow<T> : EventFlow<T>, FlowCollector<T>
@Suppress("FunctionName")
fun <T> MutableEventFlow(
replay: Int = EventFlow.DEFAULT_REPLAY
): MutableEventFlow<T> = EventFlowImpl(replay)
fun <T> MutableEventFlow<T>.asEventFlow(): EventFlow<T> = ReadOnlyEventFlow(this)
private class ReadOnlyEventFlow<T>(flow: EventFlow<T>) : EventFlow<T> by flow
private class EventFlowImpl<T>(
replay: Int
) : MutableEventFlow<T> {
private val flow: MutableSharedFlow<EventFlowSlot<T>> = MutableSharedFlow(replay = replay)
@InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) = flow
.collect { slot ->
if (!slot.markConsumed()) {
collector.emit(slot.value)
}
}
override suspend fun emit(value: T) {
flow.emit(EventFlowSlot(value))
}
}
private class EventFlowSlot<T>(val value: T) {
private val consumed: AtomicBoolean = AtomicBoolean(false)
fun markConsumed(): Boolean = consumed.getAndSet(true)
}