# VirtualListView

> Lista virtualizada de alto rendimiento que recicla componentes, al estilo de RecyclerView.

Source: https://crossfly.dev/es/docs/ui/widgets/virtuallistview — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# VirtualListView

`VirtualListView` es una lista **virtualizada**: crea y renderiza solamente los elementos visibles en la pantalla y **recicla** los componentes que salen del viewport, al estilo del `RecyclerView` de Android. Soporta miles (o millones) de elementos con apenas unas pocas decenas de componentes vivos, manteniendo el uso de memoria constante.

La fuente de datos es la interfaz `DataAdapter` (`getCount` / `createView` / `bindView`). La altura de cada elemento es **fija** (`Relative itemHeight`). Incluye callback de "cargar más" (`onLoadMore`) y spinner de carga.

> 🔴 · **Paquete:** `dev.crossfly.ui` · **Extiende:** `Component` · **Estilo:** `VirtualListViewStyle` (vía `getSkin().virtualListView()`)

## Cuándo usar
- Listas grandes (cientos de miles de filas) donde crear un componente por elemento es inviable.
- Desplazamiento infinito con carga incremental (`onLoadMore`).
- Paginación remota con caché → combina con [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter).

## Cuándo NO usar
- Listas pequeñas de texto → [`ListView`](/docs/ui/widgets/listview) es más simple y mide la altura exacta.
- Alturas de elemento realmente variables — la altura aquí es fija (elige un slot que albergue el elemento más grande).

## Apariencia
Columna vertical desplazable de elementos de altura uniforme. Durante el desplazamiento, los elementos fuera de pantalla se recogen en un pool y se reaprovechan. Un spinner opcional aparece en el pie (load-more) o en el centro (carga de página).

## Ejemplo completo (copiar, pegar y ejecutar)
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*;   // Relative
import java.util.*;                    // List, ArrayList

/** Pantalla principal ejecutable. Apunta el launcher (classname) y corre: gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      final List<String> items = new ArrayList<>();
      for (int i = 0; i < 10_000; i++)
         items.add("Item " + i);

      VirtualListView list = new VirtualListView(Relative.ofFont(2f),
         new VirtualListView.DataAdapter() {
            public int getCount() { return items.size(); }
            public Component createView() { return new Label(""); }
            public void bindView(Component v, int index) {
               ((Label) v).setText(items.get(index));
            }
         });

      add(LAY_FILL, list);   // las listas usan LAY_FILL (absorben la altura)
   }
}
```

> Los fragmentos siguientes muestran **solo el núcleo** (lo que va dentro de `onShow()`). Para ejecutarlos, colócalos en el esqueleto `MainWindow` de arriba.

## Uso básico
```java
VirtualListView list = new VirtualListView(Relative.ofFont(2f),
   new VirtualListView.DataAdapter() {
      public int getCount() { return items.size; }
      public Component createView() { return new Label(""); }
      public void bindView(Component v, int index) {
         ((Label) v).setText("Item " + index);
      }
   });
layout.add(LAY_FILL, list);
```

## Constructores
| Constructor | Descripción |
|---|---|
| `VirtualListView(Relative itemHeight, DataAdapter adapter)` | **Único** constructor. `itemHeight` es la altura fija de cada elemento; `adapter` es la fuente de datos |

> **No existe constructor sin argumentos.** Si `adapter` es `null`, el constructor llama a `createAdapter()` (que sobrescribes en una subclase). Si el adapter es un [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter), el constructor le inyecta `this` automáticamente.

### DataAdapter
```java
public interface DataAdapter {
   int getCount();                                   // total de ítems
   Component createView() throws Exception;          // crea view en blanco (solo cuando el pool está vacío)
   void bindView(Component view, int index) throws Exception; // vincula datos del índice a la view
}
```
- `getCount()` informa el total — la lista **finge** una altura `count * itemHeight` sin materializar todos los elementos.
- `createView()` solo se llama cuando el pool de reciclaje está vacío.
- `bindView()` se ejecuta muchas veces durante el desplazamiento: mantenlo **idempotente y barato**.

## Propiedades y personalización
| Propiedad / método | Tipo | Predeterminado | Qué hace |
|---|---|---|---|
| `onLoadMore(Runnable)` | `VirtualListView` | `null` | Callback disparado al desplazarse cerca del final |
| `loadMoreThreshold(int)` | `VirtualListView` | `5` | Cuántos elementos antes del final disparan el load-more (graba en `vlvStyle()`) |
| `notifyDataChanged()` | `void` | — | Llámalo tras **agregar** datos (nueva página); re-clampa el scroll y re-vincula el rango |
| `reloadData()` | `void` | — | Llámalo tras **alterar** el contenido bajo los mismos índices (búsqueda/filtro/sort); limpia caché, desplaza al inicio, re-vincula todo |
| `scrollToIndex(int)` | `void` | — | Centra el elemento en el viewport |
| `scrollToY(float)` | `void` | — | Desplaza a un offset en píxeles (sub-píxel/animable) |
| `getScrollY()` | `float` | — | Offset vertical actual (útil para header colapsable) |
| `setItemHeight(Relative)` | `void` | — | Cambia la altura del elemento y llama a `notifyDataChanged()` |
| `getItemHeight()` | `int` | — | Altura del elemento en px |
| `replaceView(int, Component)` | `void` | — | Sustituye la view activa de un índice por un componente totalmente nuevo |

## Callbacks y eventos
| Callback | Cuándo se dispara |
|---|---|
| `onLoadMore(Runnable)` | Cuando `lastVisible >= count - threshold` y no hay carga pendiente |

Los eventos de scroll (`onPan`/`onFling`/`onMouseWheel`) son **bloqueados mientras el adapter paginado espera datos** (`isWaitingForData()`), evitando desplazarse al vacío.

## Estados
- **Reciclando** — los elementos fuera del viewport vuelven al pool; al reingresar, son re-vinculados vía `bindView`.
- **Cargando (load-more)** — spinner en el pie, `loadMorePending = true` hasta `notifyDataChanged()`.
- **Esperando página** (con `PagedDataAdapter`) — scroll trabado hasta que llegue la página.

### Lista de 10.000 elementos con load-more
```java
VirtualListView list = new VirtualListView(Relative.ofFont(2f),
   new VirtualListView.DataAdapter() {
      public int getCount() { return items.size; }
      public Component createView() { return new Label(""); }
      public void bindView(Component v, int index) {
         ((Label) v).setText("Item " + index);
      }
   });
list.onLoadMore(() -> {
   carregarProximaPagina();   // agrega ítems a la fuente
   list.notifyDataChanged();  // avisa a la lista
});
layout.add(LAY_FILL, list);
```

### Cambio dinámico de adapter vía subclase
No hay `setAdapter` público en la clase base (el campo `adapter` es `protected`). Para cambiar de adapter en tiempo de ejecución, haz una subclase:
```java
class RowList extends VirtualListView {
   RowList() { super(Relative.ofFont(2f), null); }
   void setAdapter(DataAdapter a, Relative h) { adapter = a; setItemHeight(h); }
}

RowList vsc = new RowList();
final int count = 2000;
vsc.setAdapter(new VirtualListView.DataAdapter() {
   public int getCount() { return count; }
   public Component createView() { return new Label(""); }
   public void bindView(Component v, int index) {
      ((Label) v).setText("Row id: " + index);
   }
}, Relative.ofPixels(100));
```

## Buenas prácticas
- **La altura de elemento es fija** — elige un slot que albergue el elemento más grande.
- Tras **agregar** datos, llama a `notifyDataChanged()`; tras **alterar el contenido** bajo los mismos índices, llama a `reloadData()` (de lo contrario las filas ya visibles quedan con datos viejos).
- Si `bindView` crea un componente **nuevo** por índice en vez de mutar el reciclado, usa `replaceView(...)` — o acepta que esas views no vuelven al pool.

## Errores comunes
- **Asignar recursos pesados en `bindView`** — se ejecuta en cada evento de scroll. Haz un binding barato.
- **Olvidar `notifyDataChanged()` tras agregar página** — la nueva altura de contenido no se reconoce y el load-more no se resetea.
- **Usar `reloadData()` cuando solo agregaste** — desplaza al inicio; para append usa `notifyDataChanged()`.

## Ver también
- Guía del área: [`ui/`](/docs/ui) · Recetas: [`cookbook/`](/docs/cookbook)
- Componentes relacionados: [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter) · [`ListView`](/docs/ui/widgets/listview)
