Creación de layouts pt3: ScrollViews, RecyclerViews y ViewHolders

Jose Miguel Zea Guerrero
OpenLabPERU
Published in
6 min readMar 24, 2021

Introducción

En las anteriores clases hemos visto como escoger de manera correcta nuestro viewgroup y como interactuar con las distintas vistas para crear un pequeño formulario. Pero que pasaría si nuestro formulario se extiende tanto que no alcanza en nuestra pantalla? O si vemos que existen items similares, pero no sabemos como reutilizarlo en una lista? En este episodio veremos como utilizar dos poderosas herramientas como son ScrollView y RecyclerView

¿Qué es un ScrollView?

ScrollView es un ViewGroup muy especial, (Sí, un view group) ya que soporta un solo hijo que puede ser scrolleado y adaptado en tamaño. Lo recomendable es usar un ViewGroup como child para insertar muchos mas objetos.

Veamos ahora como adaptar un scrollview a una situación cotidiana, por ejemplo a nuestro formulario anterior:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
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="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/iv_logo"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="@dimen/space_20"
android:importantForAccessibility="no"
android:src="@mipmap/ic_launcher"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_20"
android:text="@string/txt_example_form"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_logo" />

<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />

<Button
android:id="@+id/btn_form"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_20"
android:text="@string/txt_validate"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

Hemos agregado un NestedScrollView como padre (es lo mismo que ScrollView, y permite darles mejores características de design), dentro colocamos nuestro ConstraintLayout con un pequeño cambio: hemos cambiado el height a wrap_content.

ScrollView con Coordinator en emulador

Podemos ver que no hay cambio alguno, pero qué pasaría si agregamos muchos mas items? Veamos la diferencia 😮

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/iv_logo"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="@dimen/space_20"
android:importantForAccessibility="no"
android:src="@mipmap/ic_launcher"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_20"
android:text="@string/txt_example_form"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_logo" />

<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />

<EditText
android:id="@+id/et_email_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email" />

<EditText
android:id="@+id/et_email_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_2" />

<EditText
android:id="@+id/et_email_4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_3" />

<EditText
android:id="@+id/et_email_5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_4" />

<EditText
android:id="@+id/et_email_6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_5" />

<EditText
android:id="@+id/et_email_7"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_6" />

<EditText
android:id="@+id/et_email_8"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_7" />

<EditText
android:id="@+id/et_email_9"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_8" />

<EditText
android:id="@+id/et_email_10"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/space_20"
android:layout_marginTop="@dimen/space_20"
android:layout_marginEnd="@dimen/space_20"
android:hint="@string/txt_insert_email"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_9" />

<Button
android:id="@+id/btn_form"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/space_20"
android:text="@string/txt_validate"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email_10" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

Hemos agregado 10 campos de edit text (si lo sé, son emails todos, pero se entiende el ejemplo). Ahora veamos como difiere de nuestro diseño anterior en el emulador:

Vista Scrolleable

Podemos observar como podemos desplazarnos dentro de nuestra vista, ahora pasemos con nuestro RecyclerView

¿Qué es un RecyclerView?

  • RecyclerView es el ViewGroup que contiene las vistas correspondientes a tus modelos de datos. Es una vista en sí misma, por lo que agregas RecyclerView a tu diseño de la misma manera en que agregarías cualquier otro elemento de la IU.
  • Cada elemento individual de la lista está definido por un objeto contenedor de vistas. Cuando se lo crea, este contenedor no tiene datos asociados. Después de crearlo, RecyclerView lo vincula a sus datos. Para definir el contenedor de vistas, debes extender RecyclerView.ViewHolder.
  • RecyclerView solicita esas vistas y las vincula a sus datos mediante llamadas a los métodos en el adaptador. Para definir el adaptador, extiende RecyclerView.Adapter.
  • El administrador de diseño organiza los elementos individuales de tu lista. Puedes usar uno de los administradores de diseño proporcionados por la biblioteca RecyclerView o puedes definir el tuyo. Todos los administradores de diseño se basan en la clase abstracta LayoutManager de la biblioteca.

Una vez visto estos conceptos, procederemos a aplicar este conocimiento. Para esto crearemos una lista de items clase, al hacer click podremos abrir una activity relacionada a esa clase. Vamos al ruedo 😯

  • Vamos a modificar nuestro layout inicial (de la clase de activity y ciclo de vida):
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • Hemos creado un RecyclerView, el cuál será modificado a continuación. Pero primero vamos a definir una lista de clases para poder enumerarlas:
class ClassModel {
var id: Int = -1
var name: String = ""

constructor(id: Int, name: String) {
this.id = id
this.name = name
}

}
  • Ahora agregaremos un conjunto de clases:
class ClassModel(var id: Int, var name: String) {

companion object {
val list: List<ClassModel> = arrayListOf(
ClassModel(3, "Third Class"),
ClassModel(4, "Fourth Class")
)
}

}
  • Hemos agregado las últimas dos clases en nuestra lista. Se encuentra dentro de un companion object, que nos permite llamar directamente la lista como si fuera una variable estática en Java. Ahora pasaremos a crear nuestro viewholder y adapter, tan necesarios para inflar la vista de los items del recyclerview. Véase patrón adapter en la referencias. Ahora si continuemos:
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:layout_margin="@dimen/space_20"
android:text="@string/text_size_18"
android:textColor="@color/black"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • Ahora que tenemos el adapter, pasaremos a inicializar nuestro recyclerview en el activity:
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.rv_list)
val linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
recyclerView.adapter = MainAdapter(ClassModel.list)
}

}
  • Hemos agregado las características de layout manager, el cual fue un linear layout manager para manejo vertical. Agregamos además nuestro adapter como variable. Ahora veremos el resultado de todo esto:

https://developer.android.com/reference/android/widget/ScrollView

RecyclerView en emulador

Ahora agreguemos un evento de click, con esto podremos mandar a nuestras distintas activities ya usadas anteriormente:

class MainAdapter(
private var list: List<ClassModel>,
private var onClick: (Int) -> Unit
) : RecyclerView.Adapter<MainAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
LayoutInflater.from(parent.context).inflate(R.layout.item_main, parent, false).let {
return ViewHolder(it, onClick)
}
}

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

override fun getItemCount(): Int = list.size

inner class ViewHolder(itemView: View, private val onClick: (Int) -> Unit) :
RecyclerView.ViewHolder(itemView) {
fun bind(model: ClassModel) {
val text = itemView.findViewById<TextView>(R.id.tv_title)
text.text = model.name
text.setOnClickListener {
onClick(model.id)
}
}
}
}
  • Hemos agregado un anonymous function como variable de click, que nos permite mandar un entero.
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.rv_list)
val linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
recyclerView.adapter = MainAdapter(ClassModel.list) { position ->
when (position) {
3 -> openThirdClass()
4 -> openFourthClass()
}
}
}

private fun openThirdClass() {
openActivity<ThirdClassActivity>()
}

private fun openFourthClass() {
openActivity<FourthClassActivity>()
}

private inline fun <reified T> openActivity() {
Intent(this, T::class.java).also {
startActivity(it)
}
}

}
  • De esta manera lo hemos implementado en nuestro activity, dando uso a la posición mandada y abriendo una nueva actividad. Después de esto se podrá probar los resultados:
Proceso de recyclerview

--

--