refactor(todo): 移除 due_at 字段,改用 order 字段管理象限内顺序
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
# 前端导航解耦与统一缓存重构设计
|
||||
|
||||
## 1. 背景与问题定义
|
||||
|
||||
当前 `apps` 端在日历(日/月)与待办页面中存在以下系统性问题:
|
||||
|
||||
1. 页面切换语义错误:将业务 tab 切换实现为 `push/go` 混用,导致页面重建与路由栈膨胀。
|
||||
2. 数据刷新触发错误:页面通过路由监听触发 `load`,频繁重复请求后端。
|
||||
3. 状态职责耦合:导航状态、页面状态、数据状态边界不清,导致“切换逻辑改动会牵出数据 bug”。
|
||||
4. 回主页语义不一致:Dock 首页按钮被 `canPop -> pop` 策略污染,行为变成“返回上页”。
|
||||
5. 缓存能力分散:仅存在局部的个人信息缓存(`SettingsUserCache`),缺少统一可复用缓存模块。
|
||||
|
||||
目标是完成一次结构化重构,建立「解耦的导航切换 + 统一缓存 + 可控一致性」体系。
|
||||
|
||||
## 2. 目标与非目标
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
1. Home/Calendar/Todo 切换不重建主页面(保持页面实例与滚动状态)。
|
||||
2. 日/月视图切换不触发整页重建和无必要网络请求。
|
||||
3. 建立统一缓存模块,合并个人信息缓存并覆盖 Calendar/Todo 数据读取。
|
||||
4. 启动体验采用「本地优先 + 后台静默刷新」策略,减少进入 App 的重复请求。
|
||||
5. 数据只在必要时刷新:手动下拉、写操作失效、生命周期关键点、缓存策略命中。
|
||||
6. 主页按钮语义固定为“回主页”,不再变成“返回上一页”。
|
||||
|
||||
### 2.2 非目标
|
||||
|
||||
1. 本次不改后端协议与接口契约。
|
||||
2. 本次不引入复杂离线同步冲突解决(如多端 CRDT)。
|
||||
3. 本次不引入全量本地数据库迁移(先基于 SharedPreferences 持久化层)。
|
||||
|
||||
## 3. 复杂度与风险分级
|
||||
|
||||
- Complexity: `S3`
|
||||
- 跨 router、calendar、todo、settings、DI 的架构级调整。
|
||||
- Risk Tier: `L1`
|
||||
- 不触及鉴权协议和支付等高风险域,但涉及导航返回栈与数据一致性高回归区。
|
||||
|
||||
## 4. 架构总览
|
||||
|
||||
### 4.1 导航分层
|
||||
|
||||
采用两级导航:
|
||||
|
||||
1. 一级(主容器):`StatefulShellRoute.indexedStack`
|
||||
- 分支:Home / Calendar / Todo
|
||||
- 作用:保活分支页面,避免 tab 切换重建。
|
||||
2. 二级(分支内部)
|
||||
- Calendar 分支:管理 month/day 主视图切换 + event detail/edit/share 子路由。
|
||||
- Todo 分支:管理 list/detail/edit 子路由。
|
||||
|
||||
### 4.2 状态与数据边界
|
||||
|
||||
1. 导航状态:Shell 当前分支 index、Calendar 内部视图类型。
|
||||
2. UI 状态:选中日期、滚动位置、拖拽态、loading/error。
|
||||
3. 数据状态:统一缓存模块管理(内存 + 持久化 + 网络回写)。
|
||||
|
||||
结论:页面只发“意图”,不直接承担缓存与路由策略。
|
||||
|
||||
## 5. 统一缓存模块设计
|
||||
|
||||
## 5.1 模块结构
|
||||
|
||||
新增 `apps/lib/core/cache/`:
|
||||
|
||||
1. `cache_key.dart`
|
||||
- 统一 key 命名规范。
|
||||
2. `cache_policy.dart`
|
||||
- TTL、软/硬过期、最小刷新间隔、刷新原因枚举。
|
||||
3. `cache_entry.dart`
|
||||
- 标准缓存实体(data/fetchedAt/expiresAt/version/dirty)。
|
||||
4. `cache_store.dart`
|
||||
- 抽象接口(get/set/remove/invalidateNamespace)。
|
||||
5. `memory_cache_store.dart`
|
||||
- 会话级热缓存。
|
||||
6. `persistent_cache_store.dart`
|
||||
- 本地冷缓存(SharedPreferences JSON)。
|
||||
7. `hybrid_cache_store.dart`
|
||||
- 两级缓存协调与 singleflight 去重。
|
||||
8. `cache_invalidator.dart`
|
||||
- 统一精准失效入口。
|
||||
|
||||
### 5.2 key 设计(首版)
|
||||
|
||||
1. 用户信息
|
||||
- `user:profile:me`
|
||||
2. 日历
|
||||
- `calendar:day:YYYY-MM-DD`
|
||||
- `calendar:month:YYYY-MM`
|
||||
3. 待办
|
||||
- `todo:list:pending`
|
||||
- `todo:list:priority:<n>`(按需)
|
||||
- `todo:detail:<id>`(按需)
|
||||
|
||||
### 5.3 策略设计(平衡型)
|
||||
|
||||
读取顺序:`memory -> persistent -> network`。
|
||||
|
||||
刷新策略:
|
||||
|
||||
1. 软过期(stale-while-revalidate)
|
||||
- 先展示缓存,后台静默刷新。
|
||||
2. 硬过期
|
||||
- 超过硬过期后必须请求网络或提示数据过旧。
|
||||
3. 最小刷新间隔
|
||||
- 避免频繁切换/回前台引发抖动请求。
|
||||
|
||||
建议默认值:
|
||||
|
||||
1. `user:profile`:软过期 30min,硬过期 24h。
|
||||
2. `calendar:day`:软过期 2min,硬过期 30min。
|
||||
3. `calendar:month`:软过期 5min,硬过期 60min。
|
||||
4. `todo:list:pending`:软过期 2min,硬过期 30min。
|
||||
|
||||
### 5.4 个人信息缓存合并方案
|
||||
|
||||
现有 `SettingsUserCache` 并入统一缓存模块:
|
||||
|
||||
1. 新建 `UserProfileRepository`(或在现有 settings service 中引入统一缓存)。
|
||||
2. `getProfile()` 通过 hybrid cache 获取 `user:profile:me`。
|
||||
3. 更新 profile 成功后立即写回缓存并同步持久化。
|
||||
4. 登出/会话失效时统一调用 `invalidateNamespace('user')`。
|
||||
|
||||
## 6. 一致性风险与解决方案
|
||||
|
||||
平衡型缓存会存在“短暂陈旧窗口”。本设计通过以下机制将体验风险降到可接受范围。
|
||||
|
||||
### 6.1 触发刷新矩阵
|
||||
|
||||
1. 手动下拉刷新:强制网络刷新。
|
||||
2. 写操作成功:精准失效受影响 key 并触发回填。
|
||||
3. App 回前台:若超过最小刷新间隔,触发静默刷新。
|
||||
4. 网络离线 -> 在线:触发静默刷新。
|
||||
5. 进入关键详情页:按策略进行 freshness check。
|
||||
|
||||
### 6.2 写后一致性
|
||||
|
||||
1. 乐观更新:本地先更新 UI 与缓存,避免“我刚改完却没变”。
|
||||
2. 失败回滚:API 失败时恢复旧值并 Toast 提示。
|
||||
3. 精准失效:不做全局清空,只失效关联 key,兼顾一致性与性能。
|
||||
|
||||
### 6.3 并发安全
|
||||
|
||||
1. singleflight:同 key 同时只允许一个网络请求。
|
||||
2. 版本保护:缓存写入比较 `updatedAt/version`,拒绝旧响应覆盖新状态。
|
||||
3. 失败兜底:请求失败不清空旧缓存,保持可读并允许重试。
|
||||
|
||||
### 6.4 可见性保障
|
||||
|
||||
1. 页面可显示“上次同步时间”(轻提示)。
|
||||
2. 硬过期数据需可见提醒(弱提示,不阻断基础浏览)。
|
||||
3. 提供稳定手动刷新入口。
|
||||
|
||||
## 7. 导航与页面职责重构
|
||||
|
||||
### 7.1 路由重构
|
||||
|
||||
1. `app_router` 引入 shell 分支,不再平铺所有主页面。
|
||||
2. Dock 切换改为 branch index 切换,不再 `push` 主页面。
|
||||
3. Calendar 内部 month/day 切换改为视图切换,不新增栈层。
|
||||
4. 事件详情/编辑等保留 `push`(细节页合理叠栈)。
|
||||
|
||||
### 7.2 回主页逻辑修正
|
||||
|
||||
1. Dock Home 统一执行“切到 Home 分支/`go('/home')`”。
|
||||
2. `returnToHomePreserveState` 仅用于非 Dock 的返回策略场景。
|
||||
3. 消除 `canPop -> pop` 对主页按钮语义的影响。
|
||||
|
||||
### 7.3 页面职责收敛
|
||||
|
||||
1. Calendar/Todo 页面移除路由监听触发 `load`。
|
||||
2. 页面只调用 repository:
|
||||
- `get(policy)`
|
||||
- `refresh(force: true)`
|
||||
- `mutate(...) + invalidate(...)`
|
||||
3. 页面不直接感知“缓存在哪一层”。
|
||||
|
||||
## 8. 分阶段实施计划(里程碑)
|
||||
|
||||
### M1 导航壳层与切换语义
|
||||
|
||||
1. 引入 shell + 分支保活。
|
||||
2. Dock 接口改造与主 tab 切换实现。
|
||||
3. Home 按钮语义修正。
|
||||
|
||||
### M2 统一缓存骨架
|
||||
|
||||
1. 新增 core cache 模块。
|
||||
2. 接入 user profile(替换 `SettingsUserCache`)。
|
||||
3. DI 注入 cache store 与 invalidator。
|
||||
|
||||
### M3 Calendar 接入
|
||||
|
||||
1. 引入 `CalendarRepository` 与 day/month key。
|
||||
2. 移除 route listener 自动刷新。
|
||||
3. 切换 month/day 时默认走缓存,不触发无必要请求。
|
||||
|
||||
### M4 Todo 接入
|
||||
|
||||
1. 引入 `TodoRepository` 与 list/detail key。
|
||||
2. 拖拽、完成、编辑后的精准失效。
|
||||
3. 下拉刷新走强制网络。
|
||||
|
||||
### M5 清理与验证
|
||||
|
||||
1. 清理旧缓存与重复加载逻辑。
|
||||
2. 补齐测试与性能观测。
|
||||
3. 评估参数并收敛默认策略。
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
### 9.1 体验验收
|
||||
|
||||
1. Home/Calendar/Todo 切换无明显重建卡顿。
|
||||
2. 日/月切换响应明显变快。
|
||||
3. 首次冷启动可先看到本地缓存内容。
|
||||
4. Dock Home 始终回主页。
|
||||
|
||||
### 9.2 网络验收
|
||||
|
||||
1. 切换页面时网络请求显著减少。
|
||||
2. 写操作后关联数据可及时更新。
|
||||
3. 手动刷新可强制拉取并回写缓存。
|
||||
|
||||
### 9.3 一致性验收
|
||||
|
||||
1. 不出现旧响应覆盖新数据。
|
||||
2. 离线后恢复在线可自动静默同步。
|
||||
3. 软过期/硬过期行为符合策略定义。
|
||||
|
||||
## 10. 测试与验证计划
|
||||
|
||||
### 10.1 单元测试
|
||||
|
||||
1. `hybrid_cache_store`:命中链路、singleflight、软硬过期判定。
|
||||
2. `cache_invalidator`:写操作触发的 key 精准失效。
|
||||
3. repository:读缓存、后台刷新、失败兜底、版本保护。
|
||||
|
||||
### 10.2 组件/页面测试(高回归)
|
||||
|
||||
1. Dock 切换不重建分支主页面。
|
||||
2. 日/月切换不重复触发全量加载。
|
||||
3. Home 按钮行为稳定。
|
||||
|
||||
### 10.3 集成回归
|
||||
|
||||
1. Calendar -> Todo -> Calendar 多轮切换请求计数。
|
||||
2. Todo 完成后列表更新与缓存一致性。
|
||||
3. profile 更新后设置页/其他依赖页可见一致。
|
||||
|
||||
## 11. 风险与回滚
|
||||
|
||||
### 11.1 主要风险
|
||||
|
||||
1. 导航壳层改造可能引发深链与返回栈回归。
|
||||
2. 缓存策略参数不当可能造成陈旧感。
|
||||
3. 早期失效 key 设计不完整可能出现局部不刷新。
|
||||
|
||||
### 11.2 控制策略
|
||||
|
||||
1. 按里程碑逐步落地,每个里程碑可单独回滚。
|
||||
2. 默认保留手动刷新兜底。
|
||||
3. 增加请求计数与缓存命中日志(开发态)。
|
||||
|
||||
### 11.3 回滚策略
|
||||
|
||||
1. 若 M1 不稳定,可先回退 shell 改造并保留缓存模块。
|
||||
2. 若缓存接入问题集中,可按域回退(user/calendar/todo 分域开关)。
|
||||
|
||||
## 12. 待确认参数(实施前锁定)
|
||||
|
||||
1. 软/硬过期默认值是否按本设计直接采用。
|
||||
2. 是否立即展示“上次同步时间”。
|
||||
3. 是否在首版启用“网络恢复自动静默刷新”。
|
||||
@@ -0,0 +1,438 @@
|
||||
# 前端导航解耦与统一缓存重构 Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 完成 Home/Calendar/Todo 的解耦切换与统一缓存改造,实现本地优先显示、后台静默刷新、写后精准失效,并修复 Dock 回主页语义。
|
||||
|
||||
**Architecture:** 路由层采用 `StatefulShellRoute.indexedStack` 维持主分支保活;数据层新增 `core/cache` 统一缓存模块(memory + persistent + hybrid);业务层通过 repository 接入缓存策略,页面仅负责发意图和渲染状态。写操作触发精准失效,读取遵循 soft/hard TTL + minimum refresh interval。
|
||||
|
||||
**Tech Stack:** Flutter, go_router, get_it, shared_preferences, flutter_test, mocktail
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立统一缓存核心模型与策略
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/core/cache/cache_entry.dart`
|
||||
- Create: `apps/lib/core/cache/cache_key.dart`
|
||||
- Create: `apps/lib/core/cache/cache_policy.dart`
|
||||
- Test: `apps/test/core/cache/cache_policy_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/core/cache/cache_policy.dart';
|
||||
|
||||
void main() {
|
||||
test('soft expired should allow stale read with background refresh', () {
|
||||
final now = DateTime(2026, 3, 20, 12);
|
||||
final policy = CachePolicy(
|
||||
softTtl: const Duration(minutes: 2),
|
||||
hardTtl: const Duration(minutes: 30),
|
||||
minRefreshInterval: const Duration(minutes: 1),
|
||||
);
|
||||
|
||||
final fetchedAt = now.subtract(const Duration(minutes: 3));
|
||||
final decision = policy.evaluate(now: now, fetchedAt: fetchedAt);
|
||||
expect(decision.canUseCached, true);
|
||||
expect(decision.shouldRefreshInBackground, true);
|
||||
expect(decision.mustBlockForNetwork, false);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_policy_test.dart`
|
||||
Expected: FAIL with missing cache policy symbols.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class CacheDecision {
|
||||
final bool canUseCached;
|
||||
final bool shouldRefreshInBackground;
|
||||
final bool mustBlockForNetwork;
|
||||
const CacheDecision({
|
||||
required this.canUseCached,
|
||||
required this.shouldRefreshInBackground,
|
||||
required this.mustBlockForNetwork,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_policy_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/cache/cache_entry.dart apps/lib/core/cache/cache_key.dart apps/lib/core/cache/cache_policy.dart apps/test/core/cache/cache_policy_test.dart
|
||||
git commit -m "feat: add unified cache policy primitives"
|
||||
```
|
||||
|
||||
### Task 2: 实现 memory/persistent/hybrid cache store(含 singleflight)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/core/cache/cache_store.dart`
|
||||
- Create: `apps/lib/core/cache/memory_cache_store.dart`
|
||||
- Create: `apps/lib/core/cache/persistent_cache_store.dart`
|
||||
- Create: `apps/lib/core/cache/hybrid_cache_store.dart`
|
||||
- Test: `apps/test/core/cache/hybrid_cache_store_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('same key concurrent load should execute loader once', () async {
|
||||
var calls = 0;
|
||||
final store = HybridCacheStore(...);
|
||||
Future<String> loader() async {
|
||||
calls += 1;
|
||||
return 'ok';
|
||||
}
|
||||
await Future.wait([
|
||||
store.getOrLoad<String>('k', loader: loader),
|
||||
store.getOrLoad<String>('k', loader: loader),
|
||||
]);
|
||||
expect(calls, 1);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/hybrid_cache_store_test.dart`
|
||||
Expected: FAIL with missing HybridCacheStore.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
final Map<String, Future<dynamic>> _inflight = {};
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/hybrid_cache_store_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/cache/cache_store.dart apps/lib/core/cache/memory_cache_store.dart apps/lib/core/cache/persistent_cache_store.dart apps/lib/core/cache/hybrid_cache_store.dart apps/test/core/cache/hybrid_cache_store_test.dart
|
||||
git commit -m "feat: implement hybrid cache store with singleflight"
|
||||
```
|
||||
|
||||
### Task 3: 接入 DI 与统一失效器
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/core/cache/cache_invalidator.dart`
|
||||
- Modify: `apps/lib/core/di/injection.dart`
|
||||
- Test: `apps/test/core/cache/cache_invalidator_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('invalidate calendar day should also invalidate month key', () {
|
||||
final inv = CacheInvalidator(...);
|
||||
inv.invalidateCalendarDay(DateTime(2026, 3, 20));
|
||||
expect(inv.wasInvalidated('calendar:day:2026-03-20'), true);
|
||||
expect(inv.wasInvalidated('calendar:month:2026-03'), true);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_invalidator_test.dart`
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class CacheInvalidator {
|
||||
void invalidateCalendarDay(DateTime date) { /* invalidate day + month */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_invalidator_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/cache/cache_invalidator.dart apps/lib/core/di/injection.dart apps/test/core/cache/cache_invalidator_test.dart
|
||||
git commit -m "refactor: wire unified cache and invalidator in di"
|
||||
```
|
||||
|
||||
### Task 4: 合并个人信息缓存(替换 SettingsUserCache)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/settings/data/services/settings_user_cache.dart`
|
||||
- Create: `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
- Modify: `apps/lib/features/settings/ui/screens/settings_screen.dart`
|
||||
- Test: `apps/test/features/settings/data/services/settings_user_cache_test.dart`
|
||||
- Create: `apps/test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('repository should return persistent cache first then refresh in background', () async {
|
||||
// Arrange cached profile in persistent store
|
||||
// Assert immediate cached result + refresh called once
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class UserProfileCacheRepository {
|
||||
Future<UserResponse> getProfile({bool forceRefresh = false}) async { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd apps && flutter test test/features/settings/data/services/settings_user_cache_test.dart test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/settings/data/services/settings_user_cache.dart apps/lib/features/settings/data/services/user_profile_cache_repository.dart apps/lib/features/settings/ui/screens/settings_screen.dart apps/test/features/settings/data/services/settings_user_cache_test.dart apps/test/features/settings/data/services/user_profile_cache_repository_test.dart
|
||||
git commit -m "refactor: merge profile cache into unified cache repository"
|
||||
```
|
||||
|
||||
### Task 5: 路由改造为 StatefulShellRoute + Dock 切换分支
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/router/app_router.dart`
|
||||
- Modify: `apps/lib/core/router/app_routes.dart`
|
||||
- Modify: `apps/lib/features/calendar/ui/widgets/bottom_dock.dart`
|
||||
- Modify: `apps/lib/features/home/ui/navigation/home_return_policy.dart`
|
||||
- Test: `apps/test/core/router/app_routes_test.dart`
|
||||
- Modify: `apps/test/features/home/ui/navigation/home_return_policy_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('dock home action should always resolve to goHome', () {
|
||||
final action = resolveHomeReturnAction(canPop: true, isAuthEntry: false);
|
||||
expect(action, HomeReturnAction.goHomeForDock);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/features/home/ui/navigation/home_return_policy_test.dart`
|
||||
Expected: FAIL with old behavior.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
enum HomeReturnAction { pop, goHome, goHomeForDock }
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd apps && flutter test test/core/router/app_routes_test.dart test/features/home/ui/navigation/home_return_policy_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/router/app_router.dart apps/lib/core/router/app_routes.dart apps/lib/features/calendar/ui/widgets/bottom_dock.dart apps/lib/features/home/ui/navigation/home_return_policy.dart apps/test/core/router/app_routes_test.dart apps/test/features/home/ui/navigation/home_return_policy_test.dart
|
||||
git commit -m "feat: switch main navigation to stateful shell tabs"
|
||||
```
|
||||
|
||||
### Task 6: Calendar repository 化并移除路由监听刷新
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/data/services/calendar_repository.dart`
|
||||
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart`
|
||||
- Modify: `apps/lib/features/calendar/ui/screens/calendar_month_screen.dart`
|
||||
- Modify: `apps/lib/features/calendar/ui/calendar_state_manager.dart`
|
||||
- Create: `apps/test/features/calendar/data/services/calendar_repository_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('getDayEvents returns cache immediately and refreshes in background', () async {
|
||||
// Arrange cache day key
|
||||
// Assert cached list emitted before network completion
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/data/services/calendar_repository_test.dart`
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class CalendarRepository {
|
||||
Future<List<ScheduleItemModel>> getDayEvents(DateTime date, {bool forceRefresh = false}) async { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/data/services/calendar_repository_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/data/services/calendar_repository.dart apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart apps/lib/features/calendar/ui/screens/calendar_month_screen.dart apps/lib/features/calendar/ui/calendar_state_manager.dart apps/test/features/calendar/data/services/calendar_repository_test.dart
|
||||
git commit -m "refactor: decouple calendar screens from route-driven reload"
|
||||
```
|
||||
|
||||
### Task 7: Todo repository 化与写后精准失效
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/todo/data/todo_repository.dart`
|
||||
- Modify: `apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart`
|
||||
- Modify: `apps/lib/features/todo/data/todo_api.dart`
|
||||
- Create: `apps/test/features/todo/todo_repository_test.dart`
|
||||
- Modify: `apps/test/features/todo/quadrant_drag_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('complete todo should optimistically update and invalidate pending list key', () async {
|
||||
// assert local list updated first, invalidator called
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/features/todo/todo_repository_test.dart`
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class TodoRepository {
|
||||
Future<void> completeTodo(String id) async { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd apps && flutter test test/features/todo/todo_repository_test.dart test/features/todo/quadrant_drag_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/todo/data/todo_repository.dart apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart apps/lib/features/todo/data/todo_api.dart apps/test/features/todo/todo_repository_test.dart apps/test/features/todo/quadrant_drag_test.dart
|
||||
git commit -m "feat: add todo cache repository and precise invalidation"
|
||||
```
|
||||
|
||||
### Task 8: App 生命周期与网络恢复刷新策略
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/core/cache/cache_refresh_coordinator.dart`
|
||||
- Modify: `apps/lib/main.dart`
|
||||
- Create: `apps/test/core/cache/cache_refresh_coordinator_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('resume should trigger refresh only when min interval elapsed', () {
|
||||
// Arrange last refreshed timestamp
|
||||
// Assert callback invocation count
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_refresh_coordinator_test.dart`
|
||||
Expected: FAIL.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```dart
|
||||
class CacheRefreshCoordinator with WidgetsBindingObserver { ... }
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd apps && flutter test test/core/cache/cache_refresh_coordinator_test.dart`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/cache/cache_refresh_coordinator.dart apps/lib/main.dart apps/test/core/cache/cache_refresh_coordinator_test.dart
|
||||
git commit -m "feat: add app lifecycle refresh coordinator"
|
||||
```
|
||||
|
||||
### Task 9: 全量验证与文档同步
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/protocols/*`(仅当路由/数据契约文档需更新时)
|
||||
- Modify: `docs/plans/2026-03-20-navigation-cache-decoupling-design.md`(回填最终参数)
|
||||
|
||||
**Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd apps && flutter test test/core/cache test/features/settings/data/services/settings_user_cache_test.dart test/features/calendar test/features/todo test/features/home/ui/navigation/home_return_policy_test.dart test/core/router/app_routes_test.dart
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 2: Run app-level verification**
|
||||
|
||||
Run: `cd apps && flutter test`
|
||||
Expected: PASS.
|
||||
|
||||
**Step 3: Static checks**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: No errors.
|
||||
|
||||
**Step 4: Manual verification checklist**
|
||||
|
||||
1. 冷启动先显示缓存,随后静默更新。
|
||||
2. Home/Calendar/Todo 来回切换不重建主页面。
|
||||
3. 日/月切换不触发无必要请求。
|
||||
4. Dock Home 始终回主页。
|
||||
5. 写后数据可见一致,失败可回滚提示。
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-03-20-navigation-cache-decoupling-design.md docs/protocols
|
||||
git commit -m "docs: finalize navigation decoupling and unified cache rollout"
|
||||
```
|
||||
|
||||
## 实施顺序约束
|
||||
|
||||
1. 必须先完成 Task 1-3 再改业务页面(否则会出现重复实现)。
|
||||
2. Task 5(路由壳层)与 Task 6/7(业务接入)要分开提交,便于回滚。
|
||||
3. 每个 Task 的测试必须在本 Task 完成后立即执行,避免堆积回归。
|
||||
4. 不允许在未通过 focused tests 的情况下进入全量验证。
|
||||
|
||||
## 回滚策略
|
||||
|
||||
1. 若导航回归:回滚 Task 5 提交,保留缓存模块提交。
|
||||
2. 若缓存一致性异常:优先回滚 Task 6/7 的 repository 接入提交。
|
||||
3. 若生命周期刷新过于频繁:关闭 Task 8 coordinator 挂载,保留手动刷新兜底。
|
||||
|
||||
## Done 定义
|
||||
|
||||
1. 所有测试与 analyze 通过。
|
||||
2. 主页按钮行为稳定,无“返回上一页”误行为。
|
||||
3. 切换页面请求数明显下降,写后一致性符合设计预期。
|
||||
4. 统一缓存已接管用户信息、日历、待办三域。
|
||||
Reference in New Issue
Block a user