- iteration-3 报告 01-08(PM 拆解/后端/Flutter/RC 首个 CERTIFIED/UI/埋点/证据基线/Git) - ADR-016 自托管 MinIO 起步预留迁云(用户确认现无云存储)、ADR-017 patbond-community :8084 + media 归 user + 作者信息跨 schema 只读、ADR-018 范围裁剪(话题剪出/单层评论)、 ADR-019 幂等按域(PUT/DELETE + request_hash)、ADR-020 聚合 feed_viewed/字典 v3/ 北极星不变/队列三项升第一波、ADR-021 Git 修订(main 更正/PR 情形触发/首发布重建 main/ 防泄漏 grep 先行) - mkdocs 挂「第三迭代」导航,build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16 KiB
03 · Flutter 前端技术评估(M3:社区)
作者:Frontend Developer 日期:2026-09-08 依据:开发计划 §M3、M2 收官报告(iteration-2/29)、22/23/26 号报告交接约定、15 号队列报告 §4 性质:开工前评估,只读分析 + 验证性测试,未改动任何生产代码。
0. 基线验证
$ flutter test # patbond-flutter dev@720865b @ Flutter 3.44.6 stable
00:27 +272: All tests passed! # 272 个测试全绿,与 M2 收官记录一致
M3 以 272 为基线,收官时只增不减。
1. 社区 demo 现状盘点(实际读码结论)
1.1 替换面总览
社区 demo 分布在四处,总计约 1700 行,其中数据层是 100% 替换、UI 骨架大半可保留:
| 文件 | 行数 | demo 面 | 可保留骨架 |
|---|---|---|---|
lib/state/app_state.dart |
97 | posts demo 列表 + shared_preferences 持久化(_postsKey)、updatePost/publishPost |
无——posts 相关全部退役;pet/locationWeather demo 归首页/创作页家具,本迭代不动 |
lib/features/home/home_page.dart |
863 | Feed segment:_PostCard 列表直读 appState.posts、客户端关键词过滤、RefreshIndicator 是 500ms 假延时、点赞就地翻转 demo 数据;_StoryRow 圈子/_PromoCard 促销硬编码 |
_PostCard 版式、天气条/问候卡/搜索框/segment 结构、服务 segment(M5 范围)全保留 |
lib/features/post/post_detail_page.dart |
277 | 整页数据 demo:toggleLike/收藏本地翻转、sendComment 本地插入(作者硬编码「萌宠新手」)、关注按钮纯 setState 布尔、分享是演示 SnackBar |
版式(大图头、作者卡、正文卡、评论列表、底部输入条)整体保留 |
lib/features/create/create_page.dart |
547 | publishPost 落 AppState;700/650/500ms 假延时是 AI 生成模拟,属 M4 范围 |
M3 只接「发布 → 社区服务」半边(草稿/发布);AI 模拟原样留给 M4 |
- 模型不复用:
PostModel/CommentModel(lib/models/models.dart)是 demo 形态——time是「刚刚」类字符串、无服务端 id/authorId、无 version/游标字段。照 M2 先例新建 community 模型,demo 模型随页面替换下线。 - 埋点基础就绪:
main_shell_page.openPost已带RouteSettings(name: postDetail),页名枚举已有post_detail;社区事件按pet_analytics.dart同款强类型封装新建community_analytics.dart。 - 图片入口集中:全仓远程图统一走
widgets/common.dart的RemoteImage(内部Image.network,仅内存缓存)——媒体缓存改造成本集中一处(§4.1),但替换波及全仓图片(含 pets 头像),需全局回归。
一句话:post_detail 数据层整页重写(UI 骨架保留),home 的 Feed segment 重做数据源与分页,create 只接发布半边,AppState.posts 退役。
1.2 可直接复用的 M2 资产
- 分层模板:
PetsController(ChangeNotifier 四态)→PetsRepository(抽象 + Api 实现)→ApiClient(错误信封、401/40101 单飞刷新重放、429 类型化)。 CursorPage<T>正典信封({items, nextCursor, hasMore})与「加载更多失败保留重试」页面交互(体重/健康事件列表已验证)。- 分端口直连模式:
--dart-define注入 base url(auth :8081 / user :8082 / pet :8083),community 服务照加PATBOND_COMMUNITY_API_BASE_URL(默认 :8084,以后端为准);同一SessionManager/TokenRefresher共享,新建一个指向 community 端口的ApiClient实例即可,网络层零改动。 - Idempotency-Key 先例:pets 域四个 POST 每次逻辑提交换新键、token 刷新重放沿用同键。
- 测试手法:
FakeRepository+Completer控时序(test/helpers/先例)、四态 widget 测试。
2. community feature 分层规划
2.1 目录与分层(照 pets 模式,一处例外)
lib/features/community/
community_models.dart # Post / PostComment / FeedPage 等,手写 JSON
community_exceptions.dart # 业务码 → 类型化异常映射
community_repository.dart # 抽象接口 + ApiCommunityRepository
feed_controller.dart # Feed 状态机(见 §2.2),Tab 级注入
post_detail_page.dart # 重写现 features/post/(旧目录随迁移删除)
post_analytics / media/... # 随工单拆分
例外在控制器职责:PetsController 的 refresh() 一次拉全量,而 Feed 是游标累积流、且详情页/首页共享同一份帖子内存副本(点赞状态要跨页一致),所以 FeedController 是 Tab 级单例(app.dart 装配注入主壳,同 PetsController),不做页面级 state。评论列表则相反——只属详情页,照 26 号报告「页面级状态按页自建」纪律放详情页 State 里,不膨胀 FeedController。
2.2 Feed 状态机(对 pets 四态的两点扩展)
enum FeedPhase { initial, loading, ready, error } // 首屏四态,同 pets
enum LoadMorePhase { idle, loading, error } // 尾部加载态,新增
class FeedController extends ChangeNotifier {
List<Post> _items; // 累积列表(多页内存缓存即「多页缓存」,不落盘)
String? _nextCursor;
bool _hasMore;
FeedPhase _phase;
LoadMorePhase _loadMorePhase;
int _generation = 0; // 刷新代次,丢弃过期响应(见下)
}
- 下拉刷新与游标的关系:刷新 = 丢弃游标、从头拉第一页、成功后整体替换累积列表(不做增量 prepend/「有新内容」提示,M3 不引入 since 语义);刷新失败保留旧列表 + SnackBar,不清空不闪空态。刷新使
_generation++,在途的旧代次加载更多响应到达时直接丢弃——这是 pets 没有的并发点,必须做,否则「刷新后旧尾页追加」会产生重复/错位。 - 加载更多:滚动近底触发;失败置
LoadMorePhase.error,尾部渲染重试条(复刻体重列表交互);hasMore=false渲染到底提示。 - 详情页同步:详情页构造注入
FeedController+ postId,读 controller 副本渲染;进入时getPost(id)拉详情并_replaceInList回写(照PetsController.getPet先例),点赞/收藏经 controller 统一走 §3 状态机,Feed 卡片与详情天然一致。 - 登出 reset():清列表回 initial,同 pets 纪律。
- 首页现有的客户端关键词过滤在真实分页下语义不成立(只能过滤已加载页),M3 建议搜索框对 Feed segment 降级为占位/隐藏,真搜索留给后端搜索接口(范围归 PM)。
3. 乐观更新回滚设计草案(点赞/收藏)
M3 前端最大新课题。核心:乐观翻转 + 快照回滚 + 单飞合并意图 + 代次守卫,点赞/收藏共用一套 ToggleSync 小状态机(字段读写与端点参数化,避免复制两份)。
3.1 状态机
对每个 postId 维护(Map 存于 FeedController,随 reset 清空):
inFlight: bool # 该 post 是否有请求在途(单飞)
pendingTarget: bool? # 在途期间用户又点出的最终意图
snapshot: (liked, likeCount) # 本轮操作链起点快照,用于回滚
- 点击:立即翻转内存副本(
hasLiked取反、likeCount ±1)并 notify——反馈是同帧的。若inFlight,只记pendingTarget并返回(不发新请求)。 - 发请求:非在途则记快照、置
inFlight,按当前目标态发送。 - 成功:若
pendingTarget与已确认态不一致 → 以 pendingTarget 为目标补发一次(连续快速点击最多两个请求,中间抖动全被合并);一致则用服务端返回的权威likeCount覆盖乐观计数(吸收他人并发点赞造成的偏差),清状态。 - 失败:恢复快照并 notify,SnackBar 轻提示(「点赞失败,请重试」),不自动重试(用户可再点,重点一次即新一轮);清状态。
- 守卫:请求携带发起时的
_generation,响应到达时代次不符(期间发生过刷新,列表已被服务端数据整体替换)→ 丢弃该响应、不回滚不覆盖——避免用陈旧快照污染新数据。快照恢复前同样校验该 postId 仍在列表且当前态仍是本轮乐观写入的目标态。
3.2 与后端幂等的配合
开发计划要求「点赞、收藏使用幂等写入」。两种契约形态对客户端的影响:
- 语义幂等(推荐):
PUT /posts/{id}/like/DELETE /posts/{id}/like,重复调用收敛到同一终态、服务端返回权威{liked, likeCount}。客户端无需 Idempotency-Key(PUT/DELETE 天然可安全重放,token 刷新后的自动重放也安全),补发/重点都不会重复计数——正是验收标准「重复点赞不重复计数」的最省事实现。 - POST + Idempotency-Key:若后端坚持
POST /likes形态,客户端沿用 pets 先例(每轮逻辑操作换新键、刷新重放同键)。代价:toggle 语义下「点了又取消」是两个不同逻辑操作两个键,键管理与 §3.1 的意图合并叠加后复杂度明显更高。
跨端待拍板(§6-A),前端强烈建议前者。评论创建则相反:非幂等 POST,照 pets 四 POST 先例带 Idempotency-Key;评论不做乐观插入(发送中态 + 成功后插入服务端返回实体),回滚一条已渲染的评论气泡收益低、复杂度高,M3 不做。
3.3 测试清单
- Controller 单测:成功覆盖计数 / 失败恢复快照 / 在途连点只发一请求且完成后补发 / 补发目标与终态一致不再发 / 刷新代次不符丢弃响应 / reset 清状态。
FakeRepository+Completer控时序。 - Widget 测试:点击图标同帧变红计数 +1;失败回滚且 SnackBar 出现;连点若干次最终态正确。
4. 媒体客户端链路草案
4.1 依赖选型(新增依赖是 M3 最大的 pubspec 变更,逐项理由)
| 能力 | 推荐包 | 备选与理由 |
|---|---|---|
| 图片选择 | image_picker(flutter.dev 官方维护,pickMultiImage 支持多选) |
wechat_assets_picker 功能强但依赖重、维护面大;M3 用系统选择器足够 |
| 压缩 | flutter_image_compress(原生编解码,快;支持质量 + 尺寸重采样 + EXIF 方向自动矫正) |
纯 Dart 的 image 包在中端机上压一张 12MP 图秒级卡顿,排除 |
| 展示缓存 | cached_network_image(磁盘缓存) |
Feed 无限流 + 反复滚动下 Image.network 仅内存缓存不可接受。改造点集中在 RemoteImage 一处,全仓受益,但需全局回归(pets 头像等) |
| 大图预览 | Flutter 内置 InteractiveViewer(零依赖,捏合缩放/平移够用) |
photo_view 手势更全(双击缩放曲线、画廊),体验不满意再引,待拍板 §6-E |
压缩策略草案:长边 ≤2048 重采样 + JPEG 质量 80(Feed 场景肉眼无损、体积约降一个量级);flutter_image_compress 默认不保留 EXIF——注意不要开 keepExif,顺带剥离 GPS 定位隐私;autoCorrectionAngle 处理方向。九宫格缩略图靠 cached_network_image 的 resize 或后端缩略图 URL(依赖后端媒体方案给不给多尺寸,向后端提需求)。
4.2 上传进度与失败重试 UI
- 进度:dio 原生
onSendProgress,无需新依赖。 - 创作页九宫格每张图独立小状态机:
待传 → 压缩中 → 上传中(进度环) → 成功 / 失败(蒙层 + 点按重试);单图失败只重传该图。 - 发布 gating:全部图片成功(拿到 mediaId/URL)才允许提交发布;正文先行、图片后台传的「先发后补」模式 M3 不做。
4.3 预签名直传 vs 后端中转(客户端影响面对比)
| 维度 | 预签名直传 | 后端中转 |
|---|---|---|
| 请求步数 | 两步:POST /media(取签名 URL)→ PUT 对象存储(+ 可能的 confirm 回调) |
一步 multipart POST |
| 网络层 | 需另建一个裸 Dio:对象存储不认 Bearer、响应不是业务信封,不能走 ApiClient/AuthInterceptor |
完全复用既有 ApiClient(鉴权/信封/40x 映射/刷新重放全白拿) |
| 错误处理 | 两段异构:取签名的业务错误 + 存储 PUT 的原始 HTTP 错误(含签名过期重取) | 一段,既有类型化异常分层 |
| 客户端成本 | 多约 1 个封装 + 裸 dio + 两段错误测试 | 最小 |
客户端两种都可行、成本差约一天。解耦手段:先冻结 MediaUploader 抽象接口(Future<MediaRef> upload(XFile file, {void Function(double) onProgress})),创作页只依赖接口,后端对象存储选型拍板后填实现——媒体不阻塞创作页开工。前端不对后端选型施加约束(§6-F)。
5. M2 遗留纳入评估
| 遗留项 | 内容 | 建议 |
|---|---|---|
| T2-12 §8 三项交互 | 单宠直进/切换器、归档入口(依赖 listPets 对 archived 的过滤语义契约确认)、sterilizedOn 编辑 | 随 M3 消化:三项都是 S 级、纯 pets 域文件,与社区工单零文件冲突,适合作为波次间隙的独立小工单;归档入口需后端先明确过滤语义 |
| 埋点队列完善(15 号 §4) | 30s 定时冲刷、指数退避(5s ×2 上限 5min)+ 429 按 Retry-After、anonymousId/lastActiveAt 持久化 |
必须随 M3 且排第一波:社区事件量(feed 加载/点赞/发布)远超 pets,现状「4xx 整批永久丢弃 + 无定时冲刷」在高频事件下丢数风险放大;三项均不依赖社区契约,可与契约冻结完全并行。429 的 Retry-After 语义依赖后端限流落地,可先实现通用退避、Retry-After 留接线点。30s 定时器测试用 fakeAsync;anonymousId 落 pb.analytics.lastActiveAt 同款 shared_preferences 键位 |
另提醒数据侧:若 M3 要开 feed 曝光类事件(post_impression),事件量将冲击持久化队列 500 条上限,采样策略需在字典 v3 评审时一并定(§6-H)。
6. 权衡与待拍板
| # | 议题 | 选项 | 推荐 |
|---|---|---|---|
| A | 点赞/收藏幂等形态(跨端契约) | ① PUT/DELETE /posts/{id}/like 语义幂等;② POST + Idempotency-Key |
①。客户端免键管理、重放天然安全、服务端回权威计数即满足「重复点赞不重复计数」(§3.2) |
| B | 并发点击策略 | ① 在途忽略点击;② 单飞 + 最终意图合并(最多补发一次);③ 300ms debounce 后发 | ②(§3.1)。①在快速「点了又取消」时 UI 与服务端脱节;③延迟真实提交、时序更难测 |
| C | 下拉刷新语义 | ① 从头拉第一页整体替换;② 增量 prepend + 新内容提示 | ①。②需要 since 游标语义与去重合并,M3 收益不匹配 |
| D | Feed 只读冷启动缓存(首页 JSON 落盘先渲染) | ① 做;② 纯在线 + 四态 | ②。M2 pets 最终拍板即纯在线(22 号:服务端唯一事实源);M3 新面已大,缓存一致性(点赞态陈旧)另添课题,留 M4+ 评估 |
| E | 大图预览 | ① InteractiveViewer 内置;② photo_view |
①,体验不达再升级,少一个依赖 |
| F | 媒体上传通道 | ① 预签名直传;② 后端中转 | 前端跟随后端选型,两案成本差约 1 天;MediaUploader 接口先冻结解耦(§4.3) |
| G | M2 遗留纳入波次 | 见 §5 | 埋点队列第一波必做;T2-12 三项作间隙工单 |
| H | post_impression 曝光事件是否 M3 开报 |
归数据侧 | 若开报须定采样,且以 §5 队列完善为前置 |
7. 风险与依赖小结
- 社区契约是关键路径:openapi 尚无任何社区路径(Feed 游标信封、点赞返回体、媒体接口、评论分页);前端第一波可并行做:埋点队列三项、
FeedController/ToggleSync状态机 + 假仓实现、RemoteImage缓存化改造、创作页九宫格 UI。 - 点赞契约形态(§6-A)影响 §3 状态机的键管理分支,建议契约评审最先拍这一项。
- M3/M4 边界:create_page 的 AI 生成模拟必须原样保留(属 M4),M3 只替换发布落库半边——工单里写明改动边界,避免顺手清理越界。
RemoteImage缓存化波及全仓图片,改动一处但回归面全局,建议独立小工单先行合入。- 关注/话题在开发计划 M3 条目内,但现状 demo 只有详情页一个孤立关注按钮、无关注流/话题页——范围裁剪归 PM 工单拆解,本评估未按全量规划。
- 本评估未改任何生产代码;测试基线 272 全绿已复验。
Frontend Developer · 2026-09-08 · patbond-flutter dev@720865b