Flutter 吸顶分组列表实战:语义桶分组 + 点击头平滑滚动
Flutter 吸顶分组列表实战语义桶分组 点击头平滑滚动作者FungLeo 适用Flutter 3.x场景时间类列表要按语义分组 组头吸顶 点组头跳到那一组看着简单细节全是坑。前言时间类的列表各位看官肯定都做过——待办、日程、消息、订单凡是带时间的产品迟早会提一句“能不能按时间分个组”我接到这个需求的时候心想这有啥难的。按日期排个序遍历一遍日期变了就插个头半小时的事儿。结果第一版跑出来界面上出现了两个本周。我盯着看了半天才反应过来我是按YYYY-MM-DD切的同一个自然周里有周三和周五两条数据那自然就切出来两组标题一算又都是本周可不就重复了么。这个 bug 一出整个列表看着就特别不专业。后来我把分组逻辑整个推翻重写改成语义桶才算真正对了。这篇就把这套东西完整记一下怎么分桶、吸顶头怎么做才是真吸顶、点组头怎么平滑滚过去。一、别按天切要按「语义桶」切我原来错在哪先看我第一版的错误思路// ❌ 按日期字符串分组同一周会切出好几组finalkey${t.year}-${t.month}-${t.day};问题在于用户脑子里的分组和日历上的分组压根不是一回事。用户想看到的是今天有几件事、明天有几件事、这周剩下的有几件事是个语义上的划分。而按天切切出来的是物理日期一周能给你切出七组标题还全叫本周。正确姿势先定桶再往桶里扔思路反过来先把桶定死每个桶全局只有一个头然后把每条数据算一下该扔进哪个桶。比如一个待办 / 已完成两个 Tab 的列表桶可以这么定从上到下待办未来方向 已逾期 / 今天 / 明天 / 后天 / 本周 / 下周 / 更晚 已完成过去方向镜像过来 今天 / 昨天 / 本周 / 上周 / 更早这么一来本周永远只有一个头多少条数据都往它里面塞重复头的问题从根上没了。三个函数搞定具体实现我拆成三个纯函数好写好测/// 1. 算这条数据属于哪个桶只判断哪一类不关心具体日期String_bucketKey(DateTimetime,DateTimenow){finaltodayDateTime(now.year,now.month,now.day);finaltargetDateTime(time.year,time.month,time.day);finaldifftarget.difference(today).inDays;if(diff0)returnoverdue;// 比今天早逾期if(diff0)returntoday;if(diff1)returntomorrow;if(diff2)returnday_after;// weekday: 周一1 ... 周日7所以本周还剩 (7 - weekday) 天if(diff7-today.weekday)returnthis_week;if(diff14-today.weekday)returnnext_week;returnlater;}/// 2. 给桶排序权重数字小的排前面const_bucketOrderString,int{overdue:0,today:1,tomorrow:2,day_after:3,this_week:4,next_week:5,later:6,};/// 3. 桶的中文标题const_bucketTitleString,String{overdue:已逾期,today:今天,tomorrow:明天,day_after:后天,this_week:本周,next_week:下周,later:更晚,};组装的时候就很轻松了ListGroupModel_buildGroups(ListItemModelitems,DateTimenow){finalmapString,ListItemModel{};for(finalitinitems){map.putIfAbsent(_bucketKey(it.time,now),()[]).add(it);}finalgroupsmap.entries// 空桶自然就不在 map 里不用额外过滤.map((e)GroupModel(key:e.key,title:_bucketTitle[e.key]!,// 桶内按时间升序items:e.value..sort((a,b)a.time.compareTo(b.time)),)).toList();// 桶之间按权重排groups.sort((a,b)_bucketOrder[a.key]!.compareTo(_bucketOrder[b.key]!));returngroups;}几个小讲究空桶不渲染。没数据的桶压根不进 map界面上也就不会出现一个光秃秃的下周头下面啥也没有。桶内升序桶间按权重。这两个排序是独立的别混在一起写。“已完成” Tab 就是把方向镜像一下把diff 0那一侧展开成昨天 / 本周 / 上周 / 更早逻辑完全对称改个映射表的事儿。那语义桶到底有哪些、分别对应什么判定我把它摊成一张表比看代码直白桶 key中文标题判定逻辑与今天的 diff 天数overdue已逾期diff 0today今天diff 0tomorrow明天diff 1day_after后天diff 2this_week本周diff 7 - weekdaynext_week下周diff 14 - weekdaylater更晚其余情况上面左边就是我第一版的惨状——按天切同一个自然周硬生生切出两个本周右边是语义桶一类一个头重复头从根上消失。二、吸顶头怎么做才是真吸顶这块我得多说两句因为我看到过不少写法其实是假吸顶——头会跟着列表一起滚走。做法 A真吸顶SliverMainAxisGroupSliverPersistentHeader想让组头在滚到顶部时钉住头本身就得是个Sliver而且要pinned: true。同时还得让它只在自己这一组的范围内钉住下一组上来的时候要被顶走——这就是SliverMainAxisGroupFlutter 3.16 之后可用干的活儿CustomScrollView(controller:_scrollController,slivers:[for(finalgroupin_groups)SliverMainAxisGroup(slivers:[SliverPersistentHeader(pinned:true,// 关键钉住delegate:_GroupHeaderDelegate(title:group.title,onTap:()_scrollToGroup(group.key),),),SliverList.builder(itemCount:group.items.length,itemBuilder:(ctx,i)_buildCard(group.items[i]),),],),],)_GroupHeaderDelegate要实现SliverPersistentHeaderDelegate最小实现长这样class_GroupHeaderDelegateextendsSliverPersistentHeaderDelegate{_GroupHeaderDelegate({requiredthis.title,requiredthis.onTap});finalStringtitle;finalVoidCallbackonTap;overridedoublegetminExtent36;overridedoublegetmaxExtent36;// 不需要伸缩两个值给一样即可overrideWidgetbuild(BuildContextcontext,double shrinkOffset,bool overlapsContent){returnGestureDetector(onTap:onTap,child:Container(// 必须给不透明背景否则吸顶时下面的内容会透上来糊成一片color:Colors.white,alignment:Alignment.centerLeft,padding:constEdgeInsets.symmetric(horizontal:16),child:Row(children:[Text(title,style:constTextStyle(fontWeight:FontWeight.w600)),constSpacer(),constIcon(Icons.keyboard_arrow_up,size:18),],),),);}overrideboolshouldRebuild(_GroupHeaderDelegate old)old.title!title;}做法 B不强求吸顶Column塞进SliverList说实话很多时候产品要的其实只是有个分组头 能点并不是真的非要钉在顶上。那就没必要上 Sliver 那套了直接把头和卡片拼成一个Column丢进SliverList就完事SliverList(delegate:SliverChildBuilderDelegate((ctx,i){finalgroup_groups[i];// 给每组第一张卡挂一个持久 key下面滚动要用finalfirstCardKey_groupKeys.putIfAbsent(group.key,GlobalKey.new);returnColumn(crossAxisAlignment:CrossAxisAlignment.stretch,children:[GroupStickyHeader(title:group.title,onTap:()_scrollToGroup(group.key),),KeyedSubtree(key:firstCardKey,child:_buildCards(group.items),),],);},childCount:_groups.length,),)但要说清楚这种写法组头是会跟着滚走的不会钉在顶部。我一开始就是这么写的还美滋滋地跟产品说吸顶做好了结果人家一滚动就发现头没了挺尴尬的哈。所以要真吸顶老老实实走做法 A。这里也有个取舍做法 B 把一整组塞进一个 item 里组内卡片是一次性全建出来的失去了列表懒加载的优势。组内数据量大的时候比如几百条还是得拆开或者走做法 A。两种做法到底差在哪一张表说清对比维度做法 ASliver 真吸顶做法 BColumn 假吸顶组头是否钉在顶部✅pinned: true滚到顶钉住❌ 跟着列表一起滚走组内是否懒加载✅SliverList.builder按需建❌ 整组一次性全建适用场景产品明确要求吸顶只要有头 能点即可三、点组头平滑滚动到那一组这个功能体验加分特别明显列表拉得老长点一下下周直接就跳过去了。实现靠Scrollable.ensureVisible前提是你得拿到目标位置的BuildContext——这就是上面那个GlobalKey的用途。// 按 group.key 复用 GlobalKey注意别在 build 里 newfinalMapString,GlobalKey_groupKeys{};void_scrollToGroup(StringgroupKey){finalctx_groupKeys[groupKey]?.currentContext;if(ctxnull)return;// 还没挂载出来就别滚了Scrollable.ensureVisible(ctx,alignment:0.08,// 停在距顶 8% 的位置别死贴边duration:constDuration(milliseconds:300),curve:Curves.easeInOut,);}两个参数值得说一下alignment: 0.080是贴着顶部1是贴着底部。我试了下正好贴顶的话组头会跟 AppBar 挤在一起看着很局促留 8% 的余量刚刚好。这个值各位看官按自己的顶部布局微调。durationcurve不给 duration 就是瞬间跳过去很生硬。300ms easeInOut是我觉得比较舒服的组合再长就显得拖沓了。关于GlobalKey的两个注意事项这俩我都栽过千万别在build方法里GlobalKey()新建。每次 build 都换一个新 key不仅currentContext抓不稳还会让整棵子树被重建滚动直接乱跳。用putIfAbsent按业务 key 缓存住这是关键。key 用完要清理。数据一刷新某些桶可能就空了但它的 key 还赖在 map 里。虽然一个 key 不占多少内存但架不住反复刷新还是顺手擦一下干净// 重建分组后把已经不存在的桶对应的 key 清掉_groupKeys.removeWhere((k,v)!_groups.any((g)g.keyk));平滑滚动那三个参数落张表方便各位照抄参数取值理由alignment0.080贴顶挤 AppBar留点余量刚合适duration300ms瞬间跳太生硬再长显拖沓curveCurves.easeInOut加减速自然不突兀四、剩下几个细节都是血泪1. 今天是什么时候的今天分桶依赖当前时间那这个时间什么时候取、多久重算一次是个真问题。我的做法是只在下拉刷新、页面重建、Tab 切换这几个时机重算滚动过程中绝对不重算。原因很简单——你要是搞个定时器每分钟重算一遍用户正好在跨零点那会儿滚列表整个分组啪地重排滚动位置直接飞了体验极差。宁可让用户看到几分钟的旧分组也别让列表在他手底下自己蹦迪。2. 组头一定要给不透明背景上面代码注释里提了一嘴这里再强调下。吸顶的时候组头是浮在内容上面的你要是没给背景色或者给了半透明下面滚过去的卡片就会从字缝里透出来糊成一坨灰特别难看。顺便给组头底部加一条 1px 的分割线吸顶时层次感会好很多。3. 组头别做太高吸顶头是常驻占屏的做个 32~40 的高度就够了。我见过做到 60 多的一屏的可用空间白白少一大块不值当。这几个细节我也顺手归成一张表细节正确做法踩坑后果今天何时算仅刷新 / 重建 / Tab 切换重算滚动中重排位置飞了组头背景不透明背景 底部 1px 分割线透出下方内容糊成一片组头高度32~40 足够60 白白占屏GlobalKey 清理重建后removeWhere清空桶反复刷新 key 堆积小结好啦吸顶分组列表这套东西就拆解完了。回头看这篇里技术含量最低但价值最高的其实是第一节那个**「语义桶」**的思路转变——从按数据的物理属性切转到按用户的心理预期切。这个转变一旦想通重复头的 bug 根本不会发生。后面的吸顶、滚动说白了都是查 API 的活儿。四条经验时间分组用语义桶一类一个头别按天切否则同周必出重复头。要真吸顶就得用SliverMainAxisGroupSliverPersistentHeader(pinned: true)Column塞SliverList那是假吸顶。点头跳转靠持久化的GlobalKeyScrollable.ensureVisiblekey 别在 build 里新建用完记得清。跨天重算只在刷新 / 重建时做滚动中重排是灾难。最后希望这篇文章能够对各位看官有所帮助。那么各位看官您做分组列表的时候是自己撸 Sliver还是直接上现成的 sticky header 库有没有更省事的路子欢迎在评论区交流。也请各位看官发财的小手点个小赞谢谢大家相关阅读Flutter 两个反直觉布局坑ListTile 水波纹 / VerticalDivider 踩坑实录Flutter 超长 StatefulWidget 拆分术part of extension on State 实战Flutter 骨架屏 Shimmer 实现不用 transform 的扫光法实战Flutter 401 自动刷新拦截器并发死锁_refreshQueue 死锁根治实录Flutter 可复用公共组件库设计与落地AppDialog/BottomSheet 等实战本文由 FungLeo 主导Deepseek 优化校阅转发请注明首发地址谢谢大家

相关新闻

如何用QtScrcpy实现电脑控制手机:跨平台Android投屏的完整解决方案

如何用QtScrcpy实现电脑控制手机:跨平台Android投屏的完整解决方案

如何用QtScrcpy实现电脑控制手机:跨平台Android投屏的完整解决方案 【免费下载链接】QtScrcpy Android real-time display control software 项目地址: https://gitcode.com/GitHub_Trending/qt/QtScrcpy 你是否曾想在电脑大屏幕上操作手机应用,却…

2026/8/10 15:28:38 阅读更多 →
货币政策工具创新与结构性优化实践

货币政策工具创新与结构性优化实践

1. 货币投放结构优化的核心逻辑 货币投放结构优化本质上是通过调整货币供给的流向、期限和工具组合,实现金融资源更精准地匹配实体经济需求。我在央行货币政策分析岗位工作期间,曾参与过多次结构性货币政策工具的设计,发现实际操作中需要重点…

2026/8/10 15:28:38 阅读更多 →
Codex写异步请求为什么容易出现状态覆盖?用请求隔离解决竞态条件

Codex写异步请求为什么容易出现状态覆盖?用请求隔离解决竞态条件

使用 Codex 修改前端项目时,有一类 Bug 很难在第一次测试中发现:页面功能看起来正常,接口也没有报错,但用户连续操作几次后,界面却显示了错误的数据。常见表现包括:用户快速切换两个账号,最后显…

2026/8/10 15:28:38 阅读更多 →

最新新闻

Excalidraw VS Code插件核心功能解析:编辑图片、切换主题与导入公共库全攻略

Excalidraw VS Code插件核心功能解析:编辑图片、切换主题与导入公共库全攻略

Excalidraw VS Code插件核心功能解析:编辑图片、切换主题与导入公共库全攻略 【免费下载链接】excalidraw-vscode Excalidraw for Visual Studio Code 项目地址: https://gitcode.com/gh_mirrors/ex/excalidraw-vscode Excalidraw for Visual Studio Code是一…

2026/8/10 20:28:26 阅读更多 →
张一鸣为什么反对蒸馏?

张一鸣为什么反对蒸馏?

7月,字节跳动Seed团队召开了一场内部会议,会议提及的内容包括把火山引擎、豆包和飞书整合的原因,也就是集中力量,才能在算力和数据上有优势。 8月初,更重要的信息才被国内外多家媒体报道出来,字节跳动创始…

2026/8/10 20:28:26 阅读更多 →
Unity依赖注入实战:基于Zenject的架构设计与性能优化

Unity依赖注入实战:基于Zenject的架构设计与性能优化

1. 项目概述:为什么Unity开发者需要依赖注入?如果你在Unity项目里写过超过1000行代码,大概率遇到过这样的场景:一个PlayerController脚本需要引用GameManager,而GameManager又需要AudioManager和UIManager,…

2026/8/10 20:28:26 阅读更多 →
Votify开发者指南:从API调用到源码解析,探索Spotify下载原理

Votify开发者指南:从API调用到源码解析,探索Spotify下载原理

Votify开发者指南:从API调用到源码解析,探索Spotify下载原理 【免费下载链接】votify A command-line app for downloading songs, podcasts and videos from Spotify. 项目地址: https://gitcode.com/gh_mirrors/vo/votify Votify是一款强大的命…

2026/8/10 20:28:26 阅读更多 →
组件通信:Props 与 Emit

组件通信:Props 与 Emit

第7课 组件通信:Props 与 Emit 能将页面拆分为独立的组件并组合使用掌握 Props 实现父组件向子组件传递数据(类型校验、默认值)掌握 Emit 实现子组件向父组件通信(自定义事件传参)理解单向数据流与“数据在谁手里&…

2026/8/10 20:28:26 阅读更多 →
JumpAppify-Desktop-Entry:将普通桌面程序转换为Run-or-Raise神器

JumpAppify-Desktop-Entry:将普通桌面程序转换为Run-or-Raise神器

JumpAppify-Desktop-Entry:将普通桌面程序转换为Run-or-Raise神器 【免费下载链接】jumpapp A run-or-raise application switcher for any X11 desktop 项目地址: https://gitcode.com/gh_mirrors/ju/jumpapp JumpApp是一款为X11桌面环境设计的Run-or-Raise…

2026/8/10 20:27:26 阅读更多 →

日新闻

GraphQL-CSS API全解析:useGqlCSS、GqlCSS组件与getStyles实用指南

GraphQL-CSS API全解析:useGqlCSS、GqlCSS组件与getStyles实用指南

GraphQL-CSS API全解析:useGqlCSS、GqlCSS组件与getStyles实用指南 【免费下载链接】graphql-css A blazing fast CSS-in-GQL™ library. 项目地址: https://gitcode.com/gh_mirrors/gr/graphql-css GraphQL-CSS是一个基于GraphQL的CSS-in-GQL™库&#xff0…

2026/8/10 0:00:02 阅读更多 →
告别语言障碍:KISS Translator 双语翻译插件终极指南

告别语言障碍:KISS Translator 双语翻译插件终极指南

告别语言障碍:KISS Translator 双语翻译插件终极指南 【免费下载链接】kiss-translator A simple, open source bilingual translation extension & Greasemonkey script (一个简约、开源的 双语对照翻译扩展 & 油猴脚本) 项目地址: https://gitcode.com/…

2026/8/10 0:00:02 阅读更多 →
BepInEx配置管理器:游戏插件配置的终极可视化解决方案

BepInEx配置管理器:游戏插件配置的终极可视化解决方案

BepInEx配置管理器:游戏插件配置的终极可视化解决方案 【免费下载链接】BepInEx.ConfigurationManager Plugin configuration manager for BepInEx 项目地址: https://gitcode.com/gh_mirrors/be/BepInEx.ConfigurationManager 你是否曾经因为游戏插件的复杂…

2026/8/10 0:00:02 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/10 1:05:29 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/10 1:05:29 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/10 1:05:29 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/10 17:07:33 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/10 1:05:29 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/10 17:07:33 阅读更多 →