feat: pagination fetch, factory fixes

This commit is contained in:
Chen Asraf
2023-07-10 01:17:22 +03:00
parent 0ed086fb03
commit 801537fb86
6 changed files with 222 additions and 105 deletions

View File

@@ -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);
}

View File

@@ -12,7 +12,7 @@ mixin ResourceBase {
@override
String toString() {
return '$runtimeType{${toJson()}}';
return '$runtimeType${toJson()}';
}
Map<String, dynamic> toJson();

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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}';