聊天列表难在身份、滚动与窗口变化
能显示十条气泡不算完成;插入、分页、键盘、大屏与读屏器同时出现时,列表才开始像产品。
消息列表在顶部插入一页历史记录,哪种 key 最安全?
- 当前索引
- 消息文本
- 稳定且唯一的消息 ID
- 每次组合生成的随机 UUID
Lazy 只创建附近内容,不替你定义身份
LazyColumn 只组合可见区域附近的 item,但默认身份仍靠位置。顶部插入历史后,旧第 0 项变成第 20 项;若没有业务 key,remember 状态、动画与图片请求可能跟错消息。索引不是身份,文本也可能重复,随机 key 则让每次都像全新内容。
LazyColumn(
state = listState,
reverseLayout = true
) {
items(
items = messages,
key = { it.id.value },
contentType = { it.kind }
) { message ->
MessageRow(message)
}
}
contentType 告诉 Lazy 容器哪些项目结构可复用。日期分隔、文字消息、图片消息的布局差异很大,分类能避免拿错误结构反复测量。key 保身份,content type 保复用类别,职责不同。
新消息不等于永远自动滚底
用户正在底部看最新消息时,新消息可自动保持底部;用户已经向上翻历史时,强行滚动会抢走阅读位置。需要从 LazyListState 派生「是否接近底部」,只在符合条件时滚动,否则显示「有新消息」入口。
键盘弹出、输入框高度变化、系统栏 inset 都会改变可用空间。使用 imePadding、安全区域 inset 与稳定滚动政策,而不是用固定屏幕高度算偏移。图片消息还应先有宽高比占位,避免加载后列表整体跳动。
分页的真相仍该落到数据层
Paging 3 可以把 Room 与网络 RemoteMediator 接起来:UI 观察本地分页,Mediator 在边界取远端页并事务写库。这样加载、刷新、重试有统一状态,旋转后不必从头请求。聊天记录若有严格顺序,要用服务端游标或序号分页,不能只靠客户端时间戳。
大屏与折叠屏上,导航列表和对话详情可以同时出现。不要把「手机竖屏」写死成业务状态;根据窗口尺寸和可用空间选一栏、双栏或支撑布局。Android 17 时代,窗口可变是常态,不再是平板特例。
可访问性是信息结构测试
读屏器需要知道一条气泡是谁、何时、什么状态,而不是把头像、姓名、正文、双勾拆成无意义的四个焦点。用语义合并与清晰 contentDescription;触控目标留足尺寸;颜色不能是发送失败的唯一信号;动态字号下不要锁死高度。
做一个可观察身份的最小列表。顶部插入新记录后,已展开状态应继续跟着原消息 ID:
插入前:m2 expanded=true,位置 1 插入后:m2 expanded=true,位置 2 身份跟消息走,不跟索引走
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages, key = { it.id }) { message ->
var expanded by rememberSaveable(message.id) {
mutableStateOf(false)
}
MessageRow(
message = message,
expanded = expanded,
onToggle = { expanded = !expanded }
)
}
}
}在 Android Studio 预览或模拟器里放三条消息,展开第二条,再往列表头插一条。先删掉 key 对比,然后恢复。用 Layout Inspector 查看重组与节点身份。
公开评论里曾有人反馈大量消息同时到达时顺序和滚动体验不佳。这里不能据此推断具体实现,但它提醒你:聊天质量横跨服务端排序、离线合并、本地查询、稳定 key、分页与滚动政策,绝不是单个列表组件的题。
「用了 LazyColumn 就不需要关心性能。」Lazy 只控制组合范围;错误 key、大对象复制、同步图片解码和高频顶层状态仍会卡。
先保证身份稳定与数据分页,再缩小状态读取、延迟图片工作,并用 release 基准验证。可访问性与自适应从信息结构开始,不是最后补丁。
答案是第 3 项。消息 ID 在插入、重排和分页后保持稳定且唯一。索引会移动,文本会重复,随机 UUID 每次重组都变;后三者都会让运行时误认业务身份。
这一章的一句话
一条消息必须同时拥有稳定的数据顺序、稳定的 UI 身份和适应变化窗口的布局。
下一章把 UI 之外的全部接起来:如果 ViewModel 只是把 Repository 的函数一一转发,所谓 MVVM 并没有产生架构,只增加了一个文件。