Kotlin Multiplatform (Mobile), un Framework pour les gouverner tous ?

Un peu de contexte

Kotlin

Impossible de parler de Kotlin Multiplatform sans tout d’abord présenter Kotlin sur lequel se base ce nouveau Framework. Développé en 2011 par JetBrains et devenu depuis le langage officiel et recommandé pour Android, Kotlin est un langage de programmation orienté objet, avec un typage statique qui permet de compiler pour la machine virtuelle Java.

Apportant son lot d’améliorations (null-safety, concision, coroutines, fonctions d’extensions, ...) et son interopérabilité avec Java et d’autres langages, nombre de développeurs ont préféré se tourner vers Kotlin pour leurs programmations du quotidien.

En parallèle, on retrouve maintenant de nombreuses applications déclinées pour différents supports : Web, Mobile, Desktop, Smartwatch, etc. Beaucoup de temps et de moyens sont souvent alloués pour décliner une même application sur chaque plateforme, avec souvent des règles et comportements métiers identiques. Il existe bien des solutions dites cross platform comme Flutter ou React Native, mais c'est souvent au prix de performances plus difficiles à optimiser, d'un look and feel natif dégradé, ou encore d'une réelle complexité quand il s'agit d'accéder aux couches basses de l'appareil.

Et s’il était possible de mutualiser du code natif afin de le réutiliser sur chaque plateforme, en tirant ainsi parti du meilleur des OS ? C’est dans ce contexte qu’intervient Kotlin Multiplatform.

Kotlin Multiplatform

Kotlin Multiplatform permet de partager du code métier entre plusieurs plateformes ; plus besoin de définir ce code pour chacune d’entre elles, il suffira de le partager au sein des sources communes. Intérêt ? Réduction du temps de développement et de maintenance pour un même code partagé tout en conservant la flexibilité et les avantages de la programmation native.

Tout projet Kotlin Multiplatform fonctionne de la façon suivante : une partie commune et une partie spécifique :

  • le code écrit en Common Kotlin contient les langages, librairies et les outils fonctionnant sur toutes les plateformes : il s’agit de la partie commune
  • le code spécifique utilise des versions spécifiques de Kotlin : (Kotlin/JVM, Kotlin/Native, Kotlin/JS)

Les possibilités de Kotlin Multiplatform permettent ainsi aux développeurs de partager leur code commun sur différentes plateformes.

Pour la suite de l’article, nous allons nous concentrer sur la partie Mobile de KM.

Kotlin Multiplatform Mobile

Kotlin multiplatform Mobile (aussi appelé KMM) a été conçu pour simplifier la création d’application mobile multiplateforme.  L’idée derrière KMM est de mutualiser votre code métier au sein d’un code partagé en Kotlin pour iOS et Android et d’écrire du code spécifique à la plateforme uniquement pour l’UI (ou fonctionnalités natives).

Ci-dessous un exemple d’utilisation de KMM sur une architecture mobile suivant le pattern MVP (Model - View - Presenter). Seule l’UI est déclinée nativement ; du code natif peut être intégré en complément sur les couches dédiées KMM.

Que vous commenciez un nouveau projet, ou que vous songiez à ajouter de nouvelles fonctionnalités à un projet natif existant (Android et iOS), KMM est une solution qui peut vous intéresser.

Du code partagé entre Android & iOS ?

En effet, une seule base de code est nécessaire : le code partagé écrit en Kotlin est ainsi compilé en bytecode JVM avec Kotlin/JVM et en binaires natifs avec Kotlin/Native (à l’aide de LLVM). Cette technologie permet à Kotlin/Native de compiler sur des plateformes où l’exécution d’une machine virtuelle n’est pas souhaitable ou possible (dans notre cas iOS).

Les promesses de KMM

Du code commun avec une implémentation spécifique si nécessaire.
Si le développement à réaliser nécessite une déclinaison propre à chaque OS (persistance locale par exemple), vous pouvez utiliser le mécanisme d'interface expect/actual pour implémenter le code spécifique à votre plateforme.
Avec ce mécanisme, déclarez vos classes / fonctions comme étant attendues (expected) dans votre code commun et fournissez leur implémentation à l’aide du mot-clé actual pour vos plateformes cibles.

En termes de performances, comment ça se passe ?
Le code partagé écrit en Kotlin est compilé sous différents formats de sortie en fonction de la cible visée : en bytecode Java pour Android et en binaires natif pour iOS pour la partie mobile. Les performances sont ainsi comparables à celles des applications natives.

Incorporation de KMM dans une application existante.
Si vous possédez une application existante, il est tout à fait possible de réaliser une nouvelle feature en KMM. Une fois le développement effectué, vous n’avez plus qu'à l'intégrer à votre projet iOS ou Android en tant que dépendance. De plus, si vous possédez déjà une application Android, vous pouvez utiliser le code déjà produit et le modifier afin de le rendre compatible avec iOS.

KMM dans la pratique

Prérequis

Intéressons-nous maintenant au code avec un cas pratique d’implémentation de KMM. Commençons par préparer notre environnement de développement pour être capable d’écrire et d’exécuter du code spécifique sur les deux plateformes (Android et iOS).

Création d’une application KMM

Votre environnement de développement est prêt, nous pouvons maintenant nous attaquer au vif du sujet. Pour cela créez un nouveau projet avec un Android Studio à jour et sélectionnez “KMM Application”

Cliquez sur Next, renseignez les différents champs (Nom de l’application, package name, …) et cliquez sur Next.

Laissez les noms par défaut concernant les différents packages et choisissez le framework de distribution que vous désirez (nous n’en parlerons pas dans l’article, son choix n’a pas d’importance ici). Cliquez ensuite sur Finish

Lancement de l’application

Pour lancer votre application, rien de plus simple, choisissez iosApp ou androidApp dans la liste des Run Configurations et cliquez sur le bouton Run (il est possible de lancer l’émulateur iOS depuis Android Studio).

On obtient les résultats suivants :

Comme on peut le constater,  l’application générée est très simple, affichant simplement l’OS et la version de l’OS utilisée par le simulateur/téléphone.

Du côté du code

Intéressons nous maintenant au contenu et à l’organisation du code. Tout d’abord, commençons par changer de vue d’organisation projet pour choisir la vue Project.

Comme vous pourrez le constater, un projet KMM est organisé de la façon suivante :

  • androidApp : contient le code source de l’application Android
  • iosApp : contient le code source de l’application iOS
  • shared : contient le code source qui sera partagé pour l’application Android et iOS.

Les packages androidApp et iosApp peuvent être ouverts comme des projets à part sur Android Studio et XCode, ils possèdent une dépendance du module shared.

Packages

Le dossier shared quant à lui contient les éléments suivants :

  • androidMain : contient le code spécifique à Android
  • commonMain : contient le code partagé entre les différentes plateformes
  • iosMain : contient le code spécifique à iOS.

Regardons maintenant le code d’un peu plus près ; voici respectivement le contenu du fichier MainActivity du dossier androidApp ainsi que du fichier ContentView du dossier iOSApp.

struct ContentView: View {
  let greet = Greeting().greeting()

  var body: some View {
     Text(greet)
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
  	ContentView()
  }
}
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
   binding.textView.text = greet()
}

Chaque plateforme appelle de façon native la fonction greeting() de la classe Greeting() contenue dans le code commun.

Penchons-nous maintenant sur cette classe et son implémentation.

class Greeting {
   fun greeting(): String {
       return "Hello, ${Platform().platform}!"
   }
}

La classe Greeting fait elle-même appel à la classe Platform. C’est ici que nous allons retrouver ce mécanisme expect/actual dont nous parlions précédemment.

Dans notre dossier commonMain vous pouvez constater que notre classe Platform a une déclaration  “attendue”  dans les dossiers iosMain et androidMain à l’aide du mot clé actual.

expect class Platform() {
   val platform: String
}
actual class Platform actual constructor() {
   actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
actual class Platform actual constructor() {
   actual val platform: String = "${UIDevice.currentDevice.systemName()} ${UIDevice.currentDevice.systemVersion}"
}

Si on récapitule, on dispose :

  • D’une application Android et d’une application iOS qui définissent leur vue nativement
  • Chaque vue appelle le code commun contenu dans notre module shared
  • On utilise ensuite le mécanisme expect/actual afin de : définir les classes et méthodes auxquelles on souhaite accéder (expect), et fournir une implémentation pour ces mêmes classes et méthodes grâce aux API spécifiques de chaque plateforme (actual)

Il ne s’agit ici que d’un simple exemple montrant le fonctionnement de Kotlin Multiplatform. Mais si vous avez compris le principe, vous constaterez que vous pourrez partager bien plus que qu’une simple String : une base de données ou encore un client Http sont des éléments que vous pourrez centraliser au sein d’une même base de code.

Et dans le cas où vous auriez besoin de définir du code spécifique, vous pourrez ainsi le faire grâce au mécanisme expect/actual dans le code commun et tirer profit de la puissance qu'offrent Kotlin/JVM et Kotlin/Native.

Conclusion ?

Flutter et React Native ont pour credo "write once, run anywhere" avec les problèmes que cela engendre (difficulté à avoir un vrai look and feel natif, soucis de performance...).
KM(M), encore très modeste mais prometteur, dirait "write your business logic once, include it anywhere".

Cette approche garde les bénéfices de la factorisation du métier (write once - test once). En revanche, elle ne bride pas le développeur dans son utilisation du kit de la plateforme cible.

Pour aller plus loin

  • Si vous souhaitez en savoir plus sur les possibilités de Kotlin Native, nous vous invitons à consulter la documentation officielle.
  • Vous pouvez également retrouver une présentation en vidéo de KMM, des exemples d’implémentation ainsi que l’utilisation de Ktor, un framework Kotlin Multiplatform qui inclut notamment un client http vous permettant d’exécuter et récupérer les réponses provenant de requêtes.
  • Côté code, retrouvez des exemples de projets KMM et un github réunissant plusieurs librairies Kotlin Multiplatform.

Enfin, il est important de préciser que Kotlin Multiplatform est encore en Alpha, ce qui explique certaines restrictions à date concernant des fonctionnalités comme la partie multithread des coroutines ; de nombreuses informations complémentaires sont à retrouver sur leur page releases.

Bonne découverte à tous.