SuperAPI AI 工具集

continuation-token-pagination-contract

编程与开发

查看项目

更新于 10-17 16:17

技能介绍

用途

该技能用于为「返回一页数据加一个下一页令牌」的接口建立统一的仓库契约。API 以 items 加一个不透明 token 的形式返回下一页,所有下游行为都从在返回类型中一次性声明这一点出发。

主要能力

  • 用返回类型表达分页语义:
// adapted: parameter names generalized; signatures otherwise as written
fun getRelatedData(id: String): Flow<Resource<Pair<List<Track>, String?>>>
fun getRadioFromEndpoint(endpoint: Endpoint): Flow<Resource<Pair<List<Track>, String?>>>
  • Pair(items, token) 同时表示当前页与请求下一页的方式。
  • null token 表示分页结束,不允许用其他方式表达结束。
  • 信封(Resource,参见 repository-resource-flow-pattern)承载失败,因此失败不会被表达为空页或 null token。
  • 消费方只保存 token,不保存页码。

使用方式

消费方加载更多时的结构如下:

// adapted: names generalized; structure as written
override fun loadMore() {
    if (queueData.value.queueState == StateSource.STATE_INITIALIZING) return   // re-entrancy guard
    val token = queueData.value.data.continuation ?: return
    scope.launch {
        _queueData.update { it.copy(queueState = StateSource.STATE_INITIALIZING) }
        songRepository.getContinueTrack(playlistId, token).lastOrNull().let { response ->
            val list = response?.first
            if (list != null) {
                loadMoreCatalog(list)                                          // restores the flag
                _queueData.update { it.copy(data = it.data.copy(continuation = response.second)) }
            } else {
                _queueData.update { it.copy(data = it.data.copy(continuation = null)) }
                // …end-of-pages handling, which must also restore the flag
            }
        }
    }
}

使 loadMore() 可安全地从滚动监听器和队列结束事件调用的 in-flight 标志属于 queue-rebuild-state-machine;此处重要的是分页路径是它的使用者之一,并且必须在每次退出时遵守它。

陷阱

  • 无法报告失败的分页读取会让网络抖动与列表结束无法区分。若某读取返回裸 Flow<Pair<List<T>?, String?>> 而非信封,实现无处放置错误,只能:
.onFailure { emit(Pair(null, null)) }

消费方读到 list == null,写入 continuation = null,并永久停止分页,因为重试所需的 token 已被抹掉。一次请求丢失就会永久截断列表,且状态中没有任何东西能将其与真正结束的列表区分开。应把所有分页读取放在同一信封之后——但信封本身并不能修复此问题,它只提供可区分的失败,并不阻止你丢弃这种区分。同一代码库中带信封的消费方会匹配干净的 is Resource.Error,却仍在该分支清空 token,以更长的路径到达相同的永久结束。

  • 停止时才清空 token,且仅在停止时清空。已耗尽的 token 留在状态中会在后续每次触发时被重试,即每次滚动一次只能回答「没有」的请求。错误路径必须做相反选择:应保留 token,因为该页仍可重试——在那里清空它正是把瞬时失败变成永久结束的原因。信封让失败可见;只有失败分支能保持列表可恢复,而这是两处独立的修改。只审计其中一处而不审计另一处,正是这个 bug 在旨在移除它的迁移之后仍然存活的原因。

  • 预取循环的失败路径必须推进循环。为前几页做准备的有限预取只在 onSuccess 内重新赋值 token。它不在上述两个签名之后——它是兄弟仓库的分页读取,即构建 radio 队列的那个——所以不要在这些方法下寻找它;要点是同一契约由多处提供,而只有一处做预取:

// adapted — names generalized; literals kept, they are the mechanism
var count = 0
while (continuation != null && count < 3) {
    remote.next(endpoint, continuation = continuation)
        .onSuccess { page ->
            data.addAll(page.items)
            continuation = page.continuation
            if (data.size >= 50) count = 3     // enough to start; stop early
            count++
        }
        .onFailure { count = 3 }               // without this, the same page forever
}

失败时 token 仍保持旧值,条件不变,因此省略计数器跳转不是重试——而是无界循环重复请求同一页。无论失败策略如何,循环变量都必须移动。

  • 预取需要两个停止条件,因为页大小未知。三页预算可能产出六条或六百条;五十条目标可能因来源返回短页而永远无法到达。页预算限制工作量,累计条目目标表达实际目标(「足够开始」),单独任何一个都是错误的保证。

  • 终止操作符是契约的一部分,且它们彼此不一致。此处调用点在同一文件中对分页读取使用 .lastOrNull()、.single() 和 .collect { }。一旦读取发出多次,它们行为不同——single() 抛异常,lastOrNull() 静默丢弃较早的发出——因此仓库后来添加缓存首次发出(参见 cache-then-network)会让一些调用方大声失败,另一些则不可见地失败。应决定分页读取是否只发出一次,在接口中写明,并保持操作符一致。

  • 对信封的两分支 when 在错误分支必须做两件事:清空或保留 token,并释放 in-flight 标志。只在成功时释放是此 bug 的经典版本:第一次失败留下标志置位,loadMore() 顶部的守卫随后拒绝所有未来调用——列表停止加载且从不显示错误,因为本应显示错误的代码再也不会运行。

验证方式

  • 强制失败——快乐路径运行证明不了任何上述内容。
  • 让第二页失败。列表必须保留第一页、保留其 token,并在再次触发时仍能分页。如果它再也不加载,说明标志未释放;如果它报告列表结束,说明失败与结束共享了表示。
  • 在每次请求时记录 token 并断言其变化。重复请求同一页的循环在网络检查器中看起来像健康流量,在 UI 中与慢速 feed 完全相同。
  • 在真正的最后一页之后断言最终 token 为 null,然后再触发一次并断言没有发起请求。

适用场景

适用于需要以 continuation token 实现分页的仓库层设计,尤其是 Kotlin 协程 Flow 与 Resource 信封结合的场景,涉及滚动加载、队列重建、预取与错误恢复。

限制

资料未提供该技能的具体安装命令、平台支持范围、收费方式或价格信息。