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 ?
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.
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 :
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
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.
rive-cpp
fonctionne avec Skia, à voir à quel point il faut l’adapter pour du CMP.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.
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.
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.
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
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.
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.
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.
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.
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 !
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.
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.
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.