1

The application started to receive some crashes (it is not reproducible 100%) due to some lifecycle issue for the Fragment.

I'm using view binding and I'm manually invalidating the binding as per Android recommendations to avoid high memory usage in case the reference to the binding is kept after the Fragment is destroyed.

private var _binding: FragmentCustomBinding? = null
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View = FragmentCustomBinding.inflate(inflater, container, false).also {
    _binding = it
}.root

override fun onDestroyView() {
    _binding = null
    super.onDestroyView()
}

override fun onSaveInstanceState(outState: Bundle) {
    outState.apply {
        putString(BUNDLE_KEY_SOME_VALUE, binding.etSomeValue.text.toString())
    }
    super.onSaveInstanceState(outState)
}

I'm getting a NullPointerException in onSaveInstanceState() as the binding is null as this was called after onDestroyView().

Any idea how I could solve this without manually creating a saved state and manually handling it?

Jonathan Simonney
  • 585
  • 1
  • 11
  • 25
Ionut Negru
  • 6,186
  • 4
  • 48
  • 78

2 Answers2

1

The binding = null is causing the issue. To get rid of the _binding = null in the correct manner use this code:

class CustomFragment : Fragment(R.layout.fragment_custom) {

  private val binding: FragmentCustomBinding by viewBinding()
  
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onViewCreated(view, savedInstanceState)
      // Any code we used to do in onCreateView can go here instead
  }
}

According to an article on this workaround:

This technique uses an optional backing field and a non-optional val which is only valid between onCreateView and onDestroyView. In onCreateView, the optional backing field is set and in onDestroyView, it is cleared. This fixes the memory leak!

miken32
  • 42,008
  • 16
  • 111
  • 154
Narendra_Nath
  • 4,578
  • 3
  • 13
  • 31
  • 1
    Thanks for the answer. Indeed, that is the cause and it works as expected, it makes sure the view does not outlive the Fragment. What does not work as intended is the fact the `onSaveInstanceState` is called when the view is not there anymore. It shouldn't create scenarios like this. I could just use the safe operator, but I will have cases where the state isn't saved. Your solution, from what I can see relies on a delegate property. This helps to reduce some boilerplate code, but I think it does the same thing and I'm not sure that it actually fixes the issue. – Ionut Negru Aug 26 '22 at 07:11
1

It seems the answer for this is in how the fragments are handled, even when they do not have a view, as changes in the Activity state can still trigger onSavedInstanceState() thus I can end up in scenarios where I am in onSavedInstanceState() but without a view. This seems to be intentional as fragments are still supported whether they have a view or not.

The recommendation was to use the view APIs for saving and restoring state (or my SavedStateRegistery).

A few more details can be found here: https://issuetracker.google.com/issues/245355409

Ionut Negru
  • 6,186
  • 4
  • 48
  • 78