LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 2
3탄은 Jose Alcérreca(블로그 저자), Jose AlceYigit Boyar 가 함께한 2019 Android Dev Summit 의 요약본 입니다.
ViewModel 에서 사용되는 패턴들
ViewModel 에서 사용하는 패턴들에 대해서 몇가지 보고, LiveData 와 Flow 의 사용 방식에 대해 비교해봅시다.
LiveData 사례 : 라이브데이터에 N 개의 값을 배출하는 경우
val currentWeather: LiveData<String> = dataSource.fetchWeather()
map 등의 데이터에 변환이 없는 경우에는 간단하게 위의 예시처럼 작성 할 수 있습니다. 간단하네요!
Flow 사례 : 라이브데이터에 N 개의 값을 배출하는 경우
우리는 liveData coroutine builder 와 Flow 의 collect 를 조합하여 사용할 수 있습니다. (collect 는 방출된 값을 받는 종료 연산자 입니다)
// 추천은 하지 않습니다 보일러 플레이트가 많자나요~!
val currentWeatherFlow: LiveData<String> = liveData {
dataSource.fetchWeatherFlow().collect {
emit(it)
}
}
위와 같이 liveData coroutine builder 와 collect 를 동시에 쓰는 방법은 너무나도 많은 코드들을 작성하게 합니다.
따라서 Flow 의 확장함수인 Flow.asLiveData() 함수를 만들었습니다. 아래처럼 사용하면 한줄로 작성 가능합니다.
val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()
LiveData 사례 : 초기에 1개의 값을 방출하고, DataSource 로 부터 오는 여러개의 데이터를 처리하는 경우
만약 DataSource 를 LiveData 에 노출시키면, 우리는 liveData coroutine builder 를 이용하여 초기값은 배출하고, 이후에는 emitSource 를 이용하여 Source 를 변경시켜주기만 하면 됩니다.
val currentWeather: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(dataSource.fetchWeather())
}
Flow 사례 : 초기 값 하나를 방출하고, DataSource 로 부터 여러개의 값을 처리하는 경우
해당 방식처럼 순진하게 작성해볼 수도 있습니다.
// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
emit(LOADING_STRING)
emitSource(
dataSource.fetchWeatherFlow().asLiveData()
)
}
동작에는 문제가 없지만, 하지만 약간 이상해보입니다 liveData 빌더를 통해 LiveData 가 만들어졌는데 이후에 asLiveData 를 사용하여 또 LiveData 를 만들고 있습니다.
그렇기 때문에 Flow 에서 제공하는 자체 API 를 사용하면 좀더 깔끔하게 작성할 수 있습니다.
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.onStart { emit(LOADING_STRING) }
.asLiveData()
onStart 함수를 사용하여 초기값을 배출하고, 사용하게 되면 이후에 LiveData 로는 한번만 변환을 해도 됩니다.
LiveData 사례 : Suspend transformation (일시 중단 가능한 변환)
만약에 DataSource 로 부터 전달되는 값을 변환해야하는 상황이 있다고 가정해봅시다. 그리고 해당 변환은 CPU 에 부하를 줄 수 있는 작업이기 때문에 suspend function 일 필요성이 있습니다.
val currentWeatherLiveData: LiveData<String> =
dataSource.fetchWeather().map { // Some heavy Transformation .... }
아마도 여러분은 가장 처음에 이런 생각을 떠올리셨을겁니다. 하지만 해당 방식은 CPU 에 부하를 줄 수 있기 때문에 사용하는 것을 추천하지 않습니다. 차라리 SwitchMap 을 사용하여 변환 작업 후 liveData coroutine builder 를 통해 값을 배출해주는 것이 성능상 훨씬 좋습니다.
val currentWeatherLiveData: LiveData<String> =
dataSource.fetchWeather().switchMap {
liveData { emit(heavyTransformation(it)) }
}
Flow 사례 : Suspend transformation (일시 중단 가능한 변환)
여기서부터는 LiveData 와 비교했을 때와 비교하면 Flow 를 사용하는 이점이 많아지는 부분입니다. 우리는 Flow 에서 제공하는 API 를 사용하여 좀더 우아하게 코들르 작성할 수 있습니다. Flow 에는 map 확장 함수가 있기 때문에, map 을 사용하게 되면 매번 업데이트에도 변환이 적용됩니다. 또한 코루틴 컨텍스트 안에서 작성되기 때문에 이런 함수들을 직접 불러 사용할 수 있습니다.
val currentWeatherFlow: LiveData<String> =
dataSource.fetchWeatherFlow()
.map { heavyTransformation(it) }
.asLiveData()
Repository 에서 사용되는 패턴들
Repositories 에 대해 이야기할 것이 많지는 않습니다. Repositories 에서는 필터링하고, 변환하고, 수집된 값들을 가져오는 역할을 하는데, LiveData 가 이런 역할을 할 것이라고 기대하진 않습니다. 다행 flow 를 사용하면 됩니다. Flow API 를 사용하여 데이터를 변환하고 결합하기만 하면 됩니다.
val currentWeatherFlow: Flow<String> =
dataSource.fetchWeatherFlow()
.map { ... }
.filter { ... }
.dropWhile { ... }
.combine { ... }
.flowOn(Dispatchers.IO)
.onCompletion { ... }
DataSource 에서 사용하는 패턴들
다시 한번 one-shot 작업(한번의 반환되는 결과를 관찰)과 Flow(여러번 반환 될 수 있는 결과를 관찰) 를 구분해보겠습니다.
DataSource 에서의 One-shot 작업
만약 suspend functions 을 제공하는 라이브러리(Retrofit, Room)를 사용하고 있다면, 우리는 간단하게 우리가 작성한 suspend 함수와 라이브러리에서 제공하는 suspend 함수들을 함께 사용하면 됩니다.
Retrofit 2.6.0 에서는 suspend Call<T> 를 제공합니다. (따라서 awaitResponse(), await() 같은 함수들을 사용하면 됩니다)
Room 2.1.0 부터 suspend 를 지원합니다
suspend fun doOneShot(param: String) : String =
retrofitClient.doSomething(param)
그러나 아직 코루틴을 제공하지 않는 라이브러리나 툴이 존재합니다. 가령 콜백 스타일이 있습니다.
해당 케이스에는 suspendCoroutine 과 suspendCancellableCoroutine 을 사용하면 됩니다. 해당 함수는 코루틴과 콜백 기반의 코드를 이어주는 Adapter 입니다 그리고 저자는 취소할 수 없는 버전인 suspendCoroutine 을 사용해야하는 이유를 찾지 못했다고 말하고 있습니다(suspendCancellableCoroutine 를 강력하게 추천합니다)
suspend fun doOneShot(param: String) : Result<String> =
suspendCancellableCoroutine { continuation ->
api.addOnCompleteListener { result ->
continuation.resume(result)
}.addOnFailureListener { error ->
continuation.resumeWithException(error)
}.fetchSomething(param)
}
suspendCoroutine, suspendCancellableCoroutine 를 사용하면 우리는 continuation 을 활용해야 합니다.
예를 들어 API 에서 완료된 상태는 continuation.resume 그리고 실패의 경우에는 continuation.resumeWithException 으로 표현됩니다.
중요한 부분은 코루틴이 어떠한 이유로 취소되면, resume 은 무시됩니다. 따라서 오래된 작업의 경우 콜백 중 하나가 호출되기 전까지는 코루틴이 활성화 되어 있습니다. 그렇기 때문에 코루틴이 어떤이유로 취소되는 경우 직접 API 요청을 취소하는 것이 좋습니다.
DataSource 에서 Flow 노출하기(부제 : flow builder)
만약 DataSource 에 fake 구현부를 만들거나, 또는 어떤 간단한 작업이 필요한 경우 flow 빌더를 사용하여 아래처럼 만들어 볼 수 있습니다.
override fun fetchWeatherFlow(): Flow<String> = flow {
var counter = 0
while(true) {
counter++
delay(2000)
emit(weatherConditions[counter % weatherConditions.size])
}
}
2초마다 모듈러 연산을 통해 날씨 상태를 배출하는 코드입니다.
콜백 기반의 API 에서의 Flow 사용
만약 callback 기반의 API 를 Flow 로 변환하여 사용하고 싶은 경우는 callbackFlow builder 를 사용하면 됩니다.
fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
val callback = object : Callback {
override fun onNextValue(value: T) {
offer(value)
}
override fun onApiError(cause: Throwable) {
close(cause)
}
override fun onCompleted() = close()
}
api.register(callback)
awaitClose { api.unregister(callback) }
}
위와 같이 어떤 이유로든 close 되면 callback 을 제거할 수도 있습니다.
결론적으로
Flow 는 LiveData 를 모든 곳에서 대체하지는 않지만, 꽤나 유용한 부분이 많습니다. 또한 StateFlow 같은 매유 유망한 실험적인 것들도 있습니다. 자바로 안드로이드 개발을 하거나, DataBinding 에 사용하는 부분 때문에 LiveData 는 한동안 deprecated 되지 않을 것입니다.
글을 쓰는 시점에서는 StateFlow 가 DataBinding 을 지원하는 군요 ㅎㅎ! 갈수록 LiveData 를 사용할 이유는 적어지는 것 같네요. Lifecycle-aware 한 부분에서 국소적으로 사용할 것 같습니다. 현재도 그렇게 사용하구 있구요!
다음 번역으로는 아래 번역을 진행할 것 같습니다!
- Sean’s post series on coroutines
- Manu’s lessons learned migrating the Android Dev Summit to coroutines (including Flow)
그럼 20000 :)
'Android > 번역' 카테고리의 다른 글
[번역] Crash Course on the Android UI Layer Part 2 (2) | 2023.12.29 |
---|---|
[번역] Crash Course on the Android UI Layer Part 1 (1) | 2023.12.28 |
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 2 (0) | 2021.06.06 |
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1 (1) | 2021.05.25 |
코루틴을 뷰에 적용하기 (1) | 2021.03.14 |