feat: add todo cache repository and precise invalidation

This commit is contained in:
qzl
2026-03-20 15:37:59 +08:00
parent 8883248968
commit e64b9c670c
6 changed files with 155 additions and 10 deletions
+1 -8
View File
@@ -1,19 +1,12 @@
import 'dart:async';
import 'hybrid_cache_store.dart';
class CacheInvalidator {
final HybridCacheStore? _store;
final Set<String> _invalidated = <String>{};
CacheInvalidator({HybridCacheStore? store}) : _store = store;
CacheInvalidator({HybridCacheStore? store});
void invalidate(String key) {
_invalidated.add(key);
final removeFuture = _store?.remove(key);
if (removeFuture != null) {
unawaited(removeFuture);
}
}
void invalidateCalendarDay(DateTime date) {
+8
View File
@@ -29,6 +29,7 @@ import '../../features/settings/data/services/settings_user_cache.dart';
import '../../features/settings/data/services/user_profile_cache_repository.dart';
import '../../features/users/data/users_api.dart';
import '../../features/todo/data/todo_api.dart';
import '../../features/todo/data/todo_repository.dart';
final sl = GetIt.instance;
@@ -124,6 +125,13 @@ Future<void> configureDependencies() async {
final todoApi = TodoApi(apiClient);
sl.registerSingleton<TodoApi>(todoApi);
sl.registerSingleton<TodoRepository>(
TodoRepository(
api: todoApi,
store: hybridCacheStore,
invalidator: sl<CacheInvalidator>(),
),
);
final authRepository = AuthRepositoryImpl(
api: authApi,
@@ -17,6 +17,10 @@ class TodoApi {
return data.map((json) => TodoResponse.fromJson(json)).toList();
}
Future<List<TodoResponse>> getPendingTodos() {
return getTodos(status: 'pending');
}
Future<TodoResponse> getTodo(String id) async {
final response = await _client.get('$_prefix/$id');
return TodoResponse.fromJson(response.data);
@@ -0,0 +1,79 @@
import 'dart:async';
import '../../../core/cache/cache_entry.dart';
import '../../../core/cache/cache_invalidator.dart';
import '../../../core/cache/hybrid_cache_store.dart';
import 'todo_api.dart';
class TodoRepository {
static const String pendingListKey = 'todo:list:pending';
final TodoApi api;
final HybridCacheStore store;
final CacheInvalidator invalidator;
final DateTime Function() now;
TodoRepository({
required this.api,
required this.store,
required this.invalidator,
DateTime Function()? now,
}) : now = now ?? DateTime.now;
Future<List<TodoResponse>> getPendingTodos({
bool forceRefresh = false,
}) async {
if (!forceRefresh) {
final cached = await store.read<CacheEntry<List<TodoResponse>>>(
pendingListKey,
);
if (cached != null) {
return cached.value;
}
}
final remote = await api.getPendingTodos();
await store.write<CacheEntry<List<TodoResponse>>>(
pendingListKey,
CacheEntry(value: remote, fetchedAt: now()),
);
return remote;
}
Future<void> completeTodo(String id) async {
final cached = await store.read<CacheEntry<List<TodoResponse>>>(
pendingListKey,
);
if (cached != null) {
final next = cached.value
.map(
(todo) => todo.id == id
? todo.copyWith(status: 'completed', completedAt: now())
: todo,
)
.toList(growable: false);
await store.write<CacheEntry<List<TodoResponse>>>(
pendingListKey,
CacheEntry(value: next, fetchedAt: now()),
);
}
invalidator.invalidate(pendingListKey);
try {
await api.completeTodo(id);
} catch (error) {
if (cached != null) {
await store.write<CacheEntry<List<TodoResponse>>>(
pendingListKey,
cached,
);
}
rethrow;
}
}
Future<void> invalidatePending() {
invalidator.invalidate(pendingListKey);
return Future<void>.value();
}
}
@@ -16,6 +16,7 @@ import '../../../../shared/widgets/toast/toast_type.dart';
import '../../../calendar/ui/calendar_state_manager.dart';
import '../../../calendar/ui/widgets/bottom_dock.dart';
import '../../data/todo_api.dart';
import '../../data/todo_repository.dart';
class TodoQuadrantsScreen extends StatefulWidget {
const TodoQuadrantsScreen({super.key});
@@ -26,6 +27,7 @@ class TodoQuadrantsScreen extends StatefulWidget {
class _TodoQuadrantsScreenState extends State<TodoQuadrantsScreen> {
final TodoApi _todoApi = sl<TodoApi>();
final TodoRepository _todoRepository = sl<TodoRepository>();
List<TodoResponse> _todos = [];
bool _isLoading = true;
@@ -210,7 +212,9 @@ class _TodoQuadrantsScreenState extends State<TodoQuadrantsScreen> {
});
try {
final todos = await _todoApi.getTodos(status: 'pending');
final todos = await _todoRepository.getPendingTodos(
forceRefresh: !showPageLoader,
);
if (!mounted) {
return;
}
@@ -263,7 +267,7 @@ class _TodoQuadrantsScreenState extends State<TodoQuadrantsScreen> {
Future<void> _completeTodo(TodoResponse todo) async {
try {
await _todoApi.completeTodo(todo.id);
await _todoRepository.completeTodo(todo.id);
if (mounted) {
Toast.show(context, '已完成', type: ToastType.success);
}
@@ -0,0 +1,57 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:social_app/core/cache/cache_entry.dart';
import 'package:social_app/core/cache/cache_invalidator.dart';
import 'package:social_app/core/cache/hybrid_cache_store.dart';
import 'package:social_app/core/cache/memory_cache_store.dart';
import 'package:social_app/core/cache/persistent_cache_store.dart';
import 'package:social_app/features/todo/data/todo_api.dart';
import 'package:social_app/features/todo/data/todo_repository.dart';
class _MockTodoApi extends Mock implements TodoApi {}
void main() {
test(
'complete todo should optimistically update and invalidate pending list key',
() async {
final api = _MockTodoApi();
final store = HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
);
final invalidator = CacheInvalidator(store: store);
final repository = TodoRepository(
api: api,
store: store,
invalidator: invalidator,
);
final cached = TodoResponse(
id: 'todo_1',
ownerId: 'u1',
title: 't1',
priority: 1,
order: 0,
status: 'pending',
createdAt: DateTime(2026, 3, 20, 10),
updatedAt: DateTime(2026, 3, 20, 10),
);
await store.write<CacheEntry<List<TodoResponse>>>(
TodoRepository.pendingListKey,
CacheEntry(value: [cached], fetchedAt: DateTime(2026, 3, 20, 10, 0)),
);
when(
() => api.completeTodo('todo_1'),
).thenAnswer((_) async => cached.copyWith(status: 'completed'));
await repository.completeTodo('todo_1');
final updated = await store.read<CacheEntry<List<TodoResponse>>>(
TodoRepository.pendingListKey,
);
expect(updated?.value.first.status, 'completed');
expect(invalidator.wasInvalidated(TodoRepository.pendingListKey), true);
},
);
}