I'm implementing an empty state screen of an app, but it is crashing after the second time it returns to the fragment.
When the app starts for the first time, it correctly creates my view, but when I add something to the previous list (main fragment -> register doc fragment (creating new item for my recycler list) -> main fragment), it crashes in the following line:
binding.imageEmptyState.hide()
From the following function:
private fun configDataCollector() = lifecycleScope.launch {
viewModel.list.collectLatest { result->
when(result) {
is ResourceState.Success -> {
binding.imageEmptyState.hide()
binding.textEmptyState.hide()
result.data?.let { values ->
adapterDoc.docs = values.toList()
}
}
is ResourceState.Empty -> {
binding.imageEmptyState.show()
binding.textEmptyState.show()
}
else -> { }
}
}
}
Receiving:
2023-01-04 11:55:35.905 14014-14014/com.tods.docreminder E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.tods.docreminder, PID: 14014
java.lang.NullPointerException
at com.tods.docreminder.feature.doc.presentation.util.BaseFragment.getBinding(BaseFragment.kt:13)
at com.tods.docreminder.feature.doc.presentation.doc.MainFragment.access$getBinding(MainFragment.kt:34)
at com.tods.docreminder.feature.doc.presentation.doc.MainFragment$configDataCollector$1$1.invokeSuspend(MainFragment.kt:255)
My resource state class:
sealed class ResourceState<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T?): ResourceState<T>(data)
class Error<T>(message: String?, data: T? = null): ResourceState<T>(data, message)
class Loading<T>: ResourceState<T>()
class Empty<T>: ResourceState<T>()
}
Base fragment:
abstract class BaseFragment<VB: ViewBinding, VM: ViewModel>: Fragment() {
private var _binding: VB? = null
protected val binding get() = _binding!!
protected abstract val viewModel: VM
override fun onCreateView
(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = recoverViewBinding(inflater, container)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
protected abstract fun recoverViewBinding(inflater: LayoutInflater, container: ViewGroup?): VB
}
Why would it happen in this situation?
Thanks for your help
If any else code is needed, I'll edit the question asap.
CodePudding user response:
A Fragment instance has a lifecycle that is longer than its views. As shown below, these functions represent lifecycles that start and stop. Views can be created and destroyed multiple times while the Fragment's lifecycle is still running.
Fragment
-onCreate
-onCreateView ** These two lines can be called multiple times
-onDestroyView ** inside the Fragment's outer lifecycle.
-onDestroy
So if you reference binding
in a coroutine that lives on the Fragment's lifecycleScope
instead of the viewLifecycle
's lifecycleScope
, then it's possible that your coroutine will try to use binding
when it is null, right at this point:
Fragment
-onCreate
-onCreateView
-onDestroyView
**Accessing binding here will throw an NPE because it is null after onDestroyView
-onDestroy
If you use viewlifecycle.lifecycleScope
for your coroutine, it will automatically be cancelled when the view is destroyed, so it will never try to use binding
at these invalid times in the Fragment's life.
It's a matter of opinion, but I want to mention that I think BaseFragments cause more problems than they solve. Some articles about the subject: