생명주기에 안전한 코루틴
lifecycle 컴포넌트를 사용한다면, 생명주기를 인식하는 코루틴을 만들 수 있다.
LifecycleOwner로써 취급되는 AppCompatActivity(ComponentActivity) 또는 Fragment를 일반적으로 사용할 때 lifecycle 컴포넌트를 사용하게 되는데 이때 lifecycleScope를 사용할 수 있다.
일반적은 코루틴 스코프와 마찬가지로 launch를 async와 같은 함수 호출을 통해 suspendable 한 작업을 할 수 있다.
lifecycleScope.launch { // 코루틴 작업 }
만약 Fragment 또는 Activity와 같은 일반적인 코루틴 스코프를 만들어 작업중이라면, LifecycleOwner가 Destoryed 될 때 실행중인 코루틴을 취소하기 위해 명시적으로 CoroutineContext.cancel()을 호출해줘야 한다.
하지만, lifecycleScope에서 실행하는 코루틴은 생명주기에 맞춰 안전하게 종료되므로 안전하다. 그 이유는 LifecycleCoroutineScopeImpl 코드 내부를 살펴보면 짐작할 수 있다.
internal class LifecycleCoroutineScopeImpl(
override val lifecycle: Lifecycle,
override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
init {
// in case we are initialized on a non-main thread, make a best effort check before
// we return the scope. This is not sync but if developer is launching on a non-main
// dispatcher, they cannot be 100% sure anyways.
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
coroutineContext.cancel()
}
}
fun register() {
launch(Dispatchers.Main.immediate) {
if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
} else {
coroutineContext.cancel()
}
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
coroutineContext.cancel()
}
}
}
Activity 또는 Fragment의 생명주기 이벤트를 받아서 파괴되었다고 판단하면, 하위 코루틴 작업들을 취소하는 것을 확인할 수 있다.
Note: ViewModel의 경우 별도의 생명주기를 갖는데, lifecycleScope대신 viewModelScope를 사용할 수 있다.
생명주기에 맞춰 코루틴 실행하기
생명주기를 인식하는 몇가지 코루틴 함수에 대해서 알아본다.
whenXXX 함수
생명주기가 특정 상태에 있지 않다면 코루틴의 실행을 정지하고 싶을 때가 있을 수 있다. whenCreated, whenStarted, whenResumed 3가지 함수가 있으며 이름에서 알 수 있듯이, 각 함수 when이후 접미어에 해당하는 생명주기에 맞춰 실행이되고, 생명주기의 상태가 충족되지 않으면 정지가 되는 똑똑한 함수다. 다음 예제코드는 생명주기 상태가 최소 STARTED 상태일 때 실행된다.
class MainActivity : AppCompatActivity() {
val TAG = "Charlezz"
val counterFlow:Flow<Int> = flow{
var counter = 0
while(true){
delay(1000)
emit(counter++)
}
}
override fun onCreated(savedInstanceState: Bundle?) {
lifecycleScope.launch {
whenStarted {
counterFlow.collect {
Log.e(TAG, "$it")
}
}
}
}
}
위 예제 코드를 실행한 뒤 중간에 홈버튼을 눌러 앱을 백그라운드 상태로 만들고 다시 앱으로 진입하여 포어그라운드 상태로 만들 때 어떠한 변화가 로그에 어떤 변화가 있는지 확인할 수 있다.
E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
(홈버튼 눌러 빠져나옴, 더 이상 로그가 출력되지 않음)
.
.
.
(수초후에 다시 앱으로 진입)
E/Charlezz: 4
E/Charlezz: 5
E/Charlezz: 6
E/Charlezz: 7
E/Charlezz: 8
.
.
.
로그에서 알 수 있듯이 생명주기의 최소상태를 충족하지 못하면 작업하던 코루틴을 정지상태(suspended)로 만들고, 다시 생명주기가 충족되면 재개(Resumed)된다.
launch 블록내에서 whenXXX 함수를 호출하는 대신 다음과 같이 lifecycleScope.launchWhenXXX 함수 한줄로 간단히 표현할 수도 있다.
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// 정지 가능한 함수 호출
} finally {
// 이 라인은 아마 생명주기가 DESTROYED가 될 때 실행된다.
}
}
}
}
생명주기에 맞춰 코루틴 재시작하기
whenXXX 함수 호출을 통해 자동으로 생명주기에 맞춰 코루틴을 시작/중단/재개 할 수 있는 방법을 알아보았다. 하지만 생명주기에 맞춰 코루틴을 시작/취소/재시작 하고 싶을 때가 있다. 이러한 경우 Lifecycle과 LifecycleOwner는 repeatOnLifecycle이라는 확장함수를 제공한다. 다음 예제는 생명주기 상태가 STARTED일 때마다 새로운 스트림을 수집하는 것을 보여준다.
class MainActivity : AppCompatActivity() {
val TAG = "Charlezz"
val counterFlow:Flow<Int> = flow{
var counter = 0
while(true){
delay(1000)
emit(counter++)
}
}
override fun onCreated(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
counterFlow.collect {
Log.e(TAG, "$it")
}
}
}
}
}
이번에도 예제 코드를 실행한 뒤 중간에 홈버튼을 눌러 앱을 백그라운드 상태로 만들고 다시 앱으로 진입하여 포어그라운드 상태로 만들 때 어떠한 변화가 로그에 어떤 변화가 있는지 확인할 수 있다.
E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
(홈버튼 눌러 빠져나옴, 더 이상 로그가 출력되지 않음)
.
.
.
(수초후에 다시 앱으로 진입)
E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
E/Charlezz: 4
.
.
.
생명주기를 충족하지 못할 때 실행하던 코루틴은 취소하고, 생명주기가 충족될 때 다시 처음부터 작업을 하는 것을 확인할 수 있다.
0개의 댓글