Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started

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 (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.

When NOT to use

  • Small text lists → 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)

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

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, the constructor injects this into 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 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

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, 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

Was this page helpful?