Skip to content

Commit 2c437a7

Browse files
authored
Merge pull request #567 from DevKor-github/feature/issue-542-schedule-local-architecture
Clarify schedule repository persistence
2 parents 12bb42e + 98e824e commit 2c437a7

9 files changed

Lines changed: 93 additions & 64 deletions

docs/Architecture.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Navigation ← AppBloc ← UserEntity ← UserRepository ← AuthDataSource ←
255255
```
256256
Schedule Form → ScheduleBloc → CreateScheduleUseCase → ScheduleRepository
257257
258-
Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity
258+
ScheduleRemoteDataSource → API
259259
```
260260

261261
## 🎯 Key Features Architecture
@@ -272,7 +272,7 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity
272272
- **CRUD operations** for schedules
273273
- **Calendar integration** with multiple view modes
274274
- **Preparation time calculation** and management
275-
- **Real-time synchronization** between local and remote data
275+
- **In-memory schedule stream updates** after remote reads and writes
276276
- **Runtime preparation flow** with official timer starts, early-start entry, and finish/lateness handling
277277
- **Cache-coherent timed-preparation resume** using snapshot metadata and fingerprint invalidation
278278
- **Automatic timer system** for schedule start notifications
@@ -285,11 +285,11 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity
285285
- **Local notifications** for preparation alerts
286286
- **Permission handling** for notification access
287287

288-
### 4. **Offline Support**
288+
### 4. **Local Runtime Persistence**
289289

290-
- **Local database** with Drift for offline data access
291-
- **Synchronization strategy** for online/offline data consistency
292-
- **Caching mechanisms** for improved performance
290+
- **Local persistence** for runtime state such as timed-preparation snapshots, early-start sessions, and alarm registry records
291+
- **Remote-only schedule persistence** for schedule CRUD/read operations
292+
- **In-memory schedule streams** for loaded schedule ranges; no offline schedule CRUD/cache contract is currently implemented
293293

294294
### 5. **Error Handling System**
295295

@@ -305,7 +305,7 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity
305305

306306
- `PreparationWithTimeLocalDataSource` persists `PreparationWithTimeEntity` per schedule using SharedPreferences.
307307
- Intended for lightweight, per-schedule timer state (elapsed time, completion) that should survive app restarts.
308-
- Repository reads canonical preparation from remote/DB; BLoC can merge it with locally persisted timing state when needed.
308+
- Repository reads canonical schedule/preparation data from the remote API; BLoC can merge it with locally persisted timing state when needed.
309309

310310
## 🧪 Testing Strategy
311311

lib/data/data_sources/schedule_local_data_source.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:injectable/injectable.dart';
21
import 'package:on_time_front/core/database/database.dart';
32
import 'package:on_time_front/data/mappers/domain_persistence_mappers.dart';
43
import 'package:on_time_front/domain/entities/schedule_entity.dart';
@@ -18,7 +17,7 @@ abstract interface class ScheduleLocalDataSource {
1817
Future<void> deleteSchedule(ScheduleEntity scheduleEntity);
1918
}
2019

21-
@Injectable(as: ScheduleLocalDataSource)
20+
// Not registered with DI; schedule repository persistence is remote-only.
2221
class ScheduleLocalDataSourceImpl implements ScheduleLocalDataSource {
2322
final AppDatabase appDatabase;
2423

lib/data/repositories/schedule_repository_impl.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import 'dart:async';
22

33
import 'package:collection/collection.dart';
44
import 'package:injectable/injectable.dart';
5-
import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart';
65
import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart';
76
import 'package:on_time_front/domain/entities/schedule_entity.dart';
87
import 'package:on_time_front/domain/repositories/schedule_repository.dart';
@@ -11,7 +10,6 @@ import 'package:rxdart/subjects.dart';
1110

1211
@Singleton(as: ScheduleRepository)
1312
class ScheduleRepositoryImpl implements ScheduleRepository {
14-
final ScheduleLocalDataSource scheduleLocalDataSource;
1513
final ScheduleRemoteDataSource scheduleRemoteDataSource;
1614
final TimedPreparationRepository timedPreparationRepository;
1715

@@ -22,7 +20,6 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
2220
final _scheduleListEquality = const ListEquality<ScheduleEntity>();
2321

2422
ScheduleRepositoryImpl({
25-
required this.scheduleLocalDataSource,
2623
required this.scheduleRemoteDataSource,
2724
required this.timedPreparationRepository,
2825
});
@@ -51,7 +48,6 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
5148
Future<void> createSchedule(ScheduleEntity schedule) async {
5249
try {
5350
await scheduleRemoteDataSource.createSchedule(schedule);
54-
//await scheduleLocalDataSource.createSchedule(schedule);
5551
_emitUpsertedSchedule(schedule);
5652
} catch (e) {
5753
rethrow;
@@ -63,7 +59,6 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
6359
try {
6460
await scheduleRemoteDataSource.deleteSchedule(schedule);
6561
await _clearTimedPreparationSafe(schedule.id);
66-
//await scheduleLocalDataSource.deleteSchedule(schedule);
6762
_emitScheduleSet(
6863
Set.from(_scheduleStreamController.value)
6964
..removeWhere((existing) => existing.id == schedule.id),
@@ -141,7 +136,6 @@ class ScheduleRepositoryImpl implements ScheduleRepository {
141136
await _clearTimedPreparationSafe(refreshedSchedule.id);
142137
}
143138
_emitUpsertedSchedule(refreshedSchedule);
144-
//await scheduleLocalDataSource.updateSchedule(schedule);
145139
} catch (e) {
146140
rethrow;
147141
}

lib/domain/use-cases/load_adjacent_schedule_with_preparation_use_case.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class LoadAdjacentScheduleWithPreparationUseCase {
1717

1818
/// Loads schedules and preparation data from the server for the given date range.
1919
/// This triggers fetching schedules and preparation from the remote data source
20-
/// and updating the local cache/stream.
20+
/// and updating repository streams.
2121
///
2222
/// [startDate] - Start date for the search range
2323
/// [endDate] - End date for the search range
@@ -29,13 +29,16 @@ class LoadAdjacentScheduleWithPreparationUseCase {
2929
await _loadSchedulesByDateUseCase(startDate, endDate);
3030

3131
// Get the schedules that were loaded
32-
final schedules =
33-
await _getSchedulesByDateUseCase(startDate, endDate).first;
32+
final schedules = await _getSchedulesByDateUseCase(
33+
startDate,
34+
endDate,
35+
).first;
3436

3537
// Load preparation for all schedules in the date range
3638
await Future.wait(
37-
schedules
38-
.map((schedule) => _loadPreparationByScheduleIdUseCase(schedule.id)),
39+
schedules.map(
40+
(schedule) => _loadPreparationByScheduleIdUseCase(schedule.id),
41+
),
3942
);
4043
}
4144
}

lib/domain/use-cases/load_preparation_by_schedule_id_use_case.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class LoadPreparationByScheduleIdUseCase {
99

1010
/// Loads preparation for the given schedule ID.
1111
/// This triggers fetching preparation from the remote data source
12-
/// and updating the local cache/stream.
12+
/// and updating the preparation repository stream/cache.
1313
///
1414
/// [scheduleId] - The ID of the schedule to load preparation for
1515
Future<void> call(String scheduleId) async {

lib/domain/use-cases/load_schedules_by_date_use_case.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class LoadSchedulesByDateUseCase {
99

1010
/// Loads schedules for the given date range.
1111
/// This triggers fetching schedules from the remote data source
12-
/// and updating the local cache/stream.
12+
/// and updating the in-memory schedule stream.
1313
///
1414
/// [startDate] - Start date of the range (inclusive)
1515
/// [endDate] - End date of the range (exclusive), or null for all schedules after startDate

test/data/repositories/schedule_repository_impl_stream_test.dart

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,11 @@
11
import 'package:flutter_test/flutter_test.dart';
2-
import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart';
32
import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart';
43
import 'package:on_time_front/data/repositories/schedule_repository_impl.dart';
54
import 'package:on_time_front/domain/entities/place_entity.dart';
65
import 'package:on_time_front/domain/entities/schedule_entity.dart';
76
import 'package:on_time_front/domain/entities/timed_preparation_snapshot_entity.dart';
87
import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart';
98

10-
class FakeScheduleLocalDataSource implements ScheduleLocalDataSource {
11-
@override
12-
Future<void> createSchedule(ScheduleEntity scheduleEntity) async {}
13-
14-
@override
15-
Future<void> deleteSchedule(ScheduleEntity scheduleEntity) async {}
16-
17-
@override
18-
Future<ScheduleEntity> getScheduleById(String id) async {
19-
throw UnimplementedError();
20-
}
21-
22-
@override
23-
Future<List<ScheduleEntity>> getSchedulesByDate(
24-
DateTime startDate,
25-
DateTime? endDate,
26-
) async {
27-
throw UnimplementedError();
28-
}
29-
30-
@override
31-
Future<void> updateSchedule(ScheduleEntity scheduleEntity) async {}
32-
}
33-
349
class FakeScheduleRemoteDataSource implements ScheduleRemoteDataSource {
3510
FakeScheduleRemoteDataSource({
3611
required this.getSchedulesByDateHandler,
@@ -98,6 +73,75 @@ class FakeTimedPreparationRepository implements TimedPreparationRepository {
9873
}
9974

10075
void main() {
76+
test('createSchedule publishes created schedule to stream cache', () async {
77+
final schedule = _schedule(
78+
id: 'created',
79+
scheduleTime: DateTime(2026, 3, 20, 9),
80+
);
81+
final repository = ScheduleRepositoryImpl(
82+
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
83+
getSchedulesByDateHandler: (_, __) async => const [],
84+
),
85+
timedPreparationRepository: FakeTimedPreparationRepository(),
86+
);
87+
88+
await repository.createSchedule(schedule);
89+
90+
final latest = await repository.scheduleStream.firstWhere(
91+
(schedules) => schedules.any((schedule) => schedule.id == 'created'),
92+
);
93+
94+
expect(latest.single, schedule);
95+
});
96+
97+
test('deleteSchedule removes deleted schedule from stream cache', () async {
98+
final schedule = _schedule(
99+
id: 'deleted',
100+
scheduleTime: DateTime(2026, 3, 20, 9),
101+
);
102+
final repository = ScheduleRepositoryImpl(
103+
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
104+
getSchedulesByDateHandler: (_, __) async => const [],
105+
),
106+
timedPreparationRepository: FakeTimedPreparationRepository(),
107+
);
108+
final events = <Set<ScheduleEntity>>[];
109+
final subscription = repository.scheduleStream.listen(events.add);
110+
addTearDown(subscription.cancel);
111+
112+
await repository.createSchedule(schedule);
113+
await repository.deleteSchedule(schedule);
114+
await pumpEventQueue();
115+
116+
expect(events.map((event) => event.map((s) => s.id).toList()), [
117+
<String>[],
118+
['deleted'],
119+
<String>[],
120+
]);
121+
});
122+
123+
test('getScheduleById publishes fetched schedule to stream cache', () async {
124+
final schedule = _schedule(
125+
id: 'fetched',
126+
scheduleTime: DateTime(2026, 3, 20, 9),
127+
);
128+
final repository = ScheduleRepositoryImpl(
129+
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
130+
getSchedulesByDateHandler: (_, __) async => const [],
131+
getScheduleByIdHandler: (_) async => schedule,
132+
),
133+
timedPreparationRepository: FakeTimedPreparationRepository(),
134+
);
135+
136+
final result = await repository.getScheduleById(schedule.id);
137+
final latest = await repository.scheduleStream.firstWhere(
138+
(schedules) => schedules.any((schedule) => schedule.id == 'fetched'),
139+
);
140+
141+
expect(result, schedule);
142+
expect(latest.single, schedule);
143+
});
144+
101145
test(
102146
'watchSchedulesByDate emits inclusive-start exclusive-end schedules sorted by time',
103147
() async {
@@ -121,7 +165,6 @@ void main() {
121165
);
122166

123167
final repository = ScheduleRepositoryImpl(
124-
scheduleLocalDataSource: FakeScheduleLocalDataSource(),
125168
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
126169
getSchedulesByDateHandler: (_, __) async => [
127170
insideLater,
@@ -164,7 +207,6 @@ void main() {
164207
);
165208

166209
final repository = ScheduleRepositoryImpl(
167-
scheduleLocalDataSource: FakeScheduleLocalDataSource(),
168210
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
169211
getSchedulesByDateHandler: (startDate, _) async {
170212
if (startDate == marchStart) {
@@ -251,7 +293,6 @@ void main() {
251293
);
252294

253295
final repository = ScheduleRepositoryImpl(
254-
scheduleLocalDataSource: FakeScheduleLocalDataSource(),
255296
scheduleRemoteDataSource: remote,
256297
timedPreparationRepository: FakeTimedPreparationRepository(),
257298
);
@@ -298,7 +339,6 @@ void main() {
298339
);
299340

300341
final repository = ScheduleRepositoryImpl(
301-
scheduleLocalDataSource: FakeScheduleLocalDataSource(),
302342
scheduleRemoteDataSource: FakeScheduleRemoteDataSource(
303343
getSchedulesByDateHandler: (_, __) async => [initialSchedule],
304344
getScheduleByIdHandler: (_) async => editedSchedule,

test/data/repositories/schedule_repository_impl_test.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class FakeTimedPreparationRepository implements TimedPreparationRepository {
3333
}
3434

3535
void main() {
36-
late MockScheduleLocalDataSource mockScheduleLocalDataSource;
3736
late MockScheduleRemoteDataSource mockScheduleRemoteDataSource;
3837
late FakeTimedPreparationRepository fakeTimedPreparationRepository;
3938
late ScheduleRepository scheduleRepository;
@@ -59,11 +58,9 @@ void main() {
5958
final tEndDate = DateTime.now().add(Duration(days: 1));
6059

6160
setUp(() {
62-
mockScheduleLocalDataSource = MockScheduleLocalDataSource();
6361
mockScheduleRemoteDataSource = MockScheduleRemoteDataSource();
6462
fakeTimedPreparationRepository = FakeTimedPreparationRepository();
6563
scheduleRepository = ScheduleRepositoryImpl(
66-
scheduleLocalDataSource: mockScheduleLocalDataSource,
6764
scheduleRemoteDataSource: mockScheduleRemoteDataSource,
6865
timedPreparationRepository: fakeTimedPreparationRepository,
6966
);

test/helpers/mock.dart

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import 'package:mockito/annotations.dart';
22
import 'package:on_time_front/core/dio/app_dio.dart';
3-
import 'package:on_time_front/data/data_sources/schedule_local_data_source.dart';
43
import 'package:on_time_front/data/data_sources/schedule_remote_data_source.dart';
54

65
import 'package:on_time_front/data/data_sources/preparation_local_data_source.dart';
76
import 'package:on_time_front/data/data_sources/preparation_remote_data_source.dart';
87

9-
@GenerateMocks(
10-
[
11-
ScheduleLocalDataSource,
12-
ScheduleRemoteDataSource,
13-
PreparationRemoteDataSource,
14-
PreparationLocalDataSource,
15-
AppDio,
16-
],
17-
)
8+
@GenerateMocks([
9+
ScheduleRemoteDataSource,
10+
PreparationRemoteDataSource,
11+
PreparationLocalDataSource,
12+
AppDio,
13+
])
1814
void main() {}

0 commit comments

Comments
 (0)