카테고리 없음

LiveData with Coroutines and Flow — Part III: LiveData and coroutines patterns(학습 및 번역)

hik14 2022. 11. 8. 20:42
<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()

ViewModel patterns

LiveData와 Flow 사용을 비교하면서 ViewModel에서 사용할 수 있는 몇 가지 패턴을 살펴보겠습니다.

LiveData: Emit N values as LiveData

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
val currentWeather: LiveData<String> = dataSource.fetchWeather()
 

변환을 수행하지 않으면 간단히 하나를 다른 하나에 할당할 수 있습니다.

Flow: Emit N values as LiveData

liveData 코루틴 빌더의 조합을 사용하고 Flow에서 collect할 수 있습니다(이는 각각의 방출된 값을 수신하는 터미널 연산자).

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
    dataSource.fetchWeatherFlow().collect {
        emit(it)
    }
}

불필요한 상용구가 많기 때문에 한 줄에서 동일한 작업을 수행하는 Flow.asLiveData() 확장 함수를 추가했습니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()

LiveData: Emit 1 initial value + N values from data source

데이터 소스가 LiveData를 노출하는 경우, emitSource를 사용하여 초기 값을 내보낸 후 업데이트를 파이프하는 데 emitSource를 사용할 수 있습니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
val currentWeather: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}

Flow: Emit 1 initial value + N values from data source

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
// Don't use this
val currentWeatherFlow: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(
        dataSource.fetchWeatherFlow().asLiveData()
    )
}

그러나 Flow의 자체 API를 활용하면 상황이 훨씬 더 깔끔하게 보입니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

val currentWeatherFlow: LiveData<String> = 
    dataSource.fetchWeatherFlow()
        .onStart { emit(LOADING_STRING) }
        .asLiveData()

onStart는 초기 값을 설정하고 이 작업을 수행하면 LiveData로 한 번만 변환하면 됩니다.

 

LiveData: Suspend transformation

데이터 소스에서 오는 것을 변환하고 싶지만 CPU를 많이 사용하므로 suspend fun 에 있다고 가정해 보겠습니다.

 

데이터 소스의 LiveData에서 switchMap을 사용한 다음 liveData 빌더로 코루틴을 생성할 수 있습니다.

이제 수신된 각 결과에 대해 방출을 호출할 수 있습니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
val currentWeatherLiveData: LiveData<String> =
    dataSource.fetchWeather().switchMap {
         liveData { emit(heavyTransformation(it)) }
    }

Flow: Suspend transformation

 Flow가 LiveData에 비해 정말 빛나는 부분입니다. 다시 Flow의 API를 사용하여 보다 우아하게 작업을 수행할 수 있습니다.

이 경우 Flow.map을 사용하여 모든 업데이트에 변환을 적용합니다.

 이미 코루틴 컨텍스트에 있으므로 직접 호출할 수 있습니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
   
val currentWeatherFlow: LiveData<String> =
    dataSource.fetchWeatherFlow()
        .map { heavyTransformation(it) }
        .asLiveData()

Repository patterns

repository에서는 할 말이 많지 않습니다.

Flow를 사용하고 Flow를 노출하는 것처럼 Flow API를 사용하여 데이터를 변환하고 결합하기만 하면 됩니다.

val currentWeatherFlow: Flow<String> =
    dataSource.fetchWeatherFlow()
        .map { ... }
        .filter { ... }
        .dropWhile { ... }
        .combine { ... }
        .flowOn(Dispatchers.IO)
        .onCompletion { ... }

Data source patterns

다시 한 번 원샷 작업과 Flow을 구분해 보겠습니다.

One-shot operations in the data source

Room 또는 Retrofit과 같은 suspend fun 을 지원하는 라이브러리를 사용하는 경우 suspend fun 에서 간단히 사용할 수 있습니다!

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

suspend fun doOneShot(param: String) : String =
    retrofitClient.doSomething(param)

그러나 일부 도구와 라이브러리는 아직 코루틴을 지원하지 않으며 콜백 기반입니다.

 

이런 경우에는 suspendCoroutine 또는 suspendCancellableCoroutine을 사용할 수 있습니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

suspend fun doOneShot(param: String) : Result<String> =
    suspendCancellableCoroutine { continuation ->
        api.addOnCompleteListener { result ->
            continuation.resume(result)
        }.addOnFailureListener { error ->
            continuation.resumeWithException(error)
        }.fetchSomething(param)
      }

당신이 suspendCancellableCoroutine을 호출하면, 당신은 continuation을 얻습니다.

 

이 예에서 우리는 complete listener 와 failure listenr 를 설정할 수 있는 API를 사용하고 있으므로 ,

데이터나 오류를 수신할 때 콜백에서continuation.resume 또는continuation.resumeWithException을 호출합니다.

 

이 코루틴이 cancel되면 resume가 무시된다,  Request에 시간이 지나서 콜백 중 하나가 실행될 때까지 코루틴이 활성화됩니다.

Exposing Flow in the data source

Flow builder

데이터 소스의 fake implementation 을 만들어야 하거나 간단한 것이 필요한 경우 flow 생성자를 사용하고 다음과 같이 할 수 있습니다.

override fun fetchWeatherFlow(): Flow<String> = flow {
    var counter = 0
    while(true) {
        counter++
        delay(2000)
        emit(weatherConditions[counter % weatherConditions.size])
    }
}

이 코드는 2초마다 기상 조건을 내보냅니다

 

Callback-based APIs

 

콜백 기반 API를 플로우로 변환하고 싶다면 callbackFlow를 사용하면 됩니다.

<!-- Copyright 2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

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) }
}

보기에는 어려워 보이지만 분리해 보면 많은 의미가 있음을 알 수 있습니다.

 

- 새로운 value 가 있을 때 offer()을 호출합니다.

- 업데이트 전송을 중단하고 싶을 때 close(cause)를 호출합니다.

- Flow이 닫힐 때 실행해야 하는 일들을 정의하기 위해 awaitClose를 사용합니다.

이는 콜백 등록을 취소하는 것들이 적합합니다.