Data Binding con Recycler View en Android

1
13250
android_jet_pack

Índice de contenidos

1. Data Binding Library

Data Binding Library es una librería que permite enlazar componentes visuales de Android (Views) a un modelo de datos. Una de las ventajas es que reduce el boilerplate de las llamadas findViewById aunque si estás familiarizado con Kotlin, en Android la paquetería kotlinx.android.synthetic soluciona este problema importando los componentes de un layout específico.

2. Recycler View

Recycler View es un componente de Android que permite mostrar listas más avanzadas. Digo más avanzadas porque podemos inflar un layout personalizado por cada componente de la lista. Si a este componente añadimos Data Binding, podemos conseguir una funcionalidad más acoplada a la capa de presentación.

3. Dependencias

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation "android.arch.lifecycle:extensions:1.1.1"
    implementation "android.arch.lifecycle:viewmodel:1.1.1"
    implementation "androidx.core:core-ktx:1.2.0-alpha03"
    implementation "androidx.recyclerview:recyclerview:1.1.0-beta03"
    implementation 'com.google.android.material:material:1.1.0-alpha09'
}

build.gradle del módulo app

4. Configuración Gradle

apply plugin: 'kotlin-kapt'

...

android{
    dataBinding {
    enabled true
    }
}

build.gradle del módulo app

5. Objetivo

Vamos a mostrar un listado personalizado de posts con un título, contenido y un botón que permita eliminar un post.

recycler view list

6. Layout personalizado

Aquí tenemos el fichero post_view.xml con la vista de cada post.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/padding_medium">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/post_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toRightOf="@id/remove_post_button"
        app:layout_constraintTop_toBottomOf="@id/remove_post_button">

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/body_post_value"
            style="@style/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/body_post_label"
            tools:text="Contenido" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/title_post_label"
            style="@style/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/title_post_label"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/title_post_value"
            style="@style/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_post_label"
            tools:text="Título de ejemplo" />

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/body_post_label"
            style="@style/title_2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/body_post_label"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_post_value" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/remove_post_button"
        style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:icon="@drawable/ic_remove_circle_outline_black_24dp"
        app:iconSize="48dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="@id/remove_post_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

El resultado es el siguiente:
recycler view layout

7. Recycler View

Si conoces Recycler View, el siguiente código te resultará familiar porque no hay nada nuevo a añadir.

class CustomRecyclerViewAdapter(private val posts: MutableList) :
    Adapter() {

    fun removePost(post: Post) {
        val index = posts.indexOf(post)
        if (index != -1) {
            posts.remove(post)
            notifyItemRemoved(index)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
        return CustomViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.post_view,
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int = posts.size

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        holder.bind(posts[position])
    }

    inner class CustomViewHolder(view: View) : ViewHolder(view) {

        fun bind(post: Post) {
            itemView.findViewById(R.id.title_post_value).text = post.title
            itemView.findViewById(R.id.body_post_value).text = post.body
            itemView.findViewById(R.id.remove_post_button).setOnClickListener {
                removePost(post)
            }
        }
    }
}

8. Main Activity

En la actividad principal instanciamos el adaptador y lo asignamos al Recycler View definido en el fichero activity_main.xml de nuestra actividad principal. Nada nuevo, ¿verdad?

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        posts_recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = CustomRecyclerViewAdapter(mutableListOf())
        }
    }
}

9. View Model

Antes de meternos con data binding me gustaría hablar un poco sobre ViewModel. Es un componente de arquitectura de Android que nos permite manejar los datos de una vista dentro de los ciclos de vida de Android. Android dispone de una serie ciclos de vida que anteriormente, para poder almacenar el estado de una vista, se tenía que realizar a través de los Bundles, que es algo laborioso de manejar entre diferentes estados de una actividad. Gracias a ViewModel resulta mucho más fácil porque el scope es el que se muestra en la siguiente imagen:

Viewmodel lifecycle

Gracias a este componente podemos almacenar el estado de una vista en cualquier ciclo de vida de la actividad y, además, permite compartir el mismo view model entre diferentes vistas. ¡Gracias, Google!

Dentro de nuestro ViewModel tenemos un MutableLiveData con una lista de Posts. Quizás te preguntaras qué es un MutableLiveData. La respuesta es sencilla, un observable:

class MainActivityViewModel(application: Application) : AndroidViewModel(application) {

    var posts: MutableLiveData = MutableLiveData()

    fun loadPosts() {
        posts.value = listOf(
            Post(1, 1, "Title 1", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(2, 2, "Title 2", "Body 2 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(3, 3, "Title 3", "Body 3 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(4, 4, "Title 4", "Body 4 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(5, 5, "Title 5", "Body 5 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(6, 6, "Title 6", "Body 6 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(7, 7, "Title 7", "Body 7"),
            Post(8, 8, "Title 8", "Body 8"),
            Post(9, 9, "Title 9", "Body 9"),
            Post(10, 10, "Title 10", "Body 10 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(11, 11, "Title 11", "Body 11 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"),
            Post(12, 12, "Title 12", "Body 12")
        )
    }

}

10. Implementación de Data Binding

En este apartado voy a explicar como asociar el modelo a la vista.

10.1. Asociar el modelo con la vista

10.1.1. activity_main.xml

Primero establecemos con qué vista vamos a realizar el data binding, que en este ejemplo es la actividad principal. Para ello tenemos que añadir las etiquetas layout y data en activity_main.xml. Dentro de data podemos crear variables que posteriormente utilizaremos en nuestra vista. Si nos fijamos en la etiqueta variable tenemos una variable llamada activityViewModel que es de tipo MainActivityViewModel.

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/press_me_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Mostrar entradas"
            app:layout_constraintBottom_toTopOf="@id/posts_recyclerView"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/posts_recyclerView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:data="@{activityViewModel.posts}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="@id/press_me_button"
            app:layout_constraintTop_toBottomOf="@id/press_me_button"
            tools:listitem="@layout/post_view" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <data>

        <variable
            name="activityViewModel"
            type="com.autentia.demo.databinding.MainActivityViewModel" />
    </data>

</layout>

En la etiqueta RecyclerView el atributo app:data=»@{activityViewModel.posts}» le estamos indicando que el atributo data es la lista del viewModel, y no, no carga los datos mágicamente en el RecyclerView. ¡Ojalá fuese tan fácil!

Cuando declaramos ambas etiquetas, automáticamente se genera una clase Java que hace referencia al fichero activity_main.xml y que es nombrada como: [Nombre fichero xml][Binding] en este ejemplo sería ActivityMainBinding.

10.1.2. MainActivity.class

Una vez que ya tenemos los datos que queremos representar en la vista, necesitamos realizar la relación entre la vista y el modelo. Para ello abrimos MainActivity y añadimos lo siguiente en el método onCreate() :

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

    val mainActivityViewModel =
        ViewModelProviders.of(this).get(MainActivityViewModel::class.java)

    val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    binding.activityViewModel = mainActivityViewModel
    binding.lifecycleOwner = this

    posts_recyclerView.apply {
        layoutManager = LinearLayoutManager(context)
        adapter = CustomRecyclerViewAdapter(mutableListOf())
    }

    press_me_button.setOnClickListener {
        mainActivityViewModel.loadPosts()
    }
}
  • Obtenemos una instancia del ViewModel:val mainActivityViewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
  • Inflamos el layout y obtenemos la actividad principal con el binding a las varibales. Gracias a esto podemos establecer los valores de las variables declaradas en el fichero activity_main.xml:val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
  • Realizamos la asignación:binding.activityViewModel = mainActivityViewModel
  • Es necesario cuando utilizamos componentes de arquitectura de Android:binding.lifecycleOwner = this
  • Definir un manejador de vistas y el adaptador al RecyclerView: posts_recyclerView.apply { layoutManager = LinearLayoutManager(context) adapter = CustomRecyclerViewAdapter(mutableListOf()) }
  • Cuando se haga clic sobre el botón de mostrar entradas cargamos la lista de posts:press_me_button.setOnClickListener { mainActivityViewModel.loadPosts() }

Bueno, pues al principio podemos pensar que ya hemos terminado, pero no es del todo así… Si nos fijamos bien en el código, el adaptador que le hemos pasado al RecyclerView tiene una lista vacía y por defecto no mostraría nada. Si pulsamos sobre el botón de mostrar entradas, debería cargar los datos en el RecyclerView. En este caso debería añadir más lógica en la actividad principal para poder visualizar los nuevos elementos, pero no es el objetivo de este tutorial. Como opinión personal, el propio componente debería de tener la responsabilidad de actualizar la vista cuando los datos cambien.

10.2. Manipular la lista del adaptador

Crear un adaptador nuevo cada vez que se pulse el botón para mostrar de nuevo el listado es una mala práctica y para ello vamos a añadir un método setData(data) en el adaptador CustomRecyclerViewAdapter para poder manipular la lista.

fun setData(data: MutableList) {
    posts.clear()
    posts.addAll(data)
    notifyDataSetChanged()
}
  1. Limpiamos la lista.
  2. Añadimos los nuevos elementos.
  3. Notificamos los cambios para que el RecyclerView vuelva a renderizar los elementos.

10.3. Observar los cambios de MutableLiveData

Te podrías preguntar para qué se necesita observar los cambios. La respuesta es muy sencilla: cuando los datos cambian en MutableLiveData nadie es notificado de estos cambios y por tanto no se actualizarían los cambios en la vista.

Además te podrías cuestionar si añadir la lógica en el ViewModel o en ActivityMain. No, tranquilos, Google está al tanto de todo y ha creado un componente llamando BindingAdapter que también forma parte de Data Binding y cuyo principal objetivo es estar atento a los cambios de un atributo declarado en el archivo activity_main.xml. Su implementación sería la siguiente:

@BindingAdapter("data")
fun setRecyclerViewProperties(recyclerView: RecyclerView?, data: MutableList?) {
    val adapter = recyclerView?.adapter
    if (adapter is CustomRecyclerViewAdapter && data != null) {
        adapter.setData(data)
    }
}

Si analizamos un poco el código, veremos que el método setRecyclerViewProperties será ejecutado cada vez que cambie de valor del atributo data, lo cual sucede cuando hacemos clic sobre el botón de mostrar entradas.

Es obligatorio que los parámetros RecyclerView y MutableList sean nullables, en caso contrario solo tendremos dolores de cabeza…

En la segunda parte del código, si el adaptador es de tipo CustomRecyclerViewAdapter, ejecutamos el método setData(data) para modifciar la lista de los Posts que tiene el adaptador (por defecto lista vacía).

¡Wuala! Si has seguido todos los pasos, nuestro Recycler View con Data Binding estará implementado. Dejo un pequeño vídeo con la implementación.

11. Conclusión

Como conclusión personal podría decir que esta implementación de Recycler View podría ser la más cercana a lo que realmente es el componente. Es decir, la responsabilidad de que los datos se han modificado de la lista y actualizar no es del programador, es del propio componente. Imagina hacer lo mismo para un TextView, ¿deberíamos montar toda esta implementación solo para escribir en el campo y notificar los cambios al componente para poder visualizarlos? Menos mal que no es así, pero es la misma historia.

12. Referencias

Recycler View

Data Binding

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad