val uiScope = lifecycleScope + CoroutineExceptionHandler { _, _ ->
// 에러 핸들링
}
샘플 프로젝트를 진행하면서 프래그먼트에 위와 같이 코드를 작성했었는데, 약간의 문제점이 있다는 것을 알게 되었다.
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
lifecycleScope는 LifecycleOwner의 생명주기와 연결되어있는 CoroutineScope이다. 그래서 lifecycleScope에서 시작하는 코루틴은 해당 생명주기가 파괴될 때 안전하게 취소된다. 프래그먼트에서 lifecycleScope를 사용하게 되면, 해당 생명주기는 프래그먼트의 생명주기를 가리키게 된다.
하지만 여기서 내가 간과한 점은 프래그먼트의 생명주기와 프래그먼트에 있는 뷰의 생명주기는 다르다는 점이다. 프래그먼트의 생명주기보다 뷰의 생명주기가 더 짧고, 프래그먼트의 생명주기 콜백 메서드도 액티비티의 생명주기 콜백 메서드와 다르다. onCreate(), onDestroy(), onCreateView(), onDestroyView() 로 프래그먼트가 생성, 파괴되었을 때와 뷰가 생성, 파괴되었을 때의 콜백 메서드로 나뉘어져있다.
그런데 샘플프로젝트에서 내가 lifecycleScope를 이용해서 진행했던 작업은 전부 뷰와 관련된 UI 업데이트 작업이었다. 그러면 프래그먼트의 생명주기를 따르는 것이 아닌 뷰의 생명주기를 따르는 것이 적절하지 않을까?
이럴 때는 viewLifecycleOwner를 사용하면 된다. viewLifecycleOwner 코드를 살펴보면 Fragment.java의 getViewLifecycleOwner()로 연결된다.
@MainThread
@NonNull
public LifecycleOwner getViewLifecycleOwner() {
if (mViewLifecycleOwner == null) {
throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
+ "getView() is null i.e., before onCreateView() or after onDestroyView()");
}
return mViewLifecycleOwner;
}
주석 설명에 따르면 프래그먼트의 뷰의 생명주기를 나타내는 LifecycleOwner를 가져온다고 되어있다. viewLifecycleOwner는 onCreateView()에서부터 onDestroyView()에서만 유효하다. 만약 이외의 콜백메서드에서 viewLifecycleOwner를 호출하면
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewLifecycleOwner // IllegalStateException 발생
}
java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
이렇게 IllegalStateException이 발생한다.
val uiScope = viewLifecycleOwner.lifecycleScope + CoroutineExceptionHandler { _, _ ->
// 에러 핸들링
}
그래서 프래그먼트의 생명주기를 따르는 것이 아닌 위와 같이 프래그먼트 뷰의 생명주기를 따르는 viewLifecycleOwner.lifecycleScope를 사용하는 것이 적절하다. 이렇게 하면 뷰가 파괴된 후에도 코루틴이 계속 실행되는 문제를 방지할 수 있다.
// 위험한 코드
lifecycleScope.launch {
someFlow.collect {
binding.textView.text = it
}
}
정리하자면 위와 같이 프래그먼트 전체 생명주기와 묶여 있는 lifecycleScope를 사용하여 코루틴을 실행하여 view에 접근하는 코드를 작성하면 view가 없는 상태에서 view에 접근하는 상황이 발생하여 NPE가 발생할 수 있다. onDestroyView 이후에는 binding 객체가 null이기 때문이다.
LiveData를 observe할 때 observer를 지정할 때도 viewLifecycleOwner로 지정해야 하는 것도, 위와 같은 문제가 발생할 수 있기 때문이다.