分页父类
当页面既需要“下拉刷新”又要“上拉加载更多”时,使用 BaseNetWorkListViewModel + BaseNetWorkListUiState + LoadMoreState + BaseNetWorkListView + RefreshLayout 的组合可以最大化复用状态管理与交互逻辑。相关文件:
app/src/main/java/com/joker/kit/core/base/viewmodel/BaseNetWorkListViewModel.ktapp/src/main/java/com/joker/kit/core/base/state/BaseNetWorkListUiState.ktapp/src/main/java/com/joker/kit/core/base/state/LoadMoreState.ktapp/src/main/java/com/joker/kit/core/ui/component/network/BaseNetWorkListView.ktapp/src/main/java/com/joker/kit/core/ui/component/refresh/
相比不分页的 BaseNetWorkViewModel,分页基类额外处理页码、刷新、加载更多及“空数据”状态,为列表场景减轻大量样板代码。
状态模型
页面状态:BaseNetWorkListUiState
| 状态 | 含义 | 默认视图 |
|---|---|---|
Loading | 首次加载或刷新中 | PageLoading() |
Success | 列表有数据 | 渲染传入的 content |
Empty | 列表为空 | EmptyData(),带重试按钮 |
Error | 加载失败 | EmptyNetwork(),带重试按钮 |
加载更多:LoadMoreState
| 状态 | 含义 |
|---|---|
PullToLoad | 可以上拉触发加载 |
Loading | 正在请求下一页 |
Success | 上一页加载成功,短暂展示 |
Error | 加载失败,通常提示“点击重试” |
NoMore | 没有更多数据了 |
BaseNetWorkListView 负责切换页面级 UI(Loading/Empty/Error/Success),成功态下交给 RefreshLayout 和业务列表渲染;LoadMoreState 则由 RefreshLayout 底部提示条消费。
BaseNetWorkListViewModel 核心能力
- 分页 Flow:子类实现
requestListData(),返回Flow<NetworkResponse<NetworkPageData<T>>>。NetworkPageData包含list与pagination,基类会根据total、size、page计算是否还有下一页。 - 自动状态管理:
_uiState、_listData、_loadMoreState、_isRefreshing通过MutableStateFlow暴露给 UI。 - 刷新与加载更多:
onRefresh()重置页码并触发请求,onLoadMore()根据shouldTriggerLoadMore(lastIndex, totalCount)判断是否递增页码。 - 最小加载时间:首屏可开启
enableMinLoadingTime,防止骨架闪烁。 - 导航联动:继承自
BaseViewModel,同样支持类型安全导航、登录拦截、observeRefreshState()。
常用流程:
- 子类在
init { initLoad() }中启动首屏请求。 loadListData()根据当前页和状态决定是否展示 Loading、如何处理错误。- 成功时首屏调用
setFirstLoadSuccessState(),加载更多则追加到_listData。 - UI 通过
listData渲染 LazyList,LoadMoreState控制底部提示。
示例:商品评论分页
以下示例与 context/青商城 原项目 goods 模块代码示例 中的评论页面结构一致,只是为了说明当前脚手架的使用方式。ViewModel 负责接入仓库分页接口,UI 则由 Scaffold + BaseNetWorkListView + RefreshLayout 组成。
ViewModel
kotlin
@HiltViewModel
class GoodsCommentViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val savedStateHandle: SavedStateHandle,
private val repository: GoodsRepository,
) : BaseNetWorkListViewModel<Comment>( // Comment 为列表项类型
navigator = navigator,
userState = userState
) {
private val goodsId: Long = savedStateHandle.toRoute<GoodsRoutes.Comment>().goodsId
init {
initLoad() // 首屏进来即加载第一页
}
override fun requestListData(): Flow<NetworkResponse<NetworkPageData<Comment>>> {
return repository.getGoodsCommentPage(
request = GoodsCommentPageRequest(
goodsId = goodsId.toString(),
page = currentPage,
size = pageSize
)
)
}
}UI 层
kotlin
@Composable
fun GoodsCommentRoute(
viewModel: GoodsCommentViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()
val listData by viewModel.listData.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
val loadMoreState by viewModel.loadMoreState.collectAsState()
Scaffold { innerPadding ->
BaseNetWorkListView(
uiState = uiState,
padding = innerPadding,
onRetry = viewModel::retryRequest
) {
RefreshLayout(
isRefreshing = isRefreshing,
loadMoreState = loadMoreState,
onRefresh = viewModel::onRefresh,
onLoadMore = viewModel::onLoadMore,
shouldTriggerLoadMore = viewModel::shouldTriggerLoadMore
) {
items(listData.size) { index ->
CommentCard(comment = listData[index])
}
}
}
}
}BaseNetWorkListView负责切换“页面级”状态(骨架/空态/错误/成功),内部成功态交给RefreshLayout。RefreshLayout结合LoadMoreState,内置下拉刷新、上拉加载更多,并通过shouldTriggerLoadMore判断触发时机;需要网格时可设置isGrid = true并提供gridContent。- 列表项遵循
designsystem的尺寸/间距常量(例如Size.kt、Spacer.kt),保持布局一致。
通过上述模式,分页列表只需关心“接口怎么请求”和“成功后如何渲染”,刷新/加载更多/错误状态交由基类和通用组件处理,从而快速实现统一体验的列表页面。 结束后若需要响应其它模块的刷新指令,直接调用 observeRefreshState() 与导航结果集成即可。
API 参考
BaseNetWorkListViewModel<T>
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
currentPage | Int | 1 | 当前请求页,onLoadMore() 会自增 |
pageSize | Int | 10 | 每页条数,用于拼接请求参数 |
uiState | StateFlow<BaseNetWorkListUiState> | Loading | 控制骨架/空态/错误/成功 |
listData | StateFlow<List<T>> | emptyList() | 已加载的聚合数据 |
loadMoreState | StateFlow<LoadMoreState> | PullToLoad | 底部加载提示状态 |
isRefreshing | StateFlow<Boolean> | false | 是否正在下拉刷新 |
enableMinLoadingTime | Boolean | false | 是否启用 240ms 最小加载动画 |
initLoad() | 函数 | - | 在 init 中调用以启动首屏请求 |
requestListData() | 函数 | - | protected abstract fun requestListData(): Flow<NetworkResponse<NetworkPageData<T>>> |
onRefresh() | 函数 | - | 重置页码并重新请求 |
onLoadMore() | 函数 | - | 触发下一页加载 |
retryRequest() | 函数 | - | 回到第一页并重新执行请求 |
shouldTriggerLoadMore(lastIndex, totalCount) | 函数 | - | 判断是否接近底部,常配合 LazyList |
observeRefreshState(backStackEntry, key) | 函数 | key = RefreshResultKey | 监听导航返回的刷新信号 |
BaseNetWorkListView
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
uiState | BaseNetWorkListUiState | - | 控制 Loading/Empty/Error/Success |
modifier | Modifier | Modifier | 调整尺寸或背景 |
padding | PaddingValues | PaddingValues() | 透传 Scaffold 的 innerPadding |
onRetry | () -> Unit | {} | 空态/错误态的重试回调 |
customLoading | @Composable (() -> Unit)? | null | 自定义加载骨架,默认 PageLoading() |
customError | @Composable (() -> Unit)? | null | 自定义错误占位,默认 EmptyNetwork() |
customEmpty | @Composable (() -> Unit)? | null | 自定义空数据占位,默认 EmptyData() |
content | @Composable () -> Unit | - | 成功态内容,通常放入 RefreshLayout |
RefreshLayout
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modifier | Modifier | Modifier | 控制整体尺寸/背景 |
isGrid | Boolean | false | 开启后使用瀑布流布局 |
listState | LazyListState? | null | 不传则内部 rememberLazyListState() |
gridState | LazyStaggeredGridState? | null | isGrid=true 时可透传外部状态 |
isRefreshing | Boolean | false | 同步头部刷新动画 |
loadMoreState | LoadMoreState | LoadMoreState.PullToLoad | 控制底部加载提示 |
scrollBehavior | TopAppBarScrollBehavior? | null | 需要折叠时传入顶部栏行为 |
onRefresh | () -> Unit | {} | 下拉刷新回调 |
onLoadMore | () -> Unit | {} | 上拉加载回调 |
shouldTriggerLoadMore | (lastIndex: Int, totalCount: Int) -> Boolean | { _, _ -> false } | 监听可见项并决定何时加载更多 |
gridContent | LazyStaggeredGridScope.() -> Unit | {} | isGrid=true 时的网格内容 |
content | LazyListScope.() -> Unit | {} | 列表内容,isGrid=false 下使用 |