Home > Blockchain >  Receiving NPE on binding when changing state of my request
Receiving NPE on binding when changing state of my request

Time:01-05

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:

  • Related