Files
social-app/docs/plans/2026-03-20-navigation-cache-decoupling-implementation-plan.md
T

538 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端导航解耦与统一缓存重构 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 完成导航分级回退(一级唯一 Home)与统一缓存改造,实现本地优先显示、后台静默刷新、写后精准失效,并落地“提醒取消即实时归档 + App 关闭时延迟归档兜底”。
**Architecture:** 路由层采用“一级唯一 Home + 二级业务页 + 三级细节页”的分级返回模型,二级页面返回统一回 Home,退出入口仅 Home;数据层新增 `core/cache` 统一缓存模块(memory + persistent + hybrid);业务层通过 repository 接入缓存策略与失效器。提醒动作采用实时归档优先,pending outbox 仅用于 App 不可用场景兜底。
**Tech Stack:** Flutter, go_router, get_it, shared_preferences, flutter_test, mocktail
---
### Task 0: 锁定导航分级与退出语义(一级/二级/三级)
**Files:**
- Modify: `apps/lib/core/router/app_router.dart`
- Modify: `apps/lib/features/home/ui/navigation/home_return_policy.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/todo/ui/screens/todo_quadrants_screen.dart`
- Modify: `apps/lib/features/settings/ui/screens/settings_screen.dart`
- Test: `apps/test/features/home/ui/navigation/home_return_policy_test.dart`
**Step 1: Write the failing test**
```dart
test('second-level pages should return to home instead of exiting app', () {
final action = resolveHomeReturnAction(
canPop: false,
isAuthEntry: false,
forceGoHome: true,
);
expect(action, HomeReturnAction.goHome);
});
```
**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 return behavior.
**Step 3: Write minimal implementation**
```dart
if (forceGoHome) return HomeReturnAction.goHome;
```
**Step 4: Run tests to verify they pass**
Run: `cd apps && flutter test 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/features/home/ui/navigation/home_return_policy.dart apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart apps/lib/features/calendar/ui/screens/calendar_month_screen.dart apps/lib/features/todo/ui/screens/todo_quadrants_screen.dart apps/lib/features/settings/ui/screens/settings_screen.dart apps/test/features/home/ui/navigation/home_return_policy_test.dart
git commit -m "feat: enforce hierarchical back navigation and home-only exit"
```
### 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: `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
- Modify: `apps/lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart`
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
- Modify: `apps/lib/main.dart`
- Modify: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart`
- Modify: `apps/test/features/calendar/reminders/reminder_notification_bridge_test.dart`
**Step 1: Write the failing test**
```dart
test('archive action should send remote archive immediately when app active', () async {
// Arrange active app + online gateway
// Act archive action
// Assert remote archive called once and local pending outbox not created
});
```
**Step 2: Run test to verify it fails**
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
Expected: FAIL with delayed-only behavior.
**Step 3: Write minimal implementation**
```dart
if (isAppActive) {
await repository.archiveNow(eventId);
} else {
await outbox.enqueueArchive(eventId);
}
```
**Step 4: Run tests to verify they pass**
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_action_executor_test.dart test/features/calendar/reminders/reminder_notification_bridge_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/calendar/reminders/reminder_action_executor.dart apps/lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart apps/lib/core/notifications/local_notification_service.dart apps/lib/main.dart apps/test/features/calendar/reminders/reminder_action_executor_test.dart apps/test/features/calendar/reminders/reminder_notification_bridge_test.dart
git commit -m "fix: prioritize realtime reminder archive with cold-start fallback"
```
### Task 10: 全量验证与文档同步
**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. 写后数据可见一致,失败可回滚提示。
6. 二级页面侧滑返回只回 Home,不直接退出。
7. 提醒点击取消时立刻归档;仅在 App 不可用时走 pending 兜底。
**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 0-3 再改业务页面(否则会出现重复实现)。
2. Task 0(分级返回)与 Task 5(路由壳层)要分开提交,便于单独回滚。
3. 每个 Task 的测试必须在本 Task 完成后立即执行,避免堆积回归。
4. 不允许在未通过 focused tests 的情况下进入全量验证。
## 回滚策略
1. 若返回语义回归:先回滚 Task 0 提交,再评估 Task 5。
2. 若缓存一致性异常:优先回滚 Task 6/7 的 repository 接入提交。
3. 若生命周期刷新过于频繁:关闭 Task 8 coordinator 挂载,保留手动刷新兜底。
4. 若提醒实时归档异常:回滚 Task 9,仅保留 outbox 兜底路径。
## Done 定义
1. 所有测试与 analyze 通过。
2. 主页按钮行为稳定,无“返回上一页”误行为。
3. 切换页面请求数明显下降,写后一致性符合设计预期。
4. 统一缓存已接管用户信息、日历、待办三域。
5. 二级页面不再可直接侧滑退出 App。
6. 提醒归档满足“实时优先、关闭兜底”策略。