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?
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!
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
Related
From a Google Codelab (can't remember which one), they adviced doing the following for fragments:
class MyFragment : Fragment() {
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = MyFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
And then accessing the views with e.g. binding.button1.
Is there a specific reason for doing it like this, with _binding and binding? Are there better methods? Perhaps an extension for Fragments - like a BaseFragment - to avoid code duplication.
It's not recommended to use BaseFragment or BaseActivity or BaseViewModel... it will just add boilerplate code to your project.
For binding you can just use it like this:
Declaration:
private var binding: MyFragmentBinding? = null
onCreateView:
binding = MyFragmentBinding.inflate(inflater, container, false)
binding?.root
Usage:
binding?.button...
binding?.text...
binding?.cardView...
onDestroyView:
binding = null
And everything is going to work just fine but we use the null check a lot (?) and it's making the code messy and we need to get a lot of null checks if we need something from a certain view, so we are sure that between onCreateView and onDestroyView, the binding is not null so we have _binding and binding:
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
We make _binding mutable with var so we can give it a value, and we make it nullable so we can clear it later.
And we have binding that have a custom getter so that means that each time we call binding it's going to return the latest value from _binding and force that it's not null with !!.
Now we seperate our variables, we have _binding to initialize and clear our binding, and we have binding that is immutable and not nullable to use it only for accessing views without null check ?
See this question for some answers about the reason why binding needs to be nullable in a fragment.
See this answer of mine where I linked some articles about the problems with BaseFragments. You can usually achieve the code reuse without the drawbacks of inheritance by using extension properties and functions.
Here is an example of a property delegate that takes care of releasing the ViewBinding reference when necessary and rebuilding it when necessary. If you use this, all you need is a single binding property. Example is from the article about this tool.
class FirstFragment: Fragment(R.layout.first_fragment) {
private val binding by viewBinding(FirstFragmentBinding::bind)
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
binding.buttonPressMe.onClick {
showToast("Hello binding!")
}
}
I just saw that CommonsWare has adressed this issue in this post.
Here is the parent class:
abstract class ViewBindingFragment<Binding : ViewBinding>(
private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding
) : Fragment() {
private var binding: Binding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return bindingInflater(inflater, container, false).apply { binding = this }.root
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
protected fun requireBinding(): Binding = binding
?: throw IllegalStateException("You used the binding before onCreateView() or after onDestroyView()")
protected fun useBinding(bindingUse: (Binding) -> Unit) {
bindingUse(requireBinding())
}
}
He then subclasses ViewBindingFragment like so:
class ListFragment :
ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
useBinding { binding ->
binding.items.layoutManager = LinearLayoutManager(context)
}
}
}
Though I am not sure it will eventually lead to less code, if useBinding { binding -> } needs to be called in several functions.
I'm playing around with Kotlin on Android and one thing makes me confused.
When I converted few Fragments from Java to Kotlin I got this:
class XFragment : Fragment() {
private var binding: FragmentXBinding? = null
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentUhfReadBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.slPower.addOnChangeListener(this)
binding!!.btnClearTagList.setOnClickListener(this)
}
// ...
private fun updateUi(){
binding!!.someTextView.text = getSomeTextViewText()
binding!!.someSlider.value = getSomeSliderValue()
}
}
I can't make binding non-nullable, because it has to be initialized after XFragment class constructor, in onCreateView() or later.
So with this approach it has to be nullable and I have to put !! everywhere.
Is there some way to avoid these !!?
The official documentation suggests this strategy:
private var _binding: FragmentXBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
Ultimately, it becomes just like requireActivity() and requireContext(). You just need to remember not to use it in a callback that might get called outside the view lifecycle.
Note, you can create your view using the super-constructor layout parameter and then bind to the pre-existing view in onViewCreated. Then you might not even need to have it in a property. I rarely need to do anything with it outside onViewCreated() and functions directly called by it:
class XFragment : Fragment(R.layout.fragment_x) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentXBinding.bind(view)
binding.slPower.addOnChangeListener(this)
binding.btnClearTagList.setOnClickListener(this)
}
}
As per the android documentation, To get the data binding within a fragment, I use a non-nullable getter, but sometimes' When I try to access it again, after I'm wait for the user to do something, I receive a NullPointerException.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = ResultProfileBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setupViews() {
// On click listener initialized right after view created, all is well so far.
binding.btnCheckResult.setOnClickListener {
// There is the crash when trying to get the nonnull binding.
binding.isLoading = true
}
}
Does anyone know what the cause of the NullPointerException crash is? I'm trying to avoid not working according to the android documentation, and do not return to use nullable binding property (e.g _binding?.isLoading). Is there another way?
I can't explain why you're having any issue in the code above since a View's click listener can only be called while it is on screen, which must logically be before onDestroyView() gets called. However, you also asked if there's any other way. Personally, I find that I never need to put the binding in a property in the first place, which would completely avoid the whole issue.
You can instead inflate the view normally, or using the constructor shortcut that I'm using in the example below that lets you skip overriding the onCreateView function. Then you can attach your binding to the existing view using bind() instead of inflate(), and then use it exclusively inside the onViewCreated() function. Granted, I have never used data binding, so I am just assuming there is a bind function like view binding has.
class MyFragment: Fragment(R.layout.result_profile) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = ResultProfileBinding.bind(view)
// Set up your UI here. Just avoid passing binding to callbacks that might
// hold the reference until after the Fragment view is destroyed, such
// as a network request callback, since that would leak the views.
// But it would be fine if passing it to a coroutine launched using
// viewLifecycleOwner.lifecycleScope.launch if it cooperates with
// cancellation.
}
}
I want to know why "private val binding get() = _binding!!" was used here?
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
I am assuming that you got that code from this page in the documentation.
Their objective is to give you a way to access the _binding value without needing to deal with the fact that _binding can be null. In the portion of their example that you did not include, they have a comment on binding that points out that it can only be used between onCreateView() and onDestroyView(). If you are in a part of your code where you can guarantee that your code will execute between those two callbacks, you can reference binding, which will return the value of _binding coerced into a not-null type (ResultProfileBinding instead of ResultProfileBinding?).
However, if you get it wrong, and you try referencing binding before onCreateView() or after onDestroyView(), you will crash with a NullPointerException.
Personally, I would avoid this approach.
I was going through developer docs for Data binding. I found the following snippet:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Can anyone let me know the principle and advantage of using two variables for binding which are used in same fragment?
At first glance, it seems like lateinit would be a more natural choice. However, the Fragment instance is still usable after onDestroyView since Fragment instances can be torn down and reattached later. lateinit won't let you change the parameter back into uninitialized state, so it's not suitable for this purpose.
Using !! can result in Kotlin NPEs, which is not great. I would suggest modifying the sample code to provide better documentation and error reporting, like this:
/** This property is only valid between onCreateView and onDestroyView. */
private val binding get() = _binding ?:
error("Binding is only valid between onCreateView and onDestroyView.")
But practically, your Fragment is not going to be so complicated that you would have trouble tracking down an error like this anyway.
binding is a non-null property with nullable backing field, so when you access it you don't have to constantly use ? to check for nullability.
It will however throw KotlinNullPointerException if accessed when it's not valid as described by the comment.
EDIT
this solution will cause a memory leak, as pointed out by IR42, and here is why
ORIGINAL ANSWER
null safety, but I think using lateinit is a better solution for that purpose
private lateinit var binding : ResultProfileBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = ResultProfileBinding.inflate(inflater, container, false)
return binding.root
}