refactor: decouple calendar screens from route-driven reload

This commit is contained in:
qzl
2026-03-20 15:34:44 +08:00
parent 0476f7f80c
commit 8883248968
6 changed files with 229 additions and 45 deletions
@@ -0,0 +1,144 @@
import 'dart:async';
import '../../../../core/cache/cache_entry.dart';
import '../../../../core/cache/cache_policy.dart';
import '../../../../core/cache/hybrid_cache_store.dart';
import '../models/schedule_item_model.dart';
class CalendarRepository {
final HybridCacheStore store;
final CachePolicy policy;
final DateTime Function() now;
final Future<List<ScheduleItemModel>> Function(DateTime date)
loadDayFromRemote;
final Future<List<ScheduleItemModel>> Function(DateTime start, DateTime end)
loadMonthFromRemote;
final Map<String, Future<void>> _refreshInFlight = <String, Future<void>>{};
CalendarRepository({
required this.store,
required this.loadDayFromRemote,
required this.loadMonthFromRemote,
CachePolicy? policy,
DateTime Function()? now,
}) : policy =
policy ??
const CachePolicy(
softTtl: Duration(minutes: 2),
hardTtl: Duration(minutes: 30),
minRefreshInterval: Duration(minutes: 1),
),
now = now ?? DateTime.now;
static String dayKey(DateTime date) {
final day =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
return 'calendar:day:$day';
}
static String monthKey(DateTime date) {
return 'calendar:month:${date.year}-${date.month.toString().padLeft(2, '0')}';
}
Future<List<ScheduleItemModel>> getDayEvents(
DateTime date, {
bool forceRefresh = false,
}) async {
final key = dayKey(date);
if (forceRefresh) {
return _refreshDayAndRead(date, key);
}
final cached = await store.read<CacheEntry<List<ScheduleItemModel>>>(key);
if (cached == null) {
return _refreshDayAndRead(date, key);
}
final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt);
if (decision.shouldRefreshInBackground) {
_refreshDayInBackground(date, key);
}
if (decision.mustBlockForNetwork || !decision.canUseCached) {
return _refreshDayAndRead(date, key);
}
return cached.value;
}
Future<List<ScheduleItemModel>> getMonthEvents(
DateTime monthStart, {
bool forceRefresh = false,
}) async {
final key = monthKey(monthStart);
if (forceRefresh) {
return _refreshMonthAndRead(monthStart, key);
}
final cached = await store.read<CacheEntry<List<ScheduleItemModel>>>(key);
if (cached == null) {
return _refreshMonthAndRead(monthStart, key);
}
final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt);
if (decision.shouldRefreshInBackground) {
_refreshMonthInBackground(monthStart, key);
}
if (decision.mustBlockForNetwork || !decision.canUseCached) {
return _refreshMonthAndRead(monthStart, key);
}
return cached.value;
}
Future<List<ScheduleItemModel>> _refreshDayAndRead(
DateTime date,
String key,
) async {
await _refreshDay(date, key);
final cached = await store.read<CacheEntry<List<ScheduleItemModel>>>(key);
return cached?.value ?? const <ScheduleItemModel>[];
}
Future<List<ScheduleItemModel>> _refreshMonthAndRead(
DateTime monthStart,
String key,
) async {
await _refreshMonth(monthStart, key);
final cached = await store.read<CacheEntry<List<ScheduleItemModel>>>(key);
return cached?.value ?? const <ScheduleItemModel>[];
}
Future<void> _refreshDay(DateTime date, String key) async {
final remote = await loadDayFromRemote(date);
await store.write<CacheEntry<List<ScheduleItemModel>>>(
key,
CacheEntry<List<ScheduleItemModel>>(value: remote, fetchedAt: now()),
);
}
Future<void> _refreshMonth(DateTime monthStart, String key) async {
final start = DateTime(monthStart.year, monthStart.month, 1);
final end = DateTime(monthStart.year, monthStart.month + 1, 0, 23, 59, 59);
final remote = await loadMonthFromRemote(start, end);
await store.write<CacheEntry<List<ScheduleItemModel>>>(
key,
CacheEntry<List<ScheduleItemModel>>(value: remote, fetchedAt: now()),
);
}
void _refreshDayInBackground(DateTime date, String key) {
_refreshInBackground(key, () => _refreshDay(date, key));
}
void _refreshMonthInBackground(DateTime monthStart, String key) {
_refreshInBackground(key, () => _refreshMonth(monthStart, key));
}
void _refreshInBackground(String key, Future<void> Function() taskFactory) {
if (_refreshInFlight.containsKey(key)) {
return;
}
final task = taskFactory().whenComplete(() {
_refreshInFlight.remove(key);
});
_refreshInFlight[key] = task;
unawaited(task);
}
}
@@ -48,4 +48,8 @@ class CalendarStateManager extends ChangeNotifier {
);
notifyListeners();
}
void refresh() {
notifyListeners();
}
}
@@ -8,7 +8,7 @@ import '../../../../core/di/injection.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_pressable.dart';
import '../../data/models/schedule_item_model.dart';
import '../../data/services/calendar_service.dart';
import '../../data/services/calendar_repository.dart';
import '../calendar_state_manager.dart';
import '../calendar_time_utils.dart';
import '../utils/event_color_resolver.dart';
@@ -67,25 +67,18 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToSelectedDate();
_setupRouteListener();
});
}
void _setupRouteListener() {
final router = GoRouter.of(context);
router.routerDelegate.addListener(_onRouteChange);
}
void _onRouteChange() {
_loadEvents();
}
void _updateMonthDates() {
_monthDates = monthDatesFor(_selectedDate);
}
Future<void> _loadEvents() async {
final events = await sl<CalendarService>().getEventsForDay(_selectedDate);
Future<void> _loadEvents({bool forceRefresh = false}) async {
final events = await sl<CalendarRepository>().getDayEvents(
_selectedDate,
forceRefresh: forceRefresh,
);
if (!mounted) {
return;
}
@@ -96,9 +89,6 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
@override
void dispose() {
try {
GoRouter.of(context).routerDelegate.removeListener(_onRouteChange);
} catch (_) {}
WidgetsBinding.instance.removeObserver(this);
_dayStripController.dispose();
super.dispose();
@@ -107,7 +97,7 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_loadEvents();
_loadEvents(forceRefresh: true);
}
}
@@ -12,7 +12,7 @@ import '../calendar_time_utils.dart';
import '../utils/event_color_resolver.dart';
import '../widgets/bottom_dock.dart';
import '../../data/models/schedule_item_model.dart';
import '../../data/services/calendar_service.dart';
import '../../data/services/calendar_repository.dart';
class CalendarMonthScreen extends StatefulWidget {
final bool resetToToday;
@@ -44,32 +44,13 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
_selectedDate = savedDate;
_currentMonth = DateTime(savedDate.year, savedDate.month, 1);
_loadMonthEvents();
WidgetsBinding.instance.addPostFrameCallback((_) {
_setupRouteListener();
});
}
void _setupRouteListener() {
final router = GoRouter.of(context);
router.routerDelegate.addListener(_onRouteChange);
}
void _onRouteChange() {
_loadMonthEvents();
}
Future<void> _loadMonthEvents() async {
final start = DateTime(_currentMonth.year, _currentMonth.month, 1);
final end = DateTime(
_currentMonth.year,
_currentMonth.month + 1,
0,
23,
59,
59,
Future<void> _loadMonthEvents({bool forceRefresh = false}) async {
final events = await sl<CalendarRepository>().getMonthEvents(
_currentMonth,
forceRefresh: forceRefresh,
);
final events = await sl<CalendarService>().getEventsForRange(start, end);
if (!mounted) {
return;
}
@@ -83,9 +64,6 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
@override
void dispose() {
try {
GoRouter.of(context).routerDelegate.removeListener(_onRouteChange);
} catch (_) {}
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -93,7 +71,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_loadMonthEvents();
_loadMonthEvents(forceRefresh: true);
}
}