LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1
Jetpack 에서 제공하는 AAC 는 안드로이드 개발에 간편한 기능들을 제공합니다. 따라서 코루틴의 작업 및 취소(Jobs and cancellation)들에 대해서 걱정할 필요가 없습니다. 단순히 작업의 범위(scope of operation)를 잘 선택해주기만 하면 됩니다.
다양한 스코프에 대해 알아봅시다!
ViewModel Scope
해당 스코프는 코루틴을 사용할 때 가장 많이 사용하는 스코프중 하나입니다. 보통 ViewModel 에서 데이터 관련한 작업들이 시작되기 때문입니다. 따라서 viewModelScope extension 을 사용하면, ViewModel 의 onCleared 메서드가 호출될 때 자동으로 작업들이 취소 되어집니다. 아래와 같이 viewModelScope.launch 와 같이 사용할수 있습니다.
class MainActivityViewModel : ViewModel {
init {
viewModelScope.launch {
// Do things!
}
}
}
Activity and Fragment scopes
비슷하게 Activity 와 fragment 에서도 lifecycleScope.launch 를 사용하여 , 특정 뷰들에 대해 작업의 범위를 지정할 수도 있습니다.
또한 launchWhenResumed, launchWhenStarted, launchWhenCreated 와 같은 함수를 사용하여, 좀더 좁은 작업 범위를 지정할 수 도 있습니다.
class MyActivity : Activity {
override fun onCreate(state: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Run
}
lifecycleScope.launchWhenResumed {
// Run
}
}
}
Fragment 에서는 뷰의 생명주기를 관리하는 lifeOwner 가 있기 때문에, viewlifecycleowner.lifecyclescope.launch { ... } 처럼 사용하면 됩니다.
Application scope
어플리케이션 처럼 긴 범위를 사용하는 예는 해당 아티클을 참고하여도 좋습니다. 코루틴은 취소가 될 수 있기 때문에 꼭 실행이 되어야 한다면 WorkManager 사용하는 것을 고려해야 합니다.
ViewModel + LiveData 조합
지금까지는 코루틴을 어떻게 범위를 가지고 시작할 수 있는지에 대해서 알아봤습니다. 그러나 아직 코루틴으로부터 결과를 잔달 받는 방법에 대해서는 알아보지 않았습니다. 어떻게 받을 수 있을까요 ? 아래와 같이 MutableLiveData 를 사용하면 됩니다
class MyViewModel : ViewModel() {
private val _result = MutableLiveData<String>()
val result: LiveData<String> = _result
init {
viewModelScope.launch {
val computationResult = doComputation()
_result.value = computationResult
}
}
}
위의 방식으로 진행해도 문제없지만, 보일러플레이트 코드가 생기게 됩니다. 결과를 뷰에 노출시킬 때, livedata coroutine builder 를 사용할 수 도 있습니다. 해당 방식으로는 immutable LiveData 를 사용하여 바로 UI 를 갱신하게 노출할 수도 있습니다. builder 와 emit 함수를 사용하면 됩니다. 하지만 해당 방식은 항상 사용하기는 어려운 상황들이 존재합니다. 그런 경우에는 위 처럼 작성해야 합니다.
class MyViewModel : ViewModel() {
val result = liveData {
emit(doComputation())
}
}
LiveData coroutine builder 와 SwitchMap 의 조합
LiveData 의 값이 변경될 때마다, 코루틴을 실행하고 싶은 경우가 있습니다. 예를 들어 데이터를 읽어오는 작업을 실행하기전에 아이디가 필요하다면, Transformations.switchMap 을 사용하여 아래와 같이 작성해볼 수 있습니다.
private val itemId = MutableLiveData<String>()
// somewhere emited itemId ...
val result = itemId.switchMap {
liveData { emit(fetchItem(it)) }
}
위의 예제는 itemId 값의 변경이 있을 때 마다, fetchItem 을 실행하여 결과를 result 에 Immutable LiveData 로 반환 합니다.
다른 라이브데이터에서의 아이템 배출하기
해당 기능은 필수적인 것은 아니지만, 우리가 코드를 작성할 때 보일러 플레이트를 줄일 수 있도록 도와줍니다. emitSource 를 사용하여 초기 값을 배출하고, 이후에 다른 값들을 배출할 수 있습니다. emitSource 를 사용하게 되면, 이전에 Souce 를 정리합니다. addSource 되어 있는 MediatorLiveData 를 제거하고 새로운 Source 를 추가합니다.
// 예시1
liveData(Dispatchers.IO) {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
// 예시2
val lastestData = liveData(Dispatchers.IO) {
emit(getCacheDataFromLocal())
emitSource(getDataFromServer())
}
코루틴 취소하기
위의 사례 처럼 코루틴의 범위를 지정 사용하는 경우는 명시적으로 작업을 취소할 필요가 없습니다. 그럼에도 불구하고 알아야하는 사항이 있습니다. 코루틴의 취소는 협력형이라는 점입니다. 즉 호출된 코루틴이 취소되면, 작업을 중단할 수 있도록 추가적인 처리가 필요합니다.
예를 들어 무한 루프인 suspend 인 함수가 있다고 가정해봅시다. 코틀린은 해당 함수의 루프를 취소할 수 없습니다. 그렇기 때문에 "협력"이 필요하게 됩니다. 작업이 활성화 상태인지 정기적으로 코루틴 스코프내에 있는 isActive 를 체크하는 방법이 있습니다.
// inner coroutine
suspend fun printPrimes() {
while(isActive) {
// Compute
}
}
// currentCoroutineContext()
suspend fun printPrimes() {
while(currentCoroutineContext().isActive) {
// Compute
}
}
그리고 kotlinx.coroutines 아래 함수(대표적인 함수 : delay)들은 모두 suspend 로 취소 가능합니다. 따라서 함수들의 내부에서는 isActive 로 취소가 되었는지 체크하는 로직이 존재합니다. 아니라면 코루틴을 사용하는 개발자들이 직접 하나하나 체크하는 로직을 넣었어야 했을 것입니다.
suspend fun printPrimes() {
while(true) { // Ok-ish because we call delay inside
// Compute
delay(1000) // 취소 가능한 함수
}
}
그래도 suspend (취소가능한) 함수 안에서는 isActive 를 사용하여 체크해주는 것이 좋습니다. 그 이유는 delay 같은 함수들이 나중에 삭제되면서 미묘한 버그를 발생시킬 수도 있기 때문입니다.
One-shot vs Multiple values (하나의 값을 반환하는 작업vs 여러번 값을 반환해야하는 작업)
반응형 UI 과 관계있는 코루틴을 이해하기 위해서는 우리는 다음을 중요하게 구분해야 합니다.
하나의 값을 반환하고 이를 구독하는 경우
viewModelScope 에서 suspend 함수를 사용하는 경우나 liveData 빌더 (liveData { .... }) 를 사용하는 경우는 논 블록 작업을 사용할 때 매우 간편한 방법중 하나입니다. *논블럭: 제어할 수 없는 대상의 작업이 끝나기 전에 제어권을 넘기는 경우
class MyViewModel {
val result = liveData {
emit(repository.fetchData())
}
}
여러 값을 반환하고 이를 구독하는 작업(시간이 지남에 따라 여러번 값을 배출할 수 있는 DataSource 에 대해 구독을 하는 경우)
해당 주제에 대해 저자는 2018년에 LiveData beyound the ViewModel 글을 다뤘습니다. 해당 아티클에서는 라이브데이터가 여러값을 배출하는데, 완전히 기능을 갖춘 스트림 빌더는 아니기 때문에, 비슷한 효과를 낼 수 있는 패턴(MediatorLiveData 와 addSource 를 사용하여 값을 combine 해서 사용하는 방식)에 대해 이야기했었습니다. 현재는 더 나은 솔루션인 코틀린의 Flow(아직 몇개는 Experimental 입니다)가 있습니다. Flow 는 반응형 스트림인 RxJava 의 기능과 비슷한 부분이 많습니다.
코루틴은 한번 값을 반환하는 논 블록 작업은 쉽게 만들었지만, flow 의 경우에는 달랐습니다. 여전히 여러번 값을 배출하고 구독하는 경우에는 복잡하고 어려웠습니다. 하지만 우리는 빠르고 견고한 반응형 UI 를 만들기 위해서라면 시간을 투자할 가치는 있습니다. 결과적으로 코틀린 언어스펙에서 제공하게 되었고, Room 같은 라이브러리에서도 Flow 를 위한 함수들을 제공하기 시작했습니다.
그렇기 때문에 여러번 변경되는 값을 구독해야하는 경우에는 Repository 와 DataSource layer 에서 Flow 를 사용하는 것이 좋습니다. 그리고 ViewModel 에서는 Lifecycle-aware 한 LiveData 를 여전히 사용하는 것이 좋습니다.
나머지는 파트 3에서 이야기하겠습니다
'Android > 번역' 카테고리의 다른 글
[번역] Crash Course on the Android UI Layer Part 1 (1) | 2023.12.28 |
---|---|
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 3 (0) | 2021.06.07 |
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1 (1) | 2021.05.25 |
코루틴을 뷰에 적용하기 (1) | 2021.03.14 |
코루틴 플로우(Flow) 읽어보기 (1) | 2021.03.08 |