init projet

This commit is contained in:
Guillaume Vern
2026-01-16 14:50:38 +01:00
parent 596870f278
commit 570df27bea
136 changed files with 5241 additions and 2 deletions
+34
View File
@@ -0,0 +1,34 @@
import '../models/pokemon.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
// Classe qui permet de récupérer les données des pokémons depuis l'API
// On utilise la librairie http pour effectuer les requêtes
// On utilise la librairie dart:convert pour convertir les données JSON en objet Dart
class PokemonApi {
static const String baseUrl = 'pokeapi.co';
static const String pokemonUrl = 'api/v2/pokemon';
static Future<Pokemon> getPokemon(int id) async {
// On utilise la méthode get de la classe http pour effectuer une requête GET
// On utilise Uri.https pour construire l'URL de la requête
// URI.https prends en paramètre le nom de domaine et le chemin de la requête
var response = await http.get(Uri.https(baseUrl, "$pokemonUrl/$id"));
if (response.statusCode != 200) {
// Si le code de retour de la requête n'est pas 200, on lève une exception
throw Exception('Erreur lors de la récupération du pokémon $id, code de retour ${response.statusCode}');
}
// On utilise la méthode jsonDecode de la librairie dart:convert pour convertir le corps de la réponse en fichier JSON
var json = jsonDecode(response.body);
String name = json['name'];
String type1 = (json['types'].length > 0 && json['types'][0]['type'] != null && json['types'][0]['type']['name'] != null) ? json['types'][0]['type']['name'] : "unknown";
String? type2 = (json['types'].length > 1 && json['types'][1]['type'] != null && json['types'][1]['type']['name'] != null) ? json['types'][1]['type']['name'] : null;
// On crée un objet Pokemon à partir du fichier JSON
return Pokemon(
name: name,
id: id,
type1: PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type1'),
type2: type2 != null ? PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.$type2') : null,
);
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
// Widget qui permet d'afficher un pokémon
// Elle prend en paramètre un pokémon
// Elle affiche l'image du pokémon, son nom et son numéro
// Elle permet également de naviguer vers la page de détail du pokémon
class PokemonTile extends StatefulWidget {
const PokemonTile(this.pokemon, {Key? key}) : super(key: key);
final Pokemon pokemon;
@override
State<PokemonTile> createState() => _PokemonTileState();
}
class _PokemonTileState extends State<PokemonTile> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// Lorsqu'on tap sur le widget, on navigue vers la page de détail du pokémon
// On utilise la méthode Navigator.pushNamed pour naviguer vers la page de détail
// On passe en paramètre du Navigator le contexte et la route de la page de détail
// on utilise "widget.pokemon" pour accéder au pokémon passé en paramètre; widget représente l'instance de la classe PokemonTile
Navigator.pushNamed(context, "/pokemon-detail", arguments: widget.pokemon);
},
child: Container(
height: 150,
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(10),
child: Center(
child: Column(
children: [
Image.network(widget.pokemon.imageUrl, height: 100),
Text(widget.pokemon.formatedName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16
),
),
],
)
),
),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../utils/pokemon_type.dart';
// Widget qui permet d'afficher un type de Pokémon
// Elle prend en paramètre un type de Pokémon
// Elle affiche le nom du type avec une couleur de fond correspondant au type
class PokemonTypeWidget extends StatelessWidget {
const PokemonTypeWidget(this.type, {Key? key}) : super(key: key);
final PokemonType type;
@override
Widget build(BuildContext context) {
String typeName = formatedTypeName(type);
Color typeColor = typeToColor(type);
return Container(
padding: const EdgeInsets.all(15),
margin: const EdgeInsets.only(right: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: typeColor,
borderRadius: BorderRadius.circular(10),
// On rajoute une bordure noire si le fond est blanc
border: typeColor == Colors.white ? Border.all(color: Colors.black) : null,
),
child: SizedBox(
width: 60,
// Discriminant pour définir la couleur du texte en fonction de la couleur de fond
child: Text(typeName, style: TextStyle(color: (typeColor == Colors.white) ? Colors.black : Colors.white), textAlign: TextAlign.center)
),
);
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:sqflite/sqflite.dart';
import '../models/pokemon.dart';
// Permet de gérer la base de données
class PokedexDatabase {
static Database? database;
static Future<void> initDatabase() async {
database = await openDatabase(
"pokedex.db", // Nom de la base de données
version: 1, // Version de la base de données, permet de gérer les migrations
onCreate: (db, version) async { // Fonction qui sera appelée lors de la création de la base de données
// Création de la table pokemon avec les colonnes id, name, type1 et type2
await db.execute("CREATE TABLE IF NOT EXISTS pokemon (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type1 TEXT NOT NULL, type2 TEXT)");
},
);
}
// Méthode qui permet de récupérer la base de données
static Future<Database> getDatabase() async {
if (database == null) {
await initDatabase(); // On initialise la base de données si elle n'est pas encore initialisée
}
return database!;
}
// Méthode qui permet d'insérer un Pokémon dans la base de données
static Future<void> insertPokemon(Pokemon pokemon) async {
Database database = await getDatabase();
await database.insert("pokemon", pokemon.toJson());
}
// Méthode qui permet de récupérer la liste des pokémons dans la base de données
static Future<List<Pokemon>> getPokemonList() async {
Database database = await getDatabase();
var response = await database.query("pokemon");
return response.map((pokemon) => Pokemon.fromJson(pokemon)).toList();
}
// Méthode qui permet de supprimer un Pokémon de la base de données
static Future<void> deletePokemon(int id) async {
Database database = await getDatabase();
await database.delete("pokemon", where: "id = ?", whereArgs: [id]);
}
// Méthode qui permet de supprimer tous les pokémons de la base de données
static Future<void> deleteAllPokemon() async {
Database database = await getDatabase();
await database.delete("pokemon");
}
// Méthode qui permet de mettre à jour un Pokémon dans la base de données
static Future<void> updatePokemon(Pokemon pokemon) async {
Database database = await getDatabase();
await database.update("pokemon", pokemon.toJson(), where: "id = ?", whereArgs: [pokemon.id]);
}
// Méthode qui permet de récupérer un Pokémon dans la base de données à partir de son ID
static Future<Pokemon?> getPokemon(int id) async {
Database database = await getDatabase();
List<Map<String, dynamic>> pokemonList = await database.query("pokemon", where: "id = ?", whereArgs: [id]);
if (pokemonList.isEmpty) {
return null;
}
return Pokemon.fromJson(pokemonList.first);
}
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'pages/pokemon_list.dart';
import 'pages/pokemon_detail.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mon super Pokédex', // Titre de l'application
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false, // Permet de masquer la bannière "Debug"
// home a été enlevé pour être remplacé par la route "/"
routes: {
'/': (context) => const PokemonListPage(), // La route "/" est la page d'accueil
'/pokemon-detail':(context) => const PokemonDetailPage(),
}
);
}
}
+84
View File
@@ -0,0 +1,84 @@
import 'package:flutter/foundation.dart' show kIsWeb; // Platform is not supported on web
import '../database/pokedex_database.dart';
import '../api/pokemon_api.dart';
import '../utils/pokemon_type.dart';
import 'package:flutter/material.dart';
// Classe représentant un Pokémon. Elle contient le nom, le numéro et les types du Pokémon.
// Elle contient aussi des propriétés calculées pour récupérer l'url de l'image du Pokémon, l'url de l'image shiny du Pokémon et l'url du cri du Pokémon.
class Pokemon {
String name;
int id;
PokemonType type1;
PokemonType? type2;
String get imageUrl => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png';
String get shinyImageUrl => 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/$id.png';
String get cryUrl => 'https://pokemoncries.com/cries/$id.mp3';
String get formatedName {
return name[0].toUpperCase() + name.substring(1);
}
Color get type1Color => typeToColor(type1);
Color get type2Color => typeToColor(type2 ?? PokemonType.unknown);
String get type1Formated => formatedTypeName(type1);
String get type2Formated => formatedTypeName(type2 ?? PokemonType.unknown);
Pokemon({
required this.name,
required this.id,
required this.type1,
this.type2, // Le type 2 n'est pas toujours présent
});
// Constructeur qui permet de créer un Pokémon à partir d'un fichier JSON récupéré depuis l'API.
// Sera aussi utilisé pour la récupération depuis la base de données
factory Pokemon.fromJson(Map<String, dynamic> json) {
return Pokemon(
name: json['name'],
id: json['id'],
// Parcours des valeurs de l'enum PokemonType et récupération de la première valeur qui correspond à la string 'PokemonType.${json['type1']}'
type1: PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.${json['type1']}'),
type2: json['type2'] != null ? PokemonType.values.firstWhere((element) => element.toString() == 'PokemonType.${json['type2']}') : null,
);
}
// Méthode qui permet de convertir un Pokémon en fichier JSON. Sera aussi utilisé pour l'insertion dans la base de données
Map<String, dynamic> toJson() {
return {
'name': name,
'id': id,
'type1': type1.toString().split('.').last, // On récupère la valeur de l'enum PokemonType sans le préfixe 'PokemonType.'
'type2': type2?.toString().split('.').last,
};
}
// Méthode qui permet de récupérer un Pokémon à partir de son ID
// Si le Pokémon n'est pas présent dans la base de données, on le récupère depuis l'API
static Future<Pokemon?> fromID(int id) async {
Pokemon? pokemon;
if (!kIsWeb) {
// La base de données n'est pas disponible sur le web
pokemon = await PokedexDatabase.getPokemon(id);
}
if (pokemon == null) {
try {
pokemon = await PokemonApi.getPokemon(id);
if (!kIsWeb) {
// On insère le Pokémon dans la base de données
await PokedexDatabase.insertPokemon(pokemon);
}
} catch (e) {
print(e);
return null;
}
}
return pokemon;
}
}
// Enum qui représente les différents types de Pokémon
enum PokemonType {
normal, fighting, flying, poison, ground, rock, bug, ghost, steel, fire, water, grass, electric, psychic, ice, dragon, dark, fairy, unknown, shadow
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_type.dart';
// Vue détail d'un Pokémon. Elle est appelée par la route "/pokemon-detail". Elle prend en paramètre un Pokémon.
// Elle affiche l'image du Pokémon, son nom, son numéro et ses types. Elle permet également de passer en mode shiny.
// Elle hérite de la classe StatefulWidget car elle a besoin de gérer un état (le mode shiny).
class PokemonDetailPage extends StatefulWidget {
const PokemonDetailPage({Key? key}) : super(key: key);
@override
State<PokemonDetailPage> createState() => _PokemonDetailPageState();
}
// La classe _PokemonDetailPageState hérite de la classe State. Elle permet de gérer l'état de la page.
// Elle contient une variable _isShiny qui permet de savoir si le mode shiny est activé ou non.
class _PokemonDetailPageState extends State<PokemonDetailPage> {
// Variable qui permet de savoir si le mode shiny est activé ou non
bool _isShiny = false;
@override
Widget build(BuildContext context) {
// On récupère le Pokémon passé en paramètre de la route
final Pokemon pokemon = ModalRoute.of(context)!.settings.arguments as Pokemon;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Le GestureDetector va permettre de détecter un tap sur l'image du Pokémon
GestureDetector(
// L'image du Pokémon est une image en ligne. On utilise donc Image.network
// On utilise la variable _isShiny pour savoir si on affiche l'image normale ou l'image shiny
child: Image.network(_isShiny ? pokemon.shinyImageUrl : pokemon.imageUrl, width: 200),
onTap:() {
// Lorsqu'on tap sur l'image, on change la valeur de la variable _isShiny
// Cela va permettre de changer l'image affichée
// On utilise la méthode setState pour dire à Flutter que la valeur de la variable a changé
setState(() {
_isShiny = !_isShiny;
});
},
),
const SizedBox(height: 20),
RichText(
text: TextSpan(
text: pokemon.formatedName, // formatedName est une propriété calculée du modèle Pokemon
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.black,
),
children: [
TextSpan(
text: " #${pokemon.id.toString().padLeft(4, "0")}",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
color: Colors.black,
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
PokemonTypeWidget(pokemon.type1),
pokemon.type2 != null ? PokemonTypeWidget(pokemon.type2!) : Container(),
],
)
]
)
),
);
}
}
+69
View File
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
import '../components/pokemon_tile.dart';
import 'package:flutter/foundation.dart' show kIsWeb; // Platform is not supported on web
// Page de la liste des pokémons. Elle est appelée par la route "/". Elle affiche la liste des 151 premiers pokémons.
// Elle hérite de la classe StatefulWidget car elle a besoin de gérer un état (la liste des pokémons).
class PokemonListPage extends StatefulWidget {
const PokemonListPage({Key? key}) : super(key: key);
@override
State<PokemonListPage> createState() => _PokemonListPageState();
}
class _PokemonListPageState extends State<PokemonListPage> {
Widget _buildPokemonTile(BuildContext context, int index) {
// On utilise un FutureBuilder pour afficher un pokémon à partir de son ID. L'index commençant à 0, on ajoute 1 pour le numéro du pokémon.
// Le FutureBuilder va permettre d'afficher un widget en fonction de l'état du Future
// Le FutureBuilder prend en paramètre un Future (ici Pokemon.fromID(index + 1))
// Il prend aussi en paramètre une fonction qui va permettre de construire le widget en fonction de l'état du Future : builder: (context, snapshot) {}
return FutureBuilder(
builder: (context, snapshot) {
if (snapshot.hasData) {
// Si le Future a réussi à récupérer les données, on affiche le widget PokemonTile
if (snapshot.data == null) {
return Text('Error while fetching pokemon #${index + 1}');
}
return PokemonTile(snapshot.data as Pokemon);
} else if (snapshot.hasError) {
// Si le Future a échoué à récupérer les données, on affiche un message d'erreur
print(snapshot.error);
return const Text('Erreur : ');
} else {
// Si le Future n'a pas encore récupéré les données, on affiche un widget de chargement
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(10),
child: const CircularProgressIndicator(),
);
}
},
future: Pokemon.fromID(index + 1),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Liste des pokémons'),
),
body: Center(
child: GridView.builder(
// Le GridView permet d'afficher une liste de widgets sous forme de grille
// On utilise le constructeur GridView.builder pour construire la grille
// Le GridView.builder prend en paramètre un itemCount qui correspond au nombre d'éléments à afficher
// Il prend aussi en paramètre un itemBuilder qui va permettre de construire chaque élément de la grille
// Le GridView.builder prend aussi en paramètre un gridDelegate qui va permettre de définir le nombre de colonnes de la grille
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: kIsWeb ? 4 : 2, // On affiche 4 colonnes sur le web et 2 colonnes sur mobile
),
itemCount: 151, // On pourrait en mettre plus mais on va se limiter aux 151 premiers pokémons
itemBuilder: _buildPokemonTile,
)
),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../models/pokemon.dart';
// Permet de mapper un type de Pokémon avec une couleur
Color typeToColor(PokemonType type) {
Map<PokemonType, Color> typeToColor = {
PokemonType.normal: Colors.white,
PokemonType.fire: Colors.red,
PokemonType.water: Colors.blue,
PokemonType.electric: Colors.yellow,
PokemonType.grass: Colors.green,
PokemonType.ice: Colors.cyan,
PokemonType.fighting: Colors.orange,
PokemonType.poison: Colors.purple,
PokemonType.ground: Colors.brown,
PokemonType.flying: Colors.indigo,
PokemonType.psychic: Colors.pink,
PokemonType.bug: Colors.lightGreen,
PokemonType.rock: Colors.grey,
PokemonType.ghost: Colors.indigo,
PokemonType.dragon: Colors.indigo,
PokemonType.dark: Colors.black45,
PokemonType.steel: Colors.grey.shade600,
PokemonType.fairy: Colors.pinkAccent,
PokemonType.unknown: Colors.transparent,
PokemonType.shadow: Colors.transparent,
};
return typeToColor[type] ?? Colors.transparent;
}
// Met le nom du type de Pokémon avec une majuscule
String formatedTypeName(PokemonType type) {
String typeName = type.toString().split('.').last.replaceAll('PokemonType.', '');
return typeName[0].toUpperCase() + typeName.substring(1);
}