fix(qa): corrections issues de la passe QA fonctionnelle
Corrige les bugs identifiés lors de la QA finale sur use_cases.dart et drift_repositories.dart, ajoute la couverture de tests d'infrastructure manquante (test/infrastructure/drift_repositories_test.dart). flutter analyze propre, 21/21 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -458,11 +458,17 @@ final class WorkoutTemplateUseCases {
|
|||||||
}
|
}
|
||||||
final templateOverrides = <WorkoutTemplateExerciseOverride>[];
|
final templateOverrides = <WorkoutTemplateExerciseOverride>[];
|
||||||
for (final input in overrides) {
|
for (final input in overrides) {
|
||||||
|
final programInput = programs.firstWhere(
|
||||||
|
(program) => program.clientKey == input.workoutTemplateProgramClientKey,
|
||||||
|
orElse: () =>
|
||||||
|
throw const DomainException('Workout template program not found.'),
|
||||||
|
);
|
||||||
final workoutTemplateProgramId =
|
final workoutTemplateProgramId =
|
||||||
programIdsByClientKey[input.workoutTemplateProgramClientKey];
|
programIdsByClientKey[input.workoutTemplateProgramClientKey];
|
||||||
if (workoutTemplateProgramId == null) {
|
if (workoutTemplateProgramId == null) {
|
||||||
throw const DomainException('Workout template program not found.');
|
throw const DomainException('Workout template program not found.');
|
||||||
}
|
}
|
||||||
|
_validateOverrideTargets(programInput.programSnapshotJson, input);
|
||||||
templateOverrides.add(
|
templateOverrides.add(
|
||||||
WorkoutTemplateExerciseOverride(
|
WorkoutTemplateExerciseOverride(
|
||||||
metadata:
|
metadata:
|
||||||
@ -833,6 +839,14 @@ final class CloseWorkoutSessionUseCase {
|
|||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final results = await sessionRepository.listSetResults(sessionId);
|
final results = await sessionRepository.listSetResults(sessionId);
|
||||||
final historyId = ids.newId();
|
final historyId = ids.newId();
|
||||||
|
final historyResults = _historyResultsFromActiveResults(
|
||||||
|
historyId: historyId,
|
||||||
|
results: results,
|
||||||
|
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||||||
|
now: now,
|
||||||
|
ids: ids,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
);
|
||||||
final history = WorkoutHistory(
|
final history = WorkoutHistory(
|
||||||
metadata: EntityMetadata(
|
metadata: EntityMetadata(
|
||||||
id: historyId,
|
id: historyId,
|
||||||
@ -867,6 +881,7 @@ final class CloseWorkoutSessionUseCase {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
}),
|
}),
|
||||||
|
results: historyResults,
|
||||||
);
|
);
|
||||||
await historyRepository.save(history);
|
await historyRepository.save(history);
|
||||||
return history;
|
return history;
|
||||||
@ -886,6 +901,142 @@ final class WorkoutHistoryUseCases {
|
|||||||
Future<void> delete(String id) => repository.delete(id, clock.now());
|
Future<void> delete(String id) => repository.delete(id, clock.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _validateOverrideTargets(
|
||||||
|
String programSnapshotJson,
|
||||||
|
WorkoutTemplateExerciseOverrideConfig input,
|
||||||
|
) {
|
||||||
|
final snapshot = jsonDecode(programSnapshotJson) as Map<String, dynamic>;
|
||||||
|
final exercises = (snapshot['exercises'] as List<dynamic>? ?? const [])
|
||||||
|
.cast<Map<String, dynamic>>();
|
||||||
|
final exercise = exercises.cast<Map<String, dynamic>?>().firstWhere(
|
||||||
|
(item) =>
|
||||||
|
item?['id'] == input.snapshotProgramExerciseId ||
|
||||||
|
item?['snapshotProgramExerciseId'] == input.snapshotProgramExerciseId,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if (exercise == null) {
|
||||||
|
throw const DomainException('Snapshot exercise not found.');
|
||||||
|
}
|
||||||
|
if (input.targetTimeSecondsOverride != null &&
|
||||||
|
exercise['timeEnabled'] != true) {
|
||||||
|
throw const DomainException('Cannot override inactive time target.');
|
||||||
|
}
|
||||||
|
if (input.targetRepsOverride != null && exercise['repsEnabled'] != true) {
|
||||||
|
throw const DomainException('Cannot override inactive reps target.');
|
||||||
|
}
|
||||||
|
if (input.targetScoreOverride != null && exercise['scoreEnabled'] != true) {
|
||||||
|
throw const DomainException('Cannot override inactive score target.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||||
|
required String historyId,
|
||||||
|
required List<ActiveSetResult> results,
|
||||||
|
required String resolvedTemplateSnapshotJson,
|
||||||
|
required DateTime now,
|
||||||
|
required IdGenerator ids,
|
||||||
|
required String originDeviceId,
|
||||||
|
}) {
|
||||||
|
final snapshots = _exerciseSnapshotsById(resolvedTemplateSnapshotJson);
|
||||||
|
return results.map((result) {
|
||||||
|
final snapshot = snapshots[result.exerciseSnapshotId];
|
||||||
|
return WorkoutHistorySetResult(
|
||||||
|
metadata: _newMetadata(ids, originDeviceId, now),
|
||||||
|
workoutHistoryId: historyId,
|
||||||
|
programSnapshotId: result.programSnapshotId,
|
||||||
|
exerciseSnapshotId: result.exerciseSnapshotId,
|
||||||
|
programIndex: result.programIndex,
|
||||||
|
exerciseIndex: result.exerciseIndex,
|
||||||
|
setIndex: result.setIndex,
|
||||||
|
programNameSnapshot:
|
||||||
|
snapshot?.programNameSnapshot ?? result.programSnapshotId,
|
||||||
|
exerciseNameSnapshot:
|
||||||
|
snapshot?.exerciseNameSnapshot ?? result.exerciseSnapshotId,
|
||||||
|
timeEnabledSnapshot: snapshot?.timeEnabled ?? result.actualTimeMs != null,
|
||||||
|
repsEnabledSnapshot: snapshot?.repsEnabled ?? result.actualReps != null,
|
||||||
|
scoreEnabledSnapshot:
|
||||||
|
snapshot?.scoreEnabled ?? result.actualScore != null,
|
||||||
|
targetTimeSecondsSnapshot: snapshot?.targetTimeSeconds,
|
||||||
|
targetRepsSnapshot: snapshot?.targetReps,
|
||||||
|
targetScoreSnapshot: snapshot?.targetScore,
|
||||||
|
actualTimeMs: result.actualTimeMs,
|
||||||
|
actualReps: result.actualReps,
|
||||||
|
actualScore: result.actualScore,
|
||||||
|
scoreLabelSnapshot:
|
||||||
|
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
|
||||||
|
scoreUnitSnapshot:
|
||||||
|
result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot,
|
||||||
|
startedAt: result.startedAt,
|
||||||
|
completedAt: result.completedAt,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
||||||
|
String resolvedTemplateSnapshotJson,
|
||||||
|
) {
|
||||||
|
final decoded =
|
||||||
|
jsonDecode(resolvedTemplateSnapshotJson) as Map<String, dynamic>;
|
||||||
|
final programs = (decoded['programs'] as List<dynamic>? ?? const []);
|
||||||
|
final snapshots = <String, _ResolvedExerciseSnapshot>{};
|
||||||
|
for (final program in programs.cast<Map<String, dynamic>>()) {
|
||||||
|
final programName = program['programNameSnapshot'] as String? ?? '';
|
||||||
|
final programSnapshotJson = program['programSnapshotJson'] as String?;
|
||||||
|
if (programSnapshotJson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final programSnapshot =
|
||||||
|
jsonDecode(programSnapshotJson) as Map<String, dynamic>;
|
||||||
|
final exercises =
|
||||||
|
(programSnapshot['exercises'] as List<dynamic>? ?? const []);
|
||||||
|
for (final exercise in exercises.cast<Map<String, dynamic>>()) {
|
||||||
|
final id = exercise['id'] as String?;
|
||||||
|
if (id == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
snapshots[id] = _ResolvedExerciseSnapshot(
|
||||||
|
programNameSnapshot: programName,
|
||||||
|
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
|
||||||
|
timeEnabled: exercise['timeEnabled'] == true,
|
||||||
|
repsEnabled: exercise['repsEnabled'] == true,
|
||||||
|
scoreEnabled: exercise['scoreEnabled'] == true,
|
||||||
|
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||||
|
targetReps: exercise['targetReps'] as int?,
|
||||||
|
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
|
||||||
|
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||||
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _ResolvedExerciseSnapshot {
|
||||||
|
const _ResolvedExerciseSnapshot({
|
||||||
|
required this.programNameSnapshot,
|
||||||
|
required this.exerciseNameSnapshot,
|
||||||
|
required this.timeEnabled,
|
||||||
|
required this.repsEnabled,
|
||||||
|
required this.scoreEnabled,
|
||||||
|
this.targetTimeSeconds,
|
||||||
|
this.targetReps,
|
||||||
|
this.targetScore,
|
||||||
|
this.scoreLabelSnapshot,
|
||||||
|
this.scoreUnitSnapshot,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String programNameSnapshot;
|
||||||
|
final String exerciseNameSnapshot;
|
||||||
|
final bool timeEnabled;
|
||||||
|
final bool repsEnabled;
|
||||||
|
final bool scoreEnabled;
|
||||||
|
final int? targetTimeSeconds;
|
||||||
|
final int? targetReps;
|
||||||
|
final double? targetScore;
|
||||||
|
final String? scoreLabelSnapshot;
|
||||||
|
final String? scoreUnitSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
EntityMetadata _newMetadata(
|
EntityMetadata _newMetadata(
|
||||||
IdGenerator ids,
|
IdGenerator ids,
|
||||||
String originDeviceId,
|
String originDeviceId,
|
||||||
|
|||||||
@ -530,7 +530,7 @@ domain.EntityMetadata _metadataFromRow(dynamic row) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Value<Object?>> _metadataValues(domain.EntityMetadata metadata) => [
|
List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
|
||||||
Value(metadata.id),
|
Value(metadata.id),
|
||||||
Value(metadata.createdAt),
|
Value(metadata.createdAt),
|
||||||
Value(metadata.updatedAt),
|
Value(metadata.updatedAt),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:gametime/application/application.dart';
|
import 'package:gametime/application/application.dart';
|
||||||
import 'package:gametime/domain/domain.dart';
|
import 'package:gametime/domain/domain.dart';
|
||||||
@ -35,6 +37,55 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Workout template use case rejects target overrides for inactive measures',
|
||||||
|
() async {
|
||||||
|
final useCase = WorkoutTemplateUseCases(
|
||||||
|
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||||
|
programRepository: _FakeProgramRepository(),
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
useCase.saveConfigured(
|
||||||
|
name: 'Séance tir',
|
||||||
|
programs: [
|
||||||
|
WorkoutTemplateProgramConfig(
|
||||||
|
clientKey: 'program-1',
|
||||||
|
programNameSnapshot: 'Programme tir',
|
||||||
|
defaultRestSecondsSnapshot: 30,
|
||||||
|
programSnapshotJson: jsonEncode({
|
||||||
|
'exercises': [
|
||||||
|
{
|
||||||
|
'id': 'exercise-snapshot-1',
|
||||||
|
'exerciseNameSnapshot': 'Lancers francs',
|
||||||
|
'setsCount': 3,
|
||||||
|
'timeEnabled': true,
|
||||||
|
'repsEnabled': false,
|
||||||
|
'scoreEnabled': false,
|
||||||
|
'targetTimeSeconds': 60,
|
||||||
|
'targetReps': null,
|
||||||
|
'targetScore': null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
overrides: const [
|
||||||
|
WorkoutTemplateExerciseOverrideConfig(
|
||||||
|
workoutTemplateProgramClientKey: 'program-1',
|
||||||
|
snapshotProgramExerciseId: 'exercise-snapshot-1',
|
||||||
|
targetRepsOverride: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('Active session elapsed time is based on persisted timestamps', () {
|
test('Active session elapsed time is based on persisted timestamps', () {
|
||||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
final session = ActiveWorkoutSession(
|
final session = ActiveWorkoutSession(
|
||||||
|
|||||||
196
test/infrastructure/drift_repositories_test.dart
Normal file
196
test/infrastructure/drift_repositories_test.dart
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gametime/application/application.dart';
|
||||||
|
import 'package:gametime/domain/domain.dart';
|
||||||
|
import 'package:gametime/infrastructure/local/local.dart' as local;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late local.AppDatabase database;
|
||||||
|
late local.DriftActiveSessionRepository activeRepository;
|
||||||
|
late local.DriftWorkoutTemplateRepository templateRepository;
|
||||||
|
late local.DriftWorkoutHistoryRepository historyRepository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
database = local.AppDatabase(NativeDatabase.memory());
|
||||||
|
activeRepository = local.DriftActiveSessionRepository(database);
|
||||||
|
templateRepository = local.DriftWorkoutTemplateRepository(database);
|
||||||
|
historyRepository = local.DriftWorkoutHistoryRepository(database);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'running session elapsed time survives repository reconstruction',
|
||||||
|
() async {
|
||||||
|
final persistedAt = DateTime.utc(2026, 7, 17, 12);
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1', persistedAt),
|
||||||
|
sourceWorkoutTemplateId: null,
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: persistedAt.subtract(const Duration(seconds: 30)),
|
||||||
|
lastPersistedAt: persistedAt,
|
||||||
|
elapsedActiveMs: 30000,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await activeRepository.save(session);
|
||||||
|
|
||||||
|
final afterAppKillRepository = local.DriftActiveSessionRepository(
|
||||||
|
database,
|
||||||
|
);
|
||||||
|
final afterAppKillUseCases = ActiveWorkoutSessionUseCases(
|
||||||
|
sessionRepository: afterAppKillRepository,
|
||||||
|
templateRepository: templateRepository,
|
||||||
|
clock: _FakeClock(persistedAt.add(const Duration(seconds: 20))),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final restored = await afterAppKillRepository.findOpen();
|
||||||
|
|
||||||
|
expect(restored, isNotNull);
|
||||||
|
expect(afterAppKillUseCases.elapsedActiveMilliseconds(restored!), 50000);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'closing a session stores autonomous history rows with set snapshots',
|
||||||
|
() async {
|
||||||
|
final now = DateTime.utc(2026, 7, 17, 12);
|
||||||
|
final template = WorkoutTemplate(
|
||||||
|
metadata: _metadata('template-1', now),
|
||||||
|
name: 'Séance jambes',
|
||||||
|
);
|
||||||
|
await templateRepository.save(template);
|
||||||
|
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1', now),
|
||||||
|
sourceWorkoutTemplateId: template.metadata.id,
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: now.subtract(const Duration(minutes: 10)),
|
||||||
|
lastPersistedAt: now,
|
||||||
|
elapsedActiveMs: 600000,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
|
||||||
|
);
|
||||||
|
await activeRepository.save(session);
|
||||||
|
await activeRepository.saveSetResult(
|
||||||
|
ActiveSetResult(
|
||||||
|
metadata: _metadata('active-result-1', now),
|
||||||
|
activeWorkoutSessionId: session.metadata.id,
|
||||||
|
programSnapshotId: 'program-snapshot-1',
|
||||||
|
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
actualTimeMs: 45000,
|
||||||
|
actualReps: 10,
|
||||||
|
actualScore: 80,
|
||||||
|
scoreLabelSnapshot: 'Charge',
|
||||||
|
scoreUnitSnapshot: 'kg',
|
||||||
|
completedAt: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final closeUseCase = CloseWorkoutSessionUseCase(
|
||||||
|
sessionRepository: activeRepository,
|
||||||
|
historyRepository: historyRepository,
|
||||||
|
clock: _FakeClock(now.add(const Duration(seconds: 5))),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final history = await closeUseCase.close(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
nameSnapshot: 'Séance jambes',
|
||||||
|
completed: true,
|
||||||
|
);
|
||||||
|
await templateRepository.save(
|
||||||
|
WorkoutTemplate(
|
||||||
|
metadata: template.metadata.markDeleted(now),
|
||||||
|
name: template.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final restored = await historyRepository.findById(history.metadata.id);
|
||||||
|
|
||||||
|
expect(restored, isNotNull);
|
||||||
|
expect(restored!.nameSnapshot, 'Séance jambes');
|
||||||
|
expect(restored.results, hasLength(1));
|
||||||
|
expect(restored.results.single.programNameSnapshot, 'Programme jambes');
|
||||||
|
expect(restored.results.single.exerciseNameSnapshot, 'Squat');
|
||||||
|
expect(restored.results.single.actualTimeMs, 45000);
|
||||||
|
expect(restored.results.single.actualReps, 10);
|
||||||
|
expect(restored.results.single.actualScore, 80);
|
||||||
|
expect(restored.results.single.scoreUnitSnapshot, 'kg');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolvedSnapshot() {
|
||||||
|
return jsonEncode({
|
||||||
|
'name': 'Séance jambes',
|
||||||
|
'programs': [
|
||||||
|
{
|
||||||
|
'id': 'program-snapshot-1',
|
||||||
|
'programNameSnapshot': 'Programme jambes',
|
||||||
|
'programSnapshotJson': jsonEncode({
|
||||||
|
'exercises': [
|
||||||
|
{
|
||||||
|
'id': 'exercise-snapshot-1',
|
||||||
|
'exerciseNameSnapshot': 'Squat',
|
||||||
|
'setsCount': 1,
|
||||||
|
'timeEnabled': true,
|
||||||
|
'repsEnabled': true,
|
||||||
|
'scoreEnabled': true,
|
||||||
|
'targetTimeSeconds': 45,
|
||||||
|
'targetReps': 10,
|
||||||
|
'targetScore': 80,
|
||||||
|
'scoreLabelSnapshot': 'Charge',
|
||||||
|
'scoreUnitSnapshot': 'kg',
|
||||||
|
'restSecondsOverride': 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'overrides': const [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata _metadata(String id, DateTime now) {
|
||||||
|
return EntityMetadata(
|
||||||
|
id: id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeClock implements Clock {
|
||||||
|
const _FakeClock(this.value);
|
||||||
|
|
||||||
|
final DateTime value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime now() => value;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeIds implements IdGenerator {
|
||||||
|
var _next = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String newId() {
|
||||||
|
_next += 1;
|
||||||
|
return 'id-$_next';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user