diff --git a/example/main.dart b/example/main.dart index 165f1c5..3023043 100644 --- a/example/main.dart +++ b/example/main.dart @@ -5,39 +5,42 @@ import 'package:pokemon_api/pokemon_api.dart'; void main() async { final api = PokemonAPIClient.instance; - final pokemonList = await api.getPokemonList(); - print('\n'); - print(pokemonList[0]); + final test = await api.getPokemonSpecies("908"); + // final test2 = await api.getPokemonSpecies("390"); - final fuecoco = await PokemonAPIClient.instance.getPokemon('fuecoco'); - - print('\n'); - print(fuecoco); - - final bulbasaur = await PokemonAPIClient.instance.getPokemon('bulbasaur'); - - final encounters = await bulbasaur.locationAreaEncounters.get(); - - print('\n'); - print(encounters); - - final locationArea = await encounters[0].locationArea.get(); - - print('\n'); - print(locationArea); - - final location = await locationArea.location.get(); - - print('\n'); - print(location); - - final region = await location.region.get(); - - print('\n'); - print(region); - - final species = await fuecoco.species.get(); - - print('\n'); - print(species); + // final pokemonList = await api.getPokemonList(PageOptions(limit: 10), maxPages: 1); + // print('\n'); + // print(pokemonList[0]); + // + // final fuecoco = await PokemonAPIClient.instance.getPokemon('389'); + // + // print('\n'); + // print(fuecoco); + // + // final bulbasaur = await PokemonAPIClient.instance.getPokemon('bulbasaur'); + // + // final encounters = await bulbasaur.locationAreaEncounters.get(); + // + // print('\n'); + // print(encounters); + // + // final locationArea = await encounters[0].locationArea.get(); + // + // print('\n'); + // print(locationArea); + // + // final location = await locationArea.location.get(); + // + // print('\n'); + // print(location); + // + // final region = await location.region.get(); + // + // print('\n'); + // print(region); + // + // final species = await fuecoco.species.get(); + // + // print('\n'); + // print(species); } diff --git a/lib/base.dart b/lib/base.dart index f08a607..ef13f12 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -12,7 +12,7 @@ mixin ResourceBase { @override String toString() { - return '$runtimeType{${toJson()}}'; + return '$runtimeType${toJson()}'; } Map toJson(); diff --git a/lib/cache.dart b/lib/cache.dart index 81e35d6..c418698 100644 --- a/lib/cache.dart +++ b/lib/cache.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; +import 'pokemon_api.dart'; + abstract class CacheManager { Future fill(Map cache); Future clear(); @@ -14,19 +16,82 @@ abstract class CacheManager { Future contains(String key); Future get(String key); - Future tryGet( + Future getOne( String key, { T Function(dynamic data)? onResult, + int? retry, + }) async { + final _retry = retry ?? 3; + Response? response; + try { + final mapper = onResult ?? ((data) => data); + if (await contains(key)) { + return mapper(await get(key)); + } + final http = Dio(); + response = await http.get(key); + final value = mapper(response.data); + add(key, response.data); + return value; + } catch (e, stack) { + if (_retry > 0) { + return getOne(key, onResult: onResult, retry: _retry - 1); + } + remove(key); + print('Error getting $key: $e.\nResponse: $response'); + print(stack); + rethrow; + } + } + + Future> _getPage( + String url, + PageOptions pageOptions, { + T Function(dynamic data)? onResult, }) async { final mapper = onResult ?? ((data) => data); - if (await contains(key)) { - return mapper(await get(key)); + final curKey = '$url?$pageOptions'; + try { + if (await contains(curKey)) { + final response = await get(curKey); + final data = Pagination.fromJson({ + ...response, + 'results': (response['results'] as List).map(mapper).toList(), + }); + return data; + } + final http = Dio(); + final response = await http.get(curKey); + if (response.data == null) { + throw Exception('No data'); + } + final data = Pagination.fromJson({ + ...response.data, + 'results': (response.data['results'] as List).map(mapper).toList(), + }); + add(curKey, response.data); + return data; + } catch (e) { + remove(curKey); + throw Exception('Error getting $url: $e'); } - final http = Dio(); - final response = await http.get(key); - add(key, response.data); - final value = mapper(response.data); - return value; + } + + Future> getPages( + String url, + PageOptions pageOptions, { + T Function(dynamic data)? onResult, + int? maxPages, + }) async { + final mapper = onResult ?? ((data) => data); + var data = await _getPage(url, pageOptions, onResult: mapper); + final pages = [data]; + + while (data.next != null && (maxPages == null || pages.length < maxPages)) { + data = await _getPage(data.next!, pageOptions, onResult: mapper); + pages.add(data); + } + return pages.expand((e) => e.results).toList(); } } @@ -66,8 +131,7 @@ class FilesystemCache extends CacheManager { } @override - Future add(String key, value) => - File('${basePath.path}/${_filename(key)}').writeAsString(jsonEncode(value)); + Future add(String key, value) => File('${basePath.path}/${_filename(key)}').writeAsString(jsonEncode(value)); @override Future clear() => basePath.delete(recursive: true); @@ -79,8 +143,7 @@ class FilesystemCache extends CacheManager { Future fill(Map cache) => Future.wait(cache.entries.map((e) => add(e.key, e.value))); @override - Future get(String key) async => - jsonDecode(await File('${basePath.path}/${_filename(key)}').readAsString()); + Future get(String key) async => jsonDecode(await File('${basePath.path}/${_filename(key)}').readAsString()); @override Future remove(String key) => File('${basePath.path}/${_filename(key)}').delete(); diff --git a/lib/named_api_resource.dart b/lib/named_api_resource.dart index 680072c..e10ee3f 100644 --- a/lib/named_api_resource.dart +++ b/lib/named_api_resource.dart @@ -1,5 +1,3 @@ -import 'package:dio/dio.dart'; - import 'pokemon_api.dart'; class NamedAPIResource with ResourceBase { @@ -10,16 +8,22 @@ class NamedAPIResource with ResourceBase { NamedAPIResource({ required this.rawData, - required this.name, + required String? name, required this.url, - }); + }) : name = name ?? 'UNNAMED RESOURCE'; factory NamedAPIResource.fromJson(Map json) { - return NamedAPIResource( - rawData: json, - name: json['name'], - url: json['url'], - ); + try { + return NamedAPIResource( + rawData: json, + name: json['name'], + url: json['url'], + ); + } catch (e, stack) { + print('Error parsing NamedAPIResource: $e, json: $json'); + print(stack); + rethrow; + } } @override @@ -39,20 +43,7 @@ class NamedAPIResource with ResourceBase { throw Exception('URL is empty'); } final api = PokemonAPIClient.instance; - if (await api.cache.contains(url)) { - return mapper(await api.cache.get(url)); - } - try { - final http = Dio(); - final result = await http.get(url); - api.cache.add(url, result.data); - final value = mapper(result.data); - return value; - } catch (e) { - print('Error in NamedAPIResource.get for url $url'); - print(e); - rethrow; - } + return api.cache.getOne(url, onResult: mapper); } } diff --git a/lib/pokemon_api.dart b/lib/pokemon_api.dart index 009ba70..ea4eefb 100644 --- a/lib/pokemon_api.dart +++ b/lib/pokemon_api.dart @@ -45,7 +45,7 @@ class PokemonAPIClient { CacheManager _cache; PokemonAPIClient({CacheManager? cache}) - : _cache = cache ?? FilesystemCache(baseDirectory: '${Directory.current.path}/.cache'); + : _cache = cache ?? MemoryCache(); // ?? FilesystemCache(baseDirectory: '${Directory.current.path}/.cache'); CacheManager get cache => _cache; setCache(CacheManager cache) => _cache = cache; @@ -56,27 +56,81 @@ class PokemonAPIClient { static void setInstance(PokemonAPIClient client) => instance = client; /// Get a list of Pokemon - Future> getPokemonList([int limit = 10]) async => cache.tryGet( - '$baseUrl/pokemon?limit=$limit', - onResult: (data) => (data['results'] as List).map((e) => PokemonResource.fromJson(e)).toList(), + Future> getPokemonList( + PageOptions pageOptions, { + int? maxPages, + }) async => + cache.getPages( + '$baseUrl/pokemon', + pageOptions, + onResult: (data) => PokemonResource.fromJson(data), + maxPages: maxPages, ); /// Get a list of Pokemon Species - Future> getPokemonSpeciesList([int limit = 10]) async => cache.tryGet( - '$baseUrl/pokemon-species?limit=$limit', - onResult: (data) => (data['results'] as List).map((e) => PokemonSpeciesResource.fromJson(e)).toList(), + Future> getPokemonSpeciesList( + PageOptions pageOptions, { + int? maxPages, + }) async => + cache.getPages( + '$baseUrl/pokemon-species', + pageOptions, + onResult: (data) => PokemonSpeciesResource.fromJson(data), + maxPages: maxPages, ); /// Get a single Pokemon by name or id - Future getPokemon(String nameOrId) async => cache.tryGet( + Future getPokemon(String nameOrId) async => cache.getOne( '$baseUrl/pokemon/$nameOrId', onResult: (data) => Pokemon.fromJson(data), ); /// Get a single Pokemon Species by name or id - Future getPokemonSpecies(String nameOrId) async => cache.tryGet( + Future getPokemonSpecies(String nameOrId) async => cache.getOne( '$baseUrl/pokemon-species/$nameOrId', onResult: (data) => PokemonSpecies.fromJson(data), ); } +class Pagination { + final List results; + final String? next; + final String? previous; + final int count; + + Pagination({ + required this.results, + this.next, + this.previous, + required this.count, + }); + + factory Pagination.fromJson(Map json) => Pagination( + results: json['results'], + next: json['next'], + previous: json['previous'], + count: json['count'], + ); +} + +class PageOptions { + final int limit; + final int offset; + + PageOptions({ + this.limit = 100, + this.offset = 0, + }); + + Map toJson() => { + 'limit': limit, + 'offset': offset, + }; + + @override + String toString() => Uri(queryParameters: { + 'limit': [limit.toString()], + 'offset': [offset.toString()], + }).query; +} + diff --git a/lib/pokemon_species.dart b/lib/pokemon_species.dart index 4c32370..f3ee1e0 100644 --- a/lib/pokemon_species.dart +++ b/lib/pokemon_species.dart @@ -12,22 +12,22 @@ class PokemonSpecies with ResourceBase { @override final Map rawData; - final int baseHappiness; + final int? baseHappiness; final int captureRate; - final NamedAPIResource color; + final NamedAPIResource? color; final List eggGroups; - final NamedAPIResource evolutionChain; + final NamedAPIResource? evolutionChain; final NamedAPIResource? evolvesFromSpecies; final List flavorTextEntries; final List formDescriptions; final bool formsSwitchable; - final int genderRate; + final int? genderRate; final List genera; final NamedAPIResource generation; final NamedAPIResource growthRate; final NamedAPIResource? habitat; final bool hasGenderDifferences; - final int hatchCounter; + final int? hatchCounter; final int id; final bool isBaby; final bool isLegendary; @@ -37,7 +37,7 @@ class PokemonSpecies with ResourceBase { final int order; final List palParkEncounters; final List pokedexNumbers; - final NamedAPIResource shape; + final NamedAPIResource? shape; final List varieties; PokemonSpecies({ @@ -75,24 +75,29 @@ class PokemonSpecies with ResourceBase { rawData: json, baseHappiness: json['base_happiness'], captureRate: json['capture_rate'], - color: NamedAPIResource.fromJson(json['color'] ?? {}), + color: json['color'] != null && json['color'].isNotEmpty ? NamedAPIResource.fromJson(json['color']) : null, eggGroups: json['egg_groups'] != null - ? List.from(json['egg_groups'].map((x) => NamedAPIResource.fromJson(x ?? {}))) + ? List.from(json['egg_groups'].map((x) => NamedAPIResource.fromJson(x))) : [], - evolutionChain: NamedAPIResource.fromJson(json['evolution_chain'] ?? {}), - evolvesFromSpecies: NamedAPIResource.fromJson(json['evolves_from_species'] ?? {}), + evolutionChain: json['evolution_chain'] != null && json['evolution_chain'].isNotEmpty + ? NamedAPIResource.fromJson({...json['evolution_chain'], 'name': 'evolution-chain'}) + : null, + evolvesFromSpecies: json['evolves_from_species'] != null && json['evolves_from_species'].isNotEmpty + ? NamedAPIResource.fromJson(json['evolves_from_species']) + : null, flavorTextEntries: json['flavor_text_entries'] != null - ? List.from(json['flavor_text_entries'].map((x) => FlavorText.fromJson(x ?? {}))) + ? List.from(json['flavor_text_entries'].map((x) => FlavorText.fromJson(x))) : [], formDescriptions: json['form_descriptions'] != null - ? List.from(json['form_descriptions'].map((x) => Description.fromJson(x ?? {}))) + ? List.from(json['form_descriptions'].map((x) => Description.fromJson(x))) : [], formsSwitchable: json['forms_switchable'], genderRate: json['gender_rate'], - genera: json['genera'] != null ? List.from(json['genera'].map((x) => Genus.fromJson(x ?? {}))) : [], - generation: NamedAPIResource.fromJson(json['generation'] ?? {}), - growthRate: NamedAPIResource.fromJson(json['growth_rate'] ?? {}), - habitat: NamedAPIResource.fromJson(json['habitat'] ?? {}), + genera: json['genera'] != null ? List.from(json['genera'].map((x) => Genus.fromJson(x))) : [], + generation: NamedAPIResource.fromJson(json['generation']), + growthRate: NamedAPIResource.fromJson(json['growth_rate']), + habitat: + json['habitat'] != null && json['habitat'].isNotEmpty ? NamedAPIResource.fromJson(json['habitat']) : null, hasGenderDifferences: json['has_gender_differences'], hatchCounter: json['hatch_counter'], id: json['id'], @@ -100,18 +105,17 @@ class PokemonSpecies with ResourceBase { isLegendary: json['is_legendary'], isMythical: json['is_mythical'], name: json['name'], - names: json['names'] != null ? List.from(json['names'].map((x) => Name.fromJson(x ?? {}))) : [], + names: json['names'] != null ? List.from(json['names'].map((x) => Name.fromJson(x))) : [], order: json['order'], palParkEncounters: json['pal_park_encounters'] != null - ? List.from( - json['pal_park_encounters'].map((x) => PalParkEncounterArea.fromJson(x ?? {}))) + ? List.from(json['pal_park_encounters'].map((x) => PalParkEncounterArea.fromJson(x))) : [], pokedexNumbers: json['pokedex_numbers'] != null - ? List.from(json['pokedex_numbers'].map((x) => PokedexNumber.fromJson(x ?? {}))) + ? List.from(json['pokedex_numbers'].map((x) => PokedexNumber.fromJson(x))) : [], - shape: NamedAPIResource.fromJson(json['shape'] ?? {}), + shape: json['shape'] != null && json['shape'].isNotEmpty ? NamedAPIResource.fromJson(json['shape']) : null, varieties: json['varieties'] != null - ? List.from(json['varieties'].map((x) => PokemonSpeciesVariety.fromJson(x ?? {}))) + ? List.from(json['varieties'].map((x) => PokemonSpeciesVariety.fromJson(x))) : [], ); @@ -119,9 +123,9 @@ class PokemonSpecies with ResourceBase { Map toJson() => { 'base_happiness': baseHappiness, 'capture_rate': captureRate, - 'color': color.toJson(), + 'color': color?.toJson(), 'egg_groups': eggGroups.map((x) => x.toJson()).toList(), - 'evolution_chain': evolutionChain.toJson(), + 'evolution_chain': evolutionChain?.toJson(), 'evolves_from_species': evolvesFromSpecies?.toJson(), 'flavor_text_entries': flavorTextEntries.map((x) => x.toJson()).toList(), 'form_descriptions': formDescriptions.map((x) => x.toJson()).toList(), @@ -142,7 +146,7 @@ class PokemonSpecies with ResourceBase { 'order': order, 'pal_park_encounters': palParkEncounters.map((x) => x.toJson()).toList(), 'pokedex_numbers': pokedexNumbers.map((x) => x.toJson()).toList(), - 'shape': shape.toJson(), + 'shape': shape?.toJson(), 'varieties': varieties.map((x) => x.toJson()).toList(), }; } @@ -154,7 +158,9 @@ class PokemonSpeciesResource extends NamedAPIResource { PokemonSpeciesResource(rawData: json, name: json['name'], url: json['url']); @override - PokemonSpecies mapper(data) => PokemonSpecies.fromJson(data); + PokemonSpecies mapper(data) { + return PokemonSpecies.fromJson(data); + } @override String toString() => 'PokemonSpeciesResource{name: $name, url: $url}';