¿Por qué RecyclerView?

RecyclerView es el componente estándar para mostrar listas en Android. A diferencia del viejo ListView, recicla las Views que salen de pantalla para mostrar nuevos items sin crear objetos nuevos constantemente. En listas con cientos o miles de items, la diferencia de rendimiento es enorme.

Las tres partes de un RecyclerView

  • RecyclerView: el widget en el XML que muestra la lista.
  • LayoutManager: decide cómo se organizan los items (vertical, horizontal, grilla).
  • Adapter: conecta la lista de datos con las Views. Es el componente central que vas a escribir.

ViewHolder — el patrón base

El ViewHolder guarda referencias a las Views de un item para no tener que buscarlas repetidamente con findViewById (que es lento). Cada item de la lista tiene su propio ViewHolder:

// El layout del item: res/layout/item_producto.xml
// (Un CardView con un TextView para nombre y otro para precio)

class ProductoViewHolder(
    private val binding: ItemProductoBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(producto: Producto, onClick: (Producto) -> Unit) {
        binding.tvNombre.text = producto.nombre
        binding.tvPrecio.text = "$${producto.precio}"
        binding.root.setOnClickListener { onClick(producto) }
    }

    companion object {
        fun create(parent: ViewGroup): ProductoViewHolder {
            val binding = ItemProductoBinding.inflate(
                LayoutInflater.from(parent.context), parent, false
            )
            return ProductoViewHolder(binding)
        }
    }
}

DiffUtil — actualizaciones eficientes

DiffUtil calcula la diferencia entre dos listas y aplica solo las animaciones necesarias (insert, remove, change) en lugar de redibujar toda la lista. Necesitás un DiffUtil.ItemCallback:

class ProductoDiffCallback : DiffUtil.ItemCallback<Producto>() {

    // ¿Es el mismo objeto en la lista? (comparar ID)
    override fun areItemsTheSame(old: Producto, new: Producto): Boolean {
        return old.id == new.id
    }

    // ¿El contenido del objeto cambió? (comparar todos los campos)
    override fun areContentsTheSame(old: Producto, new: Producto): Boolean {
        return old == new  // funciona si Producto es data class
    }
}

ListAdapter — el adapter moderno

ListAdapter combina el Adapter con DiffUtil automáticamente. Solo le pasás la nueva lista y él calcula las diferencias en un thread de background:

class ProductosAdapter(
    private val onClick: (Producto) -> Unit
) : ListAdapter<Producto, ProductoViewHolder>(ProductoDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductoViewHolder {
        return ProductoViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: ProductoViewHolder, position: Int) {
        holder.bind(getItem(position), onClick)
    }
}

getItem(position)En ListAdapter siempre usá getItem(position) en lugar de acceder directamente a una lista propia. ListAdapter administra la lista internamente y coordina con DiffUtil.

Configurar todo en el Fragment

class ProductosFragment : Fragment(R.layout.fragment_productos) {

    private val viewModel: ProductosViewModel by viewModels()

    private val adapter = ProductosAdapter { producto ->
        // Lambda de click: navegar al detalle
        findNavController().navigate(
            ProductosFragmentDirections.actionProductosToDetalle(producto.id)
        )
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Configurar RecyclerView
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = [email protected]
            // Optimización: si el tamaño de la lista no cambia el tamaño del RecyclerView
            setHasFixedSize(true)
        }

        // Observar datos y pasarlos al adapter
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    if (state is ProductosUiState.Success) {
                        adapter.submitList(state.productos)
                    }
                }
            }
        }
    }
}

LayoutManagers

// Lista vertical (el más común)
LinearLayoutManager(context)

// Lista horizontal
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)

// Grilla de 2 columnas
GridLayoutManager(context, 2)

// Grilla de ancho variable (tipo Pinterest)
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)