프로그래밍 놀이터/안드로이드, Java

[android] 안전하게 flow 를 collect 하기!

돼지왕 왕돼지 2021. 5. 11. 18:18
반응형

 

-

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

 

 

 

반응형