Android/번역

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 3

Nanamare 2021. 6. 7. 02:31
728x90

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1

 

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1

https://medium.com/androiddevelopers/livedata-with-coroutines-and-flow-part-i-reactive-uis-b20f676d25d7 LiveData with Coroutines and Flow — Part I: Reactive UIs This article is a summary of the t..

nanamare.tistory.com

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 2

 

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 2

LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 1 Jetpack 에서 제공하는 AAC 는 안드로이드 개발에 간편한 기능들을 제공합니다. 따라서 코루틴의 작업 및 취소(Jobs and cancellation)들에 대해서 걱정..

nanamare.tistory.com

 

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 한 부분에서 국소적으로 사용할 것 같습니다. 현재도 그렇게 사용하구 있구요!

 

다음 번역으로는 아래 번역을 진행할 것 같습니다!

 

그럼 20000 :) 

 

 

728x90