[android] 안전하게 flow 를 collect 하기!
-
android 에서는 보통 UI layer 에서 flow 가 collect 된다.
LifecycleOwner.addRepeatingJob, Lifecycle.repeatOnLifecycle, Flow.flowWithLifecycle 등을 이용해서 쓸데없이 res 가 낭비되지 않도록 하는 방법에 대해 알아본다.
리소스 낭비
-
CoroutineScope.launch, Flow<T>.launchIn, LifecycleCoroutineScope.launchWhenX 를 사용하는 것은
Activity 가 bg 로 들어갈 때 Job 을 수동으로 취소해주지 않는 한 안전한 방법이 아니다.
위 API 들은 app 이 bg 상태에 있을 때도 flow producer 가 buffer 에 emit 을 하도록 하고, 따라서 리소스 낭비가 발생한다.
-
fun locationFlow() = callbackFlow<Location>{
val callback = (result:LocationResult?) -> {
result?: return
try{ offer(result.lastLocation) } catch(e:Exception) {}
}
requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
.addOnFailureListener { close(e) }
awaitClose { removeLocationUpdates(callback) }
}
lifecycleScope.launchWhenStarted{
locationFlow().collect{
// new location
}
// DESTORYED 가 되어도 이 부분의 로직은 타지 않는다.
}
View 가 STARTED 일 때 collect 를 하고, STOPPED 일 때 suspend 를 한다. ( cancel 이 아닌 suspend.. )
View 가 DESTROYED 일 때 cancel 된다.
이 구문의 문제는 collect(consumer) 하는 녀석은 suspend 되더라도, emit(producer) 하는 녀석은 계속 emit 해서 리소스 낭비가 발생한다는 것이다.
lifecycleScope.launch 나 Flow.launchIn api 는 job 관리를 제대로 안 해주면, 앱이 STOPPED 상태일 때 consume 도 계속하기 때문에 더 위험하다.
-
위 문제를 해결하기 위해 쉽게 생각할 수 있는 방법은 onStart 에서 collect 하는 것을 job 으로 만들고, onStop 에서 job 을 cancel 해주는 것이다.
// in activity
private var locationUpdateJob:Job? = null
override fun onStart(){
super.onStart()
locationUpdateJob = lifecycleScope.launch{
locationFlow().collect{
// do something..
}
}
}
override fun onStop(){
locationUpdateJob?.cancel()
super.onStop()
}
좋은 방법이지만 boilerplate 이다.
LifecycleOwner.addRepeatingJob
-
lifecycle-runtime-ktx 에 정의된 LifecycleOwner.addRepeatingJob 을 이용해서 위의 boilerplate 코드를 줄일 수 있다.
// in activity
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED){
locationFlow().collect {
// do something...
}
}
}
위 구문은 전달받은 Lifecycle.State 에 마딱뜨리면 새로운 coroutine 을 create & launch 시킨다.
그리고 해당 state 에서 벗어나면 해당 coroutine 을 취소시킨다.
Activity 의 onCreate 나 Fragment 의 onViewCreated 에서 호출해주면 좋다.
* repeatOnLifecycle 을 사용하기
-
호출하는 CoroutineContext 를 유지하는 관점에서 더 유연성 있는 Lifecycle.repeatOnLifecycle 를 사용하는 것도 고려해볼만 하다.
repeatOnLifecycle 은 호출하는 coroutine 을 suspend 시키고, lifecycle 이 target state 에서 벗어나면 re-launch 를 시킨다.
그리고 Lifecycle 이 destroyed 되면 호출하는 coroutine 을 resume 시킨다.
이 API 는 1회성 setup task 에 적합하다.
// in activity
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
lifecycleScope.launch{
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
// STARTED 에서 시작되고, STOPPED 에서 cancel 된다.
}
// DESTROYED 가 되면 coroutine 이 resume 되며 이 부분 로직을 탄다.
}
}
Flow.flowWithLifecycle
-
오직 1개의 flow 만 collect 할 경우 Flow.flowWithLifecycle 을 사용하면 좋다.
이 녀석 안쪽에서는 Lifecycle.repeatOnLifecycle 함수를 사용한다.
// in activity
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// do something..
}
.launchIn(lifecycleScope)
}
Producer 를 설정하기
-
MutableStateFlow 와 MutableSharedFlow 는 subscriptionCount 라는 field 를 제공한다.
이를 이용해서 subscriptionCount 가 0 일 때 producer 를 stop 시킬 수 있다.
기본적으로 해당 flow 가 memory 상에 존재하는 한 producer 는 계속 active 상태이다.
Flow.stateIn 또는 Flow.shareIn operator 를 이용해서 sharing started policy 를 정의해줄 수 있다.
WhileSubscribed() 는 active observer 가 없을 때 produce 를 멈춘다.
Eagerly 와 Lazily 는 CoroutineScope 가 active 할 때만 producer 가 active 하다.
-
use case 에 의해 producer 가 항상 active 한 것이 좋을 수도 있다.
이 경우 항상 최신의 data 를 바로 받아볼 수 있기 때문이다.
-
나머지 내용은 정리하지 않는다.
-
참고자료 : https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
끝