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 即可快速实现。


相关推荐
恋猫de小郭4 小时前
Android Studio Panda 3 发布,CMP 导致的 Gemini 输入问题
android·ide·flutter·ios·android studio
jian110585 小时前
flutter 配置一次,第二次就失败,command not found: flutter,配置一次成功一次,第二次就失败
flutter
2501_920627616 小时前
Flutter 框架跨平台鸿蒙开发 - 数据库学习助手
数据库·学习·flutter·华为·harmonyos
2501_920627617 小时前
Flutter 框架跨平台鸿蒙开发 - 编程代码库应用
学习·算法·flutter·华为·harmonyos
芙莉莲教你写代码7 小时前
Flutter 框架跨平台鸿蒙开发 - 疫苗接种记录
flutter·华为·harmonyos
芙莉莲教你写代码8 小时前
Flutter 框架跨平台鸿蒙开发 - 步数追踪器
flutter·华为·harmonyos
2501_920627618 小时前
Flutter 框架跨平台鸿蒙开发 - 算法可视化应用
算法·flutter·华为·harmonyos
芙莉莲教你写代码8 小时前
Flutter 框架跨平台鸿蒙开发 - 家庭购物清单
flutter·华为·harmonyos
m0_6515939111 小时前
从工业5.0到实战:一个智能仓库管理系统的设计与Flutter优化
flutter