Files
patbond-doc/docs/development/iterations/iteration-3/03-flutter-technical-assessment.md
T
lixi d2867826d3
CI / docs-build (push) Successful in 55s
docs: M3 开工分析 8 份报告入档 + ADR-016~021 拍板决策
- 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>
2026-09-08 16:02:53 +08:00

16 KiB
Raw Blame History

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 结构、服务 segmentM5 范围)全保留
lib/features/post/post_detail_page.dart 277 整页数据 demotoggleLike/收藏本地翻转、sendComment 本地插入(作者硬编码「萌宠新手」)、关注按钮纯 setState 布尔、分享是演示 SnackBar 版式(大图头、作者卡、正文卡、评论列表、底部输入条)整体保留
lib/features/create/create_page.dart 547 publishPost 落 AppState700/650/500ms 假延时是 AI 生成模拟,属 M4 范围 M3 只接「发布 → 社区服务」半边(草稿/发布);AI 模拟原样留给 M4
  • 模型不复用PostModel/CommentModellib/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.dartRemoteImage(内部 Image.network,仅内存缓存)——媒体缓存改造成本集中一处(§4.1),但替换波及全仓图片(含 pets 头像),需全局回归。

一句话:post_detail 数据层整页重写(UI 骨架保留),home 的 Feed segment 重做数据源与分页,create 只接发布半边,AppState.posts 退役

1.2 可直接复用的 M2 资产

  • 分层模板:PetsControllerChangeNotifier 四态)→ PetsRepository(抽象 + Api 实现)→ ApiClient(错误信封、401/40101 单飞刷新重放、429 类型化)。
  • CursorPage<T> 正典信封({items, nextCursor, hasMore})与「加载更多失败保留重试」页面交互(体重/健康事件列表已验证)。
  • 分端口直连模式:--dart-define 注入 base urlauth :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/...   # 随工单拆分

例外在控制器职责PetsControllerrefresh() 一次拉全量,而 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)  # 本轮操作链起点快照,用于回滚
  1. 点击:立即翻转内存副本(hasLiked 取反、likeCount ±1)并 notify——反馈是同帧的。若 inFlight,只记 pendingTarget 并返回(不发新请求)。
  2. 发请求:非在途则记快照、置 inFlight,按当前目标态发送。
  3. 成功:若 pendingTarget 与已确认态不一致 → 以 pendingTarget 为目标补发一次(连续快速点击最多两个请求,中间抖动全被合并);一致则用服务端返回的权威 likeCount 覆盖乐观计数(吸收他人并发点赞造成的偏差),清状态。
  4. 失败:恢复快照并 notify,SnackBar 轻提示(「点赞失败,请重试」),不自动重试(用户可再点,重点一次即新一轮);清状态。
  5. 守卫:请求携带发起时的 _generation,响应到达时代次不符(期间发生过刷新,列表已被服务端数据整体替换)→ 丢弃该响应、不回滚不覆盖——避免用陈旧快照污染新数据。快照恢复前同样校验该 postId 仍在列表且当前态仍是本轮乐观写入的目标态。

3.2 与后端幂等的配合

开发计划要求「点赞、收藏使用幂等写入」。两种契约形态对客户端的影响:

  • 语义幂等(推荐)PUT /posts/{id}/like / DELETE /posts/{id}/like,重复调用收敛到同一终态、服务端返回权威 {liked, likeCount}。客户端无需 Idempotency-KeyPUT/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_pickerflutter.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 定时器测试用 fakeAsyncanonymousId 落 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. 风险与依赖小结

  1. 社区契约是关键路径:openapi 尚无任何社区路径(Feed 游标信封、点赞返回体、媒体接口、评论分页);前端第一波可并行做:埋点队列三项、FeedController/ToggleSync 状态机 + 假仓实现、RemoteImage 缓存化改造、创作页九宫格 UI。
  2. 点赞契约形态(§6-A)影响 §3 状态机的键管理分支,建议契约评审最先拍这一项。
  3. M3/M4 边界create_page 的 AI 生成模拟必须原样保留(属 M4),M3 只替换发布落库半边——工单里写明改动边界,避免顺手清理越界。
  4. RemoteImage 缓存化波及全仓图片,改动一处但回归面全局,建议独立小工单先行合入。
  5. 关注/话题在开发计划 M3 条目内,但现状 demo 只有详情页一个孤立关注按钮、无关注流/话题页——范围裁剪归 PM 工单拆解,本评估未按全量规划。
  6. 本评估未改任何生产代码;测试基线 272 全绿已复验。

Frontend Developer · 2026-09-08 · patbond-flutter dev@720865b