최근 코루틴 flow 에 대한 관심이 많아졌다.
rx보다 라이브러리 종속성이 없고 쓰는 방법또한 간편하다.
Rxjava도 충분히 좋은 라이브러리지만 러닝커브가 상대적으로 높고 너무나 많은 기능이 있어서 간단한 프로젝트를 할때는 오히려 과할수도 있다.
언제나 편하게 개발하고 싶은 극한의 효율을 추구하는 게으른 개발자인 나는 flow 를 이용해서 MVVM 이벤트를
제대로 관리하는 방법에 대해 공부해볼까 한다.
전에 flow에 대한 포스팅을 간단하게 한적이 있었는데
그 방식을 그대로 mvvm에 적용해도 사실 작동하는데는 큰 문제가 없다.
하지만 깊게 파보면 비효율적이고 예상치 못한 문제에 대처를 못하는걸 알 수 있을 것이다.
그걸 중점으로 이번 포스팅을 적어보겠다.
를 읽고 정리했음을 미리 밝힙니다.
최종코드가 완성되기전까지 변화된 6가지 과정에 대해서 설명이 나오는데
왜 이렇게 되었는지? 이렇게 하는 이유가 뭐였는지에 대해 충분히 납득되는 내용이라 꼭 읽어보면 좋을거 같다.
첫번째는 나도 자주 쓰던 LiveData 와 Event의 조합이다.
여기서 Event 는 여러가지가 될 수 있다. 토스트를 띄우라던지 새로운 텍스트를 출력한다던지 하는 다양한 케이스가 있을 수 있다.
처음에 직면하는 문제는 ViewModel 에서 LiveData를 이용해서 이벤트를 처리하면 이벤트가 한번만 발생하고 끝나는게 아니라 observe 될때마다 최근에 발생한 값이 다시 발생하는 문제가 있다.
나도 회사에서 개발하다가 생명주기 때문에 화면을 이동하다보니 한번 발생하고 말아야할 이벤트가 또 발생하는 경험이 있었는데 이 개념을 해결할 수 있는 방법으로
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
}
이렇게 사용하는데 코드를 읽어보면 Event 를 emit 할때 한번만 하도록 hasBeenHandled를 사용해서
사용됐다면 null 을 리턴해서 한번만 consume 되도록 만들었다는걸 알 수 있다.
이런식으로 위의 문제를 해결한다.
두번째는 SingleLiveData 다.
사실 이건 대단한 문제점이 있어서 사용하는건 아니고 코드를 좀 더 간결하게 작성하기 위해 도입되었다.
SingleLiveData 라는게 따로 제공되는건 아니고 직접 만들어준 클래스인데
LiveData를 만들때 LiveData<Evebt<XXX>> 이런식으로 사용할때마다 적어줘야하는 귀찮음을 덜어 주려고 만들어졌다.
abstract class SingleLiveData<T> {
private val liveData = MutableLiveData<Event<T>>()
protected constructor()
protected constructor(value: T) {
liveData.value = Event(value)
}
protected open fun setValue(value: T) {
liveData.value = Event(value)
}
protected open fun postValue(value: T) {
liveData.postValue(Event(value))
}
fun getValue() = liveData.value?.peekContent()
fun observe(owner: LifecycleOwner, onResult: (T) -> Unit) {
liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) }
}
fun observePeek(owner: LifecycleOwner, onResult: (T) -> Unit) {
liveData.observe(owner) { onResult(it.peekContent()) }
}
}
기존 LiveData와 동작과 사용법 자체는 크게 다르지 않지만 이렇게 만들어줌으로써
private val _showToastEvent = MutableSingleLiveData<String>()
val showToastEvent: SingleLiveData<String>() = _showToastEvent
이렇게 간단하게 적어주게 됐다.
세번쨰는 StateFlow, SharedFlow 의 이용이다.
기존 LiveData 는 안드로이드 플랫폼에서 제공하기 때문에 플랫폼에 종속된 코드를 만들 수 밖에 없었다.
또한 LiveData는 안드로이드 라이프사이클이 있는 UI 와 인터렉션하도록 디자인 되어있어서 비즈니스 레이어에서 사용은 무리가 있었다. 특히 비즈니스에 관련된 레이어를 플랫폼 독립적으로 구성하려면 더욱 더 무리가 있었다.
그러나 stateFlow 가 코루틴 언어에서 지원하게 되면서 대안으로서 사용할 수 있게 되었다.
안드로이드 플랫폼의 종속성에서 벗어난것이다.
livedata observe 대신 flow의 collect 로 바꿀 수 있게 됐다.
이전
// VieWModel
private val _showToastEvent = MutableSingleLiveData<String>()
val showToastEvent: SingleLiveData<String> = _showToastEvent
// UI
viewModel.showToastEvent.observe { text ->
// TODO
}
이후
// ViewModel
private val _showToastEvent = MutableSharedFlow<String>()
val showToastEvent = _showToastEvent.asSharedFlow()// UI
lifecycleScope.launch {
viewModel.showToastEvent.collect { text ->
// TODO
}
}
네번째는 SharedFlow + Sealed class
생략하겠다. 어렵지 않은 내용이고 원본을 읽어보는게 더 빠르다.
다섯번째는 SharedFlow + Sealed class + Lifecycle
여기서 lifecycle 의 개념이 추가되는데 이유는 아래와 같다.
Viewmodel에서 서버와 통신을 하면서 주기적으로 데이터를 받는다
UI에서 주기적으로 받는 데이터를 화면에 새로그리는 상황이다,
홈버튼을 눌러서 백그라운드로 앱이 내려갔을때는 화면에 새로 그릴 필요가 없다.
UI가 안보이는데 데이터를 observe 할 필요가 없지 않나?
라는 상황이 생길 수 있는데 이를 위해 repeatOnLifecycle() 라는 함수가 생겼다.
lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 이상 버전부터 사용 가능하다고 한다.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
someLocationProvider.locations.collect {
}
}
}
}
}
이걸 써주면 라이프사이클에 맞게 알아서 collect 와 cancle을 반복하게 된다. 아주 편하다.
마지막 여섯번째는 EventFlow + Sealed class + Lifecycle 이다.
eventFlow 라는게 새로 등장했다. 이게 뭘까.
이런 상황을 위해 만들어졌다.
특정 버튼을 누르면 이벤트가 10초뒤에 실행된다고 하자.
그런데 누르자마자 사용자가 다른 작업을 위해 홈버튼을 눌러 백그라운드로 보냈다
1분이 지나고 다시 앱을 켰을때는 해당 이벤트는 이미 유실되어서 동작을 하지 않게 될 것이다.
이걸 해결해주려면 event 가 발행됐을때 consume 되지 않았다면 따로 캐시역할을 해줄 수 있는곳에
보관했다가 observe 할 수 있는 상황이되면 consume 하게 만들어서 event를 유실하지 않는 전략이다.
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)
}
사실 이 코드는 완벽하게 이해하진 못했지만
EventFlowSlot 라는 클래스를 만들어서 사용이 됐는지 안됐는지 체크를 해주고 있다.
사용이 되지 않았다면 30번째 줄에서 emit을 해준다.
AtomicBoolean라는 개념도 등장하는데
AtomicBoolean는 boolean 자료형을 갖고 있는 wrapping 클래스입니다. AtomicBoolean 클래스는 멀티쓰레드 환경에서 동시성을 보장합니다.
자바에서 동시성 문제를 해결하는데 3가지 방법이 있습니다.
- "volatile" 은 Thread1에서 쓰고, Thread2에서 읽는 경우만 동시성을 보장합니다. 두개의 쓰레드에서 쓴다면 문제가 될 수 있습니다.
- "synchronized"를 쓰면 안전하게 동시성을 보장할 수 있습니다. 하지만 비용이 가장 큽니다.
- Atomic 클래스는 CAS(compare-and-swap)를 이용하여 동시성을 보장합니다. 여러 쓰레드에서 데이터를 write해도 문제가 없습니다.
AtomicBoolean는 synchronized 보다 적은 비용으로 동시성을 보장할 수 있습니다.
https://codechacha.com/ko/java-atomic-types/
라고 한다.
'Android' 카테고리의 다른 글
MVVM 에서의 에러처리 전략 (0) | 2022.07.19 |
---|---|
InverseBindingAdapter 에 대해 (0) | 2022.03.28 |
BindingAdapter에 대해서 (0) | 2022.03.27 |
Android Databinding (데이터 바인딩) (0) | 2022.02.26 |
안드로이드 클린 아키텍쳐에 대해 (0) | 2022.02.21 |
Android 프로젝트를 Multi Module 로 구성해보자. (0) | 2022.02.10 |
Groovy DSL 을 Kotlin DSL 로 바꿔보기 (0) | 2022.02.09 |
Android buildSrc 로 Dependency 관리하기 (0) | 2022.02.09 |