59

I started using ViewBinding. After searching for an example or some advice, I ended up posting this question here.

How do I use ViewBinding with an abstract base class that handles the same logic on views that are expected to be present in every child's layout?

Scenario:
I have a base class public abstract class BaseFragment. There are multiple Fragments that extend this base class. These Fragments have common views that are handled from the base class implementation (with the "old" findViewById()). For example, every fragment's layout is expected to contain a TextView with ID text_title. Here's how it's handled from the BaseFragment's onViewCreated():

TextView title = view.findViewById(R.id.text_title);
// Do something with the view from the base class

Now the ViewBinding API generates binding classes for each child Fragment. I can reference the views using the binding, but I can't use the concrete Bindings from the base class. Even if I introduced generics to the base class, there are too many types of fragment bindings So I discarded this solution for now.

What's the recommended way of handling the binding's views from the abstract base class? Are there any best practices? I didn't find a built-in mechanism in the API to handle this scenario in an elegant way.

When the child fragments are expected to contain common views, I could provide abstract methods that return the views from the concrete bindings of the Fragments and make them accessible from the base class. (For example protected abstract TextView getTitleView();). But is this an advantage rather than using findViewById()? Are there any other (better) solutions?

medavox
  • 175
  • 2
  • 11
Danny
  • 1,233
  • 1
  • 11
  • 19
  • I think you can write `public abstract int getLayoutResourse();` in your BaseFragment and pass it to `DataBindingUtil.inflate()` instead of `R.layout.frag_layout` or I did not understand the question – Alex Rmcf Jun 16 '20 at 12:11
  • @AlexRmcf first: I don't use DataBinding, I simply want to use ViewBinding. Yes, this would be possible to get a `ViewDataBinding` from the base class. But I can't access views via `ViewDataBinding.textTitle` for example from the base class without knowing the concrete type of binding-class. – Danny Jun 16 '20 at 12:31
  • I have the same issue, I have a base ViewHolder which contains views and childs which also contains their own views, how can I pass their View Binder to the parent ViewHolder without having to do "if (child instance of X) then binding = XBinding" – Nayk0 Jul 06 '20 at 07:28
  • hey did you find a solution to this? – ansh sachdeva Nov 12 '20 at 14:06
  • 1
    @anshsachdeva currently it seems like I have one sulution. I still need to figure out whether it works and will give an answer soon. In best case tomorrow I can respond with a positive result ;) – Danny Nov 12 '20 at 16:53
  • @heisenberg eagerly waiting for this. I have also come up with some implementations but they still look a little untidy, as they use effectively final variables. I hope your solution does not uses reflection tho, trying to stay away from those :D – ansh sachdeva Nov 12 '20 at 22:25

11 Answers11

44

Hi I have created a blog post which covers view-binding in-depth, and also includes both composition patter/delegate pattern to implement view binding as well as using inheritance checkout from the link

checkout for complete code of BaseActivity and BaseFragment along with usage

Androidbites|ViewBinding

/*
 * In Activity
 * source : https://chetangupta.net/viewbinding/
 * Author : ChetanGupta.net
 */
abstract class ViewBindingActivity<VB : ViewBinding> : AppCompatActivity() {

    private var _binding: ViewBinding? = null
    abstract val bindingInflater: (LayoutInflater) -> VB

    @Suppress("UNCHECKED_CAST")
    protected val binding: VB
        get() = _binding as VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = bindingInflater.invoke(layoutInflater)
        setContentView(requireNotNull(_binding).root)
        setup()
    }

    abstract fun setup()

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}
/*
 * In Fragment
 * source : https://chetangupta.net/viewbinding/
 * Author : ChetanGupta.net
 */
abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {

    private var _binding: ViewBinding? = null
    abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB

    @Suppress("UNCHECKED_CAST")
    protected val binding: VB
        get() = _binding as VB

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = bindingInflater.invoke(inflater, container, false)
        return requireNotNull(_binding).root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setup()
    }

    abstract fun setup()

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

For usage, advance pattern and antipattern checkout blog Androidbites|ViewBinding

Chetan Gupta
  • 1,477
  • 1
  • 13
  • 22
  • bindLayout is bindingInflater ? – hamid Mahmoodi Jan 08 '21 at 19:43
  • Thanks @Chetan, exactly what I was looking for. I've modified a bit your code to avoid "UNCHECKED_CAST" (in Kotlin): `private var _binding: ViewBindingType? = null` `protected val binding get() = requireNotNull(_binding)` `abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ViewBindingType` – Myroslav Jan 26 '21 at 13:01
  • 1
    Hi @ChetanGupta please also include an example activity/fragment in the answer. – Angad Singh May 30 '21 at 08:15
  • 4
    I'm currently dealing with the same problem but I don't see how this is a solution. How are you supposed to make the generic code that sets the text for "text_title" (in OP's case) in the ViewBindingFragment class, if you don't know yet which particular binding will be used? – Tharkius Jan 25 '22 at 12:26
  • What concept/approach is being use with `bindingInflater` here? Is it functional? I understand the concept of invoke in Kotlin but not with this one. Can anyone explain it to me? – Bitwise DEVS Jun 26 '22 at 22:30
14

I found an applicable solution for my concrete scenario and I want to share it with you.

Note that this is not an explanation on how ViewBinding works.

I created some pseudocode below. (Migrated from my solution using DialogFragments that display an AlertDialog). I hope it's almost correctly adapted to Fragments (onCreateView() vs. onCreateDialog()). I got it to work that way.

Imagine we have an abstract BaseFragment and two extending classes FragmentA and FragmentB.

First have a look at all of our layouts. Note that I moved out the reusable parts of the layout into a separate file that will be included later from the concrete fragment's layouts. Specific views stay in their fragment's layouts. Using a common layout is important for this scenario.

fragment_a.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <!-- FragmentA-specific views -->
    <EditText
        android:id="@+id/edit_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text" />
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/edit_name">

        <!-- Include the common layout -->
        <include
            layout="@layout/common_layout.xml"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </RelativeLayout>
</RelativeLayout>

fragment_b.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <!-- FragmentB-specific, differs from FragmentA -->
    <TextView
        android:id="@+id/text_explain"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/explain" />
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_explain">

        <!-- Include the common layout -->
        <include
            layout="@layout/common_layout.xml"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </RelativeLayout>
</RelativeLayout>

common_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="android.widget.RelativeLayout">

    <Button
        android:id="@+id/button_up"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/up"/>

    <Button
        android:id="@+id/button_down"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button_up"
        android:text="@string/down" />
</merge>

Next the fragment classes. First our BaseFragment implementation.

onCreateView() is the place where the bindings are inflated. We're able to bind the CommonLayoutBinding based on the fragment's bindings where the common_layout.xml is included. I defined an abstract method onCreateViewBinding() called on top of onCreateView() that returns the ViewBinding from FragmentA and FragmentB. That way I ensure that the fragment's binding is present when I need to create the CommonLayoutBinding.

Next I am able to create an instance of CommonLayoutBinding by calling commonBinding = CommonLayoutBinding.bind(binding.getRoot());. Notice that the root-view from the concrete fragment's binding is passed to bind().

getCommonBinding() allows to provide access to the CommonLayoutBinding from the extending fragments. We could be more strict: the BaseFragment should provide concrete methods that access that binding instead of make it public to it's child-classes.

private CommonLayoutBinding commonBinding; // common_layout.xml

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 
        @Nullable Bundle savedInstanceState) {
    // Make sure to create the concrete binding while it's required to 
    // create the commonBinding from it
    ViewBinding binding = onCreateViewBinding(inflater);
    // We're using the concrete layout of the child class to create our 
    // commonly used binding 
    commonBinding = CommonLayoutBinding.bind(binding.getRoot());
    // ...
    return binding.getRoot();
}

// Makes sure to create the concrete binding class from child-classes before 
// the commonBinding can be bound
@NonNull
protected abstract ViewBinding onCreateViewBinding(@NonNull LayoutInflater inflater, 
        @Nullable ViewGroup container);

// Allows child-classes to access the commonBinding to access common 
// used views
protected CommonLayoutBinding getCommonBinding() {
    return commonBinding;
}

Now have a look at one of the the child-classes, FragmentA. From onCreateViewBinding() we create our binding like we would do from onCreateView(). In principle it's still called from onCreateVIew(). This binding is used from the base class as described above. I am using getCommonBinding() to be able to access views from common_layout.xml. Every child class of BaseFragment is now able to access these views from the ViewBinding.

That way I can move up all logic based on common views to the base class.

private FragmentABinding binding; // fragment_a.xml

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 
        @Nullable Bundle savedInstanceState) {
    // Make sure commonBinding is present before calling super.onCreateView() 
    // (onCreateViewBinding() needs to deliver a result!)
    View view = super.onCreateView(inflater, container, savedInstanceState);
    binding.editName.setText("Test");
    // ...
    CommonLayoutBinding commonBinding = getCommonBinding();
    commonBinding.buttonUp.setOnClickListener(v -> {
        // Handle onClick-event...
    });
    // ...
    return view;
}

// This comes from the base class and makes sure we have the required 
// binding-instance, see BaseFragment
@Override
protected ViewBinding onCreateViewBinding(@NonNull LayoutInflater inflater, 
        @Nullable ViewGroup container) {
    binding = FragmentABinding.inflate(inflater, container, false);
    return binding;
}

Pros:

  • Reduced duplicate code by moving it to the base class. Code in all fragments is now much clearer, and reduced to the essentials
  • Cleaner layout by moving reusable views into a layout that's included via <include />

Cons:

  • Possibly not applicable where views can't be moved into a commonly used layout file
    • Views might need to be positioned differently between fragments/layouts
    • Many <included /> layouts would result in many Binding classes, nothing gained then
  • Requires another binding instance (CommonLayoutBinding). There is not only one binding class for each child (FragmentA, FragmentB) that provides access to all views in the view hierarchy

What if views can't be moved into a common layout?
I am strongly interested in how to solve this as best practice! Let's think about it: introduce a wrapper class around the concrete ViewBinding.

We could introduce an interface that provides access to commonly used views. From the Fragments we wrap our bindings in these wrapper classes. On the other hand, this would result in many wrappers for each ViewBinding-type. But we can provide these wrappers to the BaseFragment using an abstract method (an generics). BaseFragment is then able to access the views, or work on them using the defined interface methods. What do you think?

In conclusion:
Maybe it's simply an limitation of ViewBinding that one layout needs to have its own Binding-class. If you found a good solution in cases the layout can't be shared and needs to be declared duplicated in each layout, let me know please.

I don't know if this is best practice or if there are better solutions. But while this is the only known solution for my use case, it seems to be a good start!

medavox
  • 175
  • 2
  • 11
Danny
  • 1,233
  • 1
  • 11
  • 19
14

Here is complete example of my BaseViewBindingFragment that:

  • does NOT require any abstract properties or functions,
  • it relies on Java reflection (not Kotlin reflection) - see fun createBindingInstance, where VB generic type argument is used
package app.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import java.lang.reflect.ParameterizedType

/**
 * Base application `Fragment` class with overridden [onCreateView] that inflates the view
 * based on the [VB] type argument and set the [binding] property.
 *
 * @param VB The type of the View Binding class.
 */
open class BaseViewBindingFragment<VB : ViewBinding> : Fragment() {

    /** The view binding instance. */
    protected var binding: VB? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
        createBindingInstance(inflater, container).also { binding = it }.root

    override fun onDestroyView() {
        super.onDestroyView()

        binding = null
    }

    /** Creates new [VB] instance using reflection. */
    @Suppress("UNCHECKED_CAST")
    protected open fun createBindingInstance(inflater: LayoutInflater, container: ViewGroup?): VB {
        val vbType = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
        val vbClass = vbType as Class<VB>
        val method = vbClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)

        // Call VB.inflate(inflater, container, false) Java static method
        return method.invoke(null, inflater, container, false) as VB
    }
}

With minifyEnabled true, to keep the generated ViewBinding classes, add this rule into your ProGuard file:

-keepclassmembers class * implements androidx.viewbinding.ViewBinding {
    *;
}
Matej Hlatky
  • 461
  • 7
  • 14
8

Base Class will go like this

abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity(){

    protected lateinit var binding : VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = inflateLayout(layoutInflater)
        setContentView(binding.root)
    }

    abstract fun inflateLayout(layoutInflater: LayoutInflater) : VB
}

Now in your activity where you want to use

class MainActivity : BaseActivity<ActivityMainBinding>(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.tvName.text="ankit"
    }

    override fun inflateLayout(layoutInflater: LayoutInflater)  = ActivityMainBinding.inflate(layoutInflater)
}

now in onCreate just use binding as per use

pseudoankit
  • 309
  • 2
  • 6
7

I created this abstract class as a base;

abstract class BaseFragment<VB : ViewBinding> : Fragment() {

private var _binding: VB? = null

val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    _binding = inflateViewBinding(inflater, container)
    return binding.root
}

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

abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): VB

}

Usage;

class HomeFragment : BaseFragment<FragmentHomeBinding>() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    binding.textViewTitle.text = ""
}

override fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
    return FragmentHomeBinding.inflate(inflater, container, false)
}

}

Cafer Mert Ceyhan
  • 1,640
  • 1
  • 13
  • 17
3

You can pass the inflate method into the abstract class via its constructor

abstract class BaseFragment<T : ViewBinding>(
    private val viewBindingInflater: (
        inflater: LayoutInflater,
        parent: ViewGroup?,
        attachToParent: Boolean
    ) -> T
) : Fragment() {


    lateinit var viewBinding: T

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewBinding = viewBindingInflater(inflater, container, false)
        return viewBinding.root
    }
}
class MyFragment : 
    BaseFragment<MyFragmentBinding>(MyFragmentBinding::inflate) { }

MikkelT
  • 633
  • 1
  • 10
  • 16
2

Here is a little different version of the @Chetan's answer with usages.

I added the @CallSuper annotation and removed type casting.

ViewBindingActivity.kt

abstract class ViewBindingActivity<VB : ViewBinding> : AppCompatActivity() {

    abstract val bindingInflater: (LayoutInflater) -> VB

    private var _binding: VB? = null
    protected val binding: VB get() = requireNotNull(_binding)

    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        _binding = bindingInflater.invoke(layoutInflater)
        setContentView(binding.root)
    }

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

ViewBindingFragment.kt

abstract class ViewBindingFragment<VB : ViewBinding>() : Fragment() {

    protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB

    private var _binding: VB? = null
    protected val binding: VB get() = requireNotNull(_binding)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = bindingInflater.invoke(inflater, container, false)
        return binding.root
    }

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

Usage

Activity

class HomeActivity : ViewBindingActivity<ActivityHomeBinding>() {

    override val bindingInflater: (LayoutInflater) -> ActivityHomeBinding
        get() = ActivityHomeBinding::inflate

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ...
    }
}

Fragment

class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {

    override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentHomeBinding
        get() = FragmentHomeBinding::inflate
}
MaNDOOoo
  • 1,347
  • 1
  • 5
  • 19
1

Update feb 4 2021 : I have written an article after researching and getting inspiration from many sources. This article would be updated with my future experiences with view binding as our company has now ditched the synthetic binding by almost 80%.


I have also come up with a Base Class solution that uses effectively final variables. My main goal was to :

  1. handle all the binding lifecycle in a base class
  2. let child class provide the binding class instance without using that route on its own (for eg if i had an abstract function abstract fun getBind():T , the child class could implement it and call it directly. I didn't wanted that as that would make the whole point of keeping bindings in base class moot ,I believe )

So here it is. First the current structure of my app. The activities won't inflate themselves, the base class would do for them:

Child Activities and Fragments:

class MainActivity : BaseActivityCurrent(){

    var i = 0

    override val contentView: Int
        get() = R.layout.main_activity


    override fun setup() {
        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragment())
            .commitNow()

        syntheticApproachActivity()
    }


    private fun syntheticApproachActivity() {
        btText?.setOnClickListener { tvText?.text = "The current click count is ${++i}"  }
    }


    private fun fidApproachActivity() {
        val bt = findViewById<Button>(R.id.btText)
        val tv = findViewById<TextView>(R.id.tvText)

        bt.setOnClickListener { tv.text = "The current click count is ${++i}"  }
    }
}

//-----------------------------------------------------------
class MainFragment : BaseFragmentCurrent() {
    override val contentView: Int
        get() = R.layout.main_fragment


    override fun setup() {
        syntheticsApproach()
    }

    private fun syntheticsApproach() {
        rbGroup?.setOnCheckedChangeListener{ _, id ->
            when(id){
                radioBt1?.id -> tvFragOutPut?.text = "You Opt in for additional content"
                radioBt2?.id -> tvFragOutPut?.text = "You DO NOT Opt in for additional content"
            }
        }

    }

    private fun fidApproach(view: View) {
        val rg: RadioGroup? = view.findViewById(R.id.rbGroup)
        val rb1: RadioButton? = view.findViewById(R.id.radioBt1)
        val rb2: RadioButton? = view.findViewById(R.id.radioBt2)
        val tvOut: TextView? = view.findViewById(R.id.tvFragOutPut)
        val cbDisable: CheckBox? = view.findViewById(R.id.cbox)

        rg?.setOnCheckedChangeListener { _, checkedId ->
            when (checkedId) {
                rb1?.id -> tvOut?.text = "You Opt in for additional content"
                rb2?.id -> tvOut?.text = "You DO NOT Opt in for additional content"
            }
        }

        rb1?.isChecked = true
        rb2?.isChecked = false

        cbDisable?.setOnCheckedChangeListener { _, bool ->
            rb1?.isEnabled = bool
            rb2?.isEnabled = bool
        }


    }


}

Base Activities and Fragments :


abstract class BaseActivityCurrent :AppCompatActivity(){

    abstract val contentView: Int


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(contentView)
        setup()
    }

    abstract fun setup()

}
abstract class BaseFragmentCurrent : Fragment(){


    abstract val contentView: Int

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(contentView,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setup()
    }

    abstract fun setup()


}

As you can see the children classes were always easy to scale as base activities would do all the heavy work. and Since synthetics were being used extensively, there was not much of a problem. To use binding classes with the previously mentioned constraints I would:

  1. Need the child classes to implement functions that would provide data back to the parent fragments. That's the easy part, simply creating more abstract functions that return child's Binding Class's Instance would do.

  2. Store the child class's view binding in a variable (say val binding:T) such that the base class could nullify it in on destroy ad handle the lifecycle accordingly. A little tricky since the child's Binding class instance type is not known before hand. But making the parent as generic ( <T:ViewBinding>) will do the job

  3. returning the view back to the system for inflation. again, easy because thankfully for most of the components, the system accepts an inflated view and having the child's binding instance will let me provide a view back to the system

  4. Preventing the child class from using the route created in point 1 directly . think about it: if a child class had a function getBind(){...} that returns their own binding class instance, why won't they use that and instead use super.binding ? and what is stopping them from using the getBind() function in the onDestroy(), where the bindings should rather not be accessed?

So that's why I made that function void and passed a mutable list into it. the child class would now add their binding to the list that would be accessed by the parent. if they don't , it will throw an NPE . If they try to use it in on destroy or an other place, it will again throw an illegalstate exception . I also create a handy high order function withBinding(..) for easy usage.

Base Binding activity and fragment:



abstract class BaseActivityFinal<VB_CHILD : ViewBinding> : AppCompatActivity() {

    private var binding: VB_CHILD? = null


    //lifecycle
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getInflatedLayout(layoutInflater))
        setup()
    }
    override fun onDestroy() {
        super.onDestroy()
        this.binding = null
    }


    //internal functions
    private fun getInflatedLayout(inflater: LayoutInflater): View {
        val tempList = mutableListOf<VB_CHILD>()
        attachBinding(tempList, inflater)
        this.binding = tempList[0]


        return binding?.root?: error("Please add your inflated binding class instance at 0th position in list")
    }

    //abstract functions
    abstract fun attachBinding(list: MutableList<VB_CHILD>, layoutInflater: LayoutInflater)

    abstract fun setup()

    //helpers
    fun withBinding(block: (VB_CHILD.() -> Unit)?): VB_CHILD {
        val bindingAfterRunning:VB_CHILD? = binding?.apply { block?.invoke(this) }
        return bindingAfterRunning
            ?:  error("Accessing binding outside of lifecycle: ${this::class.java.simpleName}")
    }


}

//--------------------------------------------------------------------------

abstract class BaseFragmentFinal<VB_CHILD : ViewBinding> : Fragment() {

    private var binding: VB_CHILD? = null


    //lifecycle
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ) = getInflatedView(inflater, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setup()
    }

    override fun onDestroy() {
        super.onDestroy()
        this.binding = null
    }


    //internal functions
    private fun getInflatedView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        attachToRoot: Boolean
    ): View {
        val tempList = mutableListOf<VB_CHILD>()
        attachBinding(tempList, inflater, container, attachToRoot)
        this.binding = tempList[0]
        return binding?.root
            ?: error("Please add your inflated binding class instance at 0th position in list")

    }

    //abstract functions
    abstract fun attachBinding(
        list: MutableList<VB_CHILD>,
        layoutInflater: LayoutInflater,
        container: ViewGroup?,
        attachToRoot: Boolean
    )

    abstract fun setup()

    //helpers
    fun withBinding(block: (VB_CHILD.() -> Unit)?): VB_CHILD {
        val bindingAfterRunning:VB_CHILD? = binding?.apply { block?.invoke(this) }
        return bindingAfterRunning
            ?:  error("Accessing binding outside of lifecycle: ${this::class.java.simpleName}")
    }

}

Child activity and fragment:


class MainActivityFinal:BaseActivityFinal<MainActivityBinding>() {
    var i = 0

    override fun setup() {
        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragmentFinal())
            .commitNow()

        viewBindingApproach()
    }
    
    private fun viewBindingApproach() {
        withBinding {
            btText.setOnClickListener { tvText.text = "The current click count is ${++i}"  }
            btText.performClick()
        }

    }
    
    override fun attachBinding(list: MutableList<MainActivityBinding>, layoutInflater: LayoutInflater) {
        list.add(MainActivityBinding.inflate(layoutInflater))
    }
}

//-------------------------------------------------------------------

class MainFragmentFinal : BaseFragmentFinal<MainFragmentBinding>() {
   
    override fun setup() {
        bindingApproach()
    }

    private fun bindingApproach() {
        withBinding {
            rbGroup.setOnCheckedChangeListener{ _, id ->
                when(id){
                    radioBt1.id -> tvFragOutPut.text = "You Opt in for additional content"
                    radioBt2.id -> tvFragOutPut.text = "You DO NOT Opt in for additional content"
                }
            }
            radioBt1.isChecked = true
            radioBt2.isChecked = false

            cbox.setOnCheckedChangeListener { _, bool ->
                radioBt1.isEnabled = !bool
                radioBt2.isEnabled = !bool
            }
        }
    }


    override fun attachBinding(
        list: MutableList<MainFragmentBinding>,
        layoutInflater: LayoutInflater,
        container: ViewGroup?,
        attachToRoot: Boolean
    ) {
        list.add(MainFragmentBinding.inflate(layoutInflater,container,attachToRoot))
    }


}


ansh sachdeva
  • 1,220
  • 1
  • 15
  • 32
  • Doesn't really answer the question. You are using a default manager for viewBinding to which maybe all of us have already done. The OP refers to having 2 or more similar views with the same IDs, how to avoid managing 2 classes to access common properties. Let's say I have 2 ViewHolders with same elements but different XML design, how to pass a single Binding interface instead of now having to operate 2 classes. – htafoya May 03 '21 at 18:42
0

I think that an easy response is to use bind method of the common class.

I know this won't work in ALL cases, but it will for views with similar elements.

If I have two layouts row_type_1.xml and row_type_2.xml to which they share common elements, I can then do something as:

ROW_TYPE_1 -> CommonRowViewHolder(
                    RowType1Binding.inflate(LayoutInflater.from(parent.context), parent, false))

Then for type 2, instead of creating another ViewHolder that receives its own Binding class, do something as:

ROW_TYPE_2 -> {
    val type2Binding = RowType2Binding.inflate(LayoutInflater.from(parent.context), parent, false))
    CommonRowViewHolder(RowType1Binding.bind(type2Binding))
}

If instead it is a subset of components, inheritance could be placed

CommonRowViewHolder: ViewHolder {
    fun bind(binding: RowType1Holder)
}

Type2RowViewHolder: CommonRowViewHolder {

    fun bind(binding: RowType2Holder) {
        super.bind(Type1RowViewHolder.bind(binding))
        
        //perform specific views for type 2 binding ...
    }
}
htafoya
  • 18,261
  • 11
  • 80
  • 104
0
inline fun <reified BindingT : ViewBinding> AppCompatActivity.viewBindings(
    crossinline bind: (View) -> BindingT
) = object : Lazy<BindingT> {

    private var initialized: BindingT? = null

    override val value: BindingT
        get() = initialized ?: bind(
            findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
        ).also {
            initialized = it
        }

    override fun isInitialized() = initialized != null
}

ZhuGang
  • 61
  • 3
-1

This is slightly modified Kotlin version of great Chetan Gupta's answer. Avoids using "UNCHECKED_CAST".

Activity

abstract class BaseViewBindingActivity<ViewBindingType : ViewBinding> : AppCompatActivity() {

    protected lateinit var binding: ViewBindingType
    protected abstract val bindingInflater: (LayoutInflater) -> ViewBindingType

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = bindingInflater.invoke(layoutInflater)
        val view = binding.root
        setContentView(view)
    }
}

Fragment

abstract class BaseViewBindingFragment<ViewBindingType : ViewBinding> : Fragment() {

    private var _binding: ViewBindingType? = null
    protected val binding get() = requireNotNull(_binding)
    protected abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ViewBindingType

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = bindingInflater.invoke(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
Myroslav
  • 896
  • 12
  • 21