3
\$\begingroup\$

UPDATE: After getting "a kind of" affirmation from various platforms( my discussions on twitter, reddit, other stack overflow posts,etc) I have written an article on this style of view binding. Kindly check that out too, as that's the only place i am currently updating my latest version of the code (It's not behind the paywall and is completely free) . I will be happy to discuss the improvements on either of the platforms or bring those changes here too if needed.


Another non-android wording of this question could be : "Implementation that Prevents a child class from using a value that child class itself provided to the parent class".


Requirement : I have been using inheritance extensively in a personal project where base abstract activity and fragment classes would handle most of the work and expose only empty functions that are optionally extendible by the children fragment/activities. One of those work would be to inflate the layout. The child class would provide the layout as an overridden variable and the task of inflating the layout would be for the base classes. Eg:

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

    open fun setup(){}


}

The child classes would just require to provide the layout res and optionally provide setup()


Now i wish to move to view binding with the similar architecture. As There are no ways to access the correct binding class using just the layoutres, i need child classes to provide the instance of binding class. But at the same time, i would want to manage the binding class instance in the base activity/fragment, because they are supposed to manage lifecycles. So I cam up with this solution that uses effectively final variables. here is the code for base classes and their associated child classes:

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


}


Kindly review and let me know and if this could be improved. As you see ,I am using effectively final variables to prevent the children activities from using the view binding directly, that's why i initially worded the question differently. Can this made more elegant? Is there a better way to achieve what this code does?

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.