Cómo usar ViewBinding con una clase base abstracta

19 minutos de lectura

Avatar de usuario de Danny
dany

Empecé a usar ViewBinding. Después de buscar un ejemplo o algún consejo, terminé publicando esta pregunta aquí.

¿Cómo uso ViewBinding con una clase base abstracta que maneja la misma lógica en las vistas que se espera que estén presentes en el diseño de cada niño?

Guión:

tengo una clase base public abstract class BaseFragment. Hay varios fragmentos que amplían esta clase base. Estos Fragmentos tienen vistas comunes que se manejan desde la implementación de la clase base (con el “antiguo” findViewById()). Por ejemplo, se espera que el diseño de cada fragmento contenga un TextView con ID text_title. Así es como se maneja desde el BaseFragment‘s onViewCreated():

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

Ahora, la API ViewBinding genera clases de enlace para cada Fragmento secundario. Puedo hacer referencia a las vistas usando el enlace, pero no puedo usar los enlaces concretos de la clase base. Incluso si introduje genéricos en la clase base, hay demasiados tipos de enlaces de fragmentos, así que descarté esta solución por ahora.

¿Cuál es la forma recomendada de manejar las vistas del enlace desde la clase base abstracta? ¿Existen mejores prácticas? No encontré un mecanismo incorporado en la API para manejar este escenario de manera elegante.

Cuando se espera que los fragmentos secundarios contengan vistas comunes, podría proporcionar métodos abstractos que devuelvan las vistas de los enlaces concretos de los Fragmentos y hacerlos accesibles desde la clase base. (Por ejemplo protected abstract TextView getTitleView();). Pero, ¿es esto una ventaja en lugar de usar findViewById()? ¿Hay otras (mejores) soluciones?

  • creo que puedes escribir public abstract int getLayoutResourse(); en su BaseFragment y páselo a DataBindingUtil.inflate() en vez de R.layout.frag_layout o no entendí la pregunta

    – Alex Rmc

    16 de junio de 2020 a las 12:11


  • @AlexRmcf primero: no uso DataBinding, simplemente quiero usar ViewBinding. Sí, esto sería posible obtener un ViewDataBinding de la clase base. Pero no puedo acceder a las vistas a través de ViewDataBinding.textTitle por ejemplo, de la clase base sin conocer el tipo concreto de clase de enlace.

    – danny

    16 de junio de 2020 a las 12:31

  • Tengo el mismo problema, tengo un ViewHolder base que contiene vistas y childs que también contienen sus propias vistas, ¿cómo puedo pasar su View Binder al ViewHolder principal sin tener que hacer “if (instancia secundaria de X) luego vinculante = XBinding “

    – Nayk0

    6 de julio de 2020 a las 7:28

  • oye encontraste solucion a esto?

    – ansh sachdeva

    12 de noviembre de 2020 a las 14:06

  • @anshsachdeva actualmente parece que tengo una solución. Todavía tengo que averiguar si funciona y daré una respuesta pronto. En el mejor de los casos mañana puedo responder con un resultado positivo 😉

    – danny

    12 de noviembre de 2020 a las 16:53

Avatar de usuario de Chetan Gupta
Chetan Gupta

Hola, he creado una publicación de blog que cubre el enlace de vista en profundidad, y también incluye tanto el patrón de composición como el patrón de delegado para implementar el enlace de vista, así como el uso del pago de herencia desde el enlace.

Checkout para el código completo de BaseActivity y BaseFragment junto con el uso

👉Androidbites|Visualización de enlaces

/*
 * 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
    }
}

Para uso, blog de comprobación de patrones avanzados y antipatrones Androidbites|Visualización de enlaces

  • bindLayout es bindInflater ?

    –Hamid Mahmoodi

    8 de enero de 2021 a las 19:43

  • Gracias @Chetan, exactamente lo que estaba buscando. He modificado un poco tu código para evitar “UNCHECKED_CAST” (en Kotlin): private var _binding: ViewBindingType? = null protected val binding get() = requireNotNull(_binding) abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ViewBindingType

    – Myroslav

    26 de enero de 2021 a las 13:01


  • Hola, @ChetanGupta, incluye también una actividad/fragmento de ejemplo en la respuesta.

    – Angad Singh

    30 de mayo de 2021 a las 8:15

  • Actualmente estoy lidiando con el mismo problema, pero no veo cómo esto es una solución. ¿Cómo se supone que debe crear el código genérico que establece el texto para “text_title” (en el caso de OP) en la clase ViewBindingFragment, si aún no sabe qué enlace en particular se utilizará?

    – Tharkius

    25 de enero a las 12:26

  • ¿Qué concepto/enfoque se está utilizando con bindingInflater ¿aquí? ¿Es funcional? Entiendo el concepto de invocar en Kotlin pero no con este. ¿Puede alguien explicármelo?

    – DEVS bit a bit

    26 de junio a las 22:30

Avatar de usuario de Danny
dany

Encontré una solución aplicable para mi escenario concreto y quiero compartirla con ustedes.

Tenga en cuenta que esto no es una explicación sobre cómo ViewBinding obras.

Creé un pseudocódigo a continuación. (Migrado de mi solución usando DialogFragments que muestran un AlertDialog). Espero que esté casi correctamente adaptado a Fragmentos (onCreateView() contra onCreateDialog()). Conseguí que funcionara de esa manera.

Imagina que tenemos un resumen BaseFragment y dos clases de extensión FragmentA y FragmentB.

Primero echa un vistazo a todos nuestros diseños. Tenga en cuenta que moví las partes reutilizables del diseño a un archivo separado que se incluirá más adelante de los diseños del fragmento concreto. Las vistas específicas permanecen en los diseños de sus fragmentos. El uso de un diseño común es importante para este escenario.

fragmento_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>

fragmento_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>

A continuación, las clases de fragmentos. Primero nuestro BaseFragment implementación.

onCreateView() es el lugar donde se inflan las fijaciones. Somos capaces de vincular el CommonLayoutBinding basado en los enlaces del fragmento donde el common_layout.xml está incluido. Definí un método abstracto. onCreateViewBinding() llamado encima de onCreateView() que devuelve el ViewBinding de FragmentA y FragmentB. De esa manera me aseguro de que el enlace del fragmento esté presente cuando necesito crear el CommonLayoutBinding.

A continuación, puedo crear una instancia de CommonLayoutBinding llamando commonBinding = CommonLayoutBinding.bind(binding.getRoot());. Observe que la vista raíz del enlace del fragmento concreto se pasa a bind().

getCommonBinding() permite dar acceso a la CommonLayoutBinding de los fragmentos que se extienden. Podríamos ser más estrictos: el BaseFragment debe proporcionar métodos concretos que accedan a ese enlace en lugar de hacerlo público para sus clases secundarias.

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

Ahora eche un vistazo a una de las clases de niños, FragmentA. De onCreateViewBinding() creamos nuestro enlace como lo haríamos desde onCreateView(). En principio se sigue llamando desde onCreateVIew(). Este enlace se usa desde la clase base como se describe anteriormente. estoy usando getCommonBinding() para poder acceder a las vistas desde common_layout.xml. Cada clase infantil de BaseFragment ahora puede acceder a estas vistas desde el ViewBinding.

De esa manera puedo mover toda la lógica basada en vistas comunes a la clase base.

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

Ventajas:

  • Se redujo el código duplicado al moverlo a la clase base. El código en todos los fragmentos ahora es mucho más claro y se reduce a lo esencial
  • Diseño más limpio al mover las vistas reutilizables a un diseño que se incluye a través de <include />

Contras:

  • Posiblemente no aplicable donde las vistas no se pueden mover a un archivo de diseño de uso común
    • Es posible que las vistas deban colocarse de manera diferente entre fragmentos/diseños
    • Muchos <included /> los diseños darían como resultado muchas clases de enlace, nada ganado entonces
  • Requiere otra instancia de enlace (CommonLayoutBinding). No solo hay una clase vinculante para cada niño (FragmentA, FragmentB) que proporciona acceso a todas las vistas en la jerarquía de vistas

¿Qué pasa si las vistas no se pueden mover a un diseño común?

¡Estoy muy interesado en cómo resolver esto como la mejor práctica! Pensemos en ello: introduzca una clase contenedora alrededor del hormigón ViewBinding.

Podríamos introducir una interfaz que proporcione acceso a las vistas de uso común. Desde los Fragmentos envolvemos nuestros enlaces en estas clases contenedoras. Por otro lado, esto daría como resultado muchos contenedores para cada tipo ViewBinding. Pero podemos proporcionar estos envoltorios al BaseFragment usando un método abstracto (un genérico). BaseFragment entonces puede acceder a las vistas o trabajar en ellas utilizando los métodos de interfaz definidos. ¿Qué piensas?

En conclusión:

Tal vez sea simplemente una limitación de ViewBinding que un diseño necesita tener su propia clase de enlace. Si encontró una buena solución en los casos en que el diseño no se puede compartir y debe declararse duplicado en cada diseño, hágamelo saber.

No sé si esta es la mejor práctica o si hay mejores soluciones. Pero si bien esta es la única solución conocida para mi caso de uso, ¡parece ser un buen comienzo!

Aquí está el ejemplo completo de mi BaseViewBindingFragment que:

  • NO requiere ningún abstract propiedades o funciones,
  • se basa en reflejo java (no reflejo de Kotlin) – ver fun createBindingInstancedónde VB se utiliza un argumento de tipo genérico
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
    }
}

Creé esta clase abstracta como 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

}

Uso;

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

}

La clase base será así

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
}

Ahora en tu actividad donde quieras usar

class MainActivity : BaseActivity<ActivityMainBinding>(){

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

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

ahora en onCreate solo use el enlace según el uso

Actualización 4 de febrero de 2021: He escrito un artículo después de investigar y obtener inspiración de muchas fuentes. Este artículo se actualizará con mis experiencias futuras con la encuadernación de vistas, ya que nuestra empresa ha abandonado la encuadernación sintética en casi un 80 %.


También se me ocurrió una solución de clase base que usa variables finales de manera efectiva. Mi objetivo principal era:

  1. manejar todo el ciclo de vida vinculante en una clase base
  2. deje que la clase secundaria proporcione la instancia de clase vinculante sin usar esa ruta por sí sola (por ejemplo, si tuviera una función abstracta abstract fun getBind():T , la clase secundaria podría implementarlo y llamarlo directamente. No quería eso, ya que eso haría que el objetivo de mantener los enlaces en la clase base fuera discutible, creo)

Asi que aqui esta. Primero la estructura actual de mi aplicación. Las actividades no se inflarán solas, la clase base haría por ellas:

Actividades infantiles y fragmentos:

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
        }


    }


}

Actividades base y fragmentos:


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


}

Como puede ver, las clases para niños siempre fueron fáciles de escalar, ya que las actividades básicas harían todo el trabajo pesado. y Dado que los sintéticos se usaban ampliamente, no había mucho problema. Para usar clases vinculantes con las restricciones mencionadas anteriormente, haría lo siguiente:

  1. Necesita las clases secundarias para implementar funciones que proporcionen datos a los fragmentos principales. Esa es la parte fácil, simplemente crear funciones más abstractas que devuelvan la instancia de la clase de enlace del niño.

  2. Almacene el enlace de vista de la clase secundaria en una variable (digamos val binding:T) de modo que la clase base podría anularlo al destruirlo y manejar el ciclo de vida en consecuencia. Un poco complicado ya que el tipo de instancia de la clase Binding del niño no se conoce de antemano. Pero haciendo que el padre sea genérico ( <T:ViewBinding>) hará el trabajo

  3. devolviendo la vista al sistema para la inflación. nuevamente, fácil porque afortunadamente para la mayoría de los componentes, el sistema acepta una vista inflada y tener la instancia de vinculación del niño me permitirá proporcionar una vista al sistema

  4. Evitar que la clase secundaria use la ruta creada en el punto 1 directamente. piénsalo: si una clase secundaria tuviera una función getBind(){...} que devuelve su propia instancia de clase vinculante, ¿por qué no usarán eso y en su lugar usarán super.binding ? y qué les impide usar el getBind() función en onDestroy(), donde no se debería acceder a los enlaces?

Es por eso que anulé esa función y le pasé una lista mutable. la clase secundaria ahora agregaría su enlace a la lista a la que accedería el padre. si no lo hacen, lanzará un NPE. Si intentan usarlo en destruir o en otro lugar, nuevamente arrojará un illegalstate exception . También creo una práctica función de alto orden withBinding(..) para un uso fácil.

Actividad de unión a base y fragmento:



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

}

Actividad infantil y fragmento:


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


}


avatar de usuario de htafoya
htafoya

Creo que una respuesta fácil es usar bind método de la clase común.

Sé que esto no funcionará en TODOS los casos, pero lo hará para vistas con elementos similares.

Si tengo dos diseños row_type_1.xml y row_type_2.xml a los que comparten elementos comunes, entonces puedo hacer algo como:

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

Luego, para el tipo 2, en lugar de crear otro ViewHolder que reciba su propia clase Binding, haga algo como:

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

Si en cambio es un subconjunto de componentes, la herencia podría colocarse

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

Type2RowViewHolder: CommonRowViewHolder {

    fun bind(binding: RowType2Holder) {
        super.bind(Type1RowViewHolder.bind(binding))
        
        //perform specific views for type 2 binding ...
    }
}

¿Ha sido útil esta solución?