Flutter 分页列表页面实现指南


Flutter 分页列表页面实现指南

在移动端开发中,分页列表是最常见的场景之一。本文将分享一套经过实践检验的通用分页列表实现模式

一、架构分层

清晰的架构分层是代码可维护性的基础。我们采用三层架构:

复制代码
UI 层(ui 包)
  XxxView                  ← 纯 UI,watch provider,触发 init/loadMore

状态层(ckzl_core 包)
  XxxController            ← Notifier,管理分页状态,调用 Service
  XxxState                 ← 不可变状态对象

数据层(ckzl_core 包)
  XxxService               ← HTTP 封装,分页查询接口

职责划分:

· UI 层:只负责展示和用户交互,不包含业务逻辑

· 状态层:管理页面状态,处理分页逻辑,调用数据层接口

· 数据层:封装网络请求,与后端 API 交互

二、State 设计

状态对象应该是不可变的,这样可以保证状态变化的可预测性:

dart 复制代码
class XxxState {
  final bool isLoading;        // 首次加载中
  final bool isLoadingMore;    // 加载更多中
  final List<XxxData> items;
  final int pageNum;           // 当前页码
  final int pageSize;          // 每页数量
  final int total;             // 总记录数
  final bool hasMore;          // 是否还有更多
  final String? error;         // 错误信息

  XxxState({
    this.isLoading = false,
    this.isLoadingMore = false,
    this.items = const [],
    this.pageNum = 1,
    this.pageSize = 20,
    this.total = 0,
    this.hasMore = true,
    this.error,
  });

  XxxState copyWith({...}) {...}
}

💡 提示:使用 copyWith 方法可以方便地创建新状态实例,这是不可变对象的常见模式。

三、Controller 核心实现

Controller 负责状态管理和业务逻辑,核心是 init() 和 loadMore() 两个方法:

方法 说明

init() 重置状态,从第1页重新加载(下拉刷新也调这个)

loadMore() 加载下一页,hasMore + isLoadingMore 双重防重

dart 复制代码
class XxxController extends Notifier<XxxState> {
  @override
  XxxState build() => XxxState();

  Future<void> init() async {
    state = state.copyWith(
      isLoading: true,
      pageNum: 1,
      items: [],
      hasMore: true,
    );
    await _fetch(pageNum: 1, refresh: true);
  }

  Future<void> loadMore() async {
    if (!state.hasMore || state.isLoadingMore) return;
    state = state.copyWith(isLoadingMore: true);
    await _fetch(pageNum: state.pageNum + 1, refresh: false);
  }

  Future<void> _fetch({required int pageNum, required bool refresh}) async {
    final resp = await XxxService.instance.listXxx(
      pageNum: pageNum,
      pageSize: state.pageSize,
    );

    if (resp.isSuccess && resp.data != null) {
      final newItems = resp.data!.items;
      state = state.copyWith(
        isLoading: false,
        isLoadingMore: false,
        items: refresh ? newItems : [...state.items, ...newItems],
        pageNum: pageNum,
        total: resp.data!.total,
        hasMore: newItems.length >= state.pageSize,
      );
    } else {
      state = state.copyWith(
        isLoading: false,
        isLoadingMore: false,
        error: resp.msg,
      );
    }
  }
}

关键点说明:

· 双重防重:hasMore 确保还有数据时才能继续加载,isLoadingMore 防止重复请求

· 分页判断:返回数据量 >= pageSize 则继续分页,否则认为已无更多数据

· 错误处理:将错误信息保存到 state,UI 层可以据此展示错误提示

四、Provider 注册

使用 Riverpod 的 NotifierProvider 注册 Controller:

dart 复制代码
final xxxControllerProvider = NotifierProvider<XxxController, XxxState>(
  XxxController.new,
);

五、UI 页面实现

5.1 基础结构

dart 复制代码
class XxxView extends ConsumerStatefulWidget {
  const XxxView({super.key});

  @override
  ConsumerState<XxxView> createState() => _XxxViewState();
}

class _XxxViewState extends ConsumerState<XxxView> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      ref.read(xxxControllerProvider.notifier).init();
    });
  }

  // 滚动监听:距底部 200px 时触发加载更多
  void _onScroll() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      ref.read(xxxControllerProvider.notifier).loadMore();
    }
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(xxxControllerProvider);
    return Scaffold(
      body: RefreshIndicator(
        onRefresh: () async => ref.read(xxxControllerProvider.notifier).init(),
        child: _buildBody(state),
      ),
    );
  }
}

5.2 列表主体(CustomScrollView + Sliver)

推荐使用 CustomScrollView + Sliver 组合,这样可以灵活控制列表的各个部分:

dart 复制代码
Widget _buildBody(XxxState state) {
  if (state.isLoading && state.items.isEmpty) {
    return const Center(child: CircularProgressIndicator());
  }

  if (state.error != null && state.items.isEmpty) {
    return Center(child: Text('加载失败: ${state.error}'));
  }

  return CustomScrollView(
    controller: _scrollController,
    slivers: [
      // 主列表区域 - 根据布局选择 SliverGrid 或 SliverList
      SliverGrid(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 4,
          mainAxisSpacing: 4,
        ),
        delegate: SliverChildBuilderDelegate(
          (context, index) => _buildItem(state.items[index]),
          childCount: state.items.length,
        ),
      ),

      // 加载更多 Loading
      if (state.isLoadingMore)
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Center(child: CircularProgressIndicator()),
          ),
        ),

      // 没有更多提示
      if (!state.hasMore && state.items.isNotEmpty)
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Center(child: Text('没有更多了')),
          ),
        ),
    ],
  );
}

六、完整流程图
Service Controller UI User Service Controller UI User alt [有更多数据且未加载中] [无更多数据或加载中] 进入页面/下拉刷新 init() 重置状态 listXxx(pageNum=1) 返回数据 更新 state 渲染列表 滚动到底部 loadMore() listXxx(pageNum+1) 返回数据 追加数据 渲染新增项 忽略请求

七、复用新页面的步骤

当你需要实现一个新的分页列表页面时,只需按照以下步骤操作:

步骤 操作 说明

1 新建 XxxState 包含 isLoading / isLoadingMore / items / pageNum / hasMore / error 等字段

2 新建 XxxController 实现 init() / loadMore() / _fetch() 方法

3 新建 Service 方法 返回 ApiResponse,响应体包含 items + total

4 新建 UI 页面 添加 ScrollController 监听滚动,RefreshIndicator 下拉刷新,CustomScrollView 渲染列表

5 注册 Provider 使用 NotifierProvider 注册 Controller

八、常见问题与优化建议

8.1 空状态处理

当列表为空时,建议显示空状态占位图:

dart 复制代码
if (state.items.isEmpty && !state.isLoading) {
  return const Center(child: EmptyStateWidget(message: '暂无数据'));
}

8.2 错误重试

在错误状态中提供重试按钮:

dart 复制代码
if (state.error != null && state.items.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('加载失败: ${state.error}'),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: () => ref.read(xxxControllerProvider.notifier).init(),
          child: const Text('重试'),
        ),
      ],
    ),
  );
}

8.3 滚动优化

· 设置 cacheExtent 可以提前缓存列表项,提升滚动流畅度

· 对于图片列表,建议配合缓存框架(如 cached_network_image)使用

结语

这套分页列表模式经过多个项目的验证,具备良好的复用性和可维护性。核心思想是状态与 UI 分离、分页逻辑统一封装,新页面只需替换数据模型和 Service 即可快速实现。


相关推荐
jiejiejiejie_2 小时前
Flutter for OpenHarmony 应用更新检测与萌系搜索功能实战小记✨
flutter·华为·harmonyos
IntMainJhy3 小时前
Flutter 三方库 Firebase Messaging 鸿蒙化适配与实战指南(权限检查+设备Token获取全覆盖)
flutter·华为·harmonyos
liulian09164 小时前
Flutter 依赖注入与设备信息库:get_it 与 device_info_plus 的 OpenHarmony 适配指南总结
flutter·华为·学习方法·harmonyos
里欧跑得慢5 小时前
微交互设计模式:提升用户体验的细节之美
前端·css·flutter·web
stringwu5 小时前
Flutter GetX 核心坑及架构选型与可替换性方案
前端·flutter
IntMainJhy5 小时前
【flutter for open harmony】第三方库Flutter 国际化多语言的鸿蒙化适配与实战指南
数据库·flutter·华为·sqlite·harmonyos
liulian09165 小时前
【Flutter for OpenHarmony 】地图功能适配与位置显示实现指南
flutter·华为·学习方法·harmonyos
IntMainJhy5 小时前
【flutter for open harmony】Flutter SQLite 本地数据库的鸿蒙化适配与实战指南
数据库·flutter·sqlite
IntMainJhy6 小时前
【flutter for open harmony】第三方库「Flutter 聊天组件鸿蒙化适配与实战:从零搭建鸿蒙跨平台聊天页面」
flutter·华为·harmonyos
jiejiejiejie_6 小时前
Flutter for OpenHarmony 地图功能萌系实战指南:给 App 加上超萌 “小地图”✨
flutter·华为·harmonyos