# VirtualListView

> Lista virtualizada de alto desempenho que recicla componentes, à moda do RecyclerView.

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

# VirtualListView

`VirtualListView` é uma lista **virtualizada**: ela cria e renderiza somente os itens visíveis na tela e **recicla** os componentes que saem da viewport, à moda do `RecyclerView` do Android. Suporta milhares (ou milhões) de itens com poucas dezenas de componentes vivos, mantendo o uso de memória constante.

A fonte de dados é a interface `DataAdapter` (`getCount` / `createView` / `bindView`). A altura de cada item é **fixa** (`Relative itemHeight`). Inclui callback de "carregar mais" (`onLoadMore`) e spinner de carregamento.

> 🔴 · **Pacote:** `dev.crossfly.ui` · **Estende:** `Component` · **Estilo:** `VirtualListViewStyle` (via `getSkin().virtualListView()`)

## Quando usar
- Listas grandes (centenas de milhares de linhas) onde criar um componente por item é inviável.
- Rolagem infinita com carregamento incremental (`onLoadMore`).
- Paginação remota com cache → combine com [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter).

## Quando NÃO usar
- Listas pequenas de texto → [`ListView`](/docs/ui/widgets/listview) é mais simples e mede altura exata.
- Alturas de item variáveis de verdade — a altura aqui é fixa (escolha um slot que comporte o maior item).

## Aparência
Coluna vertical rolável de itens de altura uniforme. Durante o scroll, itens fora de tela são recolhidos para um pool e reaproveitados. Um spinner opcional aparece no rodapé (load-more) ou no centro (carregamento de página).

## Exemplo completo (copiar, colar e rodar)
```java
package com.exemplo;

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

/** Tela principal executável. Aponte o launcher (classname) e rode: 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);   // listas usam LAY_FILL (absorvem a altura)
   }
}
```

> Os trechos abaixo mostram **só o miolo** (o que vai dentro de `onShow()`). Para rodar, coloque-os no esqueleto `MainWindow` acima.

## 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);
```

## Construtores
| Construtor | Descrição |
|---|---|
| `VirtualListView(Relative itemHeight, DataAdapter adapter)` | **Único** construtor. `itemHeight` é a altura fixa de cada item; `adapter` é a fonte de dados |

> **Não existe construtor sem argumentos.** Se `adapter` for `null`, o construtor chama `createAdapter()` (que você sobrescreve numa subclasse). Se o adapter for um [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter), o construtor injeta `this` nele automaticamente.

### DataAdapter
```java
public interface DataAdapter {
   int getCount();                                   // total de itens
   Component createView() throws Exception;          // cria view em branco (só quando o pool está vazio)
   void bindView(Component view, int index) throws Exception; // vincula dados do índice à view
}
```
- `getCount()` informa o total — a lista **finge** uma altura `count * itemHeight` sem materializar todos os itens.
- `createView()` só é chamado quando o pool de reciclagem está vazio.
- `bindView()` roda muitas vezes durante o scroll: mantenha-o **idempotente e barato**.

## Propriedades e personalização
| Propriedade / método | Tipo | Padrão | O que faz |
|---|---|---|---|
| `onLoadMore(Runnable)` | `VirtualListView` | `null` | Callback disparado ao rolar perto do fim |
| `loadMoreThreshold(int)` | `VirtualListView` | `5` | Quantos itens antes do fim disparam o load-more (grava em `vlvStyle()`) |
| `notifyDataChanged()` | `void` | — | Chame após **acrescentar** dados (nova página); re-clampa scroll e re-vincula a faixa |
| `reloadData()` | `void` | — | Chame após **alterar** o conteúdo sob os mesmos índices (busca/filtro/sort); limpa cache, rola ao topo, re-vincula tudo |
| `scrollToIndex(int)` | `void` | — | Centraliza o item na viewport |
| `scrollToY(float)` | `void` | — | Rola para um offset em pixels (sub-pixel/animável) |
| `getScrollY()` | `float` | — | Offset vertical atual (útil para header colapsável) |
| `setItemHeight(Relative)` | `void` | — | Troca a altura do item e chama `notifyDataChanged()` |
| `getItemHeight()` | `int` | — | Altura do item em px |
| `replaceView(int, Component)` | `void` | — | Substitui a view ativa de um índice por um componente totalmente novo |

## Callbacks e eventos
| Callback | Quando dispara |
|---|---|
| `onLoadMore(Runnable)` | Quando `lastVisible >= count - threshold` e não há carregamento pendente |

Eventos de scroll (`onPan`/`onFling`/`onMouseWheel`) são **bloqueados enquanto o adapter paginado aguarda dados** (`isWaitingForData()`), evitando rolar para o vazio.

## Estados
- **Reciclando** — itens fora da viewport voltam ao pool; ao reentrarem, são re-vinculados via `bindView`.
- **Carregando (load-more)** — spinner no rodapé, `loadMorePending = true` até `notifyDataChanged()`.
- **Aguardando página** (com `PagedDataAdapter`) — scroll travado até a página chegar.

### Lista de 10.000 itens com 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();   // acrescenta itens à fonte
   list.notifyDataChanged();  // avisa a lista
});
layout.add(LAY_FILL, list);
```

### Troca dinâmica de adapter via subclasse
Não há `setAdapter` público na classe base (o campo `adapter` é `protected`). Para trocar de adapter em tempo de execução, faça uma subclasse:
```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));
```

## Boas práticas
- **Altura de item é fixa** — escolha um slot que comporte o maior item.
- Após **acrescentar** dados, chame `notifyDataChanged()`; após **alterar o conteúdo** sob os mesmos índices, chame `reloadData()` (senão as linhas já visíveis ficam com dados velhos).
- Se `bindView` criar um componente **novo** por índice em vez de mutar o reciclado, use `replaceView(...)` — ou aceite que essas views não voltam ao pool.

## Erros comuns
- **Alocar recursos pesados em `bindView`** — ele roda a cada evento de scroll. Faça binding barato.
- **Esquecer `notifyDataChanged()` após adicionar página** — a nova altura de conteúdo não é reconhecida e o load-more não reseta.
- **Usar `reloadData()` quando só acrescentou** — ele rola ao topo; para append use `notifyDataChanged()`.

## Veja também
- Guia da área: [`ui/`](/docs/ui) · Receitas: [`cookbook/`](/docs/cookbook)
- Componentes relacionados: [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter) · [`ListView`](/docs/ui/widgets/listview)
