앱을 멀티 모듈로 변경하는 과정에서 각 특정 모듈에서 발생하는 의존도 문제를 경험하며 Android에 의존적인 LiveData의 사용을 중단하고, 이로 작성 된 코드들을 Flow 로 변경하는 작업을 진행하였습니다.
이 작업을 통해 Android에 덜 의존적인 ViewModel을 작성할 수 있었습니다.
아래 나열할 내역들은 위 작업에서 주로 사용되었던 테크닉을 정리하고, 누군가 저와 동일한 작업을 진행할 때 도움이 되었으면 좋겠습니다.
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을 미연에 방지할 수 있습니다.
/* ViewModel */
val lvFontSize: NonNullMutableLiveData<Int> = NonNullMutableLiveData(12)
before
/* ViewModel */
val flowFontSize = MutableStateFlow(12)
after
저는 편의를 위해 NotNullMutableLiveData
라는 Nullable 하지 않은 MutableLiveData
라는 클래스를 만들어서 활용하였으나,
일반 MutableLiveData
역시 MutableStateFlow
를 사용하여 대체하여 사용할 수 있었습니다.
/* 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
하지만 아래와 같은 경우도 있을 수 있습니다.
만약 사용자가 화면을 잠그거나, 다른 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
/* 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에 따른 처리를 구현하는 방식을 선택하였습니다.
취향에 따라 둘 중 편한 방식으로 구현하시면 될 것 같습니다.
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