Aujourd’hui, nous allons voir comment écrire en Kotlin un module Multiplatform qui peut être partagé entre une application iOS et une application Android.
Depuis la prise en charge de la création de frameworks iOS, introduite avec Kotlin/Native 0.5 en décembre 2017, il est devenu possible de partager du code afin de créer des bibliothèques pour Android (.aar) et iOS (.framework). En revanche, ce n’est que depuis l’annonce de Kotlin/Native 0.6 que le partage de code est implementable facilement, en vertu du support de Multiplatform, une nouvelle fonctionnalité dédiée à ce cas d’usage ajoutée dans la version 1.2 du langage Kotlin.
L’objectif d’aujourd’hui sera de créer un module iOS et un module Android partageant une interface commune mais fournissant deux implémentation légèrement différentes, les deux écrites en Kotlin.
Multiplatform supporte une DSL introduisant deux nouveaux types de modules :
- « Common » : ils contiennent du code qui n’est spécifique à aucune plateforme, et des déclarations non implémentées (attendues, ou expected) qui doivent s’appuyer sur une implémentation spécifique à la plate-forme.
- « Platform » : ils contiennent un code spécifique à la plate-forme qui implémente les déclarations de l’espace réservé dans le module commun. Les modules de plate-forme peuvent également contenir d’autres codes dépendant de la plateforme
- « Regular » : ils regroupent les modules n’étant ni « Common » ni « Platform ».
Un bon exemple pour ce besoin est l’E/S natif : comme prévu, toute implémentation d’accès aux fichiers ou au réseau ne rentre pas dans le scope de la Kotlin Standard Library. Ainsi, dans le cas où notre module commun voudrait utiliser le système de fichiers, il devra s’appuyer sur deux implémentations sous-jacentes différentes qui dépendent de la plate-forme. Par exemple, pour accéder aux fichiers sur Android, nous utiliserons l’API Java.io.File
alors que sur iOS nous préférerons exploiter NSFileManager
.
Nous allons commencer par là où nous avons terminé dans notre article précédent sur la création d’un framework iOS en Kotlin.
Pour rappel notre module ne faisait rien fait sauf renvoyer une valeur de chaîne. Nous allons maintenant faire deux fois rien, en faisant en sorte que notre fonction renvoie une valeur différente en fonction du système sur lequel notre module s’exécute. Nos deux implémentations seront regroupées sous deux formes différentes : une archive Android (.aar) pour Android et un Framework pour iOS.
Pour ce faire, nous devons modifier légèrement la structure de notre projet en ajoutant deux nouveaux dossiers pour les modules Platform respectifs (myframework-ios et myframework-android) et créer un répertoire contenant src/main/kotlin/fr/xebia/myframework dans les deux sous-projets.
L’arborescence résultante devrait maintenant être la suivante :
├── build.gradle ├── gradle ├── gradlew ├── gradlew.bat ├── myframework │ ├── build.gradle │ └── src │ └── main │ └── kotlin │ └── fr │ └── xebia │ └── myframework │ └── foo.kt ├── myframework-android │ └── src │ └── main │ └── kotlin │ └── fr │ └── xebia │ └── myframework ├── myframework-ios │ └── src │ └── main │ └── kotlin │ └── fr │ └── xebia │ └── myframework └── settings.gradle
Le projet commun
Settings.gradle
Maintenant, nous devons nous assurer que Gradle reconnaît myframework-android dans le cadre de notre projet. Pour ce faire, nous devons éditer notre fichier settings.gradle et, dans la deuxième ligne, ajouter ':myframework-android'
et ':myframework-ios'
.
Le contenu du fichier complet est :
rootProject.name = 'MyProject' include ':myframework', ':myframework-android', ':myframework-ios'
Nous pouvons lancer maintenant :
./gradlew tasks
pour vérifier que notre nouvelle configuration ne génère aucune erreur.
Le fichier de build
Il est temps de modifier nos tâches de construction : dans notre build.gradle
principal, nous devons changer la version de Kotlin en 1.2.30
. Ceci est nécessaire pour utiliser la fonctionnalité multiplateforme sur Android.
Le nouveau build.gradle
complet est le suivant :
allprojects { buildscript { ext.kotlin_version = '1.2.30' repositories { jcenter() } } }
Retour au code
Revenons à notre classe Foo. Nous aimerions fournir une implémentation extrêmement basique, dans laquelle la fonction bar()
retournera une chaîne différente, en fonction du système d’exploitation. Sur Android, la chaîne renvoyée sera "bar-android"
, alors que sur iOS, "bar-ios"
.
Maintenant, il est temps d’introduire un nouveau mot-clé : expect
. Grâce à ce dernier, nous pouvons informer le compilateur que cette déclaration n’apporte aucune implémentation – mais que nous en attendons (expect
) une concrète (actual
) de l’un de nos projets.
Nous allons donc modifier le fichier Foo.kt dans le sous-projet myframework comme suit :
package fr.xebia.myframework expect class Foo() { fun bar(): String }
Aussi, nous devons déclarer que ce projet supporte Multiplatform : pour ce faire, nous allons devoir modifier le build.gradle
sur myframework/build.gradle
comme suit :
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'org.jetbrains.kotlin.platform.common' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" } repositories { jcenter() }
L’implémentation actual
Il est donc temps d’accéder aux implémentations spécifiques à la plate-forme, en commençant par Android.
Nous allons maintenant ajouter, à l’intérieur de myframework-android, un nouveau build.gradle
, contenant la configuration pour ce sous-projet.
Le fichier de build pour Android
Dans le nouveau build Gradle, nous allons déclarer que myframework-android fournit une implémentation attendue (expected
) par un autre projet, à savoir myframework
. Nous faisons cela en utilisant la portée expectedBy
.
Aussi, et surtout, n’oublions pas que nous construisons une bibliothèque Android ici, donc nous devrons ajouter tous les paramètres requis par ce contexte, sous la propriété android
.
Voici comment le fichier build.gradle recherche le sous-projet myframework-android :
buildscript { repositories { google() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:gradle:3.0.1' } } apply plugin: 'com.android.library' apply plugin: 'kotlin-platform-android' android { compileSdkVersion 26 defaultConfig { minSdkVersion 21 targetSdkVersion 26 versionCode 1 versionName '1.0' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } } repositories { jcenter() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" expectedBy project(':myframework') }
AndroidManifest
Puisque nous construisons un .aar, nous avons aussi besoin d’un AndroidManifest.xml
, qui est requis par notre cible.
Dans le projet, nous allons ajouter, à l’intérieur le dossier myframework-android/src/main
, un fichier manifeste simple.
Code
Il est temps de fournir une implémentation spécifique à la plate-forme. Dans cette implémentation extrêmement basique, la fonction bar()
renverra une chaîne différente, en fonction du système d’exploitation. Sur Android, ce sera "bar-android"
.
Pour ce faire, nous devons nous assurer que notre implémentation « réelle » :
- Appartient au même package que la déclaration originale
- Utilise les mots-clés
actual
, à la fois avant la classe (et éventuellement avant son constructeur) et avant l’implémentation de la fonction réelle.
package fr.xebia.myframework actual class Foo { actual fun bar() = "bar-android" }
iOS
Créer la contrepartie iOS ne diffère pas beaucoup de ce que nous avons appris dans notre article précédent.
Tout d’abord, nous devons ajouter un nouveau fichier build.gradle
contenant les définitions de Konan (c’est-à-dire, le compilateur Kotlin/Native). Comme mentionné, le fichier build.gradle
devrait maintenant sembler assez familier, à l’exception de la directive enableMultiplatform true
, permettant la prise en charge de Multiplatform dans ce sous-projet.
Tout comme nous venons de le faire dans la version Android, la directive expectedBy
sous dependencies
indique à gradle que notre projet contient l’implémentation actual
de myframework.
apply plugin: 'konan' buildscript { repositories { maven { url 'https://dl.bintray.com/jetbrains/kotlin-native-dependencies' } } dependencies { classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$konan_version" } } konanArtifacts { framework('KotlinMyFramework', targets: ['iphone', 'iphone_sim']) { srcDir 'src/main/kotlin' enableMultiplatform true } } dependencies { expectedBy project(':myframework') }
Code
Le code est encore une fois de quelques lignes. Nous allons ajouter une classe Foo.kt
dans myframework-ios/src/main/kotlin/fr/xebia/myframework.
Dans ce cas, l’implémentation de la classe Foo
retournera la chaîne bar-ios
. Encore une fois, pour que la classe corresponde à la classe attendue (c-à-d à expect Foo
), nous devons nous rappeler de déclarer le package
dans lequel ce fichier se trouve.
Et voici l’implémentation complète.
package fr.xebia.myframework actual class Foo { actual fun bar() = "bar-ios" }
Construction
Et c’est tout. En exécutant :
./gradlew tasks
nous devrions maintenant pouvoir obtenir toutes les tâches iOS (par exemple konanCompile) et Android.
Nous pouvons construire à la fois iOS et Android en cours d’exécution, à partir de notre dossier de projet.
./gradlew build
Les artefacts seront créés dans les dossiers myframework-ios/build et myframework-android/build.
Vous pouvez télécharger un exemple complet du code présenté dans cet article depuis la branche feature/multiplatform de ce dépôt.
Conclusion
Comme nous l’avons vu, le flux de travail introduit par Kotlin Multiplatform est facilement assimilable et a été rapidement implémenté dans Kotlin/Native. Nous sommes vraiment impatients de voir l’accueil des développeurs mobiles en ce qui concerne cette technologie.