29 KiB
🏗️ ARCHITECTURE UI-UX — SafeBite Android
Document d'architecture pour la refonte UI/UX • Version 1.0 • 25 avril 2026
📋 TABLE DES MATIÈRES
- État des lieux
- Principes directeurs
- Architecture de navigation cible
- Design System
- Spécifications des écrans — Phase 1
- Composants réutilisables
- Plan de migration
- Annexes
1. ÉTAT DES LIEUX
1.1 Architecture actuelle
L'application suit le pattern MVVM + Clean Architecture avec Jetpack Compose :
presentation/
├── MainActivity.kt # Point d'entrée
├── common/
│ ├── components/ # Composants UI réutilisables
│ │ ├── AppBars.kt # Barres de navigation
│ │ ├── Buttons.kt # Boutons standardisés
│ │ ├── Cards.kt # Cartes et surfaces
│ │ ├── Components.kt # Chips, banners, avatars
│ │ ├── Feedback.kt # Loading, erreurs
│ │ └── TextFields.kt # Champs de saisie
│ └── util/
│ └── UiState.kt # États UI génériques
├── navigation/
│ ├── NavGraph.kt # Navigation principale
│ └── Screen.kt # Routes
├── screen/
│ ├── home/HomeScreen.kt # Écran d'accueil (dashboard actuel)
│ ├── scanner/ScannerScreen.kt # Scanner code-barres
│ ├── result/ResultScreen.kt # Résultat d'analyse
│ ├── onboarding/ # Onboarding
│ ├── profile/ # Profils famille
│ ├── history/ # Historique
│ ├── settings/ # Paramètres
│ └── ocr/ # Capture OCR
└── theme/
├── Color.kt # Palette de couleurs
├── Type.kt # Typographie
├── Shape.kt # Formes
├── Dimens.kt # Espacements
├── StatusColors.kt # Couleurs de statut
└── Theme.kt # Thème Material 3
1.2 Écarts identifiés vs spec UX
| Élément | Spec UX (flux-UX.md) | Existant | Écart |
|---|---|---|---|
| Navigation | BottomNavigationView 4 onglets + FAB central | Navigation linéaire (Home → Scanner → Result) | 🔴 Majeur |
| FAB Scanner | FAB 56dp centré, chevauchant la bottom bar | Bouton dans HomeScreen | 🔴 Majeur |
| Dashboard contextuel | 3 modes (store/home/first) | HomeScreen basique | 🟠 Partiel |
| Verdict immédiat | Banner tricolore en < 500ms avec skeleton | ResultScreen complet mais sans skeleton | 🟠 Partiel |
| Skeleton screen | Shimmer animé pendant le loading | Spinner basique (LoadingIndicator) | 🟡 Mineur |
| Couleurs | Feu tricolore strict (#2ECC71, #E67E22, #E74C3C) | Palette Material 3 différente | 🟡 Mineur |
| Animations | Transitions standardisées (200-300ms) | Animations de base (fade + slide) | 🟡 Mineur |
| Accessibilité | TalkBack, contrastes WCAG AA | Partiel | 🟡 Mineur |
| Onglet Listes | 📋 Listes intelligentes | Absent | 🔴 Majeur |
| Onglet Suivi | 📊 Statistiques + historique | HistoryScreen basique | 🟠 Partiel |
1.3 Points forts de l'existant
- ✅ Architecture Clean Architecture bien structurée
- ✅ Jetpack Compose avec Material 3
- ✅ Thème light/dark fonctionnel
- ✅ Composants de base réutilisables
- ✅ Domain layer avec UseCases
- ✅ Repository pattern avec Room + Retrofit
- ✅ Hilt pour l'injection de dépendances
2. PRINCIPES DIRECTEURS
2.1 Règles prioritaires (de flux-UX.md §1)
| # | Principe | Implication technique |
|---|---|---|
| P1 | 2 taps max | Le scanner doit être accessible depuis n'importe quel écran en ≤ 2 taps |
| P2 | Verdict immédiat | Affichage du verdict en < 500ms perçues (skeleton immédiat, données async) |
| P3 | Feu tricolore | 3 couleurs sémantiques max — jamais de bleu pour les statuts |
| P4 | Icônes + couleurs | Aucune info critique basée uniquement sur la couleur |
| P5 | Guidage positif | Pas d'erreurs brutes — toujours une action de repli |
| P6 | Mobile-first | Conception une main, pouce accessible (zone inférieure) |
2.2 Contraintes techniques
- Minimum SDK : API 26 (Android 8.0)
- Framework UI : Jetpack Compose (Material 3)
- Navigation : Compose Navigation
- Caméra : CameraX + ML Kit Barcode Scanning
- Base locale : Room
- Taille APK cible : < 25 Mo
3. ARCHITECTURE DE NAVIGATION CIBLE
3.1 Structure globale
┌─────────────────────────────────────────────────────┐
│ APPLICATION │
├─────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Dashboard│ │ Listes │ │ Suivi │ │Famille│ │
│ │ 🏠 │ │ 📋 │ │ 📊 │ │ 👤 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────┘ │
│ ▲ │
│ │ │
│ ┌────┴─────────┐ <-- FAB central (56dp) │
│ │ SCANNER │ Chevauche la bottom bar │
│ └──────────────┘ Disparaît pendant le scan │
└─────────────────────────────────────────────────────┘
3.2 Nouvelles routes
sealed class Screen(val route: String) {
// ── Onglets principaux (Bottom Navigation) ──
data object Dashboard : Screen("dashboard") // Remplace Home
data object Lists : Screen("lists") // Nouvel onglet
data object Tracking : Screen("tracking") // Remplace History
data object Family : Screen("family") // Remplace ProfileList
// ── Écrans de navigation (non dans bottom nav) ──
data object Scanner : Screen("scanner") // Plein écran
data object Result : Screen("result/{barcode}") // Bottom sheet → plein écran
data object ProductDetail : Screen("product/{id}")// Fiche détaillée avec tabs
data object Onboarding : Screen("onboarding")
data object Settings : Screen("settings")
// ── Sous-écrans ──
data object ListDetail : Screen("list/{id}")
data object ListEdit : Screen("list/edit?id={id}")
data object ProfileEdit : Screen("profile/edit?id={id}")
data object OcrCapture : Screen("ocr/capture")
data object OcrReview : Screen("ocr/review")
}
3.3 Bottom Navigation — Spécification
data class BottomNavItem(
val id: String,
val iconSelected: ImageVector, // Icône remplie
val iconUnselected: ImageVector, // Icône outline
val label: String,
val contentDescription: String, // TalkBack
val badge: StateFlow<Int> = MutableStateFlow(0) // Notifications non lues
)
val bottomNavItems = listOf(
BottomNavItem(
id = "dashboard",
iconSelected = Icons.Filled.Home,
iconUnselected = Icons.Outlined.Home,
label = "Accueil",
contentDescription = "Tableau de bord"
),
BottomNavItem(
id = "lists",
iconSelected = Icons.Filled.List,
iconUnselected = Icons.Outlined.List,
label = "Listes",
contentDescription = "Mes listes de courses"
),
BottomNavItem(
id = "tracking",
iconSelected = Icons.Filled.BarChart,
iconUnselected = Icons.Outlined.BarChart,
label = "Suivi",
contentDescription = "Statistiques et historique"
),
BottomNavItem(
id = "family",
iconSelected = Icons.Filled.People,
iconUnselected = Icons.Outlined.People,
label = "Famille",
contentDescription = "Profils et réglages"
)
)
3.4 FAB — Spécification technique
fab_scanner:
size: 56dp
icon: Icons.Filled.QrCodeScanner (24dp)
container_color: "#2D3436" # Noir doux
icon_color: "#FFFFFF"
elevation: 6dp
corner_radius: 16dp # Material 3 standard
position:
horizontal: center
vertical: "bottom bar top - 28dp" # Chevauchement
behavior:
visible_on_tabs: ["dashboard", "lists", "tracking", "family"]
hidden_on: ["scanner", "result", "product_detail"]
animation_disappear: "scale(1→0.8) + alpha(1→0), 200ms"
animation_appear: "scale(0.8→1) + alpha(0→1), 200ms"
haptic_feedback: 15ms au tap
accessibility:
content_description: "Scanner un produit"
test_id: "fab_scanner"
4. DESIGN SYSTEM
4.1 Palette de couleurs — Alignement spec ↔ existant
La spec UX demande des couleurs spécifiques qui diffèrent de la palette Material 3 actuelle. Voici le mapping :
// ── COULEURS SÉMANTIQUES (Feu tricolore — hors M3) ──
// Ces couleurs restent indépendantes du thème M3 pour cohérence marque
object SemanticColors {
// Light mode
val Safe = Color(0xFF2ECC71) // Vert sécurité
val SafeContainer = Color(0xFFE8F8F5) // Fond très clair
val Warning = Color(0xFFE67E22) // Orange attention
val WarningContainer = Color(0xFFFEF5E7)
val Danger = Color(0xFFE74C3C) // Rouge danger
val DangerContainer = Color(0xFFFDEDEC)
// Dark mode
val SafeDark = Color(0xFF2ECC71)
val SafeContainerDark = Color(0xFF1A3A2A)
val WarningDark = Color(0xFFE67E22)
val WarningContainerDark = Color(0xFF3A2A1A)
val DangerDark = Color(0xFFE74C3C)
val DangerContainerDark = Color(0xFF3A1A1A)
}
// ── NEUTRES ──
object NeutralColors {
val Background = Color(0xFFF5F5F0) // Gris chaud (spec §2.1)
val Surface = Color(0xFFFFFFFF) // Blanc pur pour cartes
val TextPrimary = Color(0xFF2D3436) // Noir doux
val TextSecondary = Color(0xFF636E72) // Gris moyen
val Separator = Color(0xFFDFE6E9) // Gris clair
}
4.2 Typographie
object SafeBiteTypography {
val Display = TextStyle(
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
)
val Headline = TextStyle(
fontSize = 22.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 28.sp
)
val Body = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
lineHeight = 24.sp
)
val Caption = TextStyle(
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
lineHeight = 18.sp
)
val Button = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 20.sp
)
}
4.3 Système d'icônes de statut (daltonien-safe)
┌─────────────────────────────────────────────────────────┐
│ VERT (#2ECC71) ORANGE (#E67E22) ROUGE (#E74C3C) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ⭕ │ │ 🔺 │ │ 🔷 │ │
│ │ ✅ │ │ ⚠️ │ │ ❌ │ │
│ │ CERCLE │ │ TRIANGLE │ │ LOSANGE │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ JAMAIS l'un sans l'autre : forme + couleur + icône │
└─────────────────────────────────────────────────────────┘
4.4 Élévation & Ombres
object ElevationTokens {
val Card = 2.dp // Ombre: 0 1px 3px rgba(0,0,0,0.08)
val FAB = 6.dp // Ombre: 0 3px 8px rgba(0,0,0,0.16)
val BottomSheet = 16.dp // Ombre: 0 8px 24px rgba(0,0,0,0.20)
val Dialog = 24.dp // Ombre: 0 12px 32px rgba(0,0,0,0.24)
}
5. SPÉCIFICATIONS DES ÉCRANS — PHASE 1
5.1 Écran Scanner (refonte)
Fichier cible : app/src/main/java/com/safebite/app/presentation/screen/scanner/ScannerScreen.kt
screen_scanner:
type: "Plein écran (remplace le contenu de l'onglet actif)"
transition_in: "ContainerTransform depuis FAB, 300ms"
transition_out: "Reverse ContainerTransform, 250ms"
layout:
top_bar:
type: "Transparent overlay"
elements:
- {left: "← Retour", action: "popBackStack"}
- {right: "💡 Astuce", action: "showTips"}
camera_zone:
coverage: "70% de l'écran"
reticule:
size: "80% width, 30% height"
border: "4dp blanc, coins arrondis 12dp"
animation: "Ligne laser verte animée (haut → bas, 1.8s loop)"
instruction_text:
content: "Placez le code-barres dans le cadre"
position: "Sous le réticule, centré"
style: "Body, blanc, fond semi-transparent"
bottom_bar:
elements:
- {id: "manual_entry", label: "Saisie manuelle", icon: "keyboard"}
- {id: "torch", label: "Flash", icon: "flash_on/off", toggle: true}
states:
idle: "Camera preview + réticule + instructions"
scanning: "Réticule pulse (scale 1.0 → 1.05 → 1.0, 200ms)"
analyzing: "→ Transition vers skeleton (voir ResultScreen)"
error_camera: "Message + lien Réglages + saisie manuelle"
error_permission: "Dialog rationale + fallback saisie manuelle"
performance:
open_time: "< 300ms"
method: "Pré-initialiser CameraProvider en arrière-plan"
Changements par rapport à l'existant :
- ✅ Réticule animé existe déjà (ScanOverlay)
- ❌ Transition ContainerTransform depuis FAB à ajouter
- ❌ Top bar transparente overlay à ajouter
- ❌ Saisie manuelle du code-barres à ajouter
- ❌ Pré-initialisation caméra à implémenter
5.2 Écran Verdict (refonte majeure)
Fichier cible : app/src/main/java/com/safebite/app/presentation/screen/result/ResultScreen.kt
screen_verdict:
type: "Bottom Sheet → Plein écran au scroll"
transition_in: "Slide up from bottom, 250ms, ease-out"
# ── PHASE 1 : Skeleton immédiat ──
skeleton:
type: "Shimmer gradient animé"
duration: "1.5s loop"
layout: |
┌──────────────────────────────┐
│ ████████████ (nom produit) │
│ ██████ (marque) │
│ ░░░░░░░░░░░░ (verdict) │ ← Background coloré
│ │
│ ████████████ │
│ ██████████ │
└──────────────────────────────┘
# ── PHASE 2 : Verdict ──
verdict_banner:
height: 56dp minimum
padding: "16dp horizontal, 12dp vertical"
radius: 12dp
icon_size: 24dp
variants:
ok:
text: "✅ OK pour toute la famille"
background: "#E8F8F5"
icon: "checkmark.shield.fill"
icon_color: "#2ECC71"
warning:
text: "⚠️ Contient : NOISETTES"
subtext: "⚠️ Attention pour Julie"
background: "#FEF5E7"
icon: "exclamationmark.shield.fill"
icon_color: "#E67E22"
allergene_style: "bold, #E67E22"
danger:
text: "❌ Contient : ARACHIDES"
subtext: "❌ Interdit pour Julie (anaphylaxie)"
extra: "Ne pas consommer"
background: "#FDEDEC"
icon: "xmark.shield.fill"
icon_color: "#E74C3C"
allergene_style: "bold, #E74C3C"
# ── PHASE 3 : Actions ──
actions:
stagger_animation: "Chaque action +50ms de décalage"
buttons:
- {id: "details", label: "Voir détails", priority: primary, icon: "info"}
- {id: "alternatives", label: "Voir alternatives", priority: secondary, icon: "swap"}
- {id: "add_to_list", label: "Ajouter à la liste", priority: secondary, icon: "plus"}
- {id: "scan_again", label: "Scanner un autre", priority: tertiary, icon: "camera"}
# ── Accessibilité ──
accessibility:
announcement: "Verdict : {status}. {details}. Actions : voir détails, alternatives, ajouter à la liste."
talkback_order: "Verdict banner → Product info → Actions (top to bottom)"
Changements par rapport à l'existant :
- ✅ SafetyStatusBanner existe déjà (à adapter aux nouvelles couleurs)
- ❌ Skeleton screen shimmer à ajouter (remplacer LoadingIndicator)
- ❌ Bottom sheet transition à ajouter
- ❌ Stagger animation sur les actions à ajouter
- ❌ Verdict variants (warning/danger) à enrichir avec noms profils
- ❌ Bouton "Alternatives" à ajouter
5.3 Dashboard contextuel (nouvel écran)
Fichier cible : app/src/main/java/com/safebite/app/presentation/screen/dashboard/DashboardScreen.kt (nouveau)
screen_dashboard:
type: "Écran principal (onglet Accueil)"
replaces: "HomeScreen actuel"
# ── Détection contextuelle ──
context_detection:
store_mode:
trigger: "Géolocalisation magasin OU heure 8h-20h semaine"
confidence: "Score basé sur multiples signaux"
home_mode:
trigger: "Soirée (après 20h) OU weekend"
first_time:
trigger: "Aucun scan dans l'historique"
# ── Mode magasin ──
store_layout: |
┌──────────────────────────────┐
│ 🛒 Vous êtes en magasin ? │
│ │
│ [Scanner rapide] ← Large │
│ │
│ Votre liste en cours : │
│ ┌──────────────────────┐ │
│ │ 🥛 Lait demi-écrémé │ │
│ │ 🍞 Pain complet │ │
│ │ 🍎 Pommes x6 │ │
│ └──────────────────────┘ │
│ │
│ "3 produits restants" │
└──────────────────────────────┘
# ── Mode maison ──
home_layout: |
┌──────────────────────────────┐
│ 👋 Bonjour, Sophie │
│ │
│ 📊 Cette semaine : │
│ ████████░░ 78% produits OK │
│ │
│ 🔍 Derniers scans : │
│ ✅ Biscuit Choco │
│ ⚠️ Sauce Curry │
│ │
│ [Scanner] [Mes listes] │
└──────────────────────────────┘
# ── Premier lancement ──
first_time_layout: |
┌──────────────────────────────┐
│ 🎉 Prêt à commencer ! │
│ │
│ 📷 Scannez votre premier │
│ produit │
│ │
│ [Commencer →] │
└──────────────────────────────┘
viewmodel:
state: "DashboardUiState"
sealed_class: |
sealed class DashboardUiState {
object Loading : DashboardUiState()
data class StoreMode(val activeList: ShoppingList?, val remainingCount: Int) : DashboardUiState()
data class HomeMode(val userName: String, val weeklyStats: WeeklyStats, val recentScans: List<ScanHistory>) : DashboardUiState()
object FirstTime : DashboardUiState()
data class Error(val message: String) : DashboardUiState()
}
5.4 Fiche produit détaillée (nouvel écran)
Fichier cible : app/src/main/java/com/safebite/app/presentation/screen/product/ProductDetailScreen.kt (nouveau)
screen_product_detail:
type: "Bottom Sheet → Plein écran au scroll"
trigger: "Tap 'Voir détails' depuis verdict OU historique"
tabs:
- id: "resume"
label: "Résumé"
icon: Icons.Filled.List
content:
- "Verdict sécurité (répété)"
- "Nutri-Score visuel (A-E, pastilles)"
- "Calories / 100g"
- "Jauges sucre/sel/gras"
- id: "allergenes"
label: "Allergènes"
icon: Icons.Filled.Warning
content:
- "14 allergènes réglementaires"
- "Statut : Présent ❌ / Traces ⚠️ / Absent ✅"
- "Allergènes famille highlightés"
- id: "additifs"
label: "Additifs"
icon: Icons.Filled.Science
content:
- "Liste additifs code E"
- "Couleur : Vert/Orange/Rouge"
- "Description courte"
- "Lien 'En savoir plus' → WebView"
- id: "alternatives"
label: "Alternatives"
icon: Icons.Filled.SwapHoriz
condition: "Affiché si verdict != OK"
content:
- "Carousel horizontal produits"
- "Critère : même catégorie, sans allergène"
- "Carte : photo + nom + verdict mini"
6. COMPOSANTS RÉUTILISABLES
6.1 Nouveaux composants à créer
// ── VerdictBanner ──
@Composable
fun VerdictBanner(
verdict: VerdictType, // SAFE, WARNING, DANGER
message: String,
subMessage: String? = null,
allergenName: String? = null,
profileName: String? = null,
modifier: Modifier = Modifier
)
// ── SkeletonScreen ──
@Composable
fun ProductSkeleton(
modifier: Modifier = Modifier
)
// ── ActionButton (standardisé) ──
@Composable
fun ActionButton(
text: String,
onClick: () -> Unit,
icon: ImageVector? = null,
variant: ButtonVariant = ButtonVariant.Primary, // Primary, Secondary, Danger, Tertiary
modifier: Modifier = Modifier
)
// ── VerdictMiniBadge (pour carrousels) ──
@Composable
fun VerdictMiniBadge(
verdict: VerdictType,
size: Dp = 24.dp
)
// ── ProgressBar circulaire (dashboard) ──
@Composable
fun CircularProgress(
progress: Float, // 0.0 - 1.0
label: String,
color: Color,
size: Dp = 120.dp
)
// ── Sparkline (graphique évolution) ──
@Composable
fun SparklineChart(
data: List<Float>,
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier
)
// ── AllergenGrid (sélection profils) ──
@Composable
fun AllergenSelectionGrid(
allergens: List<AllergenType>,
selections: Map<AllergenType, AllergenLevel>,
onSelectionChange: (AllergenType, AllergenLevel) -> Unit
)
enum class AllergenLevel {
NONE, // Non sélectionné
TRACE, // ⚠️ Intolérance/traces
SEVERE // ❌ Allergie sévère
}
6.2 Composants existants à adapter
| Composant | Fichier actuel | Modification nécessaire |
|---|---|---|
SafetyStatusBanner |
Components.kt:90 |
Adapter couleurs spec + formes daltonien |
ProductCard |
Components.kt:118 |
Ajouter verdict mini badge |
AllergenChip |
Components.kt:58 |
Ajouter 3 états (absent/traces/présent) |
PrimaryButton |
Buttons.kt |
Renommer en ActionButton avec variants |
OutlinedActionButton |
Buttons.kt |
Intégrer dans ActionButton variant Secondary |
LoadingIndicator |
Feedback.kt |
Remplacer par ProductSkeleton |
SafeBiteTopAppBar |
AppBars.kt |
Ajouter support overlay transparent |
7. PLAN DE MIGRATION
7.1 Phase 1 — Scanner, Verdict, Dashboard (priorité haute)
| Étape | Action | Fichiers | Effort |
|---|---|---|---|
| 1.1 | Créer la Bottom Navigation | NavGraph.kt, Screen.kt, nouveau MainScreen.kt |
Moyen |
| 1.2 | Créer le FAB central | MainScreen.kt, components/Buttons.kt |
Moyen |
| 1.3 | Adapter les couleurs du Design System | Color.kt, StatusColors.kt |
Faible |
| 1.4 | Refondre ScannerScreen (transitions, saisie manuelle) | ScannerScreen.kt |
Moyen |
| 1.5 | Créer le Skeleton Screen | Nouveau components/Feedback.kt |
Faible |
| 1.6 | Refondre VerdictBanner (3 variantes, accessibilité) | Components.kt |
Moyen |
| 1.7 | Créer DashboardScreen (3 modes contextuels) | Nouveau screen/dashboard/ |
Élevé |
| 1.8 | Adapter ResultScreen (bottom sheet, stagger actions) | ResultScreen.kt |
Moyen |
7.2 Phase 2 — Listes intelligentes
| Étape | Action | Fichiers |
|---|---|---|
| 2.1 | Créer ListsScreen (liste des listes) | Nouveau screen/lists/ |
| 2.2 | Créer ListDetailScreen (détail d'une liste) | Nouveau screen/lists/ |
| 2.3 | Implémenter swipe actions | ListDetailScreen.kt |
| 2.4 | Intégrer alertes allergies dans les listes | ListDetailScreen.kt |
7.3 Phase 3 — Suivi & Statistiques
| Étape | Action | Fichiers |
|---|---|---|
| 3.1 | Créer TrackingScreen (stats + historique) | Nouveau screen/tracking/ |
| 3.2 | Implémenter CircularProgress | components/Feedback.kt |
| 3.3 | Implémenter SparklineChart | Nouveau components/Charts.kt |
| 3.4 | Migrer HistoryScreen vers TrackingScreen | screen/history/ → screen/tracking/ |
7.4 Phase 4 — Profils famille (améliorations)
| Étape | Action | Fichiers |
|---|---|---|
| 4.1 | Refondre FamilyScreen (grille profils) | screen/profile/ |
| 4.2 | Créer AllergenSelectionGrid | components/ |
| 4.3 | Adapter ProfileEditScreen (3 états allergie) | screen/profile/ProfileEditScreen.kt |
8. ANNEXES
8.1 États UI — Scan (mis à jour)
sealed class ScanUiState {
object Idle : ScanUiState()
data class CameraReady(val isPermissionGranted: Boolean) : ScanUiState()
data class Scanning(val analyzedFrames: Int) : ScanUiState()
object Analyzing : ScanUiState() // → Skeleton screen
data class Verdict(
val product: Product,
val verdict: VerdictType,
val matchingProfiles: List<ProfileMatch>
) : ScanUiState()
data class Error(
val errorType: ScanError,
val recoveryAction: RecoveryAction
) : ScanUiState()
}
enum class VerdictType {
SAFE, // ✅ OK famille
WARNING, // ⚠️ Allergène traces/intolérance
DANGER // ❌ Allergène critique
}
data class ProfileMatch(
val profileName: String,
val matchedAllergen: AllergenType,
val severity: AllergenLevel
)
8.2 Checklist accessibilité
accessibilite:
contrastes:
texte_normal: "Ratio ≥ 4.5:1"
texte_grand: "Ratio ≥ 3:1"
composants_ui: "Ratio ≥ 3:1"
validation: "Accessibility Scanner Android"
daltonisme:
regle: "Jamais couleur seule — toujours forme + icône + couleur"
test: "Utilisateur daltonien minimum"
zones_tactiles:
taille_min: "48dp x 48dp"
espacement_min: "8dp entre zones"
talkback:
content_description: "Tout élément interactif"
images_decoratives: "contentDescription = null"
annonces_etat: "Verdict, chargement, erreurs"
ordre_focus: "Gauche→droite, haut→bas"
texte_dynamique:
scale_max: "200% sans perte de contenu"
implementation: "sp (pas dp pour le texte)"
8.3 Performance perçue — Objectifs
| Métrique | Cible | Méthode |
|---|---|---|
| Ouverture scanner | < 300ms | Pré-initialisation caméra |
| Affichage verdict | < 500ms | Skeleton immédiat + données async |
| Transition écrans | 200-300ms | ease-out, jamais > 400ms |
| Scroll FPS | 60fps | LazyColumn + pagination |
| Taille APK | < 25 Mo | R8/ProGuard, optimisation ressources |
8.4 Diagramme de flux — Scan complet
flowchart TD
A[FAB Scanner] -->|Tap| B[ScannerScreen]
B -->|Code-barres détecté| C[Vibration 15ms]
C -->|Transition| D[Skeleton Screen]
D -->|Données reçues| E{Verdict}
E -->|SAFE| F[VerdictBanner Vert]
E -->|WARNING| G[VerdictBanner Orange]
E -->|DANGER| H[VerdictBanner Rouge]
F --> I[Actions disponibles]
G --> I
H --> I
I -->|Voir détails| J[ProductDetailScreen]
I -->|Alternatives| K[Carousel Alternatives]
I -->|Ajouter liste| L[Dialog Sélection liste]
I -->|Scanner autre| B
D -->|Timeout 3s| M[Analyse en cours...]
M -->|Timeout 8s| N[ErrorScreen]
N -->|Réessayer| B
N -->|Saisie manuelle| O[Dialog Code-barres]
O -->|Valider| D
Ce document constitue la référence pour la refonte UI-UX de l'application SafeBite Android. Il sera mis à jour au fur et à mesure de l'implémentation.