# VirtualListView

> High-performance virtualized list that recycles components, RecyclerView-style.

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

# VirtualListView

`VirtualListView` is a **virtualized** list: it creates and renders only the items visible on screen and **recycles** the components that leave the viewport, in the style of Android's `RecyclerView`. It supports thousands (or millions) of items with just a few dozen live components, keeping memory usage constant.

The data source is the `DataAdapter` interface (`getCount` / `createView` / `bindView`). The height of each item is **fixed** (`Relative itemHeight`). It includes a "load more" callback (`onLoadMore`) and a loading spinner.

> 🔴 · **Package:** `dev.crossfly.ui` · **Extends:** `Component` · **Style:** `VirtualListViewStyle` (via `getSkin().virtualListView()`)

## When to use
- Large lists (hundreds of thousands of rows) where creating one component per item is unfeasible.
- Infinite scrolling with incremental loading (`onLoadMore`).
- Remote pagination with cache → combine with [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter).

## When NOT to use
- Small text lists → [`ListView`](/docs/ui/widgets/listview) is simpler and measures exact height.
- Truly variable item heights — the height here is fixed (pick a slot that fits the largest item).

## Appearance
A vertical scrollable column of uniform-height items. During scrolling, off-screen items are collected into a pool and reused. An optional spinner appears at the footer (load-more) or in the center (page loading).

## Complete example (copy, paste, and run)
```java
package com.exemplo;

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

/** Runnable main screen. Point the launcher (classname) and run: 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);   // lists use LAY_FILL (they absorb the height)
   }
}
```

> The snippets below show **only the core** (what goes inside `onShow()`). To run them, place them in the `MainWindow` skeleton above.

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

## Constructors
| Constructor | Description |
|---|---|
| `VirtualListView(Relative itemHeight, DataAdapter adapter)` | **Only** constructor. `itemHeight` is the fixed height of each item; `adapter` is the data source |

> **There is no no-argument constructor.** If `adapter` is `null`, the constructor calls `createAdapter()` (which you override in a subclass). If the adapter is a [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter), the constructor injects `this` into it automatically.

### DataAdapter
```java
public interface DataAdapter {
   int getCount();                                   // total number of items
   Component createView() throws Exception;          // creates a blank view (only when the pool is empty)
   void bindView(Component view, int index) throws Exception; // binds data from the index to the view
}
```
- `getCount()` reports the total — the list **pretends** to have a height of `count * itemHeight` without materializing all the items.
- `createView()` is only called when the recycling pool is empty.
- `bindView()` runs many times during scrolling: keep it **idempotent and cheap**.

## Properties and customization
| Property / method | Type | Default | What it does |
|---|---|---|---|
| `onLoadMore(Runnable)` | `VirtualListView` | `null` | Callback fired when scrolling near the end |
| `loadMoreThreshold(int)` | `VirtualListView` | `5` | How many items before the end trigger load-more (writes to `vlvStyle()`) |
| `notifyDataChanged()` | `void` | — | Call after **appending** data (new page); re-clamps scroll and re-binds the range |
| `reloadData()` | `void` | — | Call after **changing** the content under the same indices (search/filter/sort); clears the cache, scrolls to top, re-binds everything |
| `scrollToIndex(int)` | `void` | — | Centers the item in the viewport |
| `scrollToY(float)` | `void` | — | Scrolls to a pixel offset (sub-pixel/animatable) |
| `getScrollY()` | `float` | — | Current vertical offset (useful for a collapsible header) |
| `setItemHeight(Relative)` | `void` | — | Swaps the item height and calls `notifyDataChanged()` |
| `getItemHeight()` | `int` | — | Item height in px |
| `replaceView(int, Component)` | `void` | — | Replaces the active view of an index with a completely new component |

## Callbacks and events
| Callback | When it fires |
|---|---|
| `onLoadMore(Runnable)` | When `lastVisible >= count - threshold` and there is no pending load |

Scroll events (`onPan`/`onFling`/`onMouseWheel`) are **blocked while the paged adapter is waiting for data** (`isWaitingForData()`), preventing scrolling into the void.

## States
- **Recycling** — items outside the viewport return to the pool; when they re-enter, they are re-bound via `bindView`.
- **Loading (load-more)** — spinner at the footer, `loadMorePending = true` until `notifyDataChanged()`.
- **Waiting for page** (with `PagedDataAdapter`) — scroll locked until the page arrives.

### List of 10,000 items with 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();   // appends items to the source
   list.notifyDataChanged();  // notifies the list
});
layout.add(LAY_FILL, list);
```

### Dynamic adapter swap via subclass
There is no public `setAdapter` in the base class (the `adapter` field is `protected`). To swap the adapter at runtime, make a subclass:
```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));
```

## Best practices
- **Item height is fixed** — pick a slot that fits the largest item.
- After **appending** data, call `notifyDataChanged()`; after **changing the content** under the same indices, call `reloadData()` (otherwise the already-visible rows keep stale data).
- If `bindView` creates a **new** component per index instead of mutating the recycled one, use `replaceView(...)` — or accept that those views will not return to the pool.

## Common mistakes
- **Allocating heavy resources in `bindView`** — it runs on every scroll event. Keep binding cheap.
- **Forgetting `notifyDataChanged()` after adding a page** — the new content height is not recognized and load-more does not reset.
- **Using `reloadData()` when you only appended** — it scrolls to the top; for append use `notifyDataChanged()`.

## See also
- Area guide: [`ui/`](/docs/ui) · Recipes: [`cookbook/`](/docs/cookbook)
- Related components: [`PagedDataAdapter`](/docs/ui/widgets/paged-data-adapter) · [`ListView`](/docs/ui/widgets/listview)
