如何实现向上滚动时的无限加载?IM场景下infinite_scroll_pagination适配
解决方案:实现底部新消息+上滚加载历史的IM分页
一、基于现有infinite_scroll_pagination库调整(无需换库)
你当前使用的库完全支持该需求,只需调整数据排序、分页逻辑和列表行为:
数据排序与分页逻辑
- 消息数据按时间升序存储/返回,旧消息在前,新消息在后。
- 以消息ID或时间戳作为分页标识,加载历史消息时,请求比当前列表最旧消息更早的数据。
- 实现
PagingSource时处理**向前加载(prepend)**逻辑:当用户滚动到列表顶部,触发加载更早的消息并插入列表开头。
核心代码示例:
class MessagePagingSource extends PagingSource<int, Message> { @override Future<PagingSourceSnapshot<int, Message>> load(LoadParams<int> params) async { try { // params.key为当前最旧消息标识,首次加载传null取最新20条 final messages = await api.getHistoryMessages( beforeId: params.key, limit: 20, ); final prevKey = messages.isEmpty ? null : messages.first.id; return PagingSourceSnapshot.prepend( data: messages, prevKey: prevKey, nextKey: null, // 新消息通过WebSocket推送,无需加载后续页 ); } catch (e) { return PagingSourceSnapshot.error(error: e); } } }列表配置与自动滚动
- 使用默认顺序的
PagedListView,无需设置reverse。 - 页面初始化后自动滚动到列表底部,确保用户看到最新消息:
final _pagingController = PagingController<int, Message>(initialPageKey: null); final _scrollController = ScrollController(); @override void initState() { super.initState(); _pagingController.addListener(() { if (_pagingController.itemList?.isNotEmpty ?? false) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); } }); } @override Widget build(BuildContext context) { return PagedListView<int, Message>( controller: _scrollController, pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate<Message>( itemBuilder: (context, message, index) => MessageItem(message: message), ), ); }- 使用默认顺序的
新消息推送处理
收到WebSocket推送的新消息时,直接追加到列表末尾并滚动到底部:void onNewMessageReceived(Message newMessage) { final currentList = _pagingController.itemList ?? []; _pagingController.itemList = [...currentList, newMessage]; WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); }
二、自行实现(无需第三方库)
如果需要完全自定义逻辑,用原生ListView.builder+ScrollController即可:
核心变量定义
final _scrollController = ScrollController(); List<Message> _messages = []; bool _isLoading = false; int? _oldestMessageId; // 记录当前列表最旧消息标识滚动监听与加载触发
在initState中添加监听,滚动到顶部附近时触发历史消息加载:@override void initState() { super.initState(); _scrollController.addListener(() { if (_scrollController.position.pixels <= 100 && !_isLoading && _oldestMessageId != null) { _loadHistoryMessages(); } }); _loadLatestMessages(); // 首次加载最新消息 }加载逻辑实现
Future<void> _loadLatestMessages() async { _isLoading = true; final messages = await api.getLatestMessages(limit: 20); _messages = messages; _oldestMessageId = messages.isNotEmpty ? messages.first.id : null; _isLoading = false; setState(() {}); // 滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); } Future<void> _loadHistoryMessages() async { _isLoading = true; setState(() {}); final historyMessages = await api.getHistoryMessages( beforeId: _oldestMessageId, limit: 20, ); if (historyMessages.isNotEmpty) { _messages.insertAll(0, historyMessages); _oldestMessageId = historyMessages.first.id; } else { _oldestMessageId = null; // 无更多历史消息 } _isLoading = false; setState(() {}); }列表构建
@override Widget build(BuildContext context) { return ListView.builder( controller: _scrollController, itemCount: _messages.length + (_isLoading ? 1 : 0), itemBuilder: (context, index) { if (index == 0 && _isLoading) { return const Center(child: CircularProgressIndicator()); } final messageIndex = _isLoading ? index - 1 : index; return MessageItem(message: _messages[messageIndex]); }, ); }
内容的提问来源于stack exchange,提问作者Bertuz
相关产品推荐
相关产品推荐

