Comment créer une application mobile de tri de déchets avec Flutter et Dart: EcoTri

Comment créer une application mobile de tri de déchets avec Flutter et Dart: EcoTri

Bienvenue dans ce tutoriel complet où nous allons construire EcoTri, une application Flutter ludique et éducative conçue pour sensibiliser au tri des déchets en Belgique. Que vous soyez débutant ou intermédiaire en Flutter, ce guide détaillé vous accompagnera pas à pas pour créer une application moderne, responsive (mobile, web, desktop) et engageante. À la fin, vous aurez un quiz interactif avec 15 questions, un minuteur, des images locales, un mode apprentissage et une interface soignée.

Prêt à coder pour la planète ? Allons-y !

Le code complet d’EcoTri est disponible sur GitHub. Clonez le dépôt, testez l’application et partagez vos idées en commentaire !


Pourquoi EcoTri ?

EcoTri est plus qu’un simple quiz : c’est un outil éducatif qui rend l’apprentissage du tri des déchets amusant et interactif. Avec des questions illustrées, un minuteur de 15 secondes, un feedback immédiat et un mode apprentissage clair, l’application est parfaite pour sensibiliser petits et grands.

Voici ce que vous allez créer :

  • Mode quiz : 15 questions à choix multiples avec images, mélangées aléatoirement.
  • Mode apprentissage : Explications sur les catégories de tri (PMC, verre, papier, etc.).
  • Interface moderne : Design épuré avec la police Montserrat, des boutons stylisés et une compatibilité multiplateforme.

Objectifs de ce tutoriel :

  • Construire une application Flutter avec une architecture claire et évolutive.
  • Implémenter un quiz interactif avec minuteur et feedback.
  • Créer un mode apprentissage pour expliquer le tri des déchets.
  • Utiliser des widgets personnalisés pour un code propre et réutilisable.
  • Assurer une interface responsive et fluide.

Prérequis :

  • Flutter SDK (version 3.22+ recommandée) installé. Suivez le guide officiel sur flutter.dev si besoin.
  • Dart 3.8.1+.
  • Un éditeur comme VS Code ou Android Studio.
  • Connaissances de base en Dart/Flutter (widgets, state, navigation).
  • Un café ou un thé pour rester motivé .

Pas encore configuré Flutter ? Téléchargez-le maintenant et suivez les instructions d’installation. Si vous rencontrez des problèmes, laissez un commentaire ci-dessous, je vous aiderai !


Étape 1 : Configurer le Projet Flutter

Commençons par poser les fondations de notre application. Cette étape configure la structure de base et les dépendances nécessaires.

1.1 Créer le projetOuvrez votre terminal et exécutez :

flutter create ecotri
cd ecotri

Cela crée une arborescence standard avec les dossiers android, ios, lib, et le fichier pubspec.yaml. C’est la base de tout projet Flutter !

1.2 Configurer pubspec.yaml

Le fichier pubspec.yaml est le cœur de la configuration. Nous allons y ajouter les dépendances et déclarer les assets (images et polices). Voici le contenu complet :

name: ecotri
description: "Jeu éducatif de tri des déchets en Flutter par (LM‑Code • lm-code.be)"
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=3.8.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  flutter_launcher_icons: ^0.13.1

flutter_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/logo.jpg"

flutter:
  uses-material-design: true
  fonts:
    - family: Montserrat
      fonts:
        - asset: assets/fonts/Montserrat-Regular.ttf
        - asset: assets/fonts/Montserrat-Bold.ttf
          weight: 700
    - family: OpenSans
      fonts:
        - asset: assets/fonts/OpenSans-Regular.ttf
  assets:
    - assets/images/
    - assets/icon/logo.jpg

Explications :

  • Dépendances : cupertino_icons pour des icônes modernes, flutter_launcher_icons pour générer l’icône de l’application.
  • Polices : Nous utilisons Montserrat (régulière et bold) et OpenSans pour un design élégant. Téléchargez ces polices depuis Google Fonts et placez-les dans assets/fonts/.
  • Assets : Le dossier assets/images/ contiendra les 15 images du quiz, et assets/icon/logo.jpg est l’icône de l’application.

Exécutez flutter pub get pour installer les dépendances.

1.3 Créer l’arborescence

Organisez votre projet avec cette structure :

ecotri/
├── assets/
│   ├── fonts/
│   │   ├── Montserrat-Regular.ttf
│   │   ├── Montserrat-Bold.ttf
│   │   └── OpenSans-Regular.ttf
│   ├── images/              # Pour banane.jpg, bouteille.jpg, etc.
│   └── icon/
│       └── logo.jpg
├── lib/
│   ├── data/
│   ├── models/
│   ├── screens/
│   ├── widgets/
│   └── main.dart
├── pubspec.yaml

Créez ces dossiers dans lib et assets. Placez les 15 images (par exemple, bottle_plastic.jpg, banana.jpg) dans assets/images/ et l’icône dans assets/icon/.

Téléchargez les polices Montserrat et OpenSans depuis Google Fonts. Besoin d’images gratuites pour le quiz ? Essayez Unsplash ou Pexels. Partagez vos sources d’images préférées en commentaire !


Étape 2 : Modéliser les Données

Pour gérer les questions du quiz, nous allons créer une classe Question et une liste de questions.

2.1 Créer question.dart

Dans lib/models/, créez question.dart. Cette classe structure chaque question avec son texte, ses options, la réponse correcte et l’image associée.

class Question {
  final String text;
  final List<String> options;
  final int correctIndex;
  final String image;

  const Question({
    required this.text,
    required this.options,
    required this.correctIndex,
    required this.image,
  });
}

Explications :

  • text : La question (ex. : “Où jeter une peau de banane ?”).
  • options : Une liste de 4 choix (ex. : [« Organique », « PMC », « Verre », « Papier »]).
  • correctIndex : L’index de la bonne réponse dans options.
  • image : Le chemin de l’image (ex. : assets/images/banana.jpg).

2.2 Créer questions.dart

Dans lib/data/, créez questions.dart avec une liste de 15 questions. Voici le code complet :

import '../models/question.dart';

const List<Question> questions = [
  Question(
    text: 'Où jeter cette bouteille en plastique ?',
    image: 'assets/images/bottle_plastic.jpg',
    options: ['Verre', 'PMC', 'Déchets spéciaux', 'Papier'],
    correctIndex: 1,
  ),
  Question(
    text: 'Où jeter un journal ?',
    image: 'assets/images/journal.jpg',
    options: ['PMC', 'Papier', 'Organique', 'Poubelle ménagère'],
    correctIndex: 1,
  ),
  Question(
    text: 'Où jeter une banane épluchée ?',
    image: 'assets/images/banana.jpg',
    options: ['Organique', 'Papier', 'PMC', 'Déchets spéciaux'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter une bouteille en verre ?',
    image: 'assets/images/bottle_glass.jpg',
    options: ['Verre', 'PMC', 'Papier', 'Poubelle ménagère'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter des piles usagées ?',
    image: 'assets/images/batteries.jpg',
    options: ['Déchets spéciaux', 'Organique', 'Papier', 'Verre'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter un carton à boissons (Tetra Pak) ?',
    image: 'assets/images/carton_tetra.jpg',
    options: ['PMC', 'Papier', 'Verre', 'Organique'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter un sac d’aspirateur usagé ?',
    image: 'assets/images/vacuum_bag.jpg',
    options: ['Poubelle ménagère', 'PMC', 'Papier', 'Déchets spéciaux'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter un pot de confiture en verre (sans couvercle) ?',
    image: 'assets/images/jam_jar.jpg',
    options: ['Verre', 'PMC', 'Papier', 'Organique'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter des médicaments périmés ?',
    image: 'assets/images/meds.jpg',
    options: ['Déchets spéciaux (pharmacie)', 'PMC', 'Organique', 'Papier'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter des épluchures de légumes ?',
    image: 'assets/images/veggie_peels.jpg',
    options: ['Organique', 'Papier', 'PMC', 'Déchets spéciaux'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter une canette en aluminium ?',
    image: 'assets/images/can_metal.jpg',
    options: ['PMC', 'Verre', 'Papier', 'Déchets spéciaux'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter une boîte en carton (colis) ?',
    image: 'assets/images/cardboard_box.jpg',
    options: ['Papier', 'PMC', 'Verre', 'Organique'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter une bouteille d’huile de friture usagée ?',
    image: 'assets/images/cooking_oil_bottle.jpg',
    options: ['Déchets spéciaux', 'PMC', 'Verre', 'Poubelle ménagère'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter une boîte à pizza graisseuse ?',
    image: 'assets/images/pizza_box_dirty.jpg',
    options: ['Poubelle ménagère', 'Papier', 'Organique', 'PMC'],
    correctIndex: 0,
  ),
  Question(
    text: 'Où jeter un pot de yaourt en plastique ?',
    image: 'assets/images/yogurt_pot.jpg',
    options: ['PMC', 'Papier', 'Verre', 'Déchets spéciaux'],
    correctIndex: 0,
  ),
];

2.3. Explications :

  • Chaque question est une instance de la classe Question.
  • Les images doivent être placées dans assets/images/ et correctement référencées.
  • Les options sont conçues pour couvrir les principales catégories de tri en Belgique.

Créez vos propres questions ! Ajoutez des déchets spécifiques à votre région ou des astuces locales. Partagez vos idées en commentaire pour inspirer d’autres lecteurs !


Étape 3 : Créer l’Écran d’Accueil (start_screen.dart)

L’écran d’accueil est la porte d’entrée de l’application. Il propose trois options : jouer au quiz, consulter le guide de tri, ou voir la page À propos.

3.1 Créer start_screen.dart

Dans lib/screens/, créez start_screen.dart. Cet écran utilise un StatelessWidget pour un design simple et responsive.

import 'package:flutter/material.dart';
import 'game_screen.dart';

class StartScreen extends StatelessWidget {
  const StartScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: LayoutBuilder(
          builder: (context, constraints) => SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
            child: ConstrainedBox(
              constraints: BoxConstraints(minHeight: constraints.maxHeight),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.recycling, size: 100, color: Colors.green),
                  const SizedBox(height: 24),
                  const Text(
                    'EcoTri',
                    style: TextStyle(
                      fontFamily: 'Montserrat',
                      fontSize: 34,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Testez vos connaissances sur le tri des déchets en Belgique',
                    textAlign: TextAlign.center,
                    style: TextStyle(fontFamily: 'OpenSans'),
                  ),
                  const SizedBox(height: 40),
                  ElevatedButton.icon(
                    icon: const Icon(Icons.play_arrow),
                    label: const Text('Commencer le jeu'),
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      backgroundColor: Colors.green,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    onPressed: () => Navigator.push(
                      context,
                      MaterialPageRoute(builder: (_) => const GameScreen()),
                    ),
                  ),
                  const SizedBox(height: 16),
                  ElevatedButton.icon(
                    icon: const Icon(Icons.menu_book),
                    label: const Text('Guide de tri'),
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      backgroundColor: Colors.white,
                      foregroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    onPressed: () => Navigator.pushNamed(context, '/guide'),
                  ),
                  const SizedBox(height: 16),
                  ElevatedButton.icon(
                    icon: const Icon(Icons.info),
                    label: const Text('À propos'),
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      backgroundColor: Colors.white,
                      foregroundColor: Colors.green,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    onPressed: () => Navigator.pushNamed(context, '/about'),
                  ),
                  const SizedBox(height: 60),
                  const Text(
                    '© 2025 LM‑Code – lm-code.be',
                    style: TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

3.2. Explications :

  • Design : L’icône de recyclage et le titre “EcoTri” utilisent la police Montserrat pour un look moderne. Les boutons sont stylisés avec des coins arrondis et des couleurs cohérentes (vert pour le thème écologique).
  • Responsive : LayoutBuilder et SingleChildScrollView garantissent que l’écran s’adapte à toutes les tailles d’écran.
  • Navigation : Les boutons utilisent Navigator.push et Navigator.pushNamed pour passer aux autres écrans.

3.3. Bonnes pratiques :

  • Utilisez const pour les widgets statiques afin d’optimiser les performances.
  • Assurez-vous que les boutons occupent toute la largeur avec minimumSize: Size.fromHeight(50).
  • Testez sur différents appareils pour vérifier la responsivité.

Personnalisez l’écran d’accueil ! Ajoutez une image de fond ou une animation avec flutter_animate. Partagez vos modifications en commentaire !


Étape 4 : Créer le Quiz (game_screen.dart)

Le cœur d’EcoTri, c’est son quiz interactif ! Cet écran affiche les questions, gère le minuteur et donne un feedback immédiat.

4.1 Créer game_screen.dart

Dans lib/screens/, créez game_screen.dart. Cet écran est un StatefulWidget pour gérer l’état dynamique (questions, score, minuteur).

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import '../data/questions.dart';
import '../models/question.dart';
import 'result_screen.dart';
import '../widgets/option_button.dart';

class GameScreen extends StatefulWidget {
  const GameScreen({Key? key}) : super(key: key);

  @override
  State<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> {
  late final List<Question> _shuffled;
  int current = 0, score = 0;
  late List<String> _opts;
  late int _correct;
  int? _selected;
  bool _answered = false, _isCorrect = false;
  static const int _maxSeconds = 15;
  int _secondsLeft = _maxSeconds;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _shuffled = List<Question>.from(questions)..shuffle(Random());
    _prepareQuestion();
  }

  void _prepareQuestion() {
    final q = _shuffled[current];
    _opts = List<String>.from(q.options)..shuffle(Random());
    _correct = _opts.indexOf(q.options[q.correctIndex]);
    _timer?.cancel();
    _secondsLeft = _maxSeconds;
    _timer = Timer.periodic(const Duration(seconds: 1), (t) {
      if (_secondsLeft == 0) {
        _handleTimeout();
      } else {
        setState(() => _secondsLeft--);
      }
    });
  }

  void _handleTimeout() {
    _timer?.cancel();
    setState(() {
      _answered = true;
      _isCorrect = false;
      _selected = null;
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Temps écoulé ! La bonne réponse était "${_opts[_correct]}".'),
        backgroundColor: Colors.red,
        duration: const Duration(seconds: 2),
      ),
    );
    Future.delayed(const Duration(seconds: 2), _nextQ);
  }

  IconData _iconFor(String l) {
    switch (l.toLowerCase()) {
      case 'verre':
        return Icons.wine_bar;
      case 'pmc':
        return Icons.recycling;
      case 'déchets spéciaux':
      case 'déchets spéciaux (pharmacie)':
        return Icons.warning;
      case 'papier':
        return Icons.description;
      case 'organique':
        return Icons.eco;
      case 'poubelle ménagère':
        return Icons.delete;
      default:
        return Icons.check_circle;
    }
  }

  void _select(int i) {
    if (_answered) return;
    _timer?.cancel();
    setState(() {
      _selected = i;
      _answered = true;
      _isCorrect = i == _correct;
      if (_isCorrect) score++;
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          _isCorrect
              ? 'Bravo ! "${_opts[i]}" est correct.'
              : 'Dommage… La bonne réponse était "${_opts[_correct]}".',
        ),
        backgroundColor: _isCorrect ? Colors.green : Colors.red,
        duration: const Duration(seconds: 2),
      ),
    );
    Future.delayed(const Duration(seconds: 2), _nextQ);
  }

  void _nextQ() {
    if (current + 1 < _shuffled.length) {
      setState(() {
        current++;
        _selected = null;
        _answered = false;
      });
      _prepareQuestion();
    } else {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (_) => ResultScreen(score: score, total: _shuffled.length),
        ),
      );
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final q = _shuffled[current];

    return Scaffold(
      appBar: AppBar(
        title: Text('Question ${current + 1}/${_shuffled.length}'),
      ),
      body: LayoutBuilder(
        builder: (context, c) => SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: ConstrainedBox(
            constraints: BoxConstraints(minHeight: c.maxHeight),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                LinearProgressIndicator(
                  value: (current + 1) / _shuffled.length,
                  backgroundColor: Colors.green.shade100,
                  valueColor: const AlwaysStoppedAnimation(Colors.green),
                ),
                const SizedBox(height: 12),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const Icon(Icons.timer, size: 18),
                    const SizedBox(width: 4),
                    Text('$_secondsLeft s',
                        style: const TextStyle(fontWeight: FontWeight.bold)),
                  ],
                ),
                const SizedBox(height: 16),
                Text(
                  q.text,
                  style: const TextStyle(fontFamily: 'Montserrat', fontSize: 20),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 16),
                ClipRRect(
                  borderRadius: BorderRadius.circular(8),
                  child: Image.asset(
                    q.image,
                    height: MediaQuery.of(context).size.height * 0.35,
                    fit: BoxFit.cover,
                    width: double.infinity,
                  ),
                ),
                const SizedBox(height: 24),
                ...List.generate(
                  _opts.length,
                  (i) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 6),
                    child: OptionButton(
                      label: _opts[i],
                      icon: _iconFor(_opts[i]),
                      isSelected: _selected == i,
                      isCorrect: _answered && i == _correct,
                      onTap: () => _select(i),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

4.2. Explications :

  • Questions aléatoires : Les questions sont mélangées avec shuffle(Random()) pour varier l’expérience.
  • Minuteur : Timer.periodic décompte 15 secondes par question. Si le temps est écoulé, un message s’affiche et la question passe.
  • Feedback : Un ScaffoldMessenger affiche “Bravo !” ou “Dommage…” avec une couleur verte ou rouge.
  • Progression : Une LinearProgressIndicator montre l’avancement dans le quiz.
  • Icônes dynamiques : La fonction _iconFor associe une icône à chaque type de déchet (par exemple, Icons.recycling pour PMC).

4.3.Bonnes pratiques :

  • Libérez les ressources avec _timer?.cancel() dans dispose.
  • Utilisez setState uniquement pour les mises à jour nécessaires.
  • Testez le minuteur pour vous assurer qu’il est fluide.

Essayez le quiz sur votre téléphone ou sur le web avec flutter run -d chrome. Combien de bonnes réponses avez-vous eues ? Partagez votre score en commentaire !


Étape 5 : Créer le Widget Personnalisé (option_button.dart)

Pour garder le code propre, nous créons un bouton réutilisable pour les options du quiz.

5.1 Créer option_button.dart

Dans lib/widgets/, créez option_button.dart.

import 'package:flutter/material.dart';

class OptionButton extends StatelessWidget {
  final String label;
  final IconData icon;
  final VoidCallback onTap;
  final bool isSelected;
  final bool isCorrect;

  const OptionButton({
    Key? key,
    required this.label,
    required this.icon,
    required this.onTap,
    this.isSelected = false,
    this.isCorrect = false,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Color border = Colors.transparent;
    Color bg = Colors.white;
    if (isSelected) {
      border = isCorrect ? Colors.green : Colors.red;
      bg = isCorrect ? Colors.green.shade50 : Colors.red.shade50;
    }

    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        elevation: 2,
        backgroundColor: bg,
        foregroundColor: Colors.black,
        side: BorderSide(color: border, width: 2),
        padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
      ),
      onPressed: onTap,
      child: Row(
        children: [
          Icon(icon, size: 20),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              label,
              style: const TextStyle(fontFamily: 'OpenSans', fontWeight: FontWeight.w600),
            ),
          ),
        ],
      ),
    );
  }
}

Explications :

  • Style Bootstrap : Les boutons ont des coins arrondis, une ombre légère et changent de couleur selon la réponse (vert pour correct, rouge pour incorrect).
  • Icônes : Chaque bouton affiche une icône correspondant au type de déchet.
  • Réutilisable : Ce widget est utilisé pour toutes les options du quiz, respectant le principe DRY (Don’t Repeat Yourself).

Bonnes pratiques :

  • Utilisez const pour le constructeur et les widgets statiques.
  • Testez les couleurs sur différents thèmes (clair/sombre).

Personnalisez les boutons ! Essayez d’ajouter une animation au clic avec flutter_animate. Montrez-nous votre design en commentaire !


Étape 6 : Créer l’Écran des Résultats (result_screen.dart)

Cet écran affiche le score final et motive l’utilisateur à rejouer.

6.1 Créer result_screen.dart

Dans lib/screens/, créez result_screen.dart.

import 'package:flutter/material.dart';

class ResultScreen extends StatelessWidget {
  final int score;
  final int total;

  const ResultScreen({Key? key, required this.score, required this.total}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final double pct = score / total;
    late String title, message;
    late IconData icon;

    if (pct >= 0.8) {
      title = 'Excellent !';
      message = 'Vous êtes un expert du tri sélectif !';
      icon = Icons.emoji_events;
    } else if (pct >= 0.6) {
      title = 'Très bien !';
      message = 'Encore un petit effort pour atteindre la perfection.';
      icon = Icons.thumb_up;
    } else if (pct >= 0.4) {
      title = 'Pas mal…';
      message = 'Revoyez le guide de tri pour progresser.';
      icon = Icons.sentiment_neutral;
    } else {
      title = 'Peut mieux faire';
      message = 'Un peu de révision et vous y arriverez !';
      icon = Icons.sentiment_dissatisfied;
    }

    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(icon, size: 100, color: Theme.of(context).primaryColor),
              const SizedBox(height: 24),
              Text(
                title,
                style: const TextStyle(
                  fontFamily: 'Montserrat',
                  fontSize: 32,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 16),
              Text(message, textAlign: TextAlign.center, style: const TextStyle(fontFamily: 'OpenSans')),
              const SizedBox(height: 32),
              Text(
                '$score / $total',
                style: const TextStyle(
                  fontFamily: 'Montserrat',
                  fontSize: 48,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 32),
              ElevatedButton.icon(
                icon: const Icon(Icons.replay),
                label: const Text('Rejouer'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
                ),
                onPressed: () => Navigator.pushReplacementNamed(context, '/'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

6.2. Explications :

  • Feedback motivant : Le message et l’icône changent selon le score (par exemple, Icons.emoji_events pour un score élevé).
  • Design clair : Le score est affiché en gros pour capter l’attention.
  • Rejouer : Le bouton renvoie à l’écran d’accueil pour recommencer le quiz.

6.3. Bonnes pratiques :

  • Utilisez des messages positifs pour encourager l’utilisateur.
  • Testez l’écran sur différentes tailles d’écran.

Quel est votre meilleur score ? Essayez de battre vos amis et partagez vos résultats en commentaire !


Étape 7 : Créer le Mode Apprentissage (guide_screen.dart)

Le mode apprentissage explique les catégories de tri de manière claire et concise.

7.1 Créer guide_screen.dart

Dans lib/screens/, créez guide_screen.dart.

import 'package:flutter/material.dart';

class GuideScreen extends StatelessWidget {
  const GuideScreen({Key? key}) : super(key: key);

  Widget _section(String title, String desc, Color color) => Card(
        margin: const EdgeInsets.symmetric(vertical: 8),
        child: ListTile(
          leading: CircleAvatar(backgroundColor: color, radius: 12),
          title: Text(
            title,
            style: const TextStyle(fontFamily: 'Montserrat', fontWeight: FontWeight.w600),
          ),
          subtitle: Text(desc, style: const TextStyle(fontFamily: 'OpenSans')),
        ),
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Guide de tri', style: TextStyle(fontFamily: 'Montserrat')),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _section(
            'PMC',
            'Bouteilles et flacons en plastique, emballages métalliques, cartons à boissons.',
            Colors.blue,
          ),
          _section(
            'Verre',
            'Bouteilles et pots en verre (sans couvercle ni bouchon).',
            Colors.teal,
          ),
          _section(
            'Papier / Carton',
            'Journaux, magazines, cartons propres sans résidu alimentaire.',
            Colors.orange,
          ),
          _section(
            'Organique',
            'Épluchures, restes de repas (selon communes).',
            Colors.brown,
          ),
          _section(
            'Déchets spéciaux',
            'Piles, médicaments, huiles de friture, produits chimiques… à rapporter au parc.',
            Colors.red,
          ),
          _section(
            'Poubelle ménagère',
            'Tout ce qui ne va nulle part ailleurs (sac d’aspirateur, litière…).',
            Colors.grey,
          ),
          const SizedBox(height: 24),
          ElevatedButton.icon(
            icon: const Icon(Icons.home),
            label: const Text('Retour au menu'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.green,
              foregroundColor: Colors.white,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
            ),
            onPressed: () => Navigator.popUntil(context, (r) => r.isFirst),
          ),
          const SizedBox(height: 24),
          const Center(
            child: Text(
              '© 2025 LM‑Code – lm-code.be',
              style: TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ),
        ],
      ),
    );
  }
}

7.2. Explications :

  • Cartes claires : Chaque catégorie de tri est affichée dans un Card avec une couleur distinctive.
  • Navigation : Le bouton “Retour au menu” utilise Navigator.popUntil pour revenir à l’écran d’accueil.
  • Responsive : ListView garantit un défilement fluide, même avec beaucoup de contenu.

7.3. Bonnes pratiques :

  • Ajoutez des descriptions courtes et précises.
  • Utilisez des couleurs distinctes pour chaque catégorie.

Ajoutez des exemples locaux à votre guide ! Par exemple, incluez les règles de tri spécifiques à votre commune. Partagez vos ajouts en commentaire !


Étape 8 : Créer la Page À Propos (about_screen.dart)

Cette page présente l’application et rend hommage à son auteur.

8.1 Créer about_screen.dart

Dans lib/screens/, créez about_screen.dart.

import 'package:flutter/material.dart';

class AboutScreen extends StatelessWidget {
  const AboutScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('À propos', style: TextStyle(fontFamily: 'Montserrat')),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'EcoTri',
              style: TextStyle(
                fontFamily: 'Montserrat',
                fontSize: 28,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            const Text(
              'EcoTri est un quiz ludique pour apprendre à trier les déchets en Belgique. '
              'Développé avec Flutter afin d’être disponible sur mobile, web et desktop.',
              style: TextStyle(fontFamily: 'OpenSans'),
            ),
            const SizedBox(height: 24),
            const Text(
              'Développeur',
              style: TextStyle(
                fontFamily: 'Montserrat',
                fontSize: 20,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                const Icon(Icons.person),
                const SizedBox(width: 8),
                const Expanded(child: Text('LM‑Code – https://lm-code.be')),
              ],
            ),
            const Spacer(),
            Center(
              child: ElevatedButton.icon(
                icon: const Icon(Icons.home),
                label: const Text('Retour au menu'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
                ),
                onPressed: () => Navigator.popUntil(context, (r) => r.isFirst),
              ),
            ),
            const SizedBox(height: 12),
            const Center(
              child: Text(
                '© 2025 LM‑Code – lm-code.be',
                style: TextStyle(fontSize: 12, color: Colors.grey),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

8.2. Explications :

  • Contenu : Une brève description de l’application et un lien vers lm-code.be.
  • Design : Simple et épuré, avec la police Montserrat pour le titre et OpenSans pour le texte.
  • Navigation : Retour à l’écran d’accueil avec un bouton central.

8.3. Bonnes pratiques :

  • Incluez un lien cliquable si vous déployez sur le web.
  • Ajoutez une icône ou un logo pour personnaliser.

Étape 9 : Configurer le Point d’Entrée (main.dart)

Le fichier main.dart initialise l’application et définit le thème global.

9.1 Modifier main.dart

Dans lib/, mettez à jour main.dart :

import 'package:flutter/material.dart';
import 'screens/start_screen.dart';
import 'screens/guide_screen.dart';
import 'screens/about_screen.dart';

void main() {
  runApp(const EcoTriApp());
}

class EcoTriApp extends StatelessWidget {
  const EcoTriApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'EcoTri',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.green,
        fontFamily: 'Montserrat',
      ),
      initialRoute: '/',
      routes: {
        '/': (_) => const StartScreen(),
        '/guide': (_) => const GuideScreen(),
        '/about': (_) => const AboutScreen(),
      },
    );
  }
}

9.2. Explanations :

  • Thème : La couleur principale est verte (Colors.green) pour refléter le thème écologique. La police par défaut est Montserrat.
  • Navigation : Les routes nommées (/, /guide, /about) simplifient la navigation.
  • Debug : debugShowCheckedModeBanner: false supprime la bannière de débogage.

9.3. Bonnes pratiques :

  • Utilisez des routes nommées pour une navigation claire.
  • Définissez un thème cohérent pour toute l’application.

Testez l’application avec flutter run. Vous préférez mobile, web ou desktop ? Dites-le-nous en commentaire !


Étape 10 : Ajouter les Assets et Tester

10.1 Ajouter les images

Placez les 15 images (par exemple, bottle_plastic.jpg, banana.jpg) dans assets/images/ et l’icône dans assets/icon/logo.jpg. Vérifiez qu’elles sont déclarées dans pubspec.yaml.

10.2 Générer l’icône

Exécutez la commande suivante pour générer l’icône de l’application :

flutter pub run flutter_launcher_icons

10.3 Tester l’application

  • Mobile : flutter run
  • Web : flutter run -d chrome
  • Desktop (Windows) : flutter run -d windows

Vérifiez que :

  • Le quiz fonctionne avec le minuteur et le feedback.
  • Les images s’affichent correctement.
  • La navigation entre les écrans est fluide.
  • L’interface est responsive sur toutes les plateformes.

Bonnes pratiques :

  • Testez sur un émulateur et un appareil réel.
  • Utilisez MediaQuery pour ajuster les tailles (par exemple, l’image du quiz occupe 35 % de la hauteur de l’écran).
  • Ajoutez un .gitignore pour ignorer les fichiers générés (build/, .idea/).

Téléchargez le projet complet sur GitHub et testez-le ! Si vous rencontrez un bug, laissez un commentaire, je vous guiderai.


Conclusion

EcoTri est une application éducative et ludique qui prouve que coder peut avoir un impact positif. En suivant ce tutoriel, vous avez appris à :

  • Structurer un projet Flutter avec une architecture claire.
  • Créer un quiz interactif avec minuteur et feedback.
  • Concevoir une interface moderne et responsive.
  • Utiliser des widgets personnalisés pour un code propre.

Ressources supplémentaires :

Clonez le projet sur GitHub, testez-le et partagez votre expérience ! Vous avez des questions ou des idées pour améliorer EcoTri ? Laissez un commentaire ci-dessous, je réponds à tout le monde ! Et si ce tutoriel vous a plu, partagez-le sur les réseaux sociaux pour inspirer d’autres développeurs.

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *