diff --git a/lib/data/repositories/pokemon_repository_impl.dart b/lib/data/repositories/pokemon_repository_impl.dart new file mode 100644 index 0000000..7af383d --- /dev/null +++ b/lib/data/repositories/pokemon_repository_impl.dart @@ -0,0 +1,63 @@ +import '../../core/config/app_constants.dart'; +import '../../core/logger.dart'; +import '../../domain/entities/pokemon.dart'; +import '../../domain/repositories/pokemon_repository.dart'; +import '../datasources/pokemon_local_datasource.dart'; +import '../datasources/pokemon_remote_datasource.dart'; + +/// Implémentation : DB locale d'abord, complétée/repliée sur l'API. +/// [local] vaut `null` sur le web (pas de SQLite). +class PokemonRepositoryImpl implements PokemonRepository { + final PokemonRemoteDataSource remote; + final PokemonLocalDataSource? local; + + PokemonRepositoryImpl({required this.remote, this.local}); + + @override + Future> getAll() async { + final localDs = local; + if (localDs == null) return remote.getAll(); + + final cached = await localDs.getAll(); + if (cached.length >= AppConstants.totalPokemon) return cached; + + try { + final remoteList = await remote.getAll(); + await localDs.saveAll(remoteList); + return localDs.getAll(); + } catch (e, st) { + AppLogger.error('Sync getAll échouée', e, st); + return cached; + } + } + + @override + Future getById(int id) async { + final localDs = local; + if (localDs != null) { + final cached = await localDs.getById(id); + if (cached != null) return cached; + } + try { + final fetched = await remote.getById(id); + if (localDs != null) await localDs.saveAll([fetched]); + return fetched; + } catch (e, st) { + AppLogger.error('getById($id) échoué', e, st); + return null; + } + } + + @override + Future saveAll(List pokemons) async => + local?.saveAll(pokemons); + + @override + Future update(Pokemon pokemon) async => local?.update(pokemon); + + @override + Future caughtCount() async => (await local?.caughtCount()) ?? 0; + + @override + Future seenCount() async => (await local?.seenCount()) ?? 0; +} diff --git a/test/data/pokemon_repository_test.dart b/test/data/pokemon_repository_test.dart new file mode 100644 index 0000000..71c77c8 --- /dev/null +++ b/test/data/pokemon_repository_test.dart @@ -0,0 +1,106 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pokeguess/domain/entities/pokemon.dart'; +import 'package:pokeguess/data/datasources/pokemon_local_datasource.dart'; +import 'package:pokeguess/data/datasources/pokemon_remote_datasource.dart'; +import 'package:pokeguess/data/repositories/pokemon_repository_impl.dart'; + +Pokemon _mk(int id, {bool caught = false}) => Pokemon( + name: 'p$id', + id: id, + type1: PokemonType.normal, + hp: 1, atk: 1, def: 1, spd: 1, + isCaught: caught, + ); + +class FakeLocal implements PokemonLocalDataSource { + final Map store; + FakeLocal([Map? init]) : store = init ?? {}; + @override + Future> getAll() async => store.values.toList(); + @override + Future getById(int id) async => store[id]; + @override + Future saveAll(List pokemons) async { + for (final p in pokemons) store[p.id] = p; + } + @override + Future update(Pokemon pokemon) async => store[pokemon.id] = pokemon; + @override + Future caughtCount() async => + store.values.where((p) => p.isCaught).length; + @override + Future seenCount() async => store.values.where((p) => p.isSeen).length; +} + +class FakeRemote implements PokemonRemoteDataSource { + final List all; + int getAllCalls = 0; + int getByIdCalls = 0; + FakeRemote(this.all); + @override + Future> getAll() async { + getAllCalls++; + return all; + } + @override + Future getById(int id) async { + getByIdCalls++; + return all.firstWhere((p) => p.id == id); + } +} + +void main() { + group('getById', () { + test('retourne le cache local sans appeler l\'API', () async { + final local = FakeLocal({5: _mk(5)}); + final remote = FakeRemote([_mk(5)]); + final repo = PokemonRepositoryImpl(remote: remote, local: local); + + final p = await repo.getById(5); + expect(p?.id, 5); + expect(remote.getByIdCalls, 0); + }); + + test('va chercher sur l\'API et met en cache si absent en local', () async { + final local = FakeLocal(); + final remote = FakeRemote([_mk(7)]); + final repo = PokemonRepositoryImpl(remote: remote, local: local); + + final p = await repo.getById(7); + expect(p?.id, 7); + expect(remote.getByIdCalls, 1); + expect(await local.getById(7), isNotNull); // mis en cache + }); + + test('sans local (web), passe directement par l\'API', () async { + final remote = FakeRemote([_mk(9)]); + final repo = PokemonRepositoryImpl(remote: remote, local: null); + final p = await repo.getById(9); + expect(p?.id, 9); + expect(remote.getByIdCalls, 1); + }); + }); + + group('getAll', () { + test('si le cache est complet, ne rappelle pas l\'API', () async { + final store = {for (var i = 1; i <= 1025; i++) i: _mk(i)}; + final local = FakeLocal(store); + final remote = FakeRemote([]); + final repo = PokemonRepositoryImpl(remote: remote, local: local); + + final list = await repo.getAll(); + expect(list.length, 1025); + expect(remote.getAllCalls, 0); + }); + + test('si le cache est incomplet, synchronise depuis l\'API', () async { + final local = FakeLocal({1: _mk(1)}); + final remote = FakeRemote([_mk(1), _mk(2), _mk(3)]); + final repo = PokemonRepositoryImpl(remote: remote, local: local); + + final list = await repo.getAll(); + expect(remote.getAllCalls, 1); + expect(list.length, 3); + }); + }); +}