如何使用ListView.custom实现带日期分隔符的聊天列表
使用ListView.custom实现带半小时间隔日期分隔符的聊天列表
问题背景
之前用ListView.separated实现聊天列表的日期分隔,但该组件会触发消息组件不必要的重建,存在性能问题。改用ListView.custom可以更好地控制子项渲染,提升性能,同时需要实现每半小时显示一次日期分隔符的需求。
解决方案思路
核心是先将原始消息列表与需要插入的日期分隔符合并成一个混合数据源,然后通过ListView.custom直接渲染这个混合列表,避免separated模式下的额外重建逻辑。
步骤1:构建混合数据源
先定义枚举和数据类区分消息项与分隔符项,再写方法遍历消息列表,判断相邻消息的时间差是否超过半小时,按需插入分隔符:
enum ChatItemType { message, dateSeparator } class ChatItem { final ChatItemType type; final Message? message; // 你的Message实体类 final DateTime? date; ChatItem.message(this.message) : type = ChatItemType.message, date = null; ChatItem.dateSeparator(this.date) : type = ChatItemType.dateSeparator, message = null; } List<ChatItem> buildMixedChatList(List<Message> messages) { if (messages.isEmpty) return []; final List<ChatItem> mixedList = []; // 先添加第一条消息的日期分隔符(列表倒序,第一条为最新消息) mixedList.add(ChatItem.dateSeparator(messages.first.createdAt.toLocal())); mixedList.add(ChatItem.message(messages.first)); for (int i = 1; i < messages.length; i++) { final currentMsg = messages[i]; final prevMsg = messages[i - 1]; // 倒序下,prevMsg是比currentMsg更新的消息 // 计算当前消息与上一条消息的时间间隔是否超过30分钟 final currentTime = currentMsg.createdAt.toLocal(); final prevTime = prevMsg.createdAt.toLocal(); final difference = prevTime.difference(currentTime).inMinutes; if (difference >= 30) { // 超过半小时,插入日期分隔符 mixedList.add(ChatItem.dateSeparator(currentTime)); } mixedList.add(ChatItem.message(currentMsg)); } return mixedList; }
步骤2:实现ListView.custom渲染
基于混合数据源渲染列表,保留原有的滚动控制器、倒序、自动滚动标签等逻辑:
// 先构建混合列表 final mixedChatItems = buildMixedChatList(messages); ListView.custom( controller: messageScrollController, reverse: true, childrenDelegate: SliverChildBuilderDelegate( (context, index) { final item = mixedChatItems[index]; if (item.type == ChatItemType.dateSeparator) { return DateLabel(date: item.date!); } else { final message = item.message!; // 保留原有的MessageBubble交互与布局逻辑 return AutoScrollTag( key: ValueKey(message.id), controller: messageScrollController, index: index, child: Padding( padding: index == 0 ? const EdgeInsets.only(bottom: 20) : EdgeInsets.zero, child: SwipeTo( onRightSwipe: widget.currentUser.id == message.user.id ? null : () => onSwipedMessage(message), onLeftSwipe: widget.currentUser.id != message.user.id ? null : () => onSwipedMessage(message), child: MessageBubble( key: ValueKey(message.id), message: message, userProfile: widget.currentUser, nextMessage: _getNextMessage(index, mixedChatItems), previousMessage: _getPreviousMessage(index, mixedChatItems), hasEmoji: hasOnlyEmojis(message.messageText ?? ''), isMe: widget.currentUser.id == message.user.id, toMessage: scrollToMessage, ), ), ), ); } }, childCount: mixedChatItems.length, // 实现组件复用逻辑,提升性能 findChildIndexCallback: (Key key) { if (key is ValueKey<String>) { // 仅处理消息项的复用,分隔符无需复用 final messageId = key.value; final index = mixedChatItems.indexWhere( (item) => item.type == ChatItemType.message && item.message?.id == messageId, ); return index != -1 ? index : null; } return null; }, ), )
步骤3:适配消息上下项获取方法
原代码中的getNext和getPrevious方法需要适配混合列表,跳过日期分隔符项:
Message? _getNextMessage(int index, List<ChatItem> mixedItems) { for (int i = index + 1; i < mixedItems.length; i++) { final item = mixedItems[i]; if (item.type == ChatItemType.message) { return item.message; } } return null; } Message? _getPreviousMessage(int index, List<ChatItem> mixedItems) { for (int i = index - 1; i >= 0; i--) { final item = mixedItems[i]; if (item.type == ChatItemType.message) { return item.message; } } return null; }
关键优化点
- 混合数据源提前计算分隔符位置,避免渲染时重复判断,减少性能消耗
ListView.custom配合findChildIndexCallback,确保消息组件按需复用,避免不必要的重建- 严格控制时间差判断逻辑,确保每半小时显示一次日期分隔符
内容的提问来源于stack exchange,提问作者Rlee128
相关产品推荐
相关产品推荐

