Flutter分页新数据未追加至ListView.builder尾部,替换旧数据
Flutter 分页加载:解决ListView.builder替换数据而非追加的问题
我有一个API,每次请求可获取10条数据条目,期望将这些新条目追加到ListView.builder的列表尾部,但目前ListView.builder会刷新列表、替换原有数据,无法实现加载更多功能。以下是问题根源、修复方案及完整代码:
问题根源
- 未维护全局数据列表:每次API请求返回的数据直接用于渲染,没有和原有数据合并
- FutureBuilder使用不当:
FutureBuilder的future参数直接绑定动态变化的getDiscussion(offSetCurrent),导致offset更新时重新发起请求并替换全部数据 - 加载状态未正确控制:缺少
isLoading状态的有效使用,可能引发重复请求
修复方案
1. 新增全局数据存储
在State类中添加一个列表,用于保存所有已加载的讨论消息:
List<DiscussionsMessages> allMessages = [];
2. 修改API请求逻辑
调整getDiscussion方法,将新获取的数据追加到全局列表,并通过setState更新UI:
Future<void> getDiscussion(int offSetCurrent) async { if (isLoading || !hasMore) return; setState(() => isLoading = true); SharedPreferences sp = await SharedPreferences.getInstance(); var parameters = { 'RangeFrom': offSetCurrent.toString(), 'AccessToken': sp.getString('AccessToken'), 'DiscussionID': '22848', }; var urlfetch = Uri( scheme: "https", host: baseUrl, path: discussionAPIPath, queryParameters: parameters, ); try { http.Response response = await http.get(urlfetch); var data = jsonDecode(response.body.toString()); var snapshot = PaginatedModel.fromJson(data); var newMessages = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages ?? []; // 将新数据追加到全局列表 setState(() { allMessages.addAll(newMessages); hasMore = newMessages.length == 10; isLoading = false; }); print('Discussion API HIT with offset $offSetCurrent, loaded ${newMessages.length} items'); } catch (e) { print(e.toString()); setState(() => isLoading = false); } }
3. 重构UI渲染逻辑
移除FutureBuilder,直接基于全局列表allMessages构建ListView,并处理加载状态:
@override Widget build(BuildContext context) { return Scaffold( body: allMessages.isEmpty ? const Center(child: CircularProgressIndicator()) : ListView.builder( controller: scrollController, itemCount: allMessages.length + (hasMore ? 1 : 0), itemBuilder: (context, idx) { if (idx < allMessages.length) { return Container( margin: const EdgeInsets.all(10), child: Html(data: allMessages[idx].message.toString()), ); } else { return Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: isLoading ? const CircularProgressIndicator() : const Text('No More Data'), ), ); } }, ), ); }
4. 完善初始化与滚动监听
调整initState,初始化加载第一页数据,并修正滚动监听逻辑:
@override void initState() { super.initState(); // 初始化加载第一页 getDiscussion(offSetCurrent); scrollController.addListener(() { // 滚动到接近底部时触发加载(提前10%触发,提升体验) if (scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.9 && !isLoading && hasMore) { setState(() { offSetCurrent += 10; getDiscussion(offSetCurrent); }); } }); }
修改后的完整代码
class PaginatedScreen extends StatefulWidget { const PaginatedScreen({Key? key}) : super(key: key); @override State<PaginatedScreen> createState() => _PaginatedScreenState(); } class _PaginatedScreenState extends State<PaginatedScreen> { final scrollController = ScrollController(); int offSetCurrent = 0; bool hasMore = true; bool isLoading = false; // 全局存储所有已加载的消息 List<DiscussionsMessages> allMessages = []; @override void initState() { super.initState(); // 初始化加载第一页数据 getDiscussion(offSetCurrent); scrollController.addListener(() { // 滚动到接近底部时触发加载(提前10%触发,提升体验) if (scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.9 && !isLoading && hasMore) { setState(() { offSetCurrent += 10; getDiscussion(offSetCurrent); }); } }); } @override void dispose() { scrollController.dispose(); super.dispose(); } Future<void> getDiscussion(int offSetCurrent) async { if (isLoading || !hasMore) return; setState(() => isLoading = true); SharedPreferences sp = await SharedPreferences.getInstance(); var parameters = { 'RangeFrom': offSetCurrent.toString(), 'AccessToken': sp.getString('AccessToken'), 'DiscussionID': '22848', }; var urlfetch = Uri( scheme: "https", host: baseUrl, path: discussionAPIPath, queryParameters: parameters, ); try { http.Response response = await http.get(urlfetch); var data = jsonDecode(response.body.toString()); var snapshot = PaginatedModel.fromJson(data); var newMessages = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages ?? []; setState(() { allMessages.addAll(newMessages); // 如果返回数据不足10条,说明没有更多数据了 hasMore = newMessages.length == 10; isLoading = false; }); print('Discussion API HIT with offset $offSetCurrent, loaded ${newMessages.length} items'); } catch (e) { print(e.toString()); setState(() => isLoading = false); } } @override Widget build(BuildContext context) { return Scaffold( body: allMessages.isEmpty ? const Center(child: CircularProgressIndicator()) : ListView.builder( controller: scrollController, itemCount: allMessages.length + (hasMore ? 1 : 0), itemBuilder: (context, idx) { if (idx < allMessages.length) { return Container( margin: const EdgeInsets.all(10), child: Html(data: allMessages[idx].message.toString()), ); } else { return Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: isLoading ? const CircularProgressIndicator() : const Text('No More Data'), ), ); } }, ), ); } }
关键说明
- 全局数据列表:
allMessages负责存储所有已加载的条目,每次新数据都追加到列表末尾 - 加载状态控制:
isLoading防止同一时间发起多个请求,hasMore判断是否还有更多数据可加载 - 滚动监听优化:提前10%触发加载,避免用户等待,提升交互体验
- 移除FutureBuilder:直接基于全局状态渲染UI,避免每次请求替换全部数据
内容的提问来源于stack exchange,提问作者Tahir
相关产品推荐
相关产品推荐

