저번 리팩토링 후 5일 동안 경과를 지켜봤습니다.
몇몇 이슈는 해결된 것을 확인했지만, 여전히 비정상 종료가 많이 일어나는 것이 보입니다.
이번엔 확실히 해결해야 할 것 같아요..
뷰 바인딩 관련 NullPointerException 예외가 대부분이었습니다.
단순히 onDestroyView()에서 binding을 null로 설정해 주는 문제가 아닌 것 같습니다.
첫 번째 오류
오류 스택을 자세히 확인해봤습니다.
오류가 발생한 코드 부분입니다.
<MyPlaylistItemsFragment.kt>
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context as Activity
fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
if (f is PlayerFragment) {
binding.emptyItem.visibility = View.VISIBLE
}
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
if (f is PlayerFragment) {
binding.emptyItem.visibility = View.GONE
}
}
}
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks,false)
}
음.. 유튜브에서는 동영상 플레이어 창이 있을 때, 뷰의 위치가 조정되더라고요.
앱 구조를 coordinatorLayout으로 작성했으면 쉬웠겠지만, motionLayout으로 하는 바람에 힘들어졌습니다.
이때, 또 뒤집어엎어야 하는 생각에 짜증이 많이 났었습니다.
시간 여유가 많지가 않아서 일시적으로 해결을 했습니다.
동영상 플레이어 창인 PlayerFragment에 대한 LifeCycleCallback을 다음과 같이 등록을 해줬었습니다.
리사이클러뷰 바로 밑에 빈 아이템을 두어, PlayerFragment의 실행 유무에 따라 visiblity를 변경해 주었습니다.
의심 가는 원인이 두 개가 있었습니다.
1. 콜백 등록 위치
onAttach() 매소드 내에서 콜백을 등록해주고 있습니다.
binding은 onCreateView() 매소드 내에서 초기화가 되기 때문에,
콜백 내의 onFragmentCreated 매소드가 호출되는 시점에서 binding이 null인 상태일 수도 있다는 생각이 들었습니다.
근데 제가 아는 한에서는, 이럴 수가 없습니다.
해당 원인으로 오류가 발생하기 위해서는, 플레이리스트 뷰가 생성되기 전에 동영상 플레이어 창을 띄워야 하는데,
손이 아무리 빨라도 상식적으로 불가능하다는 생각이 들었습니다.
혹시 모르기 때문에, 뷰가 생성되고 호출되는 onViewCreated() 매소드에
콜백을 등록해 주었습니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
if (f is PlayerFragment) {
binding.emptyItem.visibility = View.VISIBLE
}
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
if (f is PlayerFragment) {
binding.emptyItem.visibility = View.GONE
}
}
}
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks,false)
}
2. 콜백 해제 위치
제 생각에 이게 원인인 것 같아요.
override fun onDestroy() {
super.onDestroy()
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
}
override fun onDestroyView() {
super.onDestroyView()
fbinding = null
}
binding은 onDestroyView() 내에서 null 처리를 해주고, 콜백은 onDestroy() 내에서 제거를 해주고 있었습니다.
이럴 경우 뷰가 파괴된 후,
onDestroy()가 호출되기 전에 동영상 프레그먼트 창이 닫히면 nullException 예외가 발생할 것 같습니다.
다음과 같이 수정을 했습니다.
override fun onDestroyView() {
super.onDestroyView()
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
fbinding = null
}
두 번째 오류
Exception java.lang.NullPointerException:
at com.myFile.transpose.fragment.HomeFragment.getBinding (HomeFragment.kt:42)
at com.myFile.transpose.fragment.HomeFragment$onError$2.invokeSuspend (HomeFragment.kt:365)
at com.myFile.transpose.fragment.HomeFragment$onError$2.invoke
at com.myFile.transpose.fragment.HomeFragment$onError$2.invoke
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn (Undispatched.kt:89)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext (Builders.common.kt:166)
at kotlinx.coroutines.BuildersKt.withContext
at com.myFile.transpose.fragment.HomeFragment.onError (HomeFragment.kt:363)
at com.myFile.transpose.fragment.HomeFragment.access$onError (HomeFragment.kt:39)
at com.myFile.transpose.fragment.HomeFragment$coroutineExceptionHandler$1$1.invokeSuspend (HomeFragment.kt:110)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
at android.os.Handler.handleCallback (Handler.java:938)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loop (Looper.java:246)
at android.app.ActivityThread.main (ActivityThread.java:8633)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1130)
첫 오류가 발생한 시점이 coroutineExeptionHandler 부분이었습니다.
해당 코드입니다.
private val coroutineExceptionHandler = CoroutineExceptionHandler{ _, throwable ->
lifecycleScope.launch {
onError(throwable)
}
}
private suspend fun onError(throwable: Throwable){
withContext(Dispatchers.Main){
binding.mainScrollView.visibility = View.GONE
binding.errorLinearLayout.visibility = View.VISIBLE
when (throwable){
is UnknownHostException -> binding.errorMessage.text = activity.getText(R.string.network_error_text)
is SocketTimeoutException -> binding.errorMessage.text = activity.getText(R.string.load_fail_message)
else -> binding.errorMessage.text = activity.getText(R.string.quota_error_message)
}
}
}
예외 처리와, 오류 원인을 띄우기 위한 함수입니다.
네트워크 작업이기 때문에, 뷰가 파괴된 이후에 onError()가 실행이 되어 발생한 오류 같습니다.
HomeFragment는 앱의 첫 화면입니다. show, hide 방식으로 띄워주고 있습니다.
해당 뷰가 파괴되었다는 뜻은 앱을 완전히 종료시켰다는 뜻인데, 그럴 경우 오류 메시지를 띄우지 않아도 됩니다.
따라서 null이 가능한 fbinding으로 바꿔주었습니다.
private suspend fun onError(throwable: Throwable){
withContext(Dispatchers.Main){
fbinding?.mainScrollView?.visibility = View.GONE
fbinding?.errorLinearLayout?.visibility = View.VISIBLE
when (throwable){
is UnknownHostException -> fbinding?.errorMessage?.text = activity.getText(R.string.network_error_text)
is SocketTimeoutException -> fbinding?.errorMessage?.text = activity.getText(R.string.load_fail_message)
else -> fbinding?.errorMessage?.text = activity.getText(R.string.quota_error_message)
}
}
}
이게 해결책이 맞나 잘 모르겠네요..
세 번째 오류의 원인은 첫 번째 오류의 원인과 동일한 부분이었습니다.
오류를 수정한 후, 업데이트를 진행했습니다.
경과를 지켜보고 오류가 발생할 경우, 다시 포스팅하겠습니다.
'안드로이드 프로젝트 > 유튜브 음정 조절 어플리케이션' 카테고리의 다른 글
[Android] #12 mediaSession 공부 및 코드 리팩토링 - 1 (0) | 2023.06.29 |
---|---|
[Android] #11 Youtube data api 할당량 최적화 (데이터 캐싱) (0) | 2023.06.27 |
[Android] # 문제 해결 -3 앱 이슈 해결 (0) | 2023.06.06 |
[Android] #문제 해결 -2 앱이 자주 팅기는 이슈 (0) | 2023.05.26 |
[Android] #문제 해결 -1 SeekBar 관련 이슈 (0) | 2023.05.26 |
댓글