How using ViewBinding with an abstract base class

匆匆过客 提交于 2021-01-13 07:10:51

问题


I started using ViewBinding. After searching for some example or advice how to use ViewBinding with an abstract base class that should handle same logic on views expected to be present in every child's layout, I endet up posting this question here.

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 using the concrete Bindings from the base class. Even with introducing generics to the base class there are too many types of fragment-bindings that 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? Didn't found 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()? What do you think? Any other (better) solutions? Please let's start some discuss.


回答1:


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




回答2:


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

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

I created some pseudo code below to share with you. (Migrated from my solution using DialogFragments that display an AlertDialog). I hope it's almost correct 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 used 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 VewBinding 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 shure 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 principa it's still called from onCreateVIew(). This binding is used from the base class like 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 more clear and reduced to their essentials
  • More clean layout by moving reusable views into a layout that's included via the <include />

Cons:

  • Possibly not exact applicable where views can't be moved into a commonly used layout file
    • Possibly views needs to be positioned different between frgments/layouts
    • Many <included /> layouts would result in many Binding classes, nothing to win 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! Lets 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. This would result in many wrapper for each ViewBinding-type on the other hand. But we can provide these wrapper 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 the actual limit of ViewBinding that one layout needs to have it's 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 until this is the only known solution for my use case it seems to be a good start!




回答3:


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))
    }


}




来源:https://stackoverflow.com/questions/62407823/how-using-viewbinding-with-an-abstract-base-class

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!