Files
Vincent Bourdon 02a121703f feat(j0.3): socle transverse (Result/Failure, theme, router, DI)
- core/error : Result<S> maison (Ok/Err) + Failure scellee avec egalite de valeur
- core/theme : AppTheme Material 3 (palette coucher, cibles tactiles enfant)
- core/router : routes nommees child/parentGate/parent (Navigator 1, placeholders)
- core/di : conventions providers
- CLAUDE.md §7 : Result maison & Navigator 1 actes (YAGNI)
- ROADMAP : 0.3 cochee, Jalon 0 termine
- corrections code review : egalite Failure, assertions tests, Map.unmodifiable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:30:45 +02:00

8.2 KiB

Storytime — Guide projet & principes d'ingénierie

Lecteur d'histoires audio cadenassé pour le coucher, sur tablette Android. L'enfant écoute des histoires (podcasts) sans pouvoir sortir de l'application ; le parent gère les sources, le code parental et les limites.

Ce fichier est le contrat d'ingénierie du projet. Toute contribution doit le respecter.


1. Le produit en une page

  • Plateforme : Android uniquement. Flutter / Dart.
  • Lecture : streaming (pas de hors-ligne en v1).
  • Deux mondes :
    • Espace enfant (par défaut, verrouillé) : liste de titres d'histoires, gros boutons lecture/pause/suivant.
    • Espace parent (protégé par code 4 chiffres) : gestion des podcasts, limites, réglages.
  • Verrouillage : Screen Pinning (épinglage natif Android).

Contraintes connues (à ne jamais oublier)

  • L'épinglage empêche la sortie facile, mais la sortie réelle repose sur le mécanisme natif Android + le PIN de la tablette, PAS sur notre code parental. C'est un garde-fou enfant, pas un coffre-fort. Ne jamais promettre plus dans l'UI ou les messages.
  • L'épinglage ne survit pas à un redémarrage de la tablette.
  • Le code parental protège l'accès aux écrans parent et la reprise après limite, pas le désépinglage système.

2. Philosophie de développement

Le projet suit le Software Craftsmanship : code soigné, testé, simple, qui se lit comme une explication du domaine.

TDD — non négociable

Cycle Red → Green → Refactor pour toute fonctionnalité ou correction :

  1. Red : écrire un test qui décrit le comportement attendu. Le lancer, vérifier qu'il échoue pour la bonne raison.
  2. Green : écrire le minimum de code pour le faire passer.
  3. Refactor : nettoyer sans changer le comportement, tests toujours verts.

Ne jamais écrire de code de production sans test qui le justifie. Les seules exceptions : code purement déclaratif (constantes, thèmes) et glue framework triviale — et même là, le comportement observable se teste.

Clean Code

  • Noms qui révèlent l'intention ; pas d'abréviations obscures.
  • Fonctions courtes, une seule raison d'exister.
  • Pas de commentaire qui paraphrase le code ; un commentaire explique le pourquoi, jamais le quoi.
  • Pas de nombre/chaîne magique : constantes nommées.
  • Gestion d'erreur explicite (voir Result/Either, §4).
  • Boy Scout Rule : laisser le code un peu plus propre qu'on ne l'a trouvé, sans dérive hors-sujet.

YAGNI

On n'implémente que ce que le jalon courant exige. Pas d'abstraction « au cas où ».


3. Clean Architecture

Dépendances dirigées vers le domaine. Le domaine ne connaît ni Flutter, ni HTTP, ni la base de données.

Presentation  ─────►  Application (use cases)  ─────►  Domain  ◄─────  Data (infra)
  (UI/Riverpod)            (orchestration)         (cœur métier)      (impl. dépôts)

Couches

  • Domain (domain/) — entités, value objects, interfaces de dépôts (abstraites), erreurs métier. Zéro dépendance externe. Dart pur.
  • Application (application/) — use cases : une classe = une intention métier (PlayStoryUseCase, AddPodcastUseCase). Orchestrent le domaine via les interfaces de dépôts. Dart pur.
  • Data (data/) — implémentations concrètes des dépôts, data sources (HTTP iTunes, parsing RSS, SQLite, secure storage), DTO/mappers. Dépend du domaine, jamais l'inverse.
  • Presentation (presentation/) — écrans, widgets, contrôleurs d'état (Riverpod Notifiers). Appelle les use cases. Ne contient pas de logique métier.

Règle de dépendance

Une couche ne dépend que de couches plus internes. Le domaine n'importe jamais package:flutter, package:http, sqflite, etc. Toute violation est un bug d'architecture.

Injection de dépendances

Riverpod comme conteneur DI + gestion d'état. Les use cases et dépôts sont fournis via des Provider. En test, on override les providers par des doubles.


4. Conventions de code

  • État/DI : Riverpod (Notifier/AsyncNotifier).
  • Immutabilité : modèles immuables (freezed ou copyWith manuel pour les entités simples).
  • Erreurs : pas d'exception qui traverse les couches en silence. Le domaine renvoie un type Result<Success, Failure> (ou Either). Les Failure sont des types métier (NetworkFailure, InvalidFeedFailure, …). Les exceptions techniques sont capturées dans la couche data et converties en Failure.
  • Asynchrone : Future/Stream typés ; pas de dynamic.
  • Lint : flutter_lints + règles strictes activées (analysis_options.yaml). Le projet doit rester 0 warning.
  • Format : dart format ; pas de débat de style.

Arborescence cible

lib/
  core/                 # transverse : Result, erreurs de base, constantes, thème, router
  features/
    locking/            # verrouillage / épinglage
      domain/
      application/
      data/
      presentation/
    playback/           # lecture audio
    podcasts/           # recherche, RSS, abonnements
    parental/           # code parental, accès espace parent
    limits/             # minuterie, compteur d'histoires, avertissements
  main.dart
test/                   # miroir de lib/ ; tests unitaires & use cases
test/widget/            # tests de widgets
integration_test/       # tests d'intégration (épinglage, parcours bout-en-bout)

Feature-first, puis découpage en couches dans chaque feature. Un fichier = une responsabilité. Quand un fichier grossit, c'est le signal qu'il fait trop de choses : on le scinde.


5. Stratégie de test

Niveau Cible Outils
Unitaire Domain + use cases (la majorité des tests) flutter_test, mocktail
Widget Écrans/widgets isolés flutter_test
Intégration Épinglage réel, parcours enfant/parent integration_test sur appareil/émulateur
  • Les tests de domaine/use cases ne touchent jamais au réseau, au disque ni à Flutter.
  • Les dépendances externes (HTTP, RSS, stockage, plugin d'épinglage) sont derrière des interfaces, mockées en test.
  • Le jalon 1 (épinglage) exige une validation sur appareil réel : un test/POC manuel documenté en plus des tests automatisés.

6. Processus & suivi

  • docs/specs/ : la spec, découpée par jalon (un dossier par jalon). Chaque jalon a un README.md (objectif + DoD) et ses étapes en sous-fichiers numérotés.
  • ROADMAP.md : vue d'ensemble de l'avancement. À mettre à jour à la fin de chaque étape (cocher la case, dater, noter les écarts). C'est la source de vérité du « où on en est ».
  • On traite un jalon à la fois, une étape à la fois, en TDD.
  • Definition of Done d'une étape : code + tests verts + 0 warning lint + ROADMAP.md à jour.

Rituel à chaque étape terminée

  1. Tous les tests de l'étape passent.
  2. flutter analyze ne renvoie aucun warning.
  3. Cocher l'étape dans ROADMAP.md (avec la date du jour).
  4. Si un écart avec la spec est apparu, le noter dans le fichier d'étape concerné.

7. Décisions techniques figées

Sujet Choix Raison
Langage/UI Flutter / Material 3 Mono-plateforme Android, connu de l'auteur
État + DI Riverpod Testable, override facile en test
Gestion d'erreur Result<S> maison (Ok/Err) — PAS de fpdart/dartz YAGNI : on n'a besoin que de map/fold/when ; pas de dépendance externe, contrôle total
Audio just_audio + audio_service Lecture + contrôle arrière-plan
Épinglage kiosk_mode (plugin), fallback platform channel Kotlin API native Screen Pinning
Recherche podcasts API iTunes Search (gratuite, sans clé) Annuaire public
RSS dart_rss (ou webfeed) Parsing de flux
Persistance sqflite/drift (abonnements) + flutter_secure_storage (code haché) Local, sûr
Lecture seule réglages shared_preferences Compteurs/limites
Router Navigator 1 + routes nommées — PAS de go_router YAGNI : pas de deep-linking ni de navigation imbriquée en v1

Tout changement de cette table doit être justifié dans le commit et reflété ici.