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>
This commit is contained in:
Vincent Bourdon
2026-06-19 17:30:45 +02:00
parent f9cdabfe98
commit 02a121703f
10 changed files with 325 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
// Providers transverses (core).
//
// Convention : chaque feature expose ses propres providers dans
// `lib/features/<feature>/presentation/providers.dart`.
// Ce fichier agrège les providers du core ; il restera vide tant qu'aucun
// provider transverse n'est nécessaire.
+37
View File
@@ -0,0 +1,37 @@
/// Types d'échec métier du domaine.
///
/// Les features ajoutent leurs propres sous-types en étendant [Failure].
/// Les exceptions techniques (réseau, IO) sont capturées dans la couche data
/// et converties en [Failure] avant de remonter.
sealed class Failure {
const Failure(this.message);
final String message;
@override
bool operator ==(Object other) =>
other is Failure &&
other.runtimeType == runtimeType &&
other.message == message;
@override
int get hashCode => Object.hash(runtimeType, message);
@override
String toString() => '$runtimeType($message)';
}
/// Erreur de communication réseau (timeout, pas de connectivité, HTTP 5xx…).
final class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
/// Ressource demandée introuvable (HTTP 404, entité absente en base…).
final class NotFoundFailure extends Failure {
const NotFoundFailure(super.message);
}
/// Erreur inattendue non catégorisée — à affiner si elle se répète.
final class UnexpectedFailure extends Failure {
const UnexpectedFailure(super.message);
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:storytime/core/error/failure.dart';
/// Résultat d'une opération qui peut échouer.
///
/// - [Ok] : succès avec une valeur de type [S].
/// - [Err] : échec avec un [Failure] typé.
///
/// Utilisé par les use cases pour renvoyer succès ou erreur sans lever
/// d'exception à travers les couches.
sealed class Result<S> {
const Result();
/// Transforme la valeur en cas de succès ; laisse [Err] intact.
Result<T> map<T>(T Function(S value) transform) => switch (this) {
Ok<S>(:final value) => Ok<T>(transform(value)),
Err<S>(:final failure) => Err<T>(failure),
};
/// Réduit le résultat à une valeur unique en fournissant les deux branches.
T fold<T>({
required T Function(S value) onOk,
required T Function(Failure failure) onErr,
}) => switch (this) {
Ok<S>(:final value) => onOk(value),
Err<S>(:final failure) => onErr(failure),
};
/// Exécute un effet de bord selon la branche, sans retourner de valeur.
void when({
required void Function(S value) ok,
required void Function(Failure failure) err,
}) {
switch (this) {
case Ok<S>(:final value):
ok(value);
case Err<S>(:final failure):
err(failure);
}
}
}
/// Cas succès.
final class Ok<S> extends Result<S> {
const Ok(this.value);
final S value;
@override
bool operator ==(Object other) => other is Ok<S> && other.value == value;
@override
int get hashCode => value.hashCode;
@override
String toString() => 'Ok($value)';
}
/// Cas échec.
final class Err<S> extends Result<S> {
const Err(this.failure);
final Failure failure;
@override
bool operator ==(Object other) => other is Err<S> && other.failure == failure;
@override
int get hashCode => failure.hashCode;
@override
String toString() => 'Err($failure)';
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
/// Routes nommées de l'application.
///
/// Pas de `go_router` pour l'instant — Navigator simple suffit (YAGNI).
/// Les écrans réels seront branchés quand les features correspondantes
/// seront implémentées.
abstract final class AppRoutes {
/// Espace enfant — route par défaut au démarrage.
static const String child = '/';
/// Écran de saisie du code parental (accès à l'espace parent).
static const String parentGate = '/parent-gate';
/// Espace parent (gestion podcasts, limites, réglages).
static const String parent = '/parent';
}
/// Table de routage de l'application.
///
/// À enrichir au fur et à mesure que les features sont implémentées.
final Map<String, WidgetBuilder> appRoutes = Map.unmodifiable({
AppRoutes.child: (_) => const _ChildPlaceholder(),
AppRoutes.parentGate: (_) => const _ParentGatePlaceholder(),
AppRoutes.parent: (_) => const _ParentPlaceholder(),
});
class _ChildPlaceholder extends StatelessWidget {
const _ChildPlaceholder();
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Storytime')));
}
class _ParentGatePlaceholder extends StatelessWidget {
const _ParentGatePlaceholder();
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Code parental')));
}
class _ParentPlaceholder extends StatelessWidget {
const _ParentPlaceholder();
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Espace parent')));
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
/// Thème de l'application Storytime.
///
/// Palette « coucher » : violet/indigo doux, faible luminosité, contrastes
/// lisibles dans la pénombre. Composants tactiles surdimensionnés (cible enfant).
abstract final class AppTheme {
// Couleur de seed : indigo doux, hue ~250 — dans le spectre bleu-violet.
static const Color _seed = Color(0xFF5C6BC0); // indigo 400
/// Thème clair (utilisé en mode normal ; le mode sombre viendra si besoin).
static ThemeData light() => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: _seed,
brightness: Brightness.light,
),
// Boutons larges pour les doigts d'enfant.
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(minimumSize: const Size(64, 56)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(minimumSize: const Size(64, 56)),
),
);
}