I am new in Android development and can not fully understand this template for ViewBinding using abstract class. More precisely, I don’t understand why we prode inflater argument into bindingInflater lambda. We do not use this parameter explicitly and because of that it's hard for me to understand how this template actualy works.
Template
abstract class BindingFragment<out T : ViewBinding> : Fragment() {
private var _binding: ViewBinding? = null
@Suppress("UNCHECKED_CAST")
protected val binding: T
get() = _binding as T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = bindingInflater(inflater)
return _binding!!.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
protected abstract val bindingInflater: (LayoutInflater) -> ViewBinding
}
Using this in the fragment
class LoginFragment : BindingFragment<FragmentLoginBinding>() {
override val bindingInflater: (LayoutInflater) -> ViewBinding
get() = FragmentLoginBinding::inflate
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnConfirm.setOnClickListener {...}
}
}
I asked this question to some devs and they could not answer this qustion clearly. Some of them told me these:
I figured out how out modefire works and :: . I know casting, generics and how work getters and setters in Koltin.
But this code still is not clear for me.
Android Studio generates view binding class based on your layout file to easily access views inside your layout.
Let says we have fragment_login.xml
layout and LoginFragment
class like this.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv"/>
</LinearLayout>
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root // LinearLayout
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tv.setText(R.string.app_name)
}
}
In onCreateView
function, we inflated fragment_login
layout using FragmentLoginBinding
, then we returns the parent view. Since binding
is initialized first in onCreateView
function, we can use it in onViewCreated
.
But, because of the way the Fragments were designed, they can outlive their views and can be reused with a different view.
More precisely, the view you inflated fragment_login
may still be in the memory even after the fragment itself is destroyed, which can lead to memory leaks. So we need to nullify the binding when the fragment is destroyed.
class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tv.setText(R.string.app_name)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
So every time we create a new fragment, we need to inflate the required view and nullify the view, causing duplicate codes. To solve this, we can create a template class you described in your question.
In the template class, we requires 2 data.
Binding class is used to cast base ViewBinding
class into the required class (FragmentLoginBinding
in our case). The following code does it.
abstract class BindingFragment<out T : ViewBinding> : Fragment() {
private var _binding: ViewBinding? = null
@Suppress("UNCHECKED_CAST")
protected val binding: T get() = _binding as T
So if we use BindingFragment
template like this, binding
becomes FragmentLoginBinding
and we can access the views inside it.
class LoginFragment : BindingFragment<FragmentLoginBinding>()
But providing binding class alone cannot inflate the required view. So we need to pass inflate
function reference of our binding class, FragmentLoginBinding::inflate
generated by the IDE.
override val bindingInflater: (LayoutInflater) -> ViewBinding
get() = FragmentLoginBinding::inflate
So
_binding = bindingInflater(inflater)
become
_binding = FragmentLoginBinding.inflate(inflater)
bindingInflater
lambda is actually a function that returns ViewBinding
.
protected abstract val bindingInflater: (LayoutInflater) -> ViewBinding
// equals
protected abstract fun bindingInflater(inflater: LayoutInflater): ViewBinding
So, we override this function in our fragment like this.
override val bindingInflater: (LayoutInflater) -> ViewBinding
get() = FragmentLoginBinding::inflate
// equals
override fun bindingInflater(inflater: LayoutInflater): ViewBinding {
return FragmentLoginBinding.inflate(inflater)
}
Then, when we invoke bindingInflater
lambda, it actually returns the overridden value FragmentLoginBinding
.