LiveData, Event Wrapper에서 StateFlow, SharedFlow로

Donggi Hong·2022년 5월 23일
0
post-custom-banner

앱을 멀티 모듈로 변경하는 과정에서 각 특정 모듈에서 발생하는 의존도 문제를 경험하며 Android에 의존적인 LiveData의 사용을 중단하고, 이로 작성 된 코드들을 Flow 로 변경하는 작업을 진행하였습니다.

이 작업을 통해 Android에 덜 의존적인 ViewModel을 작성할 수 있었습니다.

아래 나열할 내역들은 위 작업에서 주로 사용되었던 테크닉을 정리하고, 누군가 저와 동일한 작업을 진행할 때 도움이 되었으면 좋겠습니다.

SafeCollect

suspend inline fun <T> Flow<T>.safeCollect(crossinline action: suspend (T) -> Unit) {
	collect {
		coroutineContext.ensureActive()
		action(it)
	}
}

lifeCycle 등의 이유로 Coroutine이 중단되어도, Collect는 중단되지 않고 계속해서 수집을 하는 경우가 있을 수 있습니다. 이 경우, collect는 CancellationException을 발생시킵니다.

safeCollect는 매 수집마다 Coroutine의 상태를 체크하고, 중단되어 있지 않을 때만 Collect를 이어서 진행하기에, 위 Exception을 미연에 방지할 수 있습니다.


(NotNull)LiveData

/* ViewModel */
val lvFontSize: NonNullMutableLiveData<Int> = NonNullMutableLiveData(12)

before

/* ViewModel */
val flowFontSize = MutableStateFlow(12)

after

저는 편의를 위해 NotNullMutableLiveData라는 Nullable 하지 않은 MutableLiveData라는 클래스를 만들어서 활용하였으나,
일반 MutableLiveData 역시 MutableStateFlow를 사용하여 대체하여 사용할 수 있었습니다.


Fragment에서 LiveData Observe하기

/* ViewModel */
val lvTransparency: MutableLiveData<Int> = MutableLiveData(170)
val lvFontSize: MutableLiveData<Int> = MutableLiveData(16)

/* Fragment */
mVM.lvTransparency.observe(viewLifecycleOwner) {
	// DO SOMETHING
}
mVM.lvFontSize.observe(viewLifecycleOwner) {
	// DO SOMETHING
}

before

물론 반드시 Fragment에서 ViewModel의 LiveData를 Observe 할 필요는 없습니다.

위와 같은 방식으로 MutableLiveData 대신 MutableStateFlow를 사용하였으며, Observe 대신 Fragment에서 collect를 통해 값의 변동을 수집하여 아래처럼 처리를 진행할 수 있습니다.

/* ViewModel */
val lvTransparency = MutableStateFlow(170)
val lvFontSize = MutableStateFlow(16)

/* Fragment */
lifecycleScope.launch {
	launch {
    	mVM.lvTransparency.collect {
        	// DO SOMETHING
    	}
    }
    
    launch {
    	mVM.lvFontSize.collect {
        	// DO SOMETHING
    	}
    }
}

after

하지만 아래와 같은 경우도 있을 수 있습니다.

  • 1초마다 화면에 시간을 갱신헤야 하는 시계

만약 사용자가 화면을 잠그거나, 다른 Activity가 발생된 경우는 1초마다 화면을 갱신할 필요가 없지만 계속 갱신되어 성능상으로 손해가 발생하겠죠.

원래대로라면 onStop때 collect를 해제한 후, onStart때 다시 collect 하는 방법으로 개선 시킬 수 있습니다..만!

implement "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01"

다행히도 매 번 구현시킬 필요 없도록 위 라이브러리가 implement 되어 있다면 repeatOnLifecycle이라는 이름으로 사용할 수 있습니다.

/* UI */
val lvTransparency = MutableStateFlow(170)
val lvFontSize = MutableStateFlow()

/* UI */
lifecycleScope.launch {
	viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
		launch {
        	mVM.lvTransparency.collect {
    			// DO SOMETHING
			}
		}
        
        launch {
        	mVM.lvFontSize.collect {
    			// DO SOMETHING
			}
    	}
	}
}

one step

위와 같이 사용하면 lifeCycle의 Start, Stop마다 자동으로 구독을 중지하고 계속 이어서 진행합니다.

하지만 이런식으로 반복해서 코드를 작성해야 하는게 번거롭다면 아래와 같이 Extension 함수로 만들어두고 사용할 수 있습니다.

fun LifecycleOwner.repeatOnLifeCycleStarted(
    state: Lifecycle.State = Lifecycle.State.STARTED,
    block: suspend CoroutineScope.() -> Unit
){
    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(state, block)
    }
}
/* extension */


viewLifecycleOwner.repeatOnLifeCycleStarted {
	launch {
		mVM.lvTransparency.collect {
			// DO SOMETHING
		}
	}
        
	launch {
		mVM.lvFontSize.collect {
			// DO SOMETHING
		}            
    }
}
/* UI */

two step


Livedata + Event Wrapper사용

/* ViewModel */
private val lvMakeToast = MutableLiveData<Event<String>>()
val makeToast = LiveData<Event<String>> = lvMakeToast

private val lvFinishActivity = MutableLiveData<Event<Boolean>>()
val finishActivity = LiveData<Event<Boolean>> = lvFinishActivity

lvMakeToast.value = Event("Toast Message")
lvFinishActivity.value = Event(true)

/* UI */
viewModel.lvFinishActivity.observe(viewLifecycleOwner, EventObserver {
	// DO SOMETHING
})

viewModel.lvMakeToast.observe(viewLifecycleOwner, EventObserver { msg ->
	// DO SOMETHING
})

before

구글 개발자가 사용한 이후 많은 사람들이 주로 사용하는 Event Wrapper Class를 통한 Event 전달은 아래 after과 같이 대체하여 사용할 수 있습니다.

/* ViewModel */
private var _eventFlow = MutableSharedFlow<Event>()
val eventFlow = _eventFlow.asSharedFlow()

viewModelScope.launch {
	_eventFlow.emit(Event.MakeToast("Toast Message")))
	_eventFlow.emit(EventEvent.FinishThisActivity()))
}

/* UI */
lifecycleScope.launch { 
    eventFlow.collect { event ->
        when (event) {
            is Event.MakeToast -> {
            	// DO SOMETHING
            }
            is Event.FinishThisActivity -> {
            	// DO SOMETHING
            }
        }
    }
}

/* Sealed Class */
sealed class Event {
    data class MakeToast(val message: String): Event()
    data class FinishThisActivity(val unit: Unit = Unit): Event()
}

after (1)

/* ViewModel */
private var _sfMakeToast = MutableSharedFlow<String>()
val sfMakeToast = _sfMakeToast.asSharedFlow()
private var _sfFinishActivity = MutableSharedFlow<Unit>()
val sfFinishActivity = _sfFinishActivity.asSharedFlow()

viewModelScope.launch {
	_sfMakeToast.emit("Toast Message")
	_sfFinishActivity.emit()
}

/* UI */
lifecycleScope.launch { 
    launch {
        sfMakeToast.collect { 
            // DO SOMETHING
        }
    }
    
    launch {
    	sfFinishActivity.collect {
            // DO SOMETHING
    	}
    }
}

after (2)

저는 after(1) 처럼 sealed class를 사용하여 Event의 형식을 미리 선언해두고, when문으로 Event Type에 따른 처리를 구현하는 방식을 선택하였습니다.

취향에 따라 둘 중 편한 방식으로 구현하시면 될 것 같습니다.


MediatorLiveData

val checkedAll: MediatorLiveData<Boolean> = MediatorLiveData()
val checked1: NotNullMutableLiveData<Boolean> = NotNullMutableLiveData(false)
val checked2: NotNullMutableLiveData<Boolean> = NotNullMutableLiveData(false)
val checked3: NotNullMutableLiveData<Boolean> = NotNullMutableLiveData(false)
    
checkedAll.apply {
	addSource(checked1) {
		value = checked1.value?:false && checked2.value?:false && checked3.value?:false
	}
	addSource(checked2) {
		value = checked1.value?:false && checked2.value?:false && checked3.value?:false
	}
	addSource(checked3) {
		value = checked1.value?:false && checked2.value?:false && checked3.value?:false
	}
}

before

MediatorLiveData는 여러 MutableLiveData에 의존하는 LiveData를 손쉽게 컨트롤하는데 많은 도움이 됩니다.

이는 Flow에서는 combine기능을 사용해서 비슷하게 동작하도록 구현할 수 있습니다.

val checked1 = MutableStateFlow(false)
val checked2 = MutableStateFlow(false)
val checked3 = MutableStateFlow(false)

viewModelScope.launch {
	combine(checked1, checked2, checked3) { a, b, c ->
		a && b && c
	}.collect {
		isConfirmEnable.value = it
	}
}

after

참고

ALZA - [안드로이드] repeatOnLifecycle 을 사용하며.

profile
꿈 많은 응애 안드로이드 개발자
post-custom-banner

0개의 댓글