MVVM에서 viewModel 이벤트를 받을 수 있는 방법
- SharedFlow, Sealed class로 이벤트 처리하기
- SharedFlow & Sealed class & LifeCycle로 이벤트 처리하기
- EventFlow & Sealed class Lifecyle로 이벤트 처리하기
이전 블로그에서는 아래의 내용에 대해 알아봤습니다.
- LiveData만 사용해서 이벤트 처리하기
- LivieData에 EventFlow를 래핑해서 처리하기
- SingleLiveData로 이벤트 처리하기
- StateFlow, SharedFlow로 이벤트 처리하기
SharedFlow, Sealed class로 이벤트 처리하기
SharedFlow를 사용하다 보니 생기는 문제가 모든 sharedFlow 데이터에 대해 변수를 지정해야 한다는 것입니다.
예를 들면 다음과 같이 viewModel쪽에 다수의 변수를 입력해야 합니다.
SharedFlowEventViewModel.kt
private val _someData = MutableSharedFlow<Int>()
val someData = _someData.asSharedFlow()
private val _otherData = MutableSharedFlow<String>()
val otherData = _otherData.asSharedFlow()
private val _theOtherData = MutableSharedFlow<Boolean>()
val theOtherData = _theOtherData.asSharedFlow()
private val _eventFlow = MutableSharedFlow<SharedFlowEvent>()
val eventFlow = _eventFlow.asSharedFlow()
그리고 해당 sharedFlow을 수신하기 위해 똑같은 수의 collect를 실시해야 했습니다.
Activity
lifecycleScope.launch {
launch {
sharedFlowEventViewModel.someData.collect {
}
}
launch {
sharedFlowEventViewModel.otherData.collect {
}
}
launch {
sharedFlowEventViewModel.eventFlow.collect {
}
}
launch {
sharedFlowEventViewModel.theOtherData.collect {
}
}
}
이를 event를 보내주는 클래스 하나와 해당 event를 묶어주는 sealded class를 만들어서
한 번의 collect로 모든 shreadFlow를 수신할 수 있게 되었습니다.
SharedFlowEventViewModel.kt
class SharedFlowEventViewModel : ViewModel() {
private val _eventFlow = MutableSharedFlow<SharedFlowEvent>()
val eventFlow = _eventFlow.asSharedFlow()
// 각각의 이벤트를 발생시킨다.
fun emitSomeData() {
emitEvent(SharedFlowEvent.SomeData(1))
}
fun emitOtherData() {
emitEvent(SharedFlowEvent.OtherData("abc"))
}
fun emitTheOtherData() {
emitEvent(SharedFlowEvent.TheOtherData(true))
}
// 이벤트를 flow에 넣는다.
private fun emitEvent(sharedFlowEvent: SharedFlowEvent) {
viewModelScope.launch {
_eventFlow.emit(sharedFlowEvent)
}
}
// 수신한 데이터를 각각의 이벤트에 넣어준다.
sealed class SharedFlowEvent {
data class SomeData(val number: Int): SharedFlowEvent()
data class OtherData(val text: String): SharedFlowEvent()
data class TheOtherData(val ox: Boolean): SharedFlowEvent()
}
}
그리고 Activity에서 다음과 같은 방법으로 발동하고 수신할 수 있습니다.
Activity
// 하나의 이벤트만 수신하고 어떤 이벤트인지 분류하여 각기 다른 행동을 하게 한다.
lifecycleScope.launch {
sharedFlowEventViewModel.eventFlow.collect {
when (it) {
is SharedFlowEventViewModel.SharedFlowEvent.OtherData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
is SharedFlowEventViewModel.SharedFlowEvent.SomeData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
is SharedFlowEventViewModel.SharedFlowEvent.TheOtherData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
}
}
}
binding.sharedSealedButton.setOnClickListener {
sharedFlowEventViewModel.emitSomeData()
sharedFlowEventViewModel.emitOtherData()
sharedFlowEventViewModel.emitTheOtherData()
}
위와 같은 방법으로 하나의 event만 collect 하면서 다수의 flow를 처리할 수 있습니다.
SharedFlow & Sealed class & LifeCycle로 이벤트 처리하기
위 방법처럼 eventFlow를 발생시키면 사용자가 화면을 보고 있지 않더라도
계속 collect를 하고 있는 상황이 발생합니다.
예를 들어
- 특정 flow를 collect 한다.
- 유저가 앱을 켜둔 상태에서 배경화면으로 이동한다.
- 이 상태에선 해당 데이터를 collect할 필요가 없다.
위와 같은 경우에는 다음과 같이 처리할 수 있습니다.
LifecycleOwner에 있는 repeatOnLifecycle을 사용하면 다음과 같은 확장 함수를 만들 수 있습니다.
함수를 받아서 lifecycle의 상태가 Start 상태일 때 on
Start 상태가 아닐 때 off 를 해주는 기능을 만들 수 있습니다.
LifecycleExt.kt
fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
}
}
이를 Activity에서 다음과 같이 사용할 수 있습니다.
Activity
repeatOnStarted {
sharedFlowEventViewModel.eventFlow.collect {
when (it) {
is SharedFlowEventViewModel.SharedFlowEvent.OtherData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
is SharedFlowEventViewModel.SharedFlowEvent.SomeData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
is SharedFlowEventViewModel.SharedFlowEvent.TheOtherData -> {
Timber.d("sharedFlowEventViewModel is collected: $it")
}
}
}
}
EventFlow & Sealed class Lifecyle로 이벤트 처리하기
위와 같은 방법을 사용하면 다음과 같은 케이스에 대응할 수 없었습니다.
- 앱에서 flow을 사용해서 특정 데이터를 collect
- collect중인데 사용자가 바탕화면으로 이동해 버림
- collect이 그냥 멈춰버림
- sharedFlow는 hot stream이기 때문에 값을 collect 하지 않는 상태에서 emit 하면 그냥 그 값을 버림
- 다시 앱으로 들어가면 에러가 발생하거나 처음부터 다시 collect 해야 함
그렇기 때문에 사용자가 바탕화면으로 가면 작업이 그대로 멈추고 emit된 값이 사라져버리는게 가장 큰 문제였습니다.
그래서 백그라운드 상태에서 collect되었을 때 해당 데이터를 임시로 갖고 있는 EventFlow를 만들었습니다.
EventFlow.kt
interface EventFlow<out T> : Flow<T> {
companion object {
// emit된 이벤트의 최대 저장 갯수
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
// FlowCollector를 상속하면 데이터를 sharedFlow에 emit하기 전 데이터를 컨트롤할 수 있다.
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 ->
// emit됐지만 collect되지 않은 값들을 버퍼에서 가져와서 다시 collect 처리한다.
if (!slot.markConsumed()) {
// collect된 데이터가 소비되지 않았을 경우 해당 데이터를 emit한다.
collector.emit(slot.value)
}
}
override suspend fun emit(value: T) {
flow.emit(EventFlowSlot(value))
}
}
private class EventFlowSlot<T>(val value: T) {
// 다중 thread에서 안정성을 위해 AtomicBoolean을 사용
private val consumed: AtomicBoolean = AtomicBoolean(false)
// 한 번이라도 collect된 적이 있는지 확인한다.
fun markConsumed(): Boolean = consumed.getAndSet(true)
}
새롭게 만든 MutableEventFlow는 다음과 같이 동작합니다.
- collect된 데이터는 replay 값만큼 자동으로 저장됩니다.
- 사용자가 앱을 사용 중일 때 collect 되었다면(= collect를 사용해서 받을 준비가 되어있다면)
- 해당 값을 즉시 사용합니다
- 사용자가 앱을 사용하지 않는 상태라면(= 값을 emit 했는데 collect 상태가 아님) 데이터는 collect되지 않고 sharedFlow의 버퍼에 저장된 상태가 됩니다.(저장된 데이터 안에는 타이밍에 따라 이미 collect된 데이터가 있을 수 있음)
- 사용자가 앱을 다시 사용한다면 collect에 저장되었던 값들이 들어가 이미 collect된 적이 있는지 확인하고 한 번도 collect 된 적이 없는 값만 다시 emit 합니다.
다음과 같은 방법으로 viewModel에서 사용할 수 있습니다.
EventFlowViewModel.kt
class EventFlowViewModel: ViewModel() {
// 이 부분만 MutableEventFlow를 사용하도록 바꿔준다.
private val _eventFlow = MutableEventFlow<SharedFlowEvent>()
val eventFlow = _eventFlow.asEventFlow()
// 각각의 이벤트를 발생시킨다.
fun emitSomeData() {
emitEvent(SharedFlowEvent.SomeData(1))
}
fun emitOtherData() {
emitEvent(SharedFlowEvent.OtherData("abc"))
}
fun emitTheOtherData() {
emitEvent(SharedFlowEvent.TheOtherData(true))
}
// 이벤트를 flow에 넣는다.
private fun emitEvent(sharedFlowEvent: SharedFlowEvent) {
viewModelScope.launch {
_eventFlow.emit(sharedFlowEvent)
}
}
// 수신한 데이터를 각각의 이벤트에 넣어준다.
sealed class SharedFlowEvent {
data class SomeData(val number: Int): SharedFlowEvent()
data class OtherData(val text: String): SharedFlowEvent()
data class TheOtherData(val ox: Boolean): SharedFlowEvent()
}
}
Activity나 Fragment 에서 사용방법은 동일하다.
Activity
repeatOnStarted {
eventFlowViewModel.eventFlow.collect {
when (it) {
is EventFlowViewModel.SharedFlowEvent.OtherData -> {
Timber.d("EventFlowViewModel is collected: $it")
}
is EventFlowViewModel.SharedFlowEvent.SomeData -> {
Timber.d("EventFlowViewModel is collected: $it")
}
is EventFlowViewModel.SharedFlowEvent.TheOtherData -> {
Timber.d("EventFlowViewModel is collected: $it")
}
}
}
}
위와 같은 방법으로 EventFlow를 정의하여 최적화와 동시에 이전에 emit된 데이터도 처리할 수 있는 기능을 갖게 되었다.
위 포스팅은 아래의 블로그를 참조하여 재구성했습니다!
'안드로이드(kotlin)' 카테고리의 다른 글
안드로이드 코틀린 EncryptedSharedPreferences 사용 방법 (0) | 2023.05.28 |
---|---|
안드로이드 앱 파일 만들기(APK) (0) | 2023.04.26 |
MVVM에서 viewModel 이벤트를 받을 수 있는 방법-2 (0) | 2023.03.30 |
MVVM에서 viewModel 이벤트를 받을 수 있는 방법-1 (0) | 2023.03.26 |
안드로이드 매니페스트(AndroidManifest)의 역할은 무엇일까 (0) | 2023.03.22 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글