👩💻 오늘의 할 일
오늘은 제목처럼 ViewLifeCycleOwner에 대해 제대로 알아볼 겁니다. 그동안 단순히 액티비티와 프래그먼트에서는 생명주기를 가지는 컴포넌트에서는 해당 컴포넌트의 생명주기에 종속되는 코루틴을 사용해야 한다고 알고 있었습니다.
Activity에서는 lifeCycleScope, Fragment에서는 ViewLifeCycleOwner를 사용하는게 바로 이 이유죠. 그렇다면 왜 두 컴포넌트는 각각 다른 코루틴을 사용하는 걸까요?
이는 Fragment의 두 가지 생명주기와 깊은 연관이 있습니다. 저도 안드로이드 개발을 시작한지 약 반년이 다되어 가도록 이 프래그먼트의 두 가지 생명주기에 대해 놓치고 있던 내용이라 한 번 정리하겠습니다.
📖 Fragment의 두 가지 생명주기
처음 Fragment LifeCycle에 대해 공부할 때도 봤던 그림인데 제가 간과한 부분은 오른쪽의 View LifeCycle 이었습니다. 어째서인지 onAttach와 onDetach는 빠져있네요 ...?
Fragment의 Lifecycle 이 변화하면 Callback 함수를 호출하고 바로 이 콜백 함수가 종료되는 시점에 View의 Lifecycle 에 이벤트를 전달하게 됩니다. 이전에 공부했던 내용을 리마인드 할 겸 콜백 함수에 대해 간단히 짚고 넘어가겠습니다.
onCreate()
프래그먼트가 FragmentManager에 추가된 상태로 아직 Fragment View는 생성되지 않았기 때문에 Fragment의 View와 관련된 작업을 해서는 안됩니다.
Activity의 OnCreate()처럼 onSaveInstanceState() 콜백 함수에 의에 저장된 Bundle 타입의 savedInstanceState 파라미터가 함께 제공되는데 이는 프래그먼트가 처음 생성 됐을 때는 null로 전달되며, onSaveInstanceState() 함수를 오버라이드하지 않았더라도 그 이후 재생성부터는 non-null 값으로 넘어옵니다.
onCreateView()
이 콜백 함수의 반환값으로 정상적인 Fragment View 객체를 제공했을 때만 Fragment의 View의 Lifecycle 이 생성됩니다. Fragment에서 ViewBinding을 사용 때 한 가지 주의점이 있습니다.
Fragment의 View는 onDestroyView에서 참조가 해제되는데 만약 Binding 객체가 남아있을 경우 View가 파괴더더라고 Binding 객체가 계속해서 참조를 유지할 수 있습니다.
이러한 이유 때문에 Fragment에서 ViewBinding을 사용할 때 공식문서에선 Memory Leak을 방지하기 위해 다음과 같은 방식으로 ViewBinding을 선언할 것을 권장하고 있습니다.
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
LayoutId를 받는 Fragment 생성자를 활용해 onCreateView()를 생략하고 프로그래매틱 방식으로 ViewBinding을 설정할 수 도 있습니다. 아래 onViewCreated( ) 에서 이 내용을 계속 알아보겠습니다.
onViewCreated()& View INITIALIZED
onCreateView()를 통해 반환된 View 객체는 onViewCreated()의 파라미터로 전달되고 이 객체는 Fragment 생성자에 넘겨줄 레이아웃을 inflate 한 객체입니다. 이를 활용하면 다음과 같이 ViewBinding을 선언할 수 있습니다.
class MainFragment : Fragment(R.layout.fragment_main) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentAppointmentMainBinding.bind(view)
}
}
이 시점부터는 View lifecycle owner가 INITIALIZED 상태로 업데이트 됐기 때문에 공식문서에선 이 시점에서 View를 초기화하거나 LiveData 옵저빙 하는 것을 권장하고 있습니다
저는 Fragment에서 View Date Setting을 보통 initView() 함수로 묶어서 작업했는데 이런 작업은 onViewCreated( )에서 수행해 주면 되겠네요!
onViewStateRestored() & View CREATED
이전 뷰 상태(state)가 Fragment View의 계층 구조에 복원되고 View lifecycle owner 는 이때 INITIALIZED 상태에서 CREATED 상태로 변경됩니다.
onStart() & View STARTED
Fragment 가 사용자에게 보일 수 있을 때 호출됩니다. 공식 문서 내용 중 굉장히 중요한 부분을 발견했습니다.
FragmentManager 통해 Fragment Transaction 을 안전하게 수행할 수 있기 때문에
수명 주기 인식 구성요소를 프래그먼트의 STARTED 상태와 연결하는 것이 좋습니다
저는 이 말을 보고 딱 StateFlow가 떠올랐습니다. StateFlow는 LifeCycle을 알지 못하기 때문에 특정 LifeCycle일 때 트리거되는 코루틴을 사용해야 합니다. 이때, 사용되는게 repeatOnLifeCycle인데 인자로 받은 LifeCycle일 때 내부 코드를 수행하게 됩니다.
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 필요한 동작
}
}
이 때 제가 항상 트리거 되게 만든 시점이 STARTED였는데 바로 이 때문이었습니다!! 솔직히 이유는 모르고 사용했었는데...😅
onResume() & View RESUMED
모든 Animator와 Transition 효과가 종료되고, 프래그먼트가 사용자와 상호작용할 수 있습니다. 반대로 생각하면 onResume() 이 호출되지 않은 시점에서는 입력을 시도하거나 포커스를 설정하는 등의 작업을 임의로 하면 안 되겠죠?
onPause() & View STARTED
사용자가 프래그먼트를 벗어나기 시작했지만 아직 프래그먼트가 표시된 상태일 때 호출됩니다. 이때 View의 Lifecycle 이 PAUSED 가 아닌 다시 STARTED 가 됩니다. Lifecycle 에는 PAUSE와 STOP에 해당하는 상태가 없기 때문이죠!
onStop() & View CREATED
Fragment 가 더 이상 화면에 보이지 않을 때 호출됩니다. API 28 버전부터 onSaveInstanceState() 함수와 onStop() 함수 호출 순서가 달라지게 되어 onStop() 이 FragmentTransaction 을 안전하게 수행할 수 있는 마지막 지점이 되었습니다.
onDestroyView() & View DESTROYED
모든 exit animation 과 transition 이 완료되고, Fragment 가 화면으로부터 벗어난 상태입니다. 이 시점엔 메모리 릭 방지를 위해 프래그먼트의 뷰에서 가비지를 수집할 수 있게 뷰의 모든 참조를 삭제해야 합니다. Fragment에서 viewBinding을 사용할 때 바인딩을 해제해주는 것도 이러한 이유죠
onDestroy()
프래그먼트가 삭제되거나 FragmentManager가 소멸된 상태입니다. 이 시점에서 프래그먼트는 수명 주기의 끝에 도달한 상태입니다.
📖LifecycleOwner & viewLifecycleOwner
LifecycleOwner는 Fragment 자체의 생명주기 즉, [ onAttach() ~ onDestroy() ] 와 연결되어 있습니다.
viewLifecycleOwner는 이름에서도 유추할 수 있다 싶이 프래그먼트 뷰의 생명주기 [ onCreateView ~ onDestroyView ] 와 연결되어 있습니다. 그렇다면 왜 하필 View의 LifeCycle을 따라야 할까요? 🤔
Fragment에서 Fragment로 이동하는 한 가지 가정을 통해 예시를 보면서 이해해 보겠습니다.
가정 : FragmentTransaction을 통해 Fragment를 replace 하고 addToBackStack() 합니다.
1. Fragment 생성
- Fragment의 생명주기 콜백 함수가 [ onAttach ~ onResume ] 까지 호출됩니다.
- replace로 Fragment로 교체하며 이때, addToBackStack 함수로 FirstFragment를 BackStack에 삽입합니다.
2. FragmentSecond로 이동(replace, addToBackStack)
Fragment의 생명주기가 onDestroyView 까지 호출되며 이때, Fragment의 View는 사라지지만 Fragment 자체는 BackStack에 저장되어 메모리에 남아있게 됩니다.
3. 뒤로 가기 버튼을 눌러 다시 FirstFragment로 돌아옵니다.
위 과정을 통해 확인할 수 있는 내용은 다음과 같습니다.
- Fragment의 생명주기가 Destroy까지 도달하지 않을 때 Fragment의 View가 아닌 Fragment 자체의 생명주기(LifeCycleScope)를 사용한다면 LiveData 같은 리소스들이 메모리에 그대로 남아있어 Memory Leek을 발생시킬 것입니다.
- 또한 onCreateView가 여러 번 호출되면서 Observer 중복으로 등록되어 다수의 Observer가 만들어지는 현상이 발생할 수 있습니다.
📕 후기
이렇게 Fragment의 LifeCycle와 생명주기에 따라 동작하는 Coroutine에 대해 알아봤습니다. 리소스 관리라는게 정말 조금만 무관심하면 놓치기 쉬운 문제인 것 같습니다. 이렇게 하나하나 공부하고 나니 그동안 그냥저냥 지나쳤던 요소들도 아? 이런거 때문이었구나라고 우연하게 발견할 수 도 있었고요 😀
예를 들면 repeatOnLifeCycle에서 인자로 LifeCycle.STATE.STARTED를 넘기는 이유도 이번 기회에 알 수 있었고요! 이렇게 정리하지 않았다면 STARTED가 왜 사용되는지도 알 수 없었을 것 같습니다. 물론 제 프로젝트 같이 작은 프로젝트에선 큰 문제가 안될 수 도 있지만 먼 훗날 제가 개발자가 되었을 때 이런 작은 습관 하나 하나가 더 좋은 개발자가 될 수 있는 힘이 될 것 같습니다.
참조
https://readystory.tistory.com/199
https://velog.io/@jeongminji4490/Android-LifecycleOwner-vs-viewLifecycleOwner-in-Fragment
'Android' 카테고리의 다른 글
[Android]프로젝트를 클린 아키텍처로 마이그레이션해보자 (0) | 2024.07.12 |
---|---|
버튼 중복 클릭을 막아보자 (Android ThrottleFirst) (1) | 2024.06.24 |
Room Like + StateFlow debouce와 Throttle (0) | 2024.03.31 |
[Android] 프로퍼티의 초기화 시점이 중요한 이유! (0) | 2024.01.28 |
[Android] Room Database Migration (0) | 2024.01.21 |