ListView es un componente básico de Android para mostrar listas verticales, disponible desde API 1 (Android 1.0). ListView crea una lista de elementos usando ArrayAdapter y admite diseños de celdas personalizados. Analicemos qué es ListView, cómo configurarlo y qué dificultades oculta este componente antiguo pero aún utilizado. En IT Sectr encontramos ListView en proyectos heredados y mantenemos compatibilidad con Android 4.4. Para una comparación con la alternativa moderna, lea el artículo sobre RecyclerView.
Puntos clave
ListView es un ViewGroup del paquete android.widget diseñado para mostrar una lista desplazable de elementos. ListView implementa el patrón AdapterView: los datos se pasan a través de un adaptador (Adapter), que convierte los elementos de datos en Views mostrados en pantalla. ListView existe en el SDK de Android desde la primera versión (API 1) y no requiere bibliotecas adicionales.
Arquitectura de ListView es más simple que RecyclerView: LayoutManager está integrado (solo desplazamiento vertical), las animaciones de cambio están ausentes, los divisores se configuran mediante el atributo XML android:divider. ListView agrega automáticamente OverScrollView y resaltado de enfoque, lo que simplifica la creación de prototipos. Según Statista (2025), alrededor del 15% de las aplicaciones de Android en el top 100 de Play Store aún contienen ListView — principalmente proyectos heredados con soporte mínimo.
ListView fue el componente principal de listas en Android desde 2008 hasta 2014, cuando se presentó RecyclerView en Google I/O 2014. A pesar de estar parcialmente obsoleto (soft-deprecated), ListView permanece disponible en todas las versiones de Android y no está marcado como @Deprecated en SDK API 35. Si tu proyecto admite Android 4.4 (API 19) o inferior, ListView es la única opción integrada, ya que RecyclerView requiere AndroidX y dependencias de Gradle.
ArrayAdapter es el adaptador más popular para ListView. Acepta List<T> y un diseño para cada elemento. Por defecto, ArrayAdapter llama a toString() en cada objeto y muestra el texto en simple_list_item_1 (un TextView). Para visualización personalizada, debe sobrescribir getView() o pasar un diseño personalizado en el constructor.
// ListView con ArrayAdapter (texto simple)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"Rusia", "EE.UU.", "Alemania",
"Francia", "Japón", "China", "Brasil"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
countries
)
binding.listView.adapter = adapter
// Manejo de clics
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter está diseñado para listas de datos Map. Acepta List<Map<String, Any>> y un array from/to para vincular claves de Map con IDs de widgets en el diseño. SimpleAdapter es conveniente para prototipos rápidos cuando no se necesita una clase de adaptador separada. La desventaja es la falta de tipado seguro y la complejidad con lógica personalizada.
<!-- Diseño con ListView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/divider"
android:dividerHeight="1px"
android:scrollbars="vertical" />
</LinearLayout>
Adaptador personalizado para ListView sobrescribe el método getView(), que RecyclerView divide en onCreateViewHolder + onBindViewHolder. En ListView, ambos pasos están combinados, por lo que el desarrollador debe implementar independientemente el patrón ViewHolder usando convertView (View reutilizable) y el método setTag().
// Modelo de datos
data class Product(
name: String,
price: String,
imageResId: Int
)
// Adaptador personalizado con ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder para almacenamiento en caché
private class ViewHolder(view: View) {
val nameText: TextView = view.findViewById(R.id.tvName)
val priceText: TextView = view.findViewById(R.id.tvPrice)
val imageView: ImageView = view.findViewById(R.id.ivProduct)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.item_product, parent, false)
.also { it.tag = ViewHolder(it) }
val holder = view.tag as ViewHolder
val product = getItem(position)
holder.nameText.text = product.name
holder.priceText.text = product.price
holder.imageView.setImageResource(product.imageResId)
return view
}
}
Diferencia clave con RecyclerView: en ListView, el método getView() se llama cada vez que un elemento aparece en pantalla. Si no se usan convertView y ViewHolder, cada desplazamiento crea un nuevo objeto View, lo que provoca pérdidas de memoria y retrasos en listas con más de 50 elementos. ViewHolder con view.tag es una optimización obligatoria para ListView con diseños personalizados.
ListView tiene cuatro limitaciones arquitectónicas que Google solucionó en RecyclerView: (1) solo orientación vertical — sin cuadrículas, listas horizontales o diseños escalonados; (2) sin notificaciones detalladas — cualquier cambio de datos causa un redibujado completo mediante notifyDataSetChanged(), lo que crea tirones; (3) sin animación integrada para agregar/eliminar elementos; (4) sin separación de responsabilidades — LayoutManager está integrado, ItemAnimator está ausente.
<!-- item_product.xml — diseño de elemento ListView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:textSize="14sp"
android:textColor="@color/price" />
</LinearLayout>
</LinearLayout>
Implicaciones prácticas: en listas de hasta 20 elementos, ListView funciona tan rápido como RecyclerView. Con 100+ elementos, la diferencia se vuelve notable: ListView comienza a perder FPS debido a la falta de cálculos diferenciales y ViewHolder forzado. Para listas con actualizaciones frecuentes (chat, feed, registros), ListView no es adecuado — use RecyclerView con Paging 3. En IT Sectr, migramos ListView a RecyclerView siempre que trabajamos en pantallas heredadas con más de 50 elementos.
ListView proporciona un conjunto de atributos XML para configuración rápida sin código: android:divider (color/drawable del divisor), android:dividerHeight (grosor), android:scrollbars (barras de desplazamiento), android:fastScrollEnabled (desplazamiento rápido mediante control), android:choiceMode (modo de selección: singleChoice, multipleChoice). El atributo android:entries permite llenar la lista desde un array de recursos (string-array) sin adaptador.
| Atributo | Descripción | Ejemplo de valor |
|---|---|---|
| android:divider | Color o drawable del divisor | @color/gray_light |
| android:dividerHeight | Grosor del divisor en px/dp | 1dp |
| android:entries | Array de cadenas desde recursos | @array/countries |
| android:choiceMode | Modo de selección de elementos | singleChoice |
| android:fastScrollEnabled | Desplazamiento rápido | true |
| android:overlapAnchor | Anclaje para listas emergentes (API 21+) | true |
Modo de selección: choiceMode="singleChoice" activa el estilo RadioButton donde el elemento seleccionado se resalta. choiceMode="multipleChoice" — casillas de verificación para múltiples elementos. Los datos de selección están disponibles mediante listView.checkedItemPosition o listView.checkedItemIds. Importante: al usar choiceMode, ListView usa StateListDrawable para el fondo — configure su propio selector si el azul predeterminado no coincide con el diseño.
Elección entre ListView y RecyclerView depende de los requisitos del proyecto. RecyclerView es el estándar moderno, pero ListView puede justificarse para pantallas simples con costos de mantenimiento mínimos para código heredado. Comparación en todos los parámetros clave:
| Parámetro | ListView | RecyclerView |
|---|---|---|
| Tamaño de biblioteca | Integrado en SDK (~60 KB) | AndroidX (~300 KB con dependencias) |
| Versión mínima | API 1 | API 14 (mediante AndroidX) |
| Orientación | Solo vertical | Vertical, horizontal, cuadrícula, escalonado |
| ViewHolder | Implementación manual | Obligatorio por arquitectura |
| Animación | Solo transiciones de fundido | DefaultItemAnimator (add, remove, move, change) |
| Actualizaciones detalladas | No (solo notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| Listener de clic | onItemClickListener (integrado) | Mediante ViewHolder (no integrado) |
| Rendimiento | Adecuado para listas de hasta 50 elementos | 60 FPS estables para 1000+ elementos |
Conclusión: para proyectos nuevos, use RecyclerView. Mantenga ListView solo en código heredado donde la migración sea innecesariamente costosa. Según las pautas de Material Design 3 (2026), las listas de cualquier complejidad deben usar RecyclerView para cumplir con los estándares modernos de rendimiento y accesibilidad.
Preguntas frecuentes
ListView no está marcado como @Deprecated en Android SDK API 35, pero Google no recomienda usarlo en proyectos nuevos (soft-deprecated). El componente sigue siendo funcional en todas las versiones de Android. RecyclerView ha sido el reemplazo oficial desde 2014. Si comienzas un proyecto nuevo — elige RecyclerView. Si mantienes uno existente — ListView continuará funcionando sin cambios.
ListView admite los métodos addHeaderView(View, Object, boolean) y addFooterView(View, Object, boolean). El encabezado y el pie se agregan a la lista general de elementos pero no participan en el adaptador — siempre se muestran al principio y al final de la lista. Importante: agregue header/footer antes de establecer el adaptador (setAdapter()), de lo contrario no se tendrán en cuenta. Después de agregar un encabezado, los ID de posición de los elementos se desplazan según la cantidad de encabezados.
ListView y ScrollView ambos manejan gestos verticales, lo que crea un conflicto: el ScrollView externo intercepta el desplazamiento y ListView no se desplaza. Solución: (1) no anide ListView dentro de ScrollView — use RecyclerView con NestedScrollingChild, (2) reemplace ScrollView con NestedScrollView (API 21+), (3) use ListView con altura fija (android:layout_height="...dp") dentro de ScrollView, pero esto rompe la adaptabilidad.
La limpieza de datos en ListView depende del tipo de adaptador. Para ArrayAdapter: adapter.clear() y adapter.addAll(newList) seguido de adapter.notifyDataSetChanged(). Para adaptador personalizado: actualice la lista de datos y llame a notifyDataSetChanged(). Nota: listView.invalidateViews() solo redibuja las Views pero no actualiza los datos. Siempre cambie los datos a través del adaptador, no directamente.
La altura de los elementos de ListView se establece en el XML de diseño del elemento. Si el diseño usa layout_height="wrap_content", la altura se ajusta al contenido. Para una altura fija de todos los elementos, use un adaptador personalizado que establezca LayoutParams en getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). Para alturas variables, use RecyclerView con diferentes tipos de vista.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también