Theodo apps

REX après la migration d’une feature Android/iOS vers KMP

Dans un premier article, nous avons exploré comment migrer progressivement une application native Android/iOS vers Kotlin Multiplatform (KMP). Si vous avez suivi ce guide, vous avez probablement une base de code KMP fonctionnelle… et une liste de problèmes que vous avez rencontrés !

Effectivement des problèmes j’en ai rencontré en suivant ce guide pour la migration d’une feature. Maintenant, je vous fais mon retour d’expérience pour vous préparer aux difficultés. Car oui, malgré ses promesses, une migration vers KMP n’est pas toujours un long fleuve tranquille. Certains types standards ne sont pas supportés, des frameworks courants deviennent incompatibles et certaines intégrations nécessitent des ajustements parfois contre-intuitifs.

Nous verrons ensemble les pièges liés aux types, aux systèmes réactifs, aux ressources et même aux cycles de vie des objets. Mais rassurez-vous : tout n’est pas compliqué et certaines migrations sont étonnamment simples. Alors, prêt à affronter les dernières étapes de votre transition vers KMP ?

Points difficiles

Interopérabilité Kotlin-Swift

Suite à un choix historique en 2018 et afin d’être compatible avec le maximum de projets, Kotlin n’est pas interopérable directement avec Swift, mais avec Objective-C. Ce qui nous apporte quelques limitations que vous pourrez retrouver à cette adresse : https://github.com/kotlin-hands-on/kotlin-swift-interopedia. Heureusement, l’interopérabilité fonctionne normalement pour les choses simples, et pour certains autres éléments plus complexe, SKIE vous permettra d’améliorer l’interop. Il sera utilise par exemple pour mieux traduire en swift les enums, les sealed classes, les Flows et bien d’autres.

Types Java et Android manquant

Certains types pourtant simples et régulièrement utilisés en Kotlin/Android ne sont pas disponibles en KMP, il vous faudra donc trouver des équivalents :

  • java.util.UUID : arrivé en version expérimentale des uuid en 2.0.20 pour Kotlin, vous devrez quand même changer de type
  • java.util.SortedSet: utilisez des set et triez-les à la demande
  • java.net.URL : pour les URI vous trouverez des solutions dans les librairies de la communauté comme celle-ci.

Si vous rencontrez d'autres soucis concernant les types, vous pourrez bien souvent utiliser le type Android et créer un équivalent pour iOS. Par exemple, pour les Bundle, vous pouvez utiliser une Map sur iOS et sur android il vous suffit d'utiliser la vraie implementation des bundles:

actual typealias Bundle = android.os.Bundle

Rx / LiveData

Si vous utilisez Rx, LiveData permettant à votre application d’être réactive aux changements, sachez que cela ne sera pas disponible en multiplateforme. Pour pallier cela, migrez vers les coroutines et/ou Kotlin Flow couche par couche dans vos features en utilisant un adaptateur pour communiquer avec les couches qui utilisent encore l’ancien système. En soi, ce n’est pas vraiment difficile, juste un peu long. Par contre, si votre couche présentation chaîne les observables de manière complexe, il se peut que la migration souffre de cette complexité.

Pour moi ce qui est le plus difficile sur ce sujet, c'est que les test unitaires ne sont pas les mêmes entre un test Rx et un test de coroutine. Mais il est tout de même important de s'attarder sur les tests, ils sont le meilleur garant pour vous éviter les régressions.

Migration du design system

  • Les polices de caractères ne se chargent pas exactement de la même manière en Compose Android et Compose Multiplatform. Vous aurez peut-être quelques ajustements à faire selon la manière dont vous les utilisez sur votre projet. D'ailleurs attention, en multimodules, il faudra bien configurer vos ressources pour avoir accès à vos Fonts.
  • Lottie n'est pas multiplateforme, mais il existe Kottie et Compottie pour utiliser vos animations JSON en CMP (Compose multiplatform).
  • Si vous utilisez plutôt Rive pour vos animations, à ma connaissance il n’existe pas de version multi-platforme, à vous donc d’adapter les versions des différents runtimes. rive-cpp fonctionne avec Skia, à voir à quel point il faut l’adapter pour du CMP.
  • Pour les images distantes, il est probable que la solution que vous aviez ne soit pas compatible avec KMP. Vous pouvez donc choisir de migrer vers Coil, ce qui sera d'autant plus simple si vous utilisiez Landscapist.

Les ressources

Les ressources avec Compose peuvent être utilisées sur iOS uniquement avec l'intégration locale et pas via un .xcframework.

Par ailleurs, pour un projet multi-module, il ne faut pas oublier de spécifier la génération des ressources dans l'umbrella module pour iOS :

compose.resources {
    generateResClass = always
}

Pour les polices de caractères, il y a une règle supplémentaire pour les utiliser: il faut qu'elles soient dans le module top-level et il faut les injecter dans les autres modules. Voir YouTrack CMP-4111.

Il y a encore des éléments que je n'ai pas bien saisis à ce sujet. J'ai eu des versions de mon code qui fonctionnaient avec des polices chargées dans un submodule et puis plus tard non. Il y a peut-être une histoire de cache qui joue. J'investiguerai ce point plus tard, faites-moi savoir si vous avez des infos.

Migration des vues

Le souci majeur est l'absence de previews en Compose Multiplatform sur la partie commune. La solution que j'ai choisie est de déplacer toutes mes vues dans le module commun en laissant les previews dans le code de l'app Android. J'ai choisi cette solution, car cela m'évitait également d'avoir à déplacer mes tests snapshots de non-régression paparazzi.Il est possible de visualiser vos previews Compose Multiplatform de la partie commune avec l'IDE Fleet. Malheureusement, je n'ai pas réussi à faire en sorte que fleet fonctionne sur mon projet complexe. Par ailleurs, Fleet a été abandonné par JetBrains. On croise les doigts pour que cette fonctionnalité arrive bientôt sur Android Studio.

Migration de la navigation

Selon la solution de navigation que vous aviez sur Android, la migration de cette couche peut être complètement différente.Pour ma part, c'était de la navigation entre fragments, j'ai choisi de migrer vers une navigation compose, mais il existe peut-être des solutions plus proches de la navigation android.

Intégration dans un projet Tuist

Dans un projet iOS géré par Tuist, j'ai eu des soucis pour que Xcode reconnaisse les dépendances de mon module KMP. Il faut intégrer le .framework généré par Kotlin dans la déclaration des dépendances de Tuist.

Attention, Kotlin génère par défaut un .framework et non un .xcframework, ce qui veut dire que le .framework n'est compatible que pour une architecture. L'intégration n'est donc pas des plus simples. Le mieux à faire est sûrement de compléter la tâche Gradle de génération du .framework avec une modification des dépendances de Tuist et un tuist generate. Je suis preneur de toute solution plus simple.

Sinon il y a toujours la solution de la communauté pour générer les .xcframework . Cela prend évidemment plus de temps que de builder un .framework pour une seule architecture.

Peut-être qu’une solution serait d’utiliser le .framework en debug et le .xcframework en release.

Par ailleurs, dans un projet Tuist, si vous utilisez l'intégration directe, il faudra ajouter la configuration à Tuist car sinon, la commande tuist generate va vous l'écraser

Intégration du projet KMP dans un projet iOS en archi micro-features

Au début, j'avais besoin de builder l'app deux fois pour voir mes changements dans le code commun apparaître dans mon app iOS. C'était parce que j'avais ajouté la dépendance KMP au projet de l'app plutôt que de l'ajouter aux sous-projets qui utilisaient réellement la dépendance KMP.

Les cycles de vie

Souvent, sur Android, vos objets ont un cycle de vie bien précis, ils sont liés à une activité ou un fragment. Quand vous allez migrer vos écrans sur iOS avec Compose puis la navigation également, il faudra redéfinir la durée de vie de vos objets. Vous aurez probablement quelques petites régressions à ce sujet. Par exemple, le focus d'un champ de texte qui n'est pas réinitialisé lorsqu'on revient sur l'écran. Pour vous aider à gérer les cycles de vie les plus simple, je pense que l’on peut se débrouiller avec les scopes de Koin.Attention, si vous voulez bind le cycle de vie de votre viewModel Kotlin avec votre vue SwiftUI, vous allez avoir du travail. Vous pouvez aller voir le travail de François qui s'est attelé à cette tâche.

Le maintien d'un état commun entre votre app iOS et sa partie KMP

Par exemple, le token d'authentification. Pour ma part, il est récupéré par la partie native iOS, mais je veux m'en servir dans des appels en KMP. Pour cela, j'ai créé une classe qui s'occupe de la gestion de ce token, elle est accessible depuis la partie KMP et la partie iOS. Cela me permet d'utiliser le token généré par iOS dans mes appels réseaux KMP.Plus globalement, il faudra créer des composants qui passent d'une plateforme à l'autre quand vous voulez partager ces infos.Une astuce est d'utiliser votre framework d'injection de dépendances pour créer des composants dont votre code commun se sert et qui sont implémentés par les plateformes.

Facilités

Retrofit vers KtorFit

Retrofit n'est pas compatible KMP et n'a pas prévu de l'être, pas de panique, KtorFit l'est et vous pourrez très simplement migrer vos appels réseaux.

Room

Si vous utilisiez Room sur Android, vous pouvez facilement migrer vers Room KMP à l’aide de la documentation : migration vers Room KMP et de l’exemple Fruitties. Par ailleurs, Room est enfin stable sur KMP !

Coroutines et Kotlin Flow

Si vous utilisez déjà les coroutines et Kotlin Flow sur Android, cela vous simplifiera la migration de nombreuses parties du code. Sinon faites cette migration au préalable. Regardez la partie Rx/Livedata plus haut.

Conseils

Il est plus facile d’ajouter une nouvelle feature en KMP sur les deux plateformes que de migrer une feature existante en KMP.Donc si vous en avez l'occasion, testez une nouvelle feature en KMP pour confirmer que KMP convient à votre équipe avant d'entamer une migration. L'engagement sera bien moindre et vous aurez déjà une bonne idée de ce que cela implique pour votre projet.

Avoir déjà des tests unitaires en place fait partie des points nécessaires pour faire un refactoring correct. Cela permettra de faire votre migration progressive plus rapidement et surtout qu'elle soit plus robuste.

Quand vous passez une classe du package Android vers le module commun, gardez le même nom de package pour vous éviter d'avoir à retoucher toutes les classes qui l’utilisent.

Passez par des interfaces quand cela vous facilite le refactoring.

Migrer, c’est refactorer. Apprenez à dompter les outils de refacto de votre IDE, sinon vous allez en baver. Une bonne maitrise vous permettra de limiter les régressions et améliorer votre vitesse de refactoring.

Conclusion

Migrer une application native vers Kotlin Multiplatform n’est pas un exercice anodin, mais ce n’est pas non plus une montagne infranchissable. Avec une approche progressive et une bonne préparation, vous pouvez transformer une migration en une opportunité d’améliorer votre base de code, de simplifier vos architectures et d’harmoniser vos pratiques entre plateformes.

Le principal enseignement de cette transition, c’est qu’il n’existe pas de recette universelle : chaque projet à ses spécificités et ses défis. Mais avec des tests solides, un bon usage des outils de refactoring et une stratégie de migration bien pensée, vous pouvez limiter les régressions et maximiser les bénéfices du passage à KMP.

Et si vous avez déjà migré un projet vers KMP, quelles ont été vos principales difficultés ? Faites-moi part de votre expérience, ça m’intéresse, car une migration réussie est avant tout une aventure.

Développeur mobile ?

Rejoins nos équipes