- 21~26 Flutter 社区接入五单 + 字典 v3 白名单(flutter 286→502、api 325→334) - 27 收口总表:社区 demo 三页消亡、M3 四条验收标准逐条取证、 乐观更新与媒体链路端到端、实现期修正记录 - device-verification.md 的 M3 四项真机步骤已由各单收口补全 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
24 M3 第三波:首页 Feed 接入真实数据(T3-14)
执行日期:2026-09-09
工单:T3-14 首页 Feed segment 替换真实数据——社区 demo 消亡的第一页
依赖:21 号(T3-12 数据层,CommunityController 就位)、05 号 UI 规范、06 号埋点规划(feed 域白名单已随 api dev@8089c06 就绪)
提交:patbond-flutter dev 8aac8c5(基线 1441f01)
0. 概要
home_page.dart 的 Feed segment 由「AppState demo 帖子 + 500ms 假延时刷新」
整体切换为 CommunityController 真实数据:四态首屏、尾部三态、下拉刷新与
游标翻页、聚合曝光埋点全部落地;PostCard 三形态等 4 个共享组件入
lib/core/widgets/;预签名 URL 的图片缓存 key 剥签名改造全仓生效。
未动 patbond-api;create/post_detail 的 demo 按工单边界留给 T3-15/17。
质量门禁:flutter test 421/421 全绿(基线 379,+42)、flutter analyze
0 问题、dart format --set-exit-if-changed 无 diff、compose 六容器真链路
实测通过(§5)。
1. 四态与尾部三态覆盖表
| 态 | 渲染 | 交互 | widget 测试 |
|---|---|---|---|
| 首屏 loading(initial/loading) | FeedSkeleton 连排 3 张(呼吸动效,尊重系统减弱动态设置静止 1.0) |
— | ✓ |
| 首屏 error | InlineErrorBanner(pets 同款话术映射)+ 「重试」FilledButton |
重试 = 用户刷新(计入浏览段 refreshCount) | ✓(含恢复 ready) |
| 首屏 empty | EmptyStateIllustration(forum_outlined「还没有动态」)+ CTA「发布第一条」 |
CTA → 创作 Tab | ✓ |
| ready | PostCard 列表(卡间距 16) |
见 §2 | ✓(含降级作者) |
| 尾部 loading | 24 转圈(primary)居中,上下留白 16 | 滚动近底(余量 400)自动触发,携上页 nextCursor | ✓ |
| 尾部 error | 错误话术 + 「加载失败,点此重试」 | 只走显式点按重试——失败态不随滚动通知自动重打(实测发现滚动风暴会把失败态冲掉并重复请求,已加守卫) | ✓(含事件上报与重试补页) |
| 尾部到底 | 「没有更多了」12 inkSoft 居中 |
— | ✓ |
刷新语义照 controller 契约:下拉刷新失败且旧列表在手 → 保留列表不闪空态,
SnackBar 轻提示 + feed_load_failed 上报(widget 测试覆盖「失败保留旧列表 →
再刷成功整体替换不残留」全序列)。翻页不丢不重由 controller 代次守卫保证
(21 号已测),本单 widget 测试再从 UI 侧验证:两页游标取齐后三帖各恰一张。
搜索框保留 demo 交互(客户端过滤已加载多页缓存;契约 v1.3.0 无检索端点),
过滤中不渲染尾部三态(翻页语义混淆);无命中沿用既有 EmptyState。
2. 组件落位
| 组件 | 落位 | 说明 |
|---|---|---|
PostCard |
lib/core/widgets/post_card.dart |
三形态:单图(mediaCount≤1 有封面)通栏出血 4:3;多图(mediaCount>1)走 PostMediaGrid 折叠封面;纯文字正文放宽 6 行、15/1.6。头部 PetAvatar sm32 + 名字 14/w700 + 相对时间 12 inkSoft;求助帖追加 TagPill(accent)。次级文字全部显式 inkSoft(DEBT-2 零新增) |
PostMediaGrid |
lib/core/widgets/post_media_grid.dart |
展示态:列数规则 2/4→2 列、3/5–9→3 列,格间距 4、圆角 sm12;超 9 图末格 ink 80% scrim + 白字 +N 20/w800(05 §5.1 精算,60% 档弃用)。形态偏差:FeedCard 契约只带 coverImage+mediaCount(裁剪形态),Feed 卡多图实渲染为 4:3 封面 + 右下 +N 胶囊角标(同 80% scrim 精算);真九宫格留给详情/发布页全量媒体场景。编辑态(+格/删除角标)随 T3-17 扩展 |
LikeButton |
lib/core/widgets/like_button.dart |
点赞/收藏参数化一件:未激活 inkSoft;点赞激活 error 图标 + errorDark 计数(demo Colors.red 3.13:1 修订清零,D6);收藏激活 accentDark。触控 44×44 |
FeedSkeleton |
lib/core/widgets/feed_skeleton.dart |
05 §3.7 单元结构;0.6↔1.0 呼吸 1200ms,disableAnimations 静止 |
SignedNetworkImage |
lib/core/network/signed_network_image.dart |
预签名 URL 缓存 key 剥离 X-Amz-* 签名参数(大小写不敏感、保留其余 query),RemoteImage 全仓换用——同对象两次响应 URL 必然不同,剥签名后命中同一 ImageCache 条目,未命中仍以完整签名 URL 请求 |
community_display.dart |
lib/features/community/ |
相对时间、加载失败话术、降级作者「宠友」统一占位(isDegraded 一个判定口 + PetAvatar 无图占位形态) |
T3-14 互动取舍(工单预留的选项里选了禁用态):demo 详情页按
appState.posts 查 demo id,无法渲染服务端 postId 的真实帖,导航过去即崩;
故整卡点按先弹 SnackBar「帖子详情正在接入真实数据」,点赞/收藏/评论/分享
按钮为纯展示禁用态(真实计数与激活态照常渲染,onPressed 传 null)。
T3-15 详情页重写后接导航,T3-15/16 接 ToggleSync 与激活动画。
主壳装配:app.dart 注入 communityController + feedAnalytics 进
MainShellPage → HomePage(isActive = currentIndex == 0 驱动浏览段);
openPost(PostModel) 保留给 create demo 流(T3-17 收编)。home 对
AppState.posts 的消费清零,posts 字段本体随 T3-15/17 退役。
3. 曝光结算设计(feed_viewed / feed_load_failed)
一句话:浏览段聚合——进入 Feed 面开段,离开(切 Tab / 切服务分段 /
退后台 / 页面销毁)时结算发一条 feed_viewed,postId 只作段内内存
去重键、绝不上报(06 §1.2 裁定 + 隐私红线 2)。
- 判定:卡片可见面积 ≥50%(列表视口与卡片 RenderBox 纵向交叠比例)且
驻留 ≥500ms;驻留计时在
FeedViewSegment(feed_exposure.dart),跌破 阈值/滚出视口即取消。扫描统一调度到 post-frame(滚动通知发生在本帧布局 前,同步读 RenderBox 是旧位置——实测踩到,已修)且一帧至多一次。 - 计数口径:
refreshCount= 用户下拉/错误重试(首屏自动预取不计);loadMoreCount= 触底翻页请求(含尾部显式重试);durationMs前台 时长(退后台即结算,段天然前台连续),30 分钟截断。 - 生命周期:
WidgetsBindingObserver只在离开 resumed 的第一次变更 结算(inactive→hidden→paused 级联不重复,SessionTracker 同款处理); 回前台若仍在 Feed 面开新段。settle()幂等,一段恰一条。 - feed_load_failed:refresh / load_more 双路,
failureReason网络归并 口径同 pet 域(断网/超时/5xx → network_error),errorCode仅业务码、httpStatus由五位码推导;会话失效不上报(应用即将回登录页)。 - 页名核对:Feed 属首页 Tab,
page_viewed(home)由既有 Tab 补点覆盖, 无新增页名;post_detail枚举已在(T3-15 接线导航后自动生效)。
两事件线上验证见 §5(白名单 202 accepted + platform.product_events 落库)。
4. 测试数变化
| 项 | 基线 | 本单后 |
|---|---|---|
| flutter test | 379 | 421(+42,另 1 个既有默认跳过冒烟) |
| flutter analyze | 0 | 0 |
| dart format | 无 diff | 无 diff |
新增分布:home_page widget 测试 13(四态 4、尾部三态与翻页 3、刷新失败
序列 1、曝光结算 4——切 Tab/快速滑过/退后台/切分段、取舍与搜索 2)、
PostCard 6(三形态/求助标/降级作者/操作行展示态)、PostMediaGrid +
FeedSkeleton 7、FeedViewSegment 5(fake_async 控驻留时序)、FeedAnalytics 4
(属性逐字段 + 异常映射)、缓存 key 与 provider 判等 7。另
integration_test/feed_live_test.dart 桌面真链路 1 条(环境变量门控,
默认跳过不计入套件)。
实现期修正两处(widget 测试暴露):尾部失败态被滚动通知自动重试冲掉
(加 idle 守卫);回前台 _lastLifecycle 读旧值导致不开新段(resumed
分支先置状态)。
5. compose 真链路实测
后端 patbond-api dev@8089c06(含 feed 域白名单),六容器 docker compose up -d --build 全部 Up、postgres/minio healthy。
(a)数据种子 + 接口链路(curl):注册一次性账号 → 发 26 帖
(24 纯文字 + 1 单图 + 1 双图,图走 media 两步上传:预签名 PUT 直传
MinIO → confirm → ready assetId 引用发帖,全部 published)。
GET /api/v1/feed?limit=20 首页 20 条 hasMore=true → 携 nextCursor 取第二页
6 条 hasMore=false,两页零重叠、26 条取齐;封面预签名 GET 回读字节与
上传原件 cmp 一致。feed_viewed / feed_load_failed 按客户端真实 payload
形状 POST /api/v1/events → 双双 202 accepted,platform.product_events
落库 props 完整(feedTab/durationMs/impressionCount/loadMoreCount/
refreshCount;loadType/failureReason)。
(b)Linux 桌面真跑(integration_test):
PATBOND_FEED_LIVE=1 flutter test integration_test/feed_live_test.dart -d linux
——真实 App 桌面渲染管线 + 真实 HTTP + MinIO 预签名图片(仅注入内存
token 存储,桌面无 keyring):注册 → UI 登录 → Feed 首屏真数据卡片
(含单图/多图 +1 角标卡)→ fling 触底游标翻页至「没有更多了」且最早
一帖(#1)在列(两页取齐直接证据)→ 回顶下拉刷新列表仍健。一次通过
(约 25s)。测后 docker compose down 干净退出,patbond-api 零改动。
遗留观察:桌面端 analytics 真实上报因 platform 枚举不含桌面值被服务端 整批拒绝(既有已知约束,device-verification 通用前置已记载),不影响 本单验证((a) 已按契约 platform 验真);Feed 图片加载的真机表现(蜂窝 网络、缓存命中、局域网/公网 MinIO 可达性)预登记补全见 device-verification M3 第 3 项。
6. 遗留与交接
- T3-15:详情页重写后把 PostCard
onTap接openPost导航(页名post_detail既有)、评论钮锚点;LikeButton 接 ToggleSync + §3.5 激活 动画与回滚零动画;AppState.posts消费面只剩 create/post_detail。 - T3-16/17:收藏交互、发布页(PostMediaGrid 编辑态 + UploadProgressOverlay 已在)。
- 多图九宫格全量形态:详情页拿到
Post.media全量后启用(PostMediaGrid 列数规则与 +N 已就绪并有测试)。
Frontend Developer(Flutter) 日期:2026-09-09