You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何实现带分组元素的无限滚动分页?

实现Flutter带日期分组的无限滚动分页

当然可以实现!结合分组列表和无限滚动的核心思路是维护一个累计的全局数据源,让分组列表基于所有已加载的数据渲染,同时监听滚动触发下一页加载。下面是具体的实现方案,完全适配你现有的代码结构:

方案一:自定义滚动监听 + GroupedListView(推荐)

这个方案不需要额外依赖分页包(也可复用Pagewise的分页逻辑),直接用ScrollController监听滚动,配合GroupedListView实现需求,逻辑更清晰:

import 'package:flutter/material.dart';
import 'package:grouped_list/grouped_list.dart';
import 'package:intl/intl.dart'; // 用于日期格式化

// 替换成你的实体类
class TransactionModel {
  final String title;
  final DateTime date;
  // 其他属性...

  TransactionModel({required this.title, required this.date});
}

class GroupedPagedTransactions extends StatefulWidget {
  const GroupedPagedTransactions({super.key});

  @override
  State<GroupedPagedTransactions> createState() => _GroupedPagedTransactionsState();
}

class _GroupedPagedTransactionsState extends State<GroupedPagedTransactions> {
  static const int PAGE_SIZE = 10;
  List<TransactionModel> _allTransactions = [];
  int _currentPage = 0;
  bool _isLoading = false;
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    // 初始化加载第一页
    _loadNextPage();
    // 添加滚动监听
    _scrollController.addListener(_onScroll);
  }

  Future<void> _loadNextPage() async {
    if (_isLoading) return; // 防止重复加载
    setState(() => _isLoading = true);

    try {
      // 调用你的后端接口加载数据,替换成你的BackendService.getPosts
      final List<TransactionModel> newPageData = await _mockFetchTransactions();
      
      setState(() {
        _allTransactions.addAll(newPageData);
        _currentPage++;
      });
    } catch (e) {
      // 这里处理加载错误,比如显示Toast或错误提示
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('加载失败: ${e.toString()}')),
      );
    } finally {
      setState(() => _isLoading = false);
    }
  }

  void _onScroll() {
    // 当滚动到距离底部200px时,触发下一页加载
    if (_scrollController.position.pixels >=
            _scrollController.position.maxScrollExtent - 200 &&
        !_isLoading) {
      _loadNextPage();
    }
  }

  // 模拟后端接口,实际项目中替换成真实请求
  Future<List<TransactionModel>> _mockFetchTransactions() async {
    await Future.delayed(const Duration(seconds: 1));
    final today = DateTime.now();
    return List.generate(PAGE_SIZE, (index) {
      final date = today.subtract(Duration(days: _currentPage * 2 + index % 3));
      return TransactionModel(
        title: '交易 ${_currentPage * PAGE_SIZE + index + 1}',
        date: date,
      );
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey[200],
      child: GroupedListView<TransactionModel, String>(
        controller: _scrollController,
        separator: const Divider(height: 0, color: Colors.grey),
        sort: false,
        elements: _allTransactions,
        // 构建分组头
        groupSeparatorBuilder: (dateStr) => Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          color: Colors.grey[300],
          child: Text(
            dateStr,
            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
        ),
        // 构建列表项,替换成你原来的_itemBuilder逻辑
        itemBuilder: (context, transaction) => ListTile(
          title: Text(transaction.title),
          subtitle: Text(DateFormat('HH:mm').format(transaction.date)),
        ),
        // 按日期分组的逻辑,这里把日期格式化为yyyy-MM-dd作为分组键
        groupBy: (transaction) => DateFormat('yyyy-MM-dd').format(transaction.date),
        // 添加加载更多指示器
        footer: _isLoading
            ? const Padding(
                padding: EdgeInsets.all(16.0),
                child: Center(child: CircularProgressIndicator()),
              )
            : null,
      ),
    );
  }
}

核心逻辑说明

  1. 全局数据源:_allTransactions存储所有已加载的分页数据,每次加载新页后追加到列表中。
  2. 滚动触发加载:通过ScrollController监听滚动位置,当接近底部时自动触发下一页请求。
  3. 分组渲染:GroupedListView基于全局数据源自动处理分组,确保分组头和列表项的正确展示,新加载的数据会自动融入现有分组。
  4. 状态控制:_isLoading标记防止重复请求,同时在列表底部显示加载指示器。

注意事项

  • 分组键一致性:确保groupBy函数返回的分组键格式统一(比如日期格式化要一致),避免同一个日期被拆分成多个分组。
  • 数据去重:如果后端可能返回重复数据,建议在添加到_allTransactions前做去重处理。
  • 错误重试:可以给加载失败的状态添加重试按钮,提升用户体验。

关于Pagewise包的适配

如果你想继续使用Pagewise包,其实可以复用它的PagewiseLoadController来管理分页逻辑,替换上面的_loadNextPage和_onScroll逻辑,但本质还是需要维护全局数据源给GroupedListView,所以上面的自定义方案已经足够简洁,不需要额外依赖。

内容的提问来源于stack exchange,提问作者Ramses Kouam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 10:02:42