This page was machine-translated from Portuguese and may contain errors. The Portuguese version is canonical.
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(viagetSkin().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.
When NOT to use
- Small text lists →
ListViewis 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)
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 theMainWindowskeleton above.
Basic usage
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
adapterisnull, the constructor callscreateAdapter()(which you override in a subclass). If the adapter is aPagedDataAdapter, the constructor injectsthisinto it automatically.
DataAdapter
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 ofcount * itemHeightwithout 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 = trueuntilnotifyDataChanged(). - Waiting for page (with
PagedDataAdapter) — scroll locked until the page arrives.
List of 10,000 items with load-more
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:
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, callreloadData()(otherwise the already-visible rows keep stale data). - If
bindViewcreates a new component per index instead of mutating the recycled one, usereplaceView(...)— 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 usenotifyDataChanged().
See also
- Area guide:
ui/· Recipes:cookbook/ - Related components:
PagedDataAdapter·ListView