왜 repeatOnLifecycle 앞에 viewLifecycleOwner를 붙여야할까?

DoDoBest

·

2024. 6. 26. 18:03

핵심 내용 선 정리

왜 repeatOnLifecycle 앞에 viewLifecycleOwner를 붙여야할까?

 

공식문서 예시 코드에서 repeatOnLifecycle 앞에 viewLifecycleOwner를 붙여서 사용하고 있습니다. 왜 일까요?

 

 

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

https://developer.android.com/topic/libraries/architecture/coroutines#restart

 

수명 주기 인식 구성요소로 Kotlin 코루틴 사용  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 수명 주기 인식 구성요소로 Kotlin 코루틴 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코

developer.android.com

 

우선 Fragment와 Fragment View의 lifecycle을 비교해봅시다.

Fragment와 Fragment View의 STARTED lifecycle은 onStart ~ onPause로 동일합니다.

 

https://developer.android.com/guide/fragments/lifecycle#states

 

프래그먼트 수명 주기  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 프래그먼트 수명 주기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 각 Fragment 인스턴스에는 고유한

developer.android.com

 

따라서 STARTED 이후의 Lifecycle에 따른 동작은 Fragment view의 LifecycleOwner를 사용하든, Fragment의 LifecycleOwner를 사용하든 동일합니다.

확인을 위해 Fragment에서 아래와 같이 viewModel의 StateFlow를 관찰하도록 설정했습니다.

 

    override fun onStart() {
        super.onStart()

        Log.i("TEST", "onStart is called")
    }

    override fun onResume() {
        super.onResume()

        Log.i("TEST", "onResume is called")
    }

    override fun onPause() {
        super.onPause()

        Log.i("TEST", "onPause is called")
    }

    override fun onStop() {
        super.onStop()

        Log.i("TEST", "onStop is called")
    }

    private fun setObserve() {
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                Log.i("TEST", "repeatOnLifecycle STARTED")
                viewModel.ping.collect {
                    Log.i("TEST", "[repeatOnLifecycle]ping is $it")
                }
            }
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                Log.i("TEST", "viewLifecycleOwner STARTED")
                viewModel.ping.collect {
                    Log.i("TEST", "[viewLifecycleOwner]ping is $it")
                }
            }
        }

        // ...
    }

 

ViewModel에서는 아래와 같이 0.01초마다 값을 emit 하도록 설정했습니다.

 

    val ping = MutableStateFlow<Int>(0)
    private var num = 0

    init {
        viewModelScope.launch {
            while (true) {
                delay(10)
                num++
                ping.emit(num)
            }
        }
    }

 

 

실행 결과, onStart 호출 이후 관찰을 시작하고

 

 

홈버튼을 눌러 onPause가 호출되도록 하더라도 STARTED 상태이기 때문에 관찰을 지속하며

 

 

onStop이 호출되어 CREATED 상태로 변경되면 관찰을 멈추는 것을 볼 수 있습니다.

다시 화면으로 돌아가면 onStart가 호출되고 관찰을 시작하는 것을 볼 수 있습니다.

 

 

결론

즉, STARTED 이후 상태에서는 repeatOnLifecycle 앞에 viewLifecycle을 붙이지 않아도 됨을 알 수 있습니다.

그럼에도 공식 문서에서는 왜 붙이도록 설정한 것일까요? 공식 문서 뿐만 아니라 구글 개발자 분의 블로그에서도 반드시 붙여야 한다고 적혀 있습니다.

 

 

https://manuelvivo.dev/repeatonlifecycle#:~:text=prevent%20bad%20usages.-,Wrapper%20in%20iosched,-repeatOnLifecycle%20must%20be

 

repeatOnLifecycle API design story

Learn the design decisions behind the Lifecycle.repeatOnLifecycle API. In this blog post, you’ll learn the design decisions behind the Lifecycle.repeatOnLifecycle API and why we removed some of the helper functions we added in the first alpha version of

manuelvivo.dev

 

 

이유는 STARTED, RESUMED 상태를 제외하면 Fragment와 Fragment View의 Lifecycle이 다르기 때문입니다. STARTED 상태 이후는 Lifecycle 동일하기 때문에 viewLifecyleOwner를 작성하지 않았다가, 나중에 CREATED 상태로 변경했을 때, viewLifecycleOwner를 다시 붙이는 것을 인지하지 못한다면 의도한 대로 코드가 동작하지 않을 것이며, 원인을 찾기가 매우 어려울 것입니다.

그래서 구글에서는 STARTED 이후 동일하더라도, 관습적으로 앞에 붙이라고 권장하는 것으로 이해했습니다.

 

CREATED 상태는 어떨까요?

 

STARTED 대신 CREATED 상태에서의 출력을 테스트 한 결과는 다음과 같습니다.

viewModel에서 emit 하는 빈도는 1초로 수정했습니다.

홈버튼을 누르면 onStop 상태로 Fragment와 View 모두 CREATED 상태이기 때문에 계속 관찰하는 것을 볼 수 있습니다.

 

 

화면으로 돌아와서 다른 Fragment로 전환한 경우, viewLifecycleOwner를 붙였는지 여부와 상관없이 관찰이 중단되었습니다.

repeatOnLifecyle 앞에 viewLifecycleOwner를 붙이지 않으면 Fragment의 lifecycle을 따르기 때문에 onDestroyView에서도 CREATED 상태로 collect는 유지되어야 할 것 같습니다.

그런데 왜 중단된 걸까요?

 

 

그 이유는 부모 Scope가 viewLifecycleOwner의 lifecycleScope이기 때문입니다. 내부에서 Fragment의 lifecycle을 따르더라도 부모가 Fragment View의 lifecycle을 따르기 때문에, onDestroyView 호출로 부모가 파괴됨에 따라 중단된 것입니다.

 

그러면 View의 lifecycleScope이 아닌 Fragment의 lifecycleScope을 따르도록 설정하면 어떻게 될까요?

 

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                Log.i("TEST", "repeatOnLifecycle CREATED")
                viewModel.ping.collect {
                    Log.i("TEST", "[repeatOnLifecycle]ping is $it")
                }
            }
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
                Log.i("TEST", "viewLifecycleOwner CREATED")
                viewModel.ping.collect {
                    Log.i("TEST", "[viewLifecycleOwner]ping is $it")
                }
            }
        }

 

다른 Fragment로 전환되어 onDestroyView가 호출되어도, Fragment의 lifecycle은 CREATED 상태이기 때문에 관찰이 지속되는 것을 볼 수 있습니다.

 

 

 

원래 화면으로 돌아올 경우, 기존에 생성했던 코루틴 Scope 뿐만 아니라, 새로운 코루틴 Scope가 생성되었기 때문에 관찰이 2번 되는 것을 볼 수 있습니다.

 

 

정리

             lifecycleScope
repeatOnLifecycle
Lifecycle Fragment View
Fragment CREATED 생존주기 : onCreate ~ onDestroy
관찰주기 : onCreate ~ onDestroyView
onViewCreated가 다시 호출되면 관찰하는 코루틴 Scope이 한 개 더 생성됨
생존주기 : onCreateView ~ onDestroyView
관찰주기 : onCreateView ~ onSavedInstanceState(onStop 직후)
STARTED 생존주기 : onCreate ~ onDestroy
관찰주기 : onStart ~ onPause
onViewCreated가 다시 호출되면 관찰하는 코루틴 Scope이 한 개 더 생성됨
생존주기 : onCreateView ~ onDestroyView
관찰주기 : onStart ~ onPause
View CREATED 생존주기 : onCreateView ~ onSavedInstanceState
관찰주기 : onCreateView ~ onSavedInstanceState
생존주기 : onCreateView ~ onSavedInstanceState
관찰주기 : onCreateView ~ onSavedInstanceState
STARTED 생존주기 : onStart ~ onPause
관찰주기 : onStart ~ onPause
생존주기 : onStart ~ onPause
관찰주기 : onStart ~ onPause

 

 

기본 개념 설명 - viewLifecycleOwner, lifecycleScope 그리고 repeatOnLifecycle

viewLifecycleOwner

 

Fragment View의 Lifecycle을 가지고 있는 LifecycleOwner를 의미합니다.

onCreateView ~ onDestroyView 사이에 접근할 수 있으며, 그 외에 접근하면 IllegalStateException이 발생합니다.

 

 

Fragment의 LifecycleOwner는 FragmentViewLifecycleOwner로, FragmentViewLifecycleOwner는 LifecycleOwner 인터페이스를 구현한 SavedStateRegistryOwner 인터페이스를 구현하고 있습니다.

 

 

getViewLifecycleOwnerLiveData 함수를 통해 Fragment view의 Lifecycle이 변경되는 것을 관찰할 수 있는 LiveData를 반환 받을 수 있습니다.

 

 

LifecycleOwner

 

LifecycleOwner는 Android Lifecycle을 property로 가지는 인터페이스입니다.

 

 

Fragment는 어떻게 Lifecycle을 구현하고 있을까?

 

Fragment class는 LifecycleOwner 인터페이스를 구현합니다.

 

 

Fragment에서 Lifecycle 변수는 LifecycleRegistry 클래스로 구현하고 있습니다.

변수가 함수로 구현 되어 있는 이유는, LifecycleOwner는 Kotlin 인터페이스이고, Fragment는 Java 클래스이기 때문입니다.

 

 

 

LifecycleRegistry 클래스 변수는 Fragment 생성자 호출 시점에 생성됩니다.

 

 

 

lifecycleScope

 

viewLifecycleOwner의 lifecycleScope은 아래와 같이 구현되어 있습니다.

while 문으로 되어 있는 이유는 lifecycleScope을 두 개 이상의 쓰레드가 동시에 접근해서 newScope이 2개 이상 생성된 경우, compareAndSet 함수를 늦게 호출한 함수는 newScope을 반환받을 수 없기 때문입니다.

그래서 compareAndSet 함수를 늦게 호출한 쓰레드는 while 문의 처음으로 돌아가서 다른 Thread가 생성한 newScope을 existing 변수로 받아서 사용하게 됩니다.

 

 

이겨서 눈여겨 볼 부분은 다음과 같습니다.

 

1. coroutineScope이 SupervisorJob을 이용한다는 점

 

coroutineScope 내 child가 exception 등으로 cancel이 발생해도, 다른 child는 cancel 되지 않습니다. 단, child의 child인 경우는 얘기가 달라집니다. child가 SupervisorJob으로 실행되지 않았다면 child의 child가 cancel된 경우, child의 다른 child도 cancel됩니다.

 

아래 예시에서 child B는 child X가 supervisorJob이 아니기 때문에, child A의 exception으로 발생한 child X의 취소로 같이 취소되어 출력되지 않았습니다.

 

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

fun main(): Unit = runBlocking {
    supervisorScope { // parent
        launch { // child X
            launch { // child X - child A
                delay(1000)
                throw Error("Some error")
            }
            launch { // child X - child B
                delay(2000)
                println("Will not be printed")
            }
        }

        launch { // child Y
            delay(2000)
            println("Will be printed")
        }
    }
    delay(1000)
    println("Done")
}

/**
 * Exception in thread "main" java.lang.Error: Some error
 * Will be printed
 * (1초 후)
 * Done
 */

 

2. compareAndSet을 이용한다는 점

 

Flow의 update 함수에서 사용하는 compareAndSet과 동일한 함수는 아니지만 이름이 같은 compareAndSet 함수를 만들어서 사용합니다.

 

 

repeatOnLifecycle

 

LifecycleOwner의 확장 함수로, LifecycleOwner의 lifecycle이 최소한 파라미터로 입력한 Lifecycle 일 때, 코루틴 람다 블록이 실행됩니다. 지정한 Lifecycle이 아니면 코루틴이 취소됩니다.

내부적으로 LifecycleOwner가 가지고 있는 Lifecycle의 repeatOnLifecycle 확장 함수를 호출하고 있습니다.