mirror of
https://github.com/chenasraf/pokemon_api_dart.git
synced 2026-05-17 17:48:03 +00:00
feat: pagination fetch, factory fixes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ mixin ResourceBase {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType{${toJson()}}';
|
||||
return '$runtimeType${toJson()}';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@@ -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<void> fill(Map<String, dynamic> cache);
|
||||
Future<void> clear();
|
||||
@@ -14,19 +16,82 @@ abstract class CacheManager {
|
||||
Future<bool> contains(String key);
|
||||
Future<dynamic> get(String key);
|
||||
|
||||
Future<T> tryGet<T>(
|
||||
Future<T> getOne<T>(
|
||||
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<Pagination<T>> _getPage<T>(
|
||||
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<T>.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<T>.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<List<T>> getPages<T>(
|
||||
String url,
|
||||
PageOptions pageOptions, {
|
||||
T Function(dynamic data)? onResult,
|
||||
int? maxPages,
|
||||
}) async {
|
||||
final mapper = onResult ?? ((data) => data);
|
||||
var data = await _getPage<T>(url, pageOptions, onResult: mapper);
|
||||
final pages = [data];
|
||||
|
||||
while (data.next != null && (maxPages == null || pages.length < maxPages)) {
|
||||
data = await _getPage<T>(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<void> add(String key, value) =>
|
||||
File('${basePath.path}/${_filename(key)}').writeAsString(jsonEncode(value));
|
||||
Future<void> add(String key, value) => File('${basePath.path}/${_filename(key)}').writeAsString(jsonEncode(value));
|
||||
|
||||
@override
|
||||
Future<void> clear() => basePath.delete(recursive: true);
|
||||
@@ -79,8 +143,7 @@ class FilesystemCache extends CacheManager {
|
||||
Future<void> fill(Map<String, dynamic> cache) => Future.wait(cache.entries.map((e) => add(e.key, e.value)));
|
||||
|
||||
@override
|
||||
Future<dynamic> get(String key) async =>
|
||||
jsonDecode(await File('${basePath.path}/${_filename(key)}').readAsString());
|
||||
Future<dynamic> get(String key) async => jsonDecode(await File('${basePath.path}/${_filename(key)}').readAsString());
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) => File('${basePath.path}/${_filename(key)}').delete();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'pokemon_api.dart';
|
||||
|
||||
class NamedAPIResource<T> with ResourceBase {
|
||||
@@ -10,16 +8,22 @@ class NamedAPIResource<T> with ResourceBase {
|
||||
|
||||
NamedAPIResource({
|
||||
required this.rawData,
|
||||
required this.name,
|
||||
required String? name,
|
||||
required this.url,
|
||||
});
|
||||
}) : name = name ?? 'UNNAMED RESOURCE';
|
||||
|
||||
factory NamedAPIResource.fromJson(Map<String, dynamic> 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<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<List<PokemonResource>> getPokemonList([int limit = 10]) async => cache.tryGet(
|
||||
'$baseUrl/pokemon?limit=$limit',
|
||||
onResult: (data) => (data['results'] as List<dynamic>).map((e) => PokemonResource.fromJson(e)).toList(),
|
||||
Future<List<PokemonResource>> 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<List<PokemonSpeciesResource>> getPokemonSpeciesList([int limit = 10]) async => cache.tryGet(
|
||||
'$baseUrl/pokemon-species?limit=$limit',
|
||||
onResult: (data) => (data['results'] as List<dynamic>).map((e) => PokemonSpeciesResource.fromJson(e)).toList(),
|
||||
Future<List<PokemonSpeciesResource>> 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<Pokemon> getPokemon(String nameOrId) async => cache.tryGet(
|
||||
Future<Pokemon> getPokemon(String nameOrId) async => cache.getOne(
|
||||
'$baseUrl/pokemon/$nameOrId',
|
||||
onResult: (data) => Pokemon.fromJson(data),
|
||||
);
|
||||
|
||||
/// Get a single Pokemon Species by name or id
|
||||
Future<PokemonSpecies> getPokemonSpecies(String nameOrId) async => cache.tryGet(
|
||||
Future<PokemonSpecies> getPokemonSpecies(String nameOrId) async => cache.getOne(
|
||||
'$baseUrl/pokemon-species/$nameOrId',
|
||||
onResult: (data) => PokemonSpecies.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
class Pagination<T> {
|
||||
final List<T> 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<String, dynamic> 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<String, dynamic> toJson() => {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => Uri(queryParameters: {
|
||||
'limit': [limit.toString()],
|
||||
'offset': [offset.toString()],
|
||||
}).query;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,22 +12,22 @@ class PokemonSpecies with ResourceBase {
|
||||
@override
|
||||
final Map<String, dynamic> rawData;
|
||||
|
||||
final int baseHappiness;
|
||||
final int? baseHappiness;
|
||||
final int captureRate;
|
||||
final NamedAPIResource color;
|
||||
final NamedAPIResource? color;
|
||||
final List<NamedAPIResource> eggGroups;
|
||||
final NamedAPIResource evolutionChain;
|
||||
final NamedAPIResource? evolutionChain;
|
||||
final NamedAPIResource? evolvesFromSpecies;
|
||||
final List<FlavorText> flavorTextEntries;
|
||||
final List<Description> formDescriptions;
|
||||
final bool formsSwitchable;
|
||||
final int genderRate;
|
||||
final int? genderRate;
|
||||
final List<Genus> 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<PalParkEncounterArea> palParkEncounters;
|
||||
final List<PokedexNumber> pokedexNumbers;
|
||||
final NamedAPIResource shape;
|
||||
final NamedAPIResource? shape;
|
||||
final List<PokemonSpeciesVariety> 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<NamedAPIResource>.from(json['egg_groups'].map((x) => NamedAPIResource.fromJson(x ?? {})))
|
||||
? List<NamedAPIResource>.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<FlavorText>.from(json['flavor_text_entries'].map((x) => FlavorText.fromJson(x ?? {})))
|
||||
? List<FlavorText>.from(json['flavor_text_entries'].map((x) => FlavorText.fromJson(x)))
|
||||
: [],
|
||||
formDescriptions: json['form_descriptions'] != null
|
||||
? List<Description>.from(json['form_descriptions'].map((x) => Description.fromJson(x ?? {})))
|
||||
? List<Description>.from(json['form_descriptions'].map((x) => Description.fromJson(x)))
|
||||
: [],
|
||||
formsSwitchable: json['forms_switchable'],
|
||||
genderRate: json['gender_rate'],
|
||||
genera: json['genera'] != null ? List<Genus>.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<Genus>.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<Name>.from(json['names'].map((x) => Name.fromJson(x ?? {}))) : [],
|
||||
names: json['names'] != null ? List<Name>.from(json['names'].map((x) => Name.fromJson(x))) : [],
|
||||
order: json['order'],
|
||||
palParkEncounters: json['pal_park_encounters'] != null
|
||||
? List<PalParkEncounterArea>.from(
|
||||
json['pal_park_encounters'].map((x) => PalParkEncounterArea.fromJson(x ?? {})))
|
||||
? List<PalParkEncounterArea>.from(json['pal_park_encounters'].map((x) => PalParkEncounterArea.fromJson(x)))
|
||||
: [],
|
||||
pokedexNumbers: json['pokedex_numbers'] != null
|
||||
? List<PokedexNumber>.from(json['pokedex_numbers'].map((x) => PokedexNumber.fromJson(x ?? {})))
|
||||
? List<PokedexNumber>.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<PokemonSpeciesVariety>.from(json['varieties'].map((x) => PokemonSpeciesVariety.fromJson(x ?? {})))
|
||||
? List<PokemonSpeciesVariety>.from(json['varieties'].map((x) => PokemonSpeciesVariety.fromJson(x)))
|
||||
: [],
|
||||
);
|
||||
|
||||
@@ -119,9 +123,9 @@ class PokemonSpecies with ResourceBase {
|
||||
Map<String, dynamic> 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<PokemonSpecies> {
|
||||
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}';
|
||||
|
||||
Reference in New Issue
Block a user