Kotlin vs SwiftUI : garder un produit cohérent sur iOS et Android
Guide comparatif Kotlin vs SwiftUI pour conserver un produit cohérent sur Android et iOS : navigation, état, formulaires, validation et contrôles pratiques.

Pourquoi aligner un seul produit sur deux stacks est difficile
Même quand la liste de fonctionnalités correspond, l'expérience peut paraître différente sur iOS et Android. Chaque plateforme a ses valeurs par défaut. iOS privilégie les barres d'onglets, les gestes de balayage et les feuilles modales. Android attend un bouton Retour visible, un comportement système Back fiable, et des patterns de menus et dialogues différents. Construire le même produit deux fois, et ces petits défauts s'additionnent.
Kotlin vs SwiftUI n'est pas seulement un choix de langage ou de framework. Ce sont deux ensembles d'hypothèses sur l'apparence des écrans, la mise à jour des données et le comportement des saisies. Si les exigences sont rédigées comme « faites‑le comme iOS » ou « copiez Android », un des deux côtés paraîtra toujours être un compromis.
Les équipes perdent généralement la cohérence dans les interstices entre les écrans du chemin heureux. Un flux semble aligné lors de la revue de design, puis dérive une fois que vous ajoutez des états de chargement, des demandes d'autorisation, des erreurs réseau et les cas « et si l'utilisateur part et revient ».
La parité casse souvent d'abord à des endroits prévisibles : l'ordre des écrans change quand chaque équipe « simplifie » le flux, Back et Annuler se comportent différemment, les états vide/chargement/erreur ont des formulations divergentes, les champs de formulaires acceptent des caractères différents, et le moment de la validation varie (à la frappe vs au blur vs à la soumission).
Un objectif pratique n'est pas une UI identique. C'est un ensemble d'exigences suffisamment clair pour que les deux stacks aboutissent au même endroit : mêmes étapes, mêmes décisions, mêmes cas limites et mêmes résultats.
Une approche pratique pour des exigences partagées
Le plus dur n'est pas les widgets. C'est de conserver une définition produit unique pour que les deux apps se comportent de la même façon, même si l'UI diffère légèrement.
Commencez par scinder les exigences en deux catégories :
- Doit correspondre : ordre du flux, états clés (loading/empty/error), règles des champs et textes visibles.
- Peut rester natif : transitions, styles des contrôles et petites décisions de layout.
Définissez les concepts partagés en langage clair avant que quiconque n'écrive du code. Mettez d'accord ce que signifie un « écran », ce que signifie une « route » (y compris les paramètres comme userId), ce qui compte comme un « champ de formulaire » (type, placeholder, obligatoire, clavier) et ce qu'inclut un « état d'erreur » (message, mise en surbrillance, quand il s'efface). Ces définitions réduisent les débats plus tard car les deux équipes visent la même cible.
Rédigez des critères d'acceptation qui décrivent des résultats, pas des frameworks. Exemple : « Quand l'utilisateur appuie sur Continuer, désactiver le bouton, afficher un spinner et empêcher le double‑envoi jusqu'à la fin de la requête. » C'est clair pour les deux stacks sans prescrire comment l'implémenter.
Gardez une source de vérité unique pour les détails que les utilisateurs remarquent : textes (titres, libellés de boutons, textes d'aide, messages d'erreur), comportement des états (loading/success/empty/offline/permission denied), règles de champs (obligatoire, longueur min, caractères autorisés, formatage), événements clés (submit/cancel/back/retry/timeout) et noms d'analytics si vous les suivez.
Un exemple simple : pour un formulaire d'inscription, décidez que « Le mot de passe doit faire 8 caractères ou plus, afficher l'indice de règle après le premier blur et effacer l'erreur pendant la saisie. » L'UI peut être différente ; le comportement ne doit pas l'être.
Navigation : faire correspondre les flux sans imposer une UI identique
Carteez le parcours utilisateur, pas les écrans. Écrivez le flux comme les étapes qu'un utilisateur suit pour terminer une tâche, par exemple « Parcourir - Ouvrir détail - Modifier - Confirmer - Terminé. » Une fois le chemin clair, vous pouvez choisir le style de navigation le mieux adapté à chaque plateforme sans changer ce que fait le produit.
iOS privilégie souvent les feuilles modales pour les tâches courtes et une fermeture évidente. Android s'appuie sur une pile d'historique et le bouton Back système. Les deux peuvent néanmoins soutenir le même flux si vous définissez les règles en amont.
Vous pouvez mixer les blocs usuels (onglets pour les zones top‑level, stacks pour l'exploration, modales/feuilles pour les tâches ciblées, liens profonds, étapes de confirmation pour les actions à risque) tant que le flux et les résultats ne changent pas.
Pour garder des exigences cohérentes, nommez les routes de la même manière sur chaque plateforme et alignez leurs entrées. “orderDetails(orderId)” doit signifier la même chose partout, y compris ce qui se passe quand l'ID est manquant ou invalide.
Précisez explicitement le comportement du Back et des dismissals, car c'est là que la dérive apparaît :
- Ce que fait Back depuis chaque écran (sauvegarde, abandon, demander confirmation)
- Si une modale peut être fermée (et ce que signifie cette fermeture)
- Quels écrans ne doivent jamais être atteints deux fois (éviter les pushs en double)
- Comment se comportent les deep links si l'utilisateur n'est pas connecté
Exemple : dans un flux d'inscription, iOS peut présenter les « Conditions » en feuille alors qu'Android les pousse sur la pile. C'est acceptable si les deux retournent le même résultat (accepter ou refuser) et reprennent l'inscription à la même étape.
État : garder le comportement cohérent
Si les apps semblent « différentes » même quand les écrans se ressemblent, c'est généralement la gestion d'état qui en est la cause. Avant de comparer les détails d'implémentation, mettez-vous d'accord sur les états qu'un écran peut traverser et ce que l'utilisateur peut faire dans chacun d'eux.
Rédigez le plan d'état en langage simple d'abord, et conservez‑le répétable :
- Loading : afficher un spinner et désactiver les actions principales
- Empty : expliquer ce qui manque et proposer la prochaine action pertinente
- Error : afficher un message clair et une option Réessayer
- Success : afficher les données et garder les actions activées
- Updating : garder les anciennes données visibles pendant un rafraîchissement
Puis décidez où l'état vit. L'état au niveau écran suffit pour des détails UI locaux (sélection d'onglet, focus). L'état au niveau app est préférable pour les choses sur lesquelles toute l'app s'appuie (utilisateur connecté, feature flags, profil en cache). L'important est la cohérence : si « déconnecté » est traité comme état global sur Android mais comme état d'écran sur iOS, vous aurez des écarts tels qu'une plateforme affichant des données périmées.
Rendez les effets secondaires explicites. Rafraîchir, réessayer, soumettre, supprimer et mises à jour optimistes changent tous l'état. Définissez ce qui se passe en cas de succès et d'échec, et ce que l'utilisateur voit pendant l'opération.
Exemple : une liste « Commandes ».
Lors d'un pull‑to‑refresh, gardez‑vous l'ancienne liste visible (Updating) ou la remplacez‑vous par un état Loading plein écran ? En cas d'échec du rafraîchissement, gardez‑vous la dernière liste valide et affichez une petite erreur, ou passez‑vous à un état Error complet ? Si les deux équipes répondent différemment, le produit paraîtra vite incohérent.
Enfin, mettez‑vous d'accord sur les règles de cache et de reset. Décidez quelles données sont sûres à réutiliser (comme la dernière liste chargée) et lesquelles doivent être fraîches (comme le statut de paiement). Définissez aussi quand l'état se réinitialise : en quittant l'écran, en changeant de compte ou après une soumission réussie.
Formulaires : comportements de champs qui ne doivent pas dériver
Les formulaires sont le lieu où de petites différences se transforment en tickets support. Un écran d'inscription qui semble « assez proche » peut pourtant se comporter différemment, et les utilisateurs le remarquent rapidement.
Commencez par une spécification canonique de formulaire indépendante du framework. Rédigez‑la comme un contrat : noms de champs, types, valeurs par défaut et quand chaque champ est visible. Exemple : « Le nom de l'entreprise est caché sauf si Type de compte = Entreprise. Type par défaut = Personnel. Le pays par défaut vient de la locale de l'appareil. Le code promo est optionnel. »
Puis définissez les interactions que l'on s'attend à voir identiques sur les deux plateformes. Ne laissez pas ces points à « comportement standard », car le « standard » diffère.
- Type de clavier par champ
- Autofill et comportement des identifiants enregistrés
- Ordre du focus et libellés Next/Return
- Règles de soumission (désactivé tant que non valide vs autorisé avec erreurs)
- Comportement de chargement (ce qui se verrouille, ce qui reste éditable)
Décidez comment les erreurs apparaissent (inline, résumé, ou les deux) et quand elles apparaissent (au blur, à la soumission, ou après la première modification). Une règle courante efficace : ne pas afficher d'erreurs avant la première tentative de soumission, puis maintenir les erreurs inline à jour pendant la saisie.
Planifiez la validation asynchrone dès le départ. Si « nom d'utilisateur disponible » nécessite un appel réseau, définissez la gestion des requêtes lentes ou en échec : afficher « Vérification… », dédupliquer la frappe (debounce), ignorer les réponses obsolètes et distinguer « nom déjà pris » d'« erreur réseau, réessayez ». Sans cela, les implémentations divergent facilement.
Validation : une seule règle, deux implémentations
La validation est l'endroit où la parité se casse en silence. Une appli bloque une saisie, l'autre l'accepte, et les tickets arrivent. La solution n'est pas une bibliothèque magique. C'est l'accord sur un jeu de règles en langage clair, puis son implémentation en double.
Rédigez chaque règle comme une phrase qu'un non‑développeur peut tester. Exemple : « Le mot de passe doit comporter au moins 12 caractères et inclure un chiffre. » « Le numéro de téléphone doit inclure l’indicatif pays. » « La date de naissance doit être une date réelle et l'utilisateur doit avoir 18 ans ou plus. » Ces phrases deviennent votre source de vérité.
Séparez ce qui tourne sur le téléphone et ce qui tourne sur le serveur
Les contrôles côté client doivent privilégier un retour rapide et corriger les erreurs évidentes. Les contrôles côté serveur sont la porte finale et doivent être plus stricts car ils protègent les données et la sécurité. Si le client autorise quelque chose que le serveur rejette, affichez le même message et mettez en évidence le même champ pour que l'utilisateur ne soit pas perdu.
Rédigez le texte d'erreur et le ton une fois, puis réutilisez‑les sur les deux plateformes. Décidez de détails comme dire « Saisissez » ou « Veuillez saisir », si vous utilisez la casse phrase, et à quel point vous êtes précis. Un petit écart de formulation peut donner l'impression de deux produits différents.
Les règles de locale et de formatage doivent être écrites, pas devinées. Mettez‑vous d'accord sur ce que vous acceptez et comment vous l'affichez, surtout pour les numéros de téléphone, les dates (y compris les hypothèses de fuseau), les devises, et les noms/adresses.
Un scénario simple : votre formulaire d'inscription accepte « +44 7700 900123 » sur Android mais rejette les espaces sur iOS. Si la règle est « les espaces sont autorisés, stockés en chiffres uniquement », les deux apps peuvent guider l'utilisateur de la même façon et enregistrer la même valeur nettoyée.
Étapes concrètes : garder la parité pendant le développement
Ne partez pas du code. Partez d'une spécification neutre que les deux équipes considèrent comme source de vérité.
1) Rédigez d'abord un spec neutre
Une page par flux, et restez concret : une user story, un petit tableau d'états et des règles de champs.
Pour « Inscription », définissez des états comme Idle, Editing, Submitting, Success, Error. Puis décrivez ce que l'utilisateur voit et ce que l'app fait dans chaque état. Incluez des détails comme suppression des espaces, moment d'affichage des erreurs (au blur vs à la soumission) et ce qui arrive quand le serveur rejette l'email.
2) Construisez avec une checklist de parité
Avant toute implémentation UI, créez une checklist écran par écran que iOS et Android doivent valider : routes et comportement du Back, événements clés et résultats, transitions d'état et comportement de chargement, comportement des champs et gestion des erreurs.
3) Testez les mêmes scénarios sur les deux
Exécutez le même jeu à chaque fois : le parcours nominal, puis les cas limites (réseau lent, erreur serveur, saisie invalide et reprise après mise en arrière‑plan).
4) Revoyez les écarts chaque semaine
Tenez un petit journal de parité pour que les différences ne deviennent pas permanentes : ce qui a changé, pourquoi, s'il s'agit d'une exigence vs d'une convention plateforme vs d'un bug, et ce qui doit être mis à jour (spec, iOS, Android, ou les trois). Corrigez la dérive tôt, quand les correctifs restent petits.
Erreurs fréquentes des équipes
La façon la plus simple de perdre la parité entre iOS et Android est de traiter le travail comme « faire pareil visuellement ». Le comportement compte plus que les pixels.
Un piège courant est de copier des détails UI d'une plateforme à l'autre au lieu de rédiger une intention partagée. Deux écrans peuvent avoir un aspect différent et être néanmoins « les mêmes » si ils chargent, échouent et récupèrent de la même manière.
Un autre piège est d'ignorer les attentes natives. Les utilisateurs Android attendent que le bouton Back système soit fiable. Les utilisateurs iOS attendent que le swipe back fonctionne dans la plupart des stacks, et que feuilles et dialogues système paraissent natifs. Si vous allez contre ces attentes, les utilisateurs blâmeront l'app.
Erreurs récurrentes :
- Copier l'UI au lieu de définir le comportement (états, transitions, gestion empty/error)
- Briser les habitudes de navigation natives pour garder des écrans « identiques »
- Laisser dériver la gestion des erreurs (une plateforme bloque avec une modale alors que l'autre réessaie silencieusement)
- Valider différemment côté client et serveur, provoquant des messages contradictoires
- Utiliser des valeurs par défaut différentes (auto‑capitalisation, type de clavier, ordre du focus) rendant les formulaires incohérents
Un exemple rapide : si iOS affiche « Mot de passe trop faible » pendant la saisie, mais qu'Android attend la soumission, les utilisateurs penseront qu'une app est plus stricte. Décidez une règle et un timing une fois, puis implémentez‑les deux fois.
Checklist rapide avant publication
Avant la sortie, faites une passe dédiée uniquement à la parité : pas « est‑ce que ça ressemble pareil ? », mais « est‑ce que ça veut dire la même chose ? »
- Flux et entrées correspondent à la même intention : les routes existent sur les deux plateformes avec les mêmes paramètres.
- Chaque écran gère les états essentiels : loading, empty, error, et un Retry qui répète la même requête et ramène l'utilisateur au même endroit.
- Les formulaires se comportent pareil en bordure : champs requis vs optionnels, suppression des espaces, type de clavier, autocorrect et action Next/Done.
- Les règles de validation correspondent pour une même saisie : les entrées rejetées le sont sur les deux plateformes, avec la même raison et le même ton.
- Les analytics (si utilisés) se déclenchent au même moment : définissez le moment, pas l'action UI.
Pour attraper la dérive rapidement, choisissez un flux critique (comme l'inscription) et exécutez‑le 10 fois en introduisant volontairement des erreurs : laisser des champs vides, entrer un code invalide, passer hors ligne, faire pivoter l'écran, mettre l'app en arrière‑plan pendant une requête. Si le résultat diffère, vos exigences ne sont pas encore totalement partagées.
Scénario exemple : un flux d'inscription implémenté dans les deux stacks
Imaginez le même flux d'inscription construit deux fois : Kotlin sur Android et SwiftUI sur iOS. Les exigences sont simples : Email et Mot de passe, puis écran Code de vérification, puis Succès.
La navigation peut différer sans changer l'objectif. Sur Android vous pouvez push/poper les écrans pour revenir éditer l'email. Sur iOS vous pouvez utiliser une NavigationStack et présenter l'étape du code comme une destination. La règle reste : mêmes étapes, mêmes points de sortie (Back, Renvoyer le code, Changer l'email) et même gestion des erreurs.
Pour aligner le comportement, définissez les états partagés en clair avant d'écrire du code :
- Idle : l'utilisateur n'a pas encore soumis
- Editing : l'utilisateur modifie des champs
- Submitting : requête en cours, saisies désactivées
- NeedsVerification : compte créé, en attente du code
- Verified : code accepté, continuer
- Error : afficher un message, conserver les données saisies
Puis verrouillez les règles de validation pour qu'elles correspondent exactement, même si les contrôles diffèrent :
- Email : obligatoire, trim, doit correspondre au format email
- Mot de passe : obligatoire, 8‑64 caractères, au moins 1 chiffre, au moins 1 lettre
- Code de vérification : obligatoire, exactement 6 chiffres, uniquement numérique
- Moment des erreurs : choisissez une règle (après soumission ou après blur) et gardez‑la cohérente
Les ajustements spécifiques à la plateforme sont acceptables s'ils changent la présentation, pas le sens. Par exemple, iOS peut utiliser l'autofill de code à usage unique, tandis qu'Android peut proposer une capture SMS. Documentez : ce qui change (méthode d'entrée), ce qui reste identique (6 chiffres requis, même texte d'erreur), et ce que vous testerez sur les deux (réessayer, renvoyer, back navigation, erreur hors ligne).
Prochaines étapes : garder les exigences cohérentes à mesure que l'app grandit
Après la première release, la dérive commence discrètement : un petit ajustement sur Android, une correction rapide sur iOS, et bientôt vous gérez des comportements divergents. La prévention la plus simple est d'intégrer la cohérence dans le flux hebdomadaire, pas de la laisser à un projet de rattrapage.
Transformez les exigences en spec de fonctionnalité réutilisable
Créez un petit template à réutiliser pour chaque nouvelle fonctionnalité. Restez concentré sur le comportement, pas sur les détails UI, afin que les deux stacks puissent l'implémenter de la même façon.
Incluez : objectif utilisateur et critères de réussite, écrans et événements de navigation (y compris comportement du Back), règles d'état (loading/empty/error/retry/offline), règles de formulaire (types de champs, masques, type de clavier, textes d'aide) et règles de validation (moment d'exécution, messages, bloquant vs avertissement).
Un bon spec se lit comme des notes de test. Si un détail change, le spec change d'abord.
Ajoutez une revue de parité à votre définition de « done »
Faites de la parité une étape courte et répétable. Quand une fonctionnalité est marquée complète, faites une vérification côte‑à‑côte avant de merger ou publier. Une personne exécute le même flux sur les deux plateformes et note les différences. Une checklist courte permet la validation.
Si vous voulez un endroit pour définir modèles de données et règles métiers avant de générer des apps natives, AppMaster (appmaster.io) est conçu pour construire des applications complètes, backend, web et sorties mobiles natives incluses. Même avec une plateforme partagée, gardez la checklist de parité : comportement, états et textes restent des décisions produit et demandent une revue délibérée.
L'objectif long terme est simple : quand les exigences évoluent, les deux apps évoluent la même semaine, de la même façon, sans surprises.
FAQ
Visez la parité de comportement, pas la parité pixel à pixel. Si les deux apps suivent les mêmes étapes de flux, gèrent les mêmes états (loading/empty/error) et aboutissent aux mêmes résultats, les utilisateurs percevront le produit comme cohérent même si les patterns UI iOS et Android diffèrent.
Rédigez les exigences comme des résultats attendus et des règles. Par exemple : que se passe‑t‑il quand l'utilisateur appuie sur Continuer, ce qui est désactivé, quel message s'affiche en cas d'échec, et quelles données sont conservées. Évitez des consignes du type « faites comme iOS » ou « copiez Android », car cela force souvent une plateforme à adopter un comportement inadapté.
Décidez ce qui doit correspondre (ordre du flux, règles de champs, textes visibles, comportement des états) versus ce qui peut rester natif à la plateforme (animations, style des contrôles, petites décisions de mise en page). Verrouillez tôt les éléments « must match » et traitez‑les comme le contrat que les deux équipes implémentent.
Soyez explicite par écran : que fait Back, quand il demande confirmation et ce qu'il advient des modifications non sauvegardées. Définissez aussi si les modales peuvent être fermées et ce que cela implique. Sans règles écrites, chaque plateforme adoptera ses comportements par défaut et le flux semblera incohérent.
Créez un plan d'états partagé qui nomme chaque état et ce que l'utilisateur peut faire dedans. Mettez‑vous d'accord sur des détails comme garder les anciennes données visibles pendant un rafraîchissement, ce que répète un bouton « Réessayer », et si les champs restent éditables pendant l'envoi. La plupart des différences perçues viennent du traitement des états, pas du layout.
Concevez un spec canonique de formulaire : champs, types, valeurs par défaut, règles de visibilité et comportement de soumission. Puis décrivez les interactions qui divergent souvent : type de clavier, ordre du focus, autofill, et moment d'affichage des erreurs. Si ces règles sont unifiées, le formulaire semblera identique malgré des contrôles natifs différents.
Rédigez la validation comme des phrases testables qu'une personne non technique peut vérifier, puis implémentez les mêmes règles sur les deux plateformes. Décidez aussi quand la validation s'exécute (à la frappe, au blur ou à la soumission) et conservez ce timing. Les utilisateurs remarquent quand une plateforme « gronde » plus tôt que l'autre.
Considérez le serveur comme l'autorité finale, mais alignez le retour client sur les résultats serveur. Si le serveur rejette une saisie que le client a autorisée, renvoyez un message qui met en évidence le même champ et utilise le même libellé. Ainsi on évite les tickets « Android a accepté, iOS non ».
Utilisez une checklist de parité et exécutez les mêmes scénarios sur les deux apps à chaque fois : parcours nominal, réseau lent, hors ligne, erreur serveur, saisie invalide et reprise après mise en arrière‑plan. Tenez un petit « journal de parité » des différences et décidez si chaque écart est un changement d'exigence, une convention plateforme ou un bug.
AppMaster peut vous aider en offrant un endroit unique pour définir modèles de données et logique métier réutilisables et générer des sorties mobiles natives, ainsi que backend et web. Même avec une plateforme partagée, vous devez conserver une spécification claire du comportement, des états et des textes, car ce sont des décisions produit et non des défauts de framework.


