# PagedDataAdapter

> 面向 VirtualListView 的分页 DataAdapter，带页面 LRU 缓存和异步加载。

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

# PagedDataAdapter

`PagedDataAdapter` 是面向 [`VirtualListView`](/docs/ui/widgets/virtuallistview) 的**分页** `DataAdapter`，带**页面 LRU 缓存**。它只在内存中保留若干页面；当滚动到某个页面已被丢弃的区域时，会显示加载指示器并通过 `loadPage` 异步重新加载。

它是一个**抽象**类：你需要实现 `getTotalCount`、`loadPage`、`createView` 和 `bindItem`。索引→页面的转换、缓存、加载指示器以及加载期间的滚动锁定都已现成提供。

> 🔴 · **包：** `dev.crossfly.ui` · **实现：** `VirtualListView.DataAdapter` · **用法：** 传给 `VirtualListView` 的构造函数

## 何时使用
- 数据来自网络/数据库并按**页**获取，无法全部放入内存。
- 你希望自动缓存（LRU），并在回到旧区域时透明地重新加载。

## 何时不要使用
- 所有数据已在内存中 → 直接在 [`VirtualListView`](/docs/ui/widgets/virtuallistview) 上实现 `DataAdapter`。
- 小型列表 → [`ListView`](/docs/ui/widgets/listview)。

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

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.graphics.*;   // Relative

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      final int pageSize = 20;
      final int total    = 10_000;

      PagedDataAdapter adapter = new PagedDataAdapter(pageSize, 5) { // 每页 20 项，共 5 页
         public int getTotalCount() { return total; }
         public void loadPage(int pageIndex, PageCallback cb) {
            // 模拟一次加载（请替换为后台的网络/数据库调用）。
            Object[] data = new Object[pageSize];
            for (int i = 0; i < pageSize; i++)
               data[i] = "Item " + (pageIndex * pageSize + i);
            cb.onPageLoaded(pageIndex, data);
         }
         public Component createView() { return new Label(""); }
         public void bindItem(Component view, Object item, int index) {
            ((Label) view).setText(item.toString());
         }
      };

      VirtualListView list = new VirtualListView(Relative.ofFont(2f), adapter);

      add(LAY_FILL, list);   // 列表使用 LAY_FILL（占满高度）
   }
}
```

> 下面的片段**只展示核心部分**（放在 `onShow()` 里的内容）。要运行它们，请放入上面的 `MainWindow` 骨架中。

## 基本用法
```java
PagedDataAdapter adapter = new PagedDataAdapter(20, 5) { // 每页 20 项，内存中缓存 5 页
   public int getTotalCount() { return serverTotal; }
   public void loadPage(int pageIndex, PageCallback cb) {
      api.fetch(pageIndex, data -> cb.onPageLoaded(pageIndex, data)); // 可以放到后台
   }
   public Component createView() { return new Label(""); }
   public void bindItem(Component view, Object item, int index) {
      ((Label) view).setText(item.toString());
   }
};
VirtualListView list = new VirtualListView(Relative.ofFont(2f), adapter);
layout.add(LAY_FILL, list);
```

## 构造函数
| 构造函数 | 描述 |
|---|---|
| `PagedDataAdapter(int pageSize, int maxCachedPages)` | `pageSize`：每页项数。`maxCachedPages`：内存中保留的页面数（LRU） |

这两个字段都是 `public final`。当你把此 adapter 传给 `VirtualListView` 的构造函数时，`VirtualListView` 会将自身注入到该 adapter 中。

## 需要实现的抽象方法
| 方法 | 描述 |
|---|---|
| `int getTotalCount()` | 服务器上的项总数（由 `getCount()` 使用） |
| `void loadPage(int pageIndex, PageCallback callback)` | 异步加载一页；完成时调用 `callback.onPageLoaded(pageIndex, items)`（可在任意线程） |
| `Component createView() throws Exception` | 创建一个空白视图 |
| `void bindItem(Component view, Object item, int index) throws Exception` | 将原始项（`Object`）绑定到视图 —— 请转换为你的类型 |

## 有用的属性与方法
| 方法 | 类型 | 作用 |
|---|---|---|
| `isAvailable(int index)` | `boolean` | 该项是否已在缓存中 |
| `clearCache()` | `void` | 清空缓存和加载状态 |
| `getCount()` | `int` | 委托给 `getTotalCount()` |

## 工作原理（概要）
- `bindView(view, index)` 转换出 `pageIndex = index / pageSize` 和 `itemInPage = index % pageSize`。
  - 页面在缓存中 → 立即 `bindItem`（`waitingForData = false`）。
  - 页面缺失 → `waitingForData = true` 并触发 `requestPage(pageIndex)`。
- `requestPage` 避免重复请求（`loading`），显示加载指示器并**暂停 fling**；回调返回时，用 `Component.onUI(...)` 编组到 UI 线程，写入页面，隐藏加载指示器，强制重新绑定该范围（`invalidateRange`）并**恢复 fling**。
- `cache` 是一个处于访问顺序（access-order）模式的 `LinkedHashMap`，当超过 `maxCachedPages` 时丢弃最旧的页面（LRU）。

## 回调与事件
| 回调 | 触发时机 |
|---|---|
| `PageCallback.onPageLoaded(int pageIndex, Object[] items)` | 你在完成页面加载时调用（可在任意线程） |

## 状态
- **`waitingForData`** —— 只要有页面在加载就为 `true`；向 `VirtualListView` 发出信号以**锁定滚动**。
- **LRU** —— 旧页面被丢弃；滚动回去时会重新加载（新的加载指示器 + 请求）。

## 示例 —— 类型化项的转换
```java
PagedDataAdapter adapter = new PagedDataAdapter(50, 4) {
   public int getTotalCount() { return repo.total(); }
   public void loadPage(int page, PageCallback cb) {
      repo.loadAsync(page, 50, lista -> cb.onPageLoaded(page, lista.toArray()));
   }
   public Component createView() { return new Label(""); }
   public void bindItem(Component view, Object item, int index) {
      Cliente c = (Cliente) item;            // 对原始 Object 进行 cast
      ((Label) view).setText(c.getNome());
   }
};
VirtualListView list = new VirtualListView(Relative.ofFont(2.4f), adapter);
```

## 最佳实践
- 页面中的项以 `Object[]` 到达 —— 在 `bindItem` 中将 `item` 转换为你的类型。
- 在回调之前**不要**在 `loadPage` 内调用 UI API —— adapter 在收到 `onPageLoaded` 时已在内部切换到 UI 线程。
- 根据滚动模式调整 `maxCachedPages`：更大的缓存可避免回滚时重新加载，代价是更多内存。

## 常见错误
- **用错误的 `pageIndex` 调用 `onPageLoaded`** —— adapter 用该索引写入缓存并使正确的范围失效。
- **在 `loadPage` 中阻塞** —— 它必须是异步的；UI 线程不能等待网络。

## 另请参阅
- 领域指南：[`ui/`](/docs/ui) · 实用配方：[`cookbook/`](/docs/cookbook)
- 相关组件：[`VirtualListView`](/docs/ui/widgets/virtuallistview)
