Do I have to free my custom ArrayAdapters in fragments, like I do it with binding? And also what about ArrayList that holds the data?
Inside my Fragment class I have:
private var binding: FragmentUhfReadBinding? = null // init onCreateView, free onDestroyView, use onViewCreated
private var listAdapter: ReadUhfTagInfoItemViewAdapter? = null // init in onViewCreated
private val arrayList: ArrayList<ReadUhfTagInfo?>? = ArrayList()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentUhfReadBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
// do I have to set `listAdapter` to null here?
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listAdapter = ReadUhfTagInfoItemViewAdapter(this.context, arrayList)
binding!!.lvData.adapter = listAdapter
}
Yes, because your adapter is referencing a context (which is the attached Activity).
It's also referencing a list that you have created only in your Fragment, but it's debatable whether you should care if that's leaked. It would be more typical for backing list data to be in a ViewModel so it could be reused, in which case this list reference your adapter is holding wouldn't be a problem.
Although your example doesn't show it, it's also very common for an adapter to expose some kind of listener for view interactions. So when you add this, and your listeners capture references to other stuff in the fragment, then the adapter would also be leaking the memory of those things.
However, it is not common in the first place to need your binding and your adapter to be in class properties. If you use them solely within onViewCreated() and functions called by onViewCreated(), then you don't need to worry about clearing references to them, and you won't have the ugly use of !!, so it will be more robust.
Instead of inflating a view with onCreateView(), you can pass a layout id to the superconstructor, and then create your binding by calling bind with the pre-existing view in onCreateView().
class MyFragment: Fragment(R.layout.fragment_uhf_read) {
private val arrayList: ArrayList<ReadUhfTagInfo?> = ArrayList()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentUhfReadBinding.bind(view)
val listAdapter = ReadUhfTagInfoItemViewAdapter(this.context, arrayList)
binding.lvData.adapter = listAdapter
// ... other code using binding and listAdapter
}
}
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 am developing an app with Firebase. But whenever I use the onViewCreated method, the button does not respond to any clicks. But when I use the onCreateView, it works.
Here is my LoginFragment (Button does not respond to clicks):
class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentLoginBinding.inflate(layoutInflater)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS NOT WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
}
}
In this code, my button responds to clicks:
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInFlater,
container: ViewGroup?,
savedInstanceState: Bundle?
) {
val view = inflater.inflate(R.layout.fragment_login, container, false)
val binding = FragmentLoginBinding.bind(view)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
return view
}
}
Can someone explain to me why the first approach did not work?
The problem is in the fact that in onViewCreated you are creating a binding object with FragmentLoginBinding.inflate(layoutInflater) but you are not connecting that binding to the view, so whatever you do with that object will not have effect on the view.
FragmentLoginBinding.inflate(layoutInflater) creates a new binding object and also inflate a new view to which it is connected. But you are not using that view in your fragment, so using that method is not the correct choice.
So you can do something like:
val binding = FragmentLoginBinding.bind(getView())
inside onViewCreated if you really want, and that will create a binding with the view you have in your fragment.
Said that, creating the binding already in onCreateView is actually recommended by the Android documentation.
I have a ParentFragment and a ChildFragment. They are working pretty fine.
My problem is that in the future I might create many child fragments and this makes me write this boilerplate code for every single child fragment. Thus, I would like to optimize my ParentFragment so that I do not have to write boilerplate code for every single new child framment I create in the future.
ParentFragment
abstract class ParentFragment<T: ViewDataBinding>: Fragment() {
#LayoutRes
abstract fun getLayoutResId(): Int
protected lateinit var binding: T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return DataBindingUtil.inflate<T>(inflater, getLayoutResId(), container, false).apply { binding = this }.root
}
ChildFragment
class ChildFragment: ParentFragment<FragmentChildBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//THIS IS THE BOILERPLATE METHOD I AM TALKING ABOUT.
//I WOULD LIKE TO MOVE THIS CODE IN THE PARENTFRAGMENT
initBinding()
}
#LayoutRes
override fun getLayoutResId() = R.layout.fragment_child
fun initBinding() {
val viewModel: ChildViewModel = getViewModel() //This method is from Koin
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
I tried to move this initBinding method code into ParentFragment but I got errors. Any suggestions would be greatly appreciated.
So, your current issue is that you want to make your initBinding() function common in such a way that all child fragment can access it.
Now issue is that logic inside initBinding comes with koin dependency that gets resolved dynamically. So, each child fragment may have different ViewModel instance.
There is one way you can resolve this problem:
Take your ViewModel object as method parameter so that you can inject it from your child fragment while having method in parent fragment. check out below how:
initBinding() Method in ParentFragment with viewModel as method parameter that we will pass on from child fragments.
abstract class ParentFragment<T: ViewDataBinding>: Fragment() {
// Other stuffs removed for sack of simplicity
protected fun initBinding(viewModel: ViewModel) {
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
Accessing it from ChildFragment:
class ChildFragment: ParentFragment<FragmentChildBinding>() {
// Other stuffs removed for sack of simplicity
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// THIS IS HOW YOU CAN CONSUME THIS METHOD
initBinding(getViewModel())
}
}
You need to call serVariable() on your binding object to set ViewModel in the parent Fragment. Optionally in your parent Fragment you can get ViewModel through abstract function if child Fragments have their own ViewModels.
You can follow this post to solve your problem.