refactor(apps): 主题系统迁移至 ColorScheme + 扩展架构并支持 Dark Mode
This commit is contained in:
@@ -1,140 +0,0 @@
|
||||
# Repository 缓存层抽象优化
|
||||
|
||||
## 问题描述
|
||||
|
||||
### 现有架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ HybridCacheStore │
|
||||
│ (Memory + Persistent 二级缓存) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ CacheEntry<T> │
|
||||
│ (value + fetchedAt 时间戳) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ CachePolicy │
|
||||
│ (softTtl / hardTtl / minRefreshInterval)│
|
||||
├─────────────────────────────────────────┤
|
||||
│ CacheInvalidator │
|
||||
│ (统一失效管理) │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CalendarRepository │ ← 重复实现
|
||||
│ TodoRepository │ ← 重复实现
|
||||
│ UsersRepository │ ← 重复实现
|
||||
│ ... │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 重复内容
|
||||
|
||||
| 重复内容 | 例子 |
|
||||
|---------|------|
|
||||
| key 命名空间 | `calendar:day:$day`、`todo:list:pending` |
|
||||
| 缓存读取逻辑 | `store.read<CacheEntry<...>>(key)` |
|
||||
| 数据转换 | API 返回 → CacheEntry 包装 |
|
||||
| 刷新逻辑 | `_refreshDayAndRead()` |
|
||||
| 强制刷新 | `forceRefresh` 参数处理 |
|
||||
| 后台刷新防重 | `_refreshInFlight` map |
|
||||
|
||||
### 涉及文件
|
||||
|
||||
- `apps/lib/features/calendar/data/services/calendar_repository.dart`
|
||||
- `apps/lib/features/todo/data/todo_repository.dart`
|
||||
- `apps/lib/features/contacts/data/users/users_repository_impl.dart`
|
||||
- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
|
||||
## 建议方案
|
||||
|
||||
### 1. 抽取 `CachedRepository` 基类
|
||||
|
||||
```dart
|
||||
abstract class CachedRepository<T, R> {
|
||||
HybridCacheStore get store;
|
||||
CacheInvalidator get invalidator;
|
||||
CachePolicy get policy;
|
||||
|
||||
String get namespace; // 'calendar', 'todo', etc.
|
||||
|
||||
Future<T> getOrLoad(
|
||||
String key, {
|
||||
bool forceRefresh = false,
|
||||
required Future<R> Function() loader,
|
||||
});
|
||||
|
||||
Future<void> invalidate(String key);
|
||||
|
||||
String buildKey(String suffix);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 各模块简化
|
||||
|
||||
```dart
|
||||
// CalendarRepository
|
||||
class CalendarRepository extends CachedRepository<List<ScheduleItemModel>, ScheduleItemModel> {
|
||||
@override
|
||||
String get namespace => 'calendar';
|
||||
|
||||
@override
|
||||
Future<List<ScheduleItemModel>> getDayEvents(DateTime date, {bool forceRefresh}) {
|
||||
return getOrLoad(
|
||||
'day:${_formatDate(date)}',
|
||||
forceRefresh: forceRefresh,
|
||||
loader: () => calendarService.getEventsForDay(date),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) =>
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// TodoRepository
|
||||
class TodoRepository extends CachedRepository<List<TodoResponse>, TodoResponse> {
|
||||
@override
|
||||
String get namespace => 'todo';
|
||||
|
||||
Future<List<TodoResponse>> getPendingTodos({bool forceRefresh = false}) {
|
||||
return getOrLoad(
|
||||
'list:pending',
|
||||
forceRefresh: forceRefresh,
|
||||
loader: () => api.getPendingTodos(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 可选:泛型缓存装饰器
|
||||
|
||||
```dart
|
||||
class CachedApiCall<T> {
|
||||
final HybridCacheStore store;
|
||||
final CachePolicy policy;
|
||||
final String key;
|
||||
final DateTime Function() now;
|
||||
|
||||
Future<T> execute(Future<T> Function() loader);
|
||||
}
|
||||
```
|
||||
|
||||
## 收益
|
||||
|
||||
| 收益 | 说明 |
|
||||
|------|------|
|
||||
| 减少重复代码 | 各 Repository 移除 60%+ 相似逻辑 |
|
||||
| 统一缓存行为 | 刷新策略、key 格式、并发控制一致 |
|
||||
| 易维护 | 修复 bug 或优化逻辑只需改一处 |
|
||||
| 易测试 | 基类可独立测试,子类继承即可 |
|
||||
|
||||
## 前置依赖
|
||||
|
||||
- 现有 `HybridCacheStore`、`CacheEntry`、`CachePolicy`、`CacheInvalidator` 已就绪
|
||||
- 无需引入新依赖
|
||||
|
||||
## 状态
|
||||
|
||||
- [ ] 待评估优先级
|
||||
- [ ] 待设计 CachedRepository 基类接口
|
||||
- [ ] 先在一个 Repository 上试点
|
||||
- [ ] 推广到其他 Repository
|
||||
@@ -1,90 +0,0 @@
|
||||
# AppTheme 硬编码颜色且缺失 Dark Mode
|
||||
|
||||
## 问题描述
|
||||
|
||||
### 1. 颜色硬编码
|
||||
|
||||
`AppTheme` 和各组件大量直接引用 `AppColors` 静态常量,而非 `Theme.of(context).colorScheme`:
|
||||
|
||||
```dart
|
||||
// app_theme.dart
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.background, // 硬编码
|
||||
foregroundColor: AppColors.slate900, // 硬编码
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary, // 硬编码
|
||||
foregroundColor: AppColors.primaryForeground,
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
这导致:
|
||||
- 主题切换时颜色不会改变
|
||||
- 组件无法响应系统深色模式
|
||||
- 违反 Flutter Material Design 规范
|
||||
|
||||
### 2. 缺失 Dark Mode
|
||||
|
||||
`AppTheme` 只有 `light` getter,没有 `dark`:
|
||||
|
||||
```dart
|
||||
static ThemeData get light => ThemeData(...);
|
||||
```
|
||||
|
||||
`LinksyApp` 硬编码使用 light:
|
||||
```dart
|
||||
theme: AppTheme.light,
|
||||
locale: const Locale('zh'),
|
||||
```
|
||||
|
||||
## 正确做法
|
||||
|
||||
### 颜色应使用 ThemeData
|
||||
|
||||
```dart
|
||||
// 正确示例
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
|
||||
// ColorScheme 应由 ThemeData 生成
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primary,
|
||||
brightness: Brightness.light, // 或 Brightness.dark
|
||||
),
|
||||
```
|
||||
|
||||
### 支持 Dark Mode
|
||||
|
||||
```dart
|
||||
class AppTheme {
|
||||
static ThemeData get light => ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primary,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData get dark => ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primary,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `apps/lib/core/theme/app_theme.dart`
|
||||
- `apps/lib/core/theme/design_tokens.dart`
|
||||
|
||||
## 修复优先级
|
||||
|
||||
**低** - 当前只有 light 模式,不影响功能
|
||||
@@ -1,43 +0,0 @@
|
||||
# AuthSessionBootstrapper 旧代码应删除
|
||||
|
||||
## 文件位置
|
||||
|
||||
`apps/lib/app/startup/auth_session_bootstrapper.dart`
|
||||
|
||||
## 问题描述
|
||||
|
||||
`AuthSessionBootstrapper` 是遗留代码,用于在用户登录时同步日历事件和通知提醒。
|
||||
|
||||
### 代码问题
|
||||
|
||||
```dart
|
||||
Future<void> syncForAuthState(AuthState state) async {
|
||||
if (state is! AuthAuthenticated) {
|
||||
_syncedUserId = null;
|
||||
return;
|
||||
}
|
||||
// 获取180天日历事件并重建通知提醒
|
||||
final events = await _calendarService.getEventsForRange(start, end);
|
||||
await _notificationService.rebuildUpcomingReminders(events);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
1. **同步逻辑已迁移** - `CalendarService` 和 `LocalNotificationService` 应自己管理缓存生命周期,无需登录时手动触发
|
||||
2. **内存缓存不可靠** - `_syncedUserId` 仅内存存储,App 重启后失效
|
||||
3. **静默失败** - 同步失败被 `catch (_)` 吞掉,无日志无重试
|
||||
4. **180 天硬编码** - 时间范围未从配置读取
|
||||
|
||||
## 处理方式
|
||||
|
||||
**直接删除**:
|
||||
- 删除 `apps/lib/app/startup/auth_session_bootstrapper.dart`
|
||||
- 确认无调用处后,清理 `startup/` 目录(若为空)
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `apps/lib/app/startup/auth_session_bootstrapper.dart`
|
||||
|
||||
## 修复优先级
|
||||
|
||||
**低** - 功能层面暂无影响,但属于应清理的技术债
|
||||
@@ -1,49 +0,0 @@
|
||||
# LinksyApp 强制依赖 ChatBloc
|
||||
|
||||
## 问题描述
|
||||
|
||||
`LinksyApp` (app.dart) 作为应用根节点,被迫在 `MultiBlocProvider` 中注入 `ChatBloc`:
|
||||
|
||||
```dart
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthBloc>.value(value: authBloc),
|
||||
BlocProvider<ChatBloc>(
|
||||
create: (_) => ChatBloc(apiClient: sl<IApiClient>()),
|
||||
),
|
||||
],
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
这导致:
|
||||
1. 应用启动时就创建 `ChatBloc` 实例(内存浪费)
|
||||
2. `LinksyApp` 需要知道"存在 ChatBloc 这个 Feature"
|
||||
3. 违反单一职责原则:根节点应只负责全局配置,不应了解具体 Feature
|
||||
|
||||
## 根本原因
|
||||
|
||||
`HomeScreen` 是默认首页,其内部需要 `ChatBloc`。为了让它通过 `context.read<ChatBloc>()` 获取,被迫在根节点提供。
|
||||
|
||||
## 正确做法
|
||||
|
||||
ChatBloc 应该在路由级别按需注入:
|
||||
|
||||
```dart
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_) => ChatBloc(apiClient: sl<IApiClient>()),
|
||||
child: const HomeScreen(),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `apps/lib/app/app.dart`
|
||||
- `apps/lib/features/home/presentation/screens/home_screen.dart`
|
||||
|
||||
## 修复优先级
|
||||
|
||||
**中等** - 功能正常但架构不合理,属于技术债
|
||||
@@ -1,118 +0,0 @@
|
||||
# main.dart 与认证模块耦合
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前 `main.dart` 直接依赖了 `AuthBloc` 和 `AuthStarted`,违反了依赖反转原则。
|
||||
|
||||
## 当前代码
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
import 'features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import 'features/auth/presentation/bloc/auth_event.dart';
|
||||
|
||||
void main() async {
|
||||
// ...
|
||||
final authBloc = sl<AuthBloc>();
|
||||
authBloc.add(AuthStarted());
|
||||
runApp(LinksyApp(authBloc: authBloc));
|
||||
}
|
||||
```
|
||||
|
||||
## 问题
|
||||
|
||||
1. **启动逻辑与认证模块耦合**
|
||||
- main.dart 需要知道 `AuthBloc` 的存在
|
||||
- 需要知道 `AuthStarted` 事件
|
||||
- 需要手动触发启动事件
|
||||
|
||||
2. **AuthBloc 被暴露3层**
|
||||
```
|
||||
main.dart → LinksyApp → createAppRouter → redirect()
|
||||
```
|
||||
每层都传 authBloc,不优雅
|
||||
|
||||
## 建议方案
|
||||
|
||||
### 1. 启动逻辑下沉到 LinksyApp
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await configureDependencies();
|
||||
await AppConstants.init();
|
||||
runApp(LinksyApp());
|
||||
}
|
||||
|
||||
// app.dart (LinksyApp)
|
||||
class LinksyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authBloc = sl<AuthBloc>();
|
||||
authBloc.add(AuthStarted());
|
||||
|
||||
return BlocProvider.value(
|
||||
value: authBloc,
|
||||
child: // ...
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 路由守卫由 LinksyApp 内部管理
|
||||
|
||||
```dart
|
||||
// app.dart
|
||||
class LinksyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
final router = GoRouter.of(context);
|
||||
if (state is AuthUnauthenticated) {
|
||||
router.go(AppRoutes.authLogin);
|
||||
} else if (state is AuthAuthenticated) {
|
||||
if (router.matchedLocation == AppRoutes.authLogin) {
|
||||
router.go(AppRoutes.homeMain);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MaterialApp.router(
|
||||
routerConfig: createAppRouter(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. createAppRouter 不再需要 authBloc 参数
|
||||
|
||||
```dart
|
||||
// app_router.dart
|
||||
GoRouter createAppRouter() {
|
||||
return GoRouter(
|
||||
// 不再有 redirect 回调
|
||||
// 路由守卫由 BlocListener 在 LinksyApp 统一处理
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 收益
|
||||
|
||||
| 收益 | 说明 |
|
||||
|------|------|
|
||||
| 解耦 | main.dart 完全不知道 AuthBloc 存在 |
|
||||
| 单一职责 | LinksyApp 统一管理状态监听和路由跳转 |
|
||||
| 易测试 | main.dart 不再需要 mock AuthBloc |
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `apps/lib/main.dart`
|
||||
- `apps/lib/app/app.dart`
|
||||
- `apps/lib/app/router/app_router.dart`
|
||||
- `apps/lib/features/auth/presentation/bloc/auth_bloc.dart`
|
||||
|
||||
## 状态
|
||||
|
||||
- [ ] 待修复
|
||||
@@ -1,85 +0,0 @@
|
||||
# SharedPreferences 缺少统一管理模型
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前 `SharedPreferences` 的使用散落各处,缺乏统一的数据模型约束:
|
||||
|
||||
### 现状
|
||||
|
||||
1. **Key 散落**
|
||||
- `reminder_notification_callbacks.dart` 中定义:`'calendar_reminder_pending_notification_responses_v1'`
|
||||
- 各处直接使用字符串 key,容易拼写错误或冲突
|
||||
|
||||
2. **重复获取实例**
|
||||
```dart
|
||||
final prefs = await SharedPreferences.getInstance(); // 每次都重新获取
|
||||
```
|
||||
|
||||
3. **序列化逻辑分散**
|
||||
- `ReminderNotificationCallbacks` 自己处理 JSON 序列化/反序列化
|
||||
- 其他模块可能重复相同逻辑
|
||||
|
||||
4. **注册但未统一封装**
|
||||
- `injection.dart` 只注册了 `SharedPreferences` 实例
|
||||
- 没有封装成可复用的数据访问层
|
||||
|
||||
## 影响
|
||||
|
||||
- 维护困难:Key 散落,修改时需要全局搜索
|
||||
- 容易出错:拼写错误难以发现
|
||||
- 代码重复:序列化逻辑可能在多处重复实现
|
||||
- 可测试性差:直接依赖 `SharedPreferences.getInstance()`
|
||||
|
||||
## 建议方案
|
||||
|
||||
### 1. 创建 `AppPreferences` 数据模型
|
||||
|
||||
```dart
|
||||
class AppPreferences {
|
||||
static const String _pendingNotificationsKey =
|
||||
'calendar_reminder_pending_notification_responses_v1';
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
AppPreferences(this._prefs);
|
||||
|
||||
List<NotificationResponse> get pendingNotifications {
|
||||
final list = _prefs.getStringList(_pendingNotificationsKey) ?? [];
|
||||
return list.map(_decode).toList();
|
||||
}
|
||||
|
||||
Future<void> setPendingNotifications(List<NotificationResponse> value) {
|
||||
return _prefs.setStringList(_pendingNotificationsKey, value.map(_encode).toList());
|
||||
}
|
||||
|
||||
// 其他偏好设置...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在 injection.dart 中注册
|
||||
|
||||
```dart
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
sl.registerSingleton<AppPreferences>(AppPreferences(sharedPreferences));
|
||||
```
|
||||
|
||||
### 3. 使用方通过接口访问
|
||||
|
||||
```dart
|
||||
// 之前
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(key, value);
|
||||
|
||||
// 之后
|
||||
sl<AppPreferences>().setPendingNotifications(value);
|
||||
```
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `apps/lib/app/di/injection.dart` - 注册逻辑
|
||||
- `apps/lib/features/notification/data/services/reminder_notification_callbacks.dart` - 主要使用方
|
||||
- `apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart` - 另一使用方
|
||||
|
||||
## 状态
|
||||
|
||||
- [ ] 待修复
|
||||
@@ -1,123 +0,0 @@
|
||||
# 服务层与 Repository 层职责混乱
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前 `CalendarService`、`SettingsUserCache`、`UserProfileCacheRepository` 等服务/仓库职责边界模糊,存在大量重复逻辑和不必要的封装。
|
||||
|
||||
## 问题1:SettingsUserCache 不该存在
|
||||
|
||||
### 当前结构
|
||||
|
||||
```
|
||||
SettingsUserCache UserProfileCacheRepository
|
||||
┌─────────────────┐ ┌─────────────────────────┐
|
||||
│ - _cachedUser │ ←→ │ - HybridCacheStore │
|
||||
│ - getProfile() │ │ - CachePolicy │
|
||||
│ - set() │ │ - getProfile() │
|
||||
│ - invalidate() │ │ - setCached() │
|
||||
└─────────────────┘ └─────────────────────────┘
|
||||
```
|
||||
|
||||
### 问题
|
||||
|
||||
- `SettingsUserCache` 只是给 `UserProfileCacheRepository` 包了一层内存缓存
|
||||
- 两者的 `getProfile()`、`invalidate()` 逻辑几乎相同
|
||||
- 这是重复包装,应该合并
|
||||
|
||||
## 问题2:Repository 缓存逻辑重复
|
||||
|
||||
### 涉及文件
|
||||
|
||||
- `apps/lib/features/calendar/data/services/calendar_repository.dart`
|
||||
- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
- `apps/lib/features/todo/data/todo_repository.dart`
|
||||
|
||||
### 代码重复率:90%
|
||||
|
||||
```dart
|
||||
// CalendarRepository
|
||||
Future<List<ScheduleItemModel>> getDayEvents({bool forceRefresh}) async {
|
||||
if (forceRefresh) return _refreshDayAndRead(...);
|
||||
final cached = await store.read<CacheEntry<...>>(key);
|
||||
if (cached == null) return _refreshDayAndRead(...);
|
||||
final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt);
|
||||
if (decision.shouldRefreshInBackground) _refreshInBackground();
|
||||
if (decision.mustBlockForNetwork || !decision.canUseCached) {
|
||||
return _refreshDayAndRead(...);
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
// UserProfileCacheRepository
|
||||
Future<UserResponse> getProfile({bool forceRefresh}) async {
|
||||
if (forceRefresh) return _refreshAndRead();
|
||||
final cached = await store.read<CacheEntry<...>>(cacheKey);
|
||||
if (cached == null) return _refreshAndRead();
|
||||
final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt);
|
||||
if (decision.shouldRefreshInBackground) _refreshInBackground();
|
||||
if (decision.mustBlockForNetwork || !decision.canUseCached) {
|
||||
return _refreshAndRead();
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
```
|
||||
|
||||
## 问题3:CalendarService 不必要的延迟初始化
|
||||
|
||||
```dart
|
||||
class CalendarService {
|
||||
CalendarApi? _calendarApi;
|
||||
|
||||
CalendarApi get _api {
|
||||
if (_calendarApi != null) return _calendarApi;
|
||||
_calendarApi = CalendarApi(_apiClient); // 为什么懒加载?
|
||||
return _calendarApi;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
已经传入了 `IApiClient`,API 还在构造时懒加载,多此一举。
|
||||
|
||||
## 问题4:分层不清
|
||||
|
||||
| 类名 | 类型 | 问题 |
|
||||
|------|------|------|
|
||||
| `CalendarService` | Service | 依赖 Repository,该叫 Repository |
|
||||
| `UserProfileCacheRepository` | Repository | 名字带 Cache,但 Repository 都带缓存 |
|
||||
| `SettingsUserCache` | ??? | 内存缓存层,不该独立存在 |
|
||||
| `TodoRepository` | Repository | 正确 |
|
||||
|
||||
## 应该的设计
|
||||
|
||||
```
|
||||
Repository 层(纯数据 + 缓存)
|
||||
├── CalendarRepository ← 继承 CachedRepository
|
||||
├── UserProfileRepository ← 继承 CachedRepository
|
||||
└── TodoRepository ← 继承 CachedRepository
|
||||
|
||||
Service 层(业务逻辑 + 跨 Repository 编排)
|
||||
├── CalendarService ← 只做业务编排,不直接调 API
|
||||
├── NotificationService ← 跨模块通知逻辑
|
||||
└── ReminderActionExecutor ← 跨模块提醒执行
|
||||
```
|
||||
|
||||
## 修复步骤
|
||||
|
||||
1. **删除** `SettingsUserCache`,合并到 `UserProfileCacheRepository`
|
||||
2. **抽取** `CachedRepository` 基类(见 `docs/todo/2026-03-27-repository缓存抽象.md`)
|
||||
3. **简化** `CalendarService`,移除不必要的懒加载
|
||||
4. **统一命名**:
|
||||
- 带缓存的 Repository 统一继承基类
|
||||
- Service 只做业务编排,不处理缓存
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `apps/lib/features/calendar/data/services/calendar_service.dart`
|
||||
- `apps/lib/features/calendar/data/services/calendar_repository.dart`
|
||||
- `apps/lib/features/settings/data/services/settings_user_cache.dart`
|
||||
- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
- `apps/lib/features/todo/data/todo_repository.dart`
|
||||
|
||||
## 状态
|
||||
|
||||
- [ ] 待修复
|
||||
@@ -1,34 +0,0 @@
|
||||
# 路由语义混乱:根路径 `/` 定义为登录页
|
||||
|
||||
## 问题描述
|
||||
|
||||
`app_routes.dart` 中根路径 `/` 被定义为登录页:
|
||||
|
||||
```dart
|
||||
static const authBoot = '/boot';
|
||||
static const authLogin = '/'; // 根路径是登录页
|
||||
static const homeMain = '/home'; // 首页反而在 /home
|
||||
```
|
||||
|
||||
这导致:
|
||||
- `/` 应该指向首页的直觉 expectation 违反
|
||||
- 根路径无法放置真实首页内容
|
||||
- 与 `homeMain = '/home'` 语义不一致
|
||||
|
||||
## 正确做法
|
||||
|
||||
根路径 `/` 应保留给首页,登录页应使用独立路径如 `/login`:
|
||||
|
||||
```dart
|
||||
static const authLogin = '/login';
|
||||
static const homeMain = '/';
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `apps/lib/app/router/app_routes.dart`
|
||||
- `apps/lib/app/router/app_router.dart`
|
||||
|
||||
## 修复优先级
|
||||
|
||||
**低** - 功能正常,属于历史遗留设计问题
|
||||
@@ -0,0 +1,126 @@
|
||||
# App Theme Dark Mode and Hardcoded Color Migration Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Enable automatic light/dark mode following system settings and gradually migrate UI color usage from direct `AppColors` references to theme-driven semantic colors.
|
||||
|
||||
**Architecture:** Introduce a dual-theme foundation in `AppTheme` (`light` and `dark`) and route app root to `ThemeMode.system`. Then migrate color access in batches: first shared widgets, then high-traffic feature pages, then long-tail modules. Keep `AppColors` as design-token source only inside theme/token layers during migration.
|
||||
|
||||
**Tech Stack:** Flutter Material 3 (`ThemeData`, `ColorScheme`, `ThemeMode`), existing design tokens (`AppColors`, `AppSpacing`, `AppRadius`), flutter analyze/test verification.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Theme Foundation and System Dark Mode Wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/theme/app_theme.dart`
|
||||
- Modify: `apps/lib/app/app.dart`
|
||||
|
||||
**Step 1: Build dual-theme entry points**
|
||||
|
||||
- Add `AppTheme.dark` and a shared private builder `AppTheme._buildTheme(Brightness)`.
|
||||
- Ensure light and dark themes both use Material 3 and `ColorScheme.fromSeed`.
|
||||
|
||||
**Step 2: Remove theme-level hardcoded semantic colors**
|
||||
|
||||
- Replace direct `AppColors` usage in app bar/button/input decoration theme with color-scheme-driven values.
|
||||
- Keep radii and spacing tokens unchanged.
|
||||
|
||||
**Step 3: Wire root app to system mode**
|
||||
|
||||
- In `MaterialApp.router`, set `theme`, `darkTheme`, and `themeMode: ThemeMode.system`.
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: no new analyzer errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Shared Widget Batch (P0) - Replace Direct AppColors with Theme Semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/shared/widgets/error_retry_surface.dart`
|
||||
- Modify: `apps/lib/shared/widgets/confirm_sheet.dart`
|
||||
- Modify: `apps/lib/shared/widgets/destructive_action_sheet.dart`
|
||||
- Modify: `apps/lib/shared/widgets/toast/toast_type_config.dart`
|
||||
|
||||
**Step 1: Migrate simple error surface**
|
||||
|
||||
- Use `Theme.of(context).colorScheme.error` for retry message color.
|
||||
|
||||
**Step 2: Migrate confirm/destructive sheets**
|
||||
|
||||
- Replace sheet background/border/text/primary-action color bindings with `ColorScheme` semantic roles (`surface`, `outlineVariant`, `onSurface`, `onSurfaceVariant`, `primary`, `onPrimary`, `error`, `onError`).
|
||||
|
||||
**Step 3: Migrate toast style mapping**
|
||||
|
||||
- Replace static feedback colors with semantic color-scheme mappings per toast type.
|
||||
- Preserve existing icon and l10n label behavior.
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: no new analyzer errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Batch Migration Governance and Rollout Rules
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md`
|
||||
|
||||
**Step 1: Document batch strategy**
|
||||
|
||||
- Add phased rollout strategy: Foundation -> Shared widgets (P0) -> Core pages (P1) -> Long tail (P2) -> guardrails.
|
||||
|
||||
**Step 2: Define acceptance and rollback criteria**
|
||||
|
||||
- Include per-batch verification (`flutter analyze`, targeted widget tests/manual dark mode checks).
|
||||
- Require small-scope commits per batch for safe rollback.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: P1/P2 Execution Backlog (Follow-up)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/home/**`
|
||||
- Modify: `apps/lib/features/settings/**`
|
||||
- Modify: `apps/lib/features/auth/**`
|
||||
- Modify: remaining `apps/lib/features/**`
|
||||
|
||||
**Step 1: Prioritize by user path**
|
||||
|
||||
- P1 order: home -> settings -> auth.
|
||||
- P2: remaining feature pages and low-frequency screens.
|
||||
|
||||
**Step 2: Migrate per file with semantic mapping**
|
||||
|
||||
- Replace direct `AppColors` usage in widget code with `Theme.of(context).colorScheme` or scoped theme helpers.
|
||||
- Keep visual behavior equivalent first; optimize polish after semantic migration is stable.
|
||||
|
||||
**Step 3: Verify per batch**
|
||||
|
||||
Run:
|
||||
- `cd apps && flutter analyze`
|
||||
- `cd apps && flutter test` (targeted where tests exist)
|
||||
|
||||
Expected: no regressions in targeted flows and theme switching.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: dark mode contrast regression in legacy widgets.
|
||||
- Mitigation: migrate shared widgets first, then high-traffic pages with screenshot/manual checks.
|
||||
- Risk: mixed semantic + static colors during transition.
|
||||
- Mitigation: enforce migration order and avoid adding new direct `AppColors` in UI layers.
|
||||
- Risk: broad-scope change fatigue.
|
||||
- Mitigation: ship in small batches with isolated verification and rollback points.
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- `MaterialApp` follows `ThemeMode.system` with `theme` and `darkTheme` configured.
|
||||
- Theme layer no longer relies on static semantic colors for app-wide component themes.
|
||||
- P0 shared widget batch migrated to theme semantics.
|
||||
- Migration plan and execution progress documented in bug tracker doc.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Data Repositories Cache Strategy Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Establish a shared cache abstraction and shared data-repository entrypoints so cross-feature data access no longer depends on direct feature-to-feature data imports.
|
||||
|
||||
**Architecture:** Keep cache infrastructure centralized in `apps/lib/data/cache/`, and expose cross-feature data through `apps/lib/data/repositories/` facades registered in DI as singletons. Feature screens consume repositories, while cache policy/key/invalidation remain in repository layer.
|
||||
|
||||
**Tech Stack:** Flutter, Dart, GetIt, flutter_test
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Finalize Shared Cache Foundation
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/data/cache/cached_repository.dart`
|
||||
- Modify: `apps/lib/features/settings/data/services/user_profile_cache_repository.dart`
|
||||
- Test: `apps/test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add an invalidate-vs-inflight regression case in `user_profile_cache_repository_test.dart` asserting stale in-flight refresh cannot restore `cachedUser` after invalidate.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
Expected: FAIL before generation-guard fix.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add generation/version guard in `UserProfileCacheRepository` so `invalidate()` increments generation and stale async loader results are ignored for in-memory snapshot updates.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/data/cache/cached_repository.dart apps/lib/features/settings/data/services/user_profile_cache_repository.dart apps/test/features/settings/data/services/user_profile_cache_repository_test.dart
|
||||
git commit -m "refactor: harden user profile cache invalidation race handling"
|
||||
```
|
||||
|
||||
### Task 2: Introduce Shared Data Repositories Module
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/data/repositories/inbox_repository.dart`
|
||||
- Create: `apps/lib/data/repositories/calendar_event_repository.dart`
|
||||
- Create: `apps/lib/data/repositories/user_repository.dart`
|
||||
- Modify: `apps/lib/app/di/injection.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a DI registration smoke test (or lightweight compile-level usage test) that resolves the new repositories from GetIt.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test <new-test-file>`
|
||||
|
||||
Expected: FAIL because repositories are not yet defined/registered.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement repository facades that wrap existing APIs (`InboxApi`, `CalendarApi`, `UsersApi`) and register them as singletons in DI.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test <new-test-file>`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/data/repositories apps/lib/app/di/injection.dart
|
||||
git commit -m "refactor: add shared data repositories module"
|
||||
```
|
||||
|
||||
### Task 3: Migrate Cross-Feature Screens to Shared Repositories
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart`
|
||||
- Modify: `apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart`
|
||||
- Modify: `apps/lib/features/todo/presentation/screens/todo_edit_screen.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add targeted tests for message detail/list data loading and todo edit schedule loading using repository dependencies.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test <affected-test-files>`
|
||||
|
||||
Expected: FAIL before dependency switch.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Replace direct API injections with shared repository injections from `apps/lib/data/repositories/*`.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test <affected-test-files>`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart apps/lib/features/todo/presentation/screens/todo_edit_screen.dart
|
||||
git commit -m "refactor: switch cross-feature screens to shared repositories"
|
||||
```
|
||||
|
||||
### Task 4: Verification and Cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/AGENTS.md`
|
||||
- Modify: `docs/bugs/2026-03-27-repository缓存抽象.md`
|
||||
- Modify: `docs/bugs/服务层与Repository层职责混乱.md`
|
||||
|
||||
**Step 1: Run full targeted verification**
|
||||
|
||||
Run: `flutter analyze lib/data/cache lib/data/repositories lib/app/di/injection.dart lib/features/messages/presentation/screens/message_invite_detail_screen.dart lib/features/messages/presentation/screens/message_invite_list_screen.dart lib/features/todo/presentation/screens/todo_edit_screen.dart`
|
||||
|
||||
Expected: No issues.
|
||||
|
||||
**Step 2: Run regression tests**
|
||||
|
||||
Run: `flutter test test/data/cache/cached_repository_test.dart test/features/settings/data/services/user_profile_cache_repository_test.dart test/app/router/app_router_redirect_test.dart`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 3: Update docs status**
|
||||
|
||||
Mark relevant bug docs as in-progress/resolved and document new layering rules.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/AGENTS.md docs/bugs/2026-03-27-repository缓存抽象.md docs/bugs/服务层与Repository层职责混乱.md
|
||||
git commit -m "docs: document shared repository and cache strategy"
|
||||
```
|
||||
Reference in New Issue
Block a user