Guide d'ingénierie Android et UI/UX Mobile
Un cadre opérationnel pratique et éprouvé en production pour concevoir et construire des applications Android natives avec Jetpack Compose, Material 3, une résilience offline-first et une ergonomie mobile centrée sur l'humain.
1. Ergonomie mobile de base et zone de pouce
Les appareils mobiles sont tenus à une main plus de 70 % du temps. Concevez les interfaces autour de l'accessibilité du pouce, du retour tactile et d'une friction physique minimale.
1.1 Hiérarchie d'accessibilité ergonomique
+---------------------------+ <- Haut de l'écran (Affichage uniquement)
| Statut et en-têtes | Le plus difficile à atteindre. Réservez aux
| Titre, jauge de quota | statuts passifs, barres de recherche et jauges.
+---------------------------+
| | <- Zone d'atteinte moyenne
| Contenu défilable | Idéal pour les éléments de liste, détails et
| Cartes de boîte, extraits| cartes de contenu navigable.
| |
+---------------------------+
| Cibles d'action principales| <- Zone de pouce optimale (40 % inférieurs)
| Boutons Envoyer, Onglets | Le plus facile à taper. Navigations, CTA et
| Feuilles inférieures, Filtres| contrôles interactifs ici.
+---------------------------+
1.2 Normes de cible tactile non négociables
- Taille interactive minimale : Chaque élément cliquable doit atteindre ou dépasser $48 \times 48\text{dp}$ (
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)), même si l'icône visuelle fait $24\text{dp}$. - Marges de bord et zones de sécurité : Enveloppez toujours les mises en page racine Compose dans
Scaffoldou appliquezModifier.windowInsetsPadding(WindowInsets.safeDrawing)pour tenir compte des découpes de caméra, des gestes et des barres de navigation. - Contrôles de formulaire en bas : Placez les champs de texte, les boutons de soumission et les boîtes de dialogue de confirmation dans
ModalBottomSheetou des cartes épinglées en bas afin que le clavier logiciel (imePadding()) n'obscurcisse pas le champ actif.
2. Vitesse et performance perçue
Les utilisateurs mobiles perçoivent immédiatement le délai. Ne bloquez jamais la navigation et n'affichez jamais d'états de chargement vides lorsque des données précédemment récupérées sont disponibles.
2.1 La règle du zéro squelette (Stale-While-Revalidate)
- Le problème : Afficher un squelette de chargement pleine page ou un spinner circulaire chaque fois que l'utilisateur tape sur l'onglet Boîte de réception semble lent, déstabilisant et non poli.
- La solution : Implémentez un modèle de cache Stale-While-Revalidate (SWR) :
- Stockez les éléments récupérés dans un stockage local chiffré (
EncryptedSharedPreferencesou SQLite/Room). - Lors de l'entrée dans l'écran, affichez les éléments en cache immédiatement (délai de 0ms).
- Revalidez en arrière-plan si le cache a plus de 5 à 10 minutes.
- Lorsque les données actualisées arrivent, ajoutez/mettez à jour les éléments en douceur sans vider la liste ni afficher d'espaces réservés vides.
- Affichez uniquement des indicateurs subtils (par exemple, un spinner pull-to-refresh ou une petite barre de progression en haut) lors des rafraîchissements en arrière-plan.
- Stockez les éléments récupérés dans un stockage local chiffré (
2.2 Motif Outbox Offline-First
N'échouez jamais une action utilisateur parce que l'appareil mobile a une connexion cellulaire instable ou est temporairement en mode avion.
flowchart TD
UserAction["L'utilisateur partage un fichier / tape une note"] --> LocalDraft["1. Enregistrer dans les brouillons Outbox locaux"]
LocalDraft --> RenderOutbox["2. Afficher immédiatement dans la carte de file Outbox"]
RenderOutbox --> CheckNet{"Connecté au réseau ?"}
CheckNet -- Oui --> DirectUpload["3. Téléversement direct"]
CheckNet -- Non --> EnqueueWork["3. Planifier un travail WorkManager (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Attendre Wi-Fi / Cellulaire"]
WaitConn --> AutoDispatch["5. Téléversement en arrière-plan avec backoff exponentiel"]
DirectUpload --> Success["6. Mettre à jour l'état UI & notifier Toast"]
AutoDispatch --> Success
États UI de l'Outbox :
- En attente : "En attente du réseau…" avec une icône de nuage en pause.
- Téléversement : "Envoi 42 %…" avec un indicateur de progression linéaire.
- Échec / Nouvel essai : Indicateur de statut clair avec boutons explicites Réessayer et Ignorer.
3. Mouvement, transitions et retours haptiques tactiles
Le design Android moderne (Android 14/15+) nécessite un mouvement dynamique basé sur la physique et un retour sensoriel tactile pour se sentir natif et réactif.
3.1 Gestion de l'affichage bord à bord et des barres système
Dessinez toujours derrière les barres système globalement dans MainActivity.kt :
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 Gestes de retour prédictifs
Activez le retour prédictif dans AndroidManifest.xml (android:enableOnBackInvokedCallback="true") et utilisez PredictiveBackHandler pour les feuilles inférieures et les tiroirs de détail :
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// Mettre à l'échelle ou décaler la feuille de détail proportionnellement à backEvent.progress (0f à 1f)
detailScale = 1f - (backEvent.progress * 0.1f)
}
// L'utilisateur a confirmé le geste de retour
closeDetail()
} catch (e: CancellationException) {
// L'utilisateur a annulé le geste de retour, revenir à 1f
detailScale = 1f
}
}
3.3 Transitions d'éléments partagés
Lors de la transition entre une ligne de liste de la boîte de réception et une vue détaillée d'un élément, enveloppez les écrans dans SharedTransitionLayout :
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
Utilisez .sharedElement() ou .sharedBounds() sur les cartes de vignettes, les icônes d'avatar et les titres pour créer des transitions transparentes.
3.4 Retours haptiques tactiles
Ne laissez jamais les actions utilisateur silencieuses lorsqu'un jalon physique se produit :
- Glissements seuils (Swipe-to-Dismiss) : Déclenchez
HapticFeedbackConstantsCompat.GESTURE_STARTlorsque le glissement franchit le seuil d'action. - Copier dans le presse-papiers / Envoi terminé : Déclenchez
HapticFeedbackConstantsCompat.CONFIRM. - Échec de validation / Erreur réseau : Déclenchez
HapticFeedbackConstantsCompat.REJECT.
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 Physique du ressort plutôt qu'easing linéaire
Évitez les animations linéaires rigides. Utilisez toujours la physique du ressort pour les gestes, les feuilles et les rejets :
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. Actions intelligentes en un tap et intégration système
Les interfaces mobiles doivent minimiser la charge cognitive en reconnaissant le type de charge utile et en proposant l'action système native la plus pertinente.
4.1 Matrice d'actions contextuelles
| Type de charge utile | Action principale | Action secondaire | Intégration système |
|---|---|---|---|
| URL / Lien web | Ouvrir dans le navigateur (Intent.ACTION_VIEW) | Copier le lien | Chrome Custom Tabs ou navigateur système par défaut |
| Texte / Extrait | Copier le texte | Partager (Intent.ACTION_SEND) | ClipboardManager Android avec Toast / Snackbar instantané |
| Fichier / Média | Télécharger dans un dossier | Ouvrir le fichier | Storage Access Framework (SAF) + FileProvider |
4.2 Téléchargements directs de fichiers avec dossier par défaut personnalisé
Demander à l'utilisateur de choisir un dossier via le sélecteur de fichiers système à chaque téléchargement crée une friction inutile.
- Permettez à l'utilisateur de sélectionner un Dossier de téléchargement par défaut une fois dans les Paramètres / Hub en utilisant
ActivityResultContracts.OpenDocumentTree(). - Persistez l'autorisation de manière permanente :
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - Lors du tap sur Télécharger :
- Si un dossier par défaut est défini : Diffusez les octets directement en utilisant
DocumentFile.createFile(), affichez la progression en direct et déclenchez un Toast natif de fin ("Téléchargé document.pdf dans Vault"). - Si aucun dossier par défaut n'est défini : Revenez à
ActivityResultContracts.CreateDocument().
- Si un dossier par défaut est défini : Diffusez les octets directement en utilisant
4.3 Ouverture sécurisée de fichiers avec FileProvider
N'utilisez jamais d'URI file:// brutes non sécurisées. Déléguez toujours l'affichage des fichiers aux applications Android natives en utilisant FileProvider :
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "Ouvrir avec..."))
5. Authentification mobile sans friction
Saisir des URL de serveur et des mots de passe longs sur les écrans tactiles mobiles est sujet aux fautes de frappe.
5.1 Hiérarchie de priorité de connexion
- Priorité 1 : Scan de code QR par caméra (Appairage en 1 seconde)
- Affichez un code QR d'appairage temporaire et signé sur l'application web de bureau.
- L'utilisateur mobile ouvre l'application, tape Scanner le code QR, pointe la caméra vers l'écran.
- L'application analyse l'origine, le jeton et le nom de l'appareil en une seule passe—authentifié instantanément.
- Priorité 2 : Nom d'utilisateur et mot de passe avec URL serveur repliable
- 99 % des connexions utilisent l'hôte configuré par défaut.
- Présentez uniquement les champs Nom d'utilisateur et Mot de passe par défaut.
- Masquez l'URL du serveur derrière un bouton repliable subtil :
"Configuration du serveur (avancé)".
5.2 Persistance de session glissante de 14 jours
- Stockez les jetons d'accès (
va1_) et de rafraîchissement (vr1_) dansEncryptedSharedPreferencesAndroid. - Faites glisser/faire pivoter automatiquement le jeton de rafraîchissement en cas d'utilisation active.
- Tant que l'utilisateur ouvre l'application dans les 14 jours, il reste connecté sans se réauthentifier.
6. Mises en page adaptatives (Foldables et tablettes)
Construisez des écrans qui s'adaptent en douceur aux classes de taille de fenêtre en utilisant WindowSizeClass :
- Compact (
<600dp) : Colonne unique, barre de navigation inférieure, feuilles inférieures modales. - Moyen (
600dp - 840dp) : Rail de navigation, grille à deux colonnes. - Large (
>840dp) : Disposition maître-détail persistante à deux volets côte à côte (Liste à gauche, inspecteur à droite).
7. Contraste des couleurs Material 3 et accessibilité des états (WCAG 2.2 AA / AAA)
Les appareils mobiles sont particulièrement sujets aux défaillances de contraste en raison de l'éclairage ambiant variable (lumière directe du soleil, éblouissement extérieur, atténuation OLED de l'économiseur de batterie). Visez un contraste de texte WCAG 2.2 AAA ($\ge 7:1$) et une conformité stricte avec WCAG 1.4.11 (contraste des limites non textuelles $\ge 3:1$) et WCAG 1.4.1 (utilisation de la couleur).
7.1 Le piège du FilterChip en mode sombre Material 3
- Le défaut : Par défaut,
FilterChipde Material 3 en mode sombre utilisesecondaryContainer(#37474F) pour son arrière-plan sélectionné sur lasurfacesombre (#14181E). Cela donne un rapport de contraste inacceptable de 1,83:1. En faible luminosité ou éblouissement, les utilisateurs ne peuvent pas distinguer quel chip est actif. - Le correctif :
- Appliquez une définition de limite à contraste élevé avec
selectedBorderColor = MaterialTheme.colorScheme.primary(qui produit un rapport de contraste de 9,88:1 en mode sombre). - Définissez
selectedBorderWidth = 1.5.dp. - Conformez-vous à WCAG 1.4.1 (états non dépendants de la seule couleur) en fournissant une icône de coche de tête :
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
- Appliquez une définition de limite à contraste élevé avec
7.2 Normes de lisibilité en plein soleil et éblouissement
- Texte principal vs arrière-plan : Standardisez le mode sombre sur un texte à contraste élevé (
onSurface#E1E2E8ou#F8FAFCsur#14181E), atteignant $\ge 13:1$ de contraste (dépassant WCAG AAA). - Contours des contrôles de formulaire :
OutlinedTextFieldet les bordures des cases à cocher/radio doivent utiliser des jetonsoutlinequi maintiennent au moins $3:1$ de contraste avec les cartes conteneurs (Color(0xFF8D9199)ourgba(255, 255, 255, 0.20)explicite). - Lignes de préréglages compacts : Lors de la disposition de lignes de chips compactes (par exemple les préréglages de rétention
1h,4h,12h,1d), conservez les tailles cibles et appliquezmaxLines = 1avecselectedBorderColorafin que les chips ne s'enroulent jamais et ne paraissent pas ambigus.
8. Dépôts open source de référence
Inspectez ces projets Android open source de pointe pour l'inspiration en matière de conception et d'implémentation :
android/nowinandroid: L'échantillon Compose Material 3 de référence de Google démontrant la thématisation dynamique, les mises en page adaptatives et une architecture propre.skydoves/Pokedex-Compose: Transitions d'éléments partagés de qualité production (.sharedBounds/.sharedElement) et gestes de feuille de détail fluides.plainhub/plain-app: Implémentation remarquable de la gestion du téléphone local, du thème Material You et de la synchronisation client-serveur réactive.Gurupreet/ComposeCookBook: Catalogue exhaustif d'animations Compose de production, de barres inférieures et de micro-interactions canvas personnalisées.signalapp/Signal-Android: Référence absolue pour le stockage local chiffré, les workers Outbox en arrière-plan et la résilience réseau.