Search code examples
androidkotlinandroid-viewbinding

Why do we provide inflater argument into BindingInflater lambda?


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:

  • "inflater is just a reference to the inflate binding function.The ViewBinding interface does not have an inflate function, but each of its implementations has one. That's why you have to transfer it manually"
  • What you have written is a reference to the ViewBinding::inflate method, and this method accepts inflater. And in order to be able to point to the method, it must have the same parameters

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.


Solution

  • 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.

    1. binding class
    2. binding method (binding function)

    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.