# 如何实现加载更多（无限滚动）

> 当用户滚动到虚拟列表末尾附近时自动追加下一批数据，无需手动翻页。

Source: https://crossfly.dev/zh/docs/cookbook/lists/carregar-mais — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# 如何实现加载更多（无限滚动）

**目的。** 在用户接近列表末尾时自动加载更多数据，将项目追加到现有数据集（append）。
与带 LRU 缓存的分页（[paginacao.md](/docs/cookbook/lists/paginacao)）不同，这里由你控制数据集，
列表仅在需要获取更多数据时发出通知。

## 使用的 API
- `VirtualListView.onLoadMore(Runnable callback)` — 当滚动接近末尾时触发。每次只有一个待处理的触发（`loadMorePending`），避免重复调用。
- `VirtualListView.loadMoreThreshold(int threshold)` — 距离末尾多少项时触发回调。默认 `5`。
- `VirtualListView.notifyDataChanged()` — 在追加新项后**必须**调用；它会清除 `loadMorePending`、隐藏加载指示器并重新测量内容。

## 完整示例（复制、粘贴并运行）
```java
package com.exemplo;

import java.util.*;   // List / ArrayList

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*;   // Relative（用于 VirtualListView）

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   final List<String> dados = new ArrayList<>();
   int proxima = 0;

   /** 向数据集追加一批 30 个项目（模拟服务器返回的一页）。 */
   private void carregarPagina()
   {
      for (int i = 0; i < 30; i++)
         dados.add("Registro " + (proxima++));
   }

   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      carregarPagina();   // 填充第一批

      VirtualListView lista = new VirtualListView(Relative.ofFont(3f), new VirtualListView.DataAdapter()
      {
         public int getCount()                    { return dados.size(); }
         public Component createView()            { return new Label(""); }
         public void bindView(Component v, int i) throws Exception
         {
            ((Label) v).setText(dados.get(i));
         }
      });

      lista.loadMoreThreshold(8);   // 在末尾前 8 项开始拉取（默认为 5）
      lista.onLoadMore(() ->
      {
         // 接近末尾时触发一次；后续触发仅在 notifyDataChanged 之后
         onUI(() ->
         {
            carregarPagina();             // 追加（新索引在末尾）
            lista.notifyDataChanged();    // 放行下一次 load-more 并隐藏 spinner
         });
      });

      add(LAY_FILL, lista);
   }
}
```

> 下面的片段只展示**核心部分**。要运行它，请将其放入上面的 `MainWindow` 骨架中。

## 最小片段
```java
// 已在源码核实：VirtualListView.java（onLoadMore / loadMoreThreshold / notifyDataChanged）
final Array<String> dados = new Array<>();
carregarPagina(dados, 0);   // 填充第一批

VirtualListView lista = new VirtualListView(Relative.ofFont(3f), new VirtualListView.DataAdapter()
{
   public int getCount()                    { return dados.size(); }
   public Component createView()            { return new Label(""); }
   public void bindView(Component v, int i) throws Exception
   {
      ((Label) v).setText(dados.get(i));
   }
});

lista.loadMoreThreshold(8);   // 在末尾前 8 项开始拉取（默认为 5）
lista.onLoadMore(() ->
{
   // 接近末尾时触发一次；后续触发仅在 notifyDataChanged 之后
   api.proximaPagina(novos ->
   {
      dados.addAll(novos);          // 追加（新索引在末尾）
      lista.notifyDataChanged();    // 放行下一次 load-more 并隐藏 spinner
   });
});

layout.add(1f, lista);
```

## 深入了解
- `VirtualListView` 在加载更多处于待处理状态时会自动在**底部显示加载指示器**（由 `VirtualListViewStyle.showSpinner` 控制）。
- 如果你**不**调用 `notifyDataChanged()`，`loadMorePending` 将卡在 `true`，回调将永远不再触发。
- 需要带有限缓存的可丢弃页面（而非无限追加）？使用 [paginacao.md](/docs/cookbook/lists/paginacao)。
- 组件指南：[VirtualListView](/docs/ui/widgets/virtuallistview) · 姊妹配方：[lista-virtual-grande.md](/docs/cookbook/lists/lista-virtual-grande) · 权威来源：第 15 章
