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

439 lines
15 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/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. 统一缓存已接管用户信息、日历、待办三域。