주로 연속적으로 값을 가져와야 하는 상황에서는 코루틴으로 코드를 짜기 쉽지가 않다.
예를들어, 시시각각 변화하는 온도를 가져오는 올때 코드를 보자.
lifecycleScope.launch {
val temperatures = withContext(Dispactchers.IO){
getTemperature()
}
for(temperature in temperatures) {
Log.d("TEST", "$temperature")
}
}
fun getTemperature(): MutableList<Int> {
val list = mutableListOf<Int>()
repeat(5){
list.add(20 + it)
}
return list
}
리스트를 반환하게 했지만 값을 모두 가져온 후에 출력하는 문제가 생긴다.
이 문제를 해결하기 위해서는 코드 블록 사이의 구성을 복잡하게 만들어야 한다. 코루틴은 값을 반복적으로 가져오는 그것을 처리하는 구조보다는 한번에 무언가를 하는 것에 적합하다.
채널은 일종의 파이프라인이다. 채널을 열고 한쪽에서 값을 보내면 다른 쪽에서 수신하는 개념이다.
위에 코루틴으로 짠 코드를 채널을 사용해 코드를 짜보자
val channel = Channel<Int>()
lifecycleScope.launch {
repeat(5){
channel.send(20 + it)
}
}
lifecycleScope.launch {
for(temperature in channel) {
Log.d("TEST", "$temperature")
}
}
이 코드를 실행하면 모두 받고 출력하는 이전 코드와 달리 값을 받을때마다 출력해준다.
두 코루틴이 채널을 통해서 값을 주고 받을 수 있다. 채널에 send 메서드를 통해 값을 보내면 반대편에서 값을 받을 수 있다. 값을 받기 위해서는 receive 메서드를 받아야하는데 for in 문에서는 자동으로 값을 수신해서 전달해준다. for in은 채널이 닫힐 때까지 값을 받는다. 채널을 닫기 위해서는 명시적으로 close 메서드를 호출할 수 있다.
채널을 만들고 코루틴을 만들어서 값을 전달하는 과정은 반복적인 과정이다. 이 과정을 간단히 만들어주는 빌더 produce가 있다.
val channel = lifecycleScope.produce<Int>{
repeat(5){
channel.send(20 + it)
}
}
lifecycleScope.launch {
for(temperature in channel) {
Log.d("TEST", "$temperature")
}
}
하지만 채널은 우리가 수신을 하기 전에도 데이터를 보내기 시작한다. 이런 특성 때문에 채널을 Hot이라고 부른다. 반면에 우리가 원하는 시점에 데이터를 가져오는 도구는 Cold라고 말한다. Flow는 cold 도구이다.
Flow로 짠 코드를 보자.
val flow: Flow<Int> = flow {
repeat(5){
emit(20 + it)
}
}
lifecycleScope.launch {
flow.collect { temperature ->
Log.d("TEST", "$temperature")
}
}
flow 빌더를 이용해서 블록을 열고 여기에 emit을 수행하는 것이 전부다. 이 블록은 collect를 호출하기 전까지 수행되지 않기 때문에 cold라고 한다.
collect 확장 함수가 호출되면 flow가 차례대로 수행된다. 단 flow 빌더 블록 내에서 다른 suspend 함수나 delay 등을 호출할 수 있지만 코루틴 빌더를 호출해서 디스패처를 부분적으로 변경할 수는 없다. Flow 가 수행되는 스레드를 변경하고 싶으면 flowOn 메서드를 사용한다.
val flow: Flow<Int> = flow {
repeat(5){
emit(20 + it)
}
}.flowOn(Dispatchers.IO)
lifecycleScope.launch {
flow.collect { temperature ->
Log.d("TEST", "$temperature")
}
}
만약 flow로 전달된 값을 조작하고 싶다면 map을 사용할 수 있다.
val flow: Flow<Int> = flow {
repeat(5){
emit(20 + it)
}
}.flowOn(Dispatchers.IO)
lifecycleScope.launch {
flow.map {
it * 2
}.collect { temperature ->
Log.d("TEST", "$temperature")
}
}
map 람다를 넣어 값을 조작했다.
map 도 다른 스레드에서 동작시킬수 있다.
val flow: Flow<Int> = flow {
repeat(5){
emit(20 + it)
}
}.flowOn(Dispatchers.IO)
lifecycleScope.launch {
flow.map {
it * 2
}.flowOn(Dispatchers.Default)
.collect { temperature ->
Log.d("TEST", "$temperature")
}
}
예외가 발생했을 때는 catch 오퍼레이터를 통해 처리할 수 있다.
val flow: Flow<Int> = flow {
repeat(5){
emit(20 + it)
}
throw java.lang.RuntimeException("error")
}.catch { exception ->
Log.d("TEST", "$exception")
}.flowOn(Dispatchers.IO)
정리 감사합니다!