Il faut dire que le COVID, plusieurs confinements, et des changements de règles coté boulot ont fait que poster sur un blog était devenu peu pratique. De plus, ces dernières années, mon rôle se limite plutôt à faire de l’administration système que du développement.
Il n'aura échappé à personne que, ces derniers temps, Java semble être, euh, au creux de la vague... et ce pour diverses raisons sur lesquelles nous ne nous étendrons pas. Bien que Kotlin de JetBrains existe depuis 2011, ce n'est que depuis 2019 qu'il a pris son essor quand Google a décidé d'en faire le langage de développement préféré pour Android. Outre le fait que AndroidStudio soit basé sur IntelliJ IDEA de JetBrains, sans doute auront-ils jugé le langage assez mature et suffisamment compatible avec la JVM au cœur de leur OS mobile. Bien que j'utilise IDEA depuis un peu plus de 10 ans déjà, je ne m’étais encore jamais penché jusqu'à présent sur Kotlin. J'ai vu qu'il existe un framework JavaFX pour Kotlin nommé TornadoFX mais il ne semble plus être maintenu et je ne suis pas totalement sûr de l’intérêt de l'utiliser étant donné que, en théorie, on devrait pouvoir directement écrire une app JavaFX en Kotlin sans trop de difficulté.
Prérequis
Vous avez besoin de :
- La dernière version de IntelliJ IDEA Community avec le support de Java et Kotlin ;
- Une version récente de l'OpenJDK (20 ou 21 feront l'affaire) ;
- Une version récente du SDK JavaFX (20 ou 21 feront l'affaire) pour votre OS à télécharger chez Gluon (ou un autre fournisseur) ;
- Une version récente des fichiers JMOD de JavaFX (20 ou 21 feront l'affaire) pour votre OS à télécharger chez Gluon (ou un autre fournisseur) - cela sera utile pour la création d'une application native.
Optionnel et non couvert par ce blog:
- De quoi signer numériquement votre JAR ;
- De quoi signer numériquement votre exécutable Windows ou macOS ;
- Un créateur d'installeur pour Windows (cependant jpackage peut directement créer des paquets RPM ou DEB pour Linux ou des images DMG pour macOS ainsi que des installeurs MSI pour Windows)
- De quoi signer numériquement votre installeur Windows ou votre image macOS.
Désarchivez votre JDK et votre SDK JavaFX dans des emplacements vides. Personnellement, je place en général les fichiers JMOD de JavaFX dans un répertoire jmods dans la racine du SDK JavaFX mais vous pouvez les mettre où bon vous semble.
Installez ensuite IntelliJ IDEA, puis lancez l'IDE.
Création du projet
Sur l’écran d’accueil, optez pour la création d'un nouveau projet vide en faisant New Project mais ne choissisez pas un générateur prédéfini. Dans la configuration de ce projet vide, donnez-lui un nom, puis choisissez Kotlin au lieu de Java en tant que Langage. Si le JDK qui vous intéresse n'est pas disponible, allez dans le menu déroulant JDK et faites Add SDK puis naviguez vers la racine de votre copie du JDK avant de valider votre choix. Sélectionnez ce JDK à votre retour dans la configuration du projet. Vérifiez que tous vos réglages sont corrects et appuyez sur Create.
Une fois, le projet généré, vous pouvez aller dans File -> Project Structure... ou cliquez avec le bouton de droite sur le nœud racine du projet et choisissez Open Module Settings. Allez dans Libraries, et appuyez sur le bouton + en haut de la colonne listant les bibliothèques (ne pas appuyer sur le bouton + dans l’écran détaillant le contenu du runtime Kotlin pour Java, cela a pour effet d'ajouter des fichiers a la bibliothèque sélectionnée). Dans Project Library, choisissez Java et naviguez jusqu'au répertoire lib contenu dans votre SDK JavaFX, puis sélectionnez tous les fichiers JAR avant d'appuyer sur OK. Lorsque Intellj IDEA vous demande d'ajouter la bibliothèque à votre projet, faite Cancel. Dans la colonne listant les bibliothèques, vous pouvez sans doute voir que IDEA vous a rajouté une bibliothèque nommée javafx-swt ou quelque chose de similaire. Donnez-lui un nom correct, genre javafx-21, en éditant le champ Name sur l’écran détaillant le contenu de cette bibliothèque. Pensez à appuyer sur Apply en bas de boite de dialogue pour valider ce changement de nom.
Au niveau du projet, basculez désormais sur les réglages Modules, puis dans l'onglet Dependancies. Appuyez sur le bouton + situes sous l’entrée Module SDK. Choisissez Library... et sélectionnez votre nouvelle bibliothèque JavaFX javafx-21 avant de faire Add Selected. Vous pouvez maintenant fermer les réglages du projet en appuyant sur le bouton OK en bas de la boite de dialogue.
Attention : il semble y avoir un bug de longue date dans IntelliJ qui fait que si vous n'avez pas appuyé sur Apply quand vous avez changé le nom de la bibliothèque, l'IDE va avoir tendance à réutiliser le nom précédent de la bibliothèque après fermeture des réglages du projet. Donc, si vous avez un soucis ultérieurs avec JavaFX qui n’est pas détecté par l'IDE, pensez à revenir sur l’écran Modules -> Dependancies et à vérifier que la bibliothèque incluse porte bien son nouveau nom javafx-21 et non pas son ancien nom javafx-swt. Si le nom est incorrect, supprimez la bibliothèque en utilisant le bouton - et rajoutez-la à nouveau avec le bouton + comme décrit précédemment. Ceci devrait résoudre vos soucis.
Une fois de retour dans la vue principale du projet, dépliez l'arborescence src -> main -> kotlin et cliquez avec le bouton de droite sur le nœud kotlin et faites New -> Package. Donnez le nom test à ce package. Puis, cliquez une fois de plus avec le bouton de droite sur le nœud kotlin et faites New -> module-info.java. Cela créera un nouveau fichier module-info.java sous le nœud kotlin dans lequel vous pouvez mettre l’ébauche de module suivante :
Code Java : | Sélectionner tout |
1 2 3 | module test { exports test; } |
Application basique
Nous allons commencer par une application JavaFX très simple : afficher une fenêtre vide. Cliquez avec le bouton de droite sur le nœud test (celui qui correspond au package) et faites New -> Java Class. Appelez cette nouvelle classe MainFX et validez. Vous pouvez saisir le code suivant dans votre classe :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package test; import javafx.application.Application; import javafx.stage.Stage; public final class MainFX extends Application { public static void main(final String... args) { Application.launch(args); } @Override public void start(final Stage stage) throws Exception { stage.setTitle("Test FX"); stage.setWidth(800); stage.setHeight(600); stage.show() } } |
Tout ce que fait ce code est d'initialiser une application, de configurer sa fenêtre en lui donnant un titre et des dimensions, puis de l'afficher.
Si le SDK JavaFX est correctement installé et détecté par IntelliJ (voir plus haut), l'IDE vous indiquera des erreurs dans le code, et surtout soulignera une partie des lignes d'importation de classe en rouge. En passant votre curseur dessus, une infobulle vous indiquera que vous avez besoin de rajouter des modules manquants à l'appel. Cliquez sur une des lignes fautives et faites ALT+ENTRÉE au clavier, puis choisissez Add required 'javafx.graphics' directive to module-info.java.... Cela ajoutera la ligne requires javafx.graphics; dans votre fichier module-info.java et devrait enlever toutes les erreurs présentes dans votre classe.
Dans l'arborescence du projet, cliquez avec le bouton de droite sur le nœud MainFX et choisissez Run 'MainFX.main()'. Une fois votre code compilé, la fenêtre vide de notre application de test de devrait apparaître sur l’écran. C'est bien, mais on veut faire la même chose en Kotlin, non ? Fermez votre fenêtre et cliquez avec le bouton de droite sur le nœud du package test, puis choisissez New -> Kotlin Class/File, et donnez le nom MainKt à cette nouvelle classe. Vous pouvez mettre le contenu suivant dans cette classe :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | package test import javafx.application.Application import javafx.stage.Stage class MainKt : Application() { override fun start(stage: Stage) { stage.title = "Test Kt" stage.width = 800.0 stage.height = 600.0 stage.show() } } |
On peut voir que, bien que similaire, le code Kotlin est un peu moins verbeux que le code Java. La déclaration d’héritage permet directement l'invocation d'un constructeur de la classe parente pouvant prendre des paramètres. Outre l'absence de ; en fin de ligne (ils sont optionnels), je n'ai pas eut besoin d’invoquer un setter pour changer le titre de la fenêtre ou encore ses dimensions. Par contre j'ai du changer mes valeurs numériques entières en nombres flottants car Kotlin utilise un typage plus fort que Java. Également, dans la déclaration des paramètres de ma méthode (désormais une fonction), le nom de la variable vient avant son type. Reste un soucis, comment lancer cette application ? Contrairement à Java, Kotlin n'a pas le concept de membre ou de méthode statiques dans les classes. Or nous en utilisions 2 précédemment : main() qui est le point de lancement habituel des programmes Java et Application.launch() qui permet d'initialiser une app JavaFX (en initialisant les runtimes au passage, en créant une instance de notre classe et en gérant automatiquement tout ce qui est création de la fenêtre native, avant d'invoquer la methode start() de notre classe avec la fenêtre en paramètre).
Le premier soucis est assez simple à gérer : il suffit de déclarer une fonction main() globale qui servira à faire la même chose. Il vous suffit donc de placer la fonction suivante en dehors du corps de la classe :
Code Kotlin : | Sélectionner tout |
1 2 3 | fun main() { println("Hello World!") } |
IDEA soulignement immédiatement println() en rouge en indiquant dans une infobulle que la fonction println() est définie dans le module kotlin.stdlib et que ce dernier est absent de notre projet. Vous pouvez corriger ce soucis en choisissant Add required 'kotlin.stdlib' directive to module-info.java... directement dans l'infobulle ou en faisant ALT+ENTRÉE sur la ligne incriminée. Cela rajoutera la ligne requires kotlin.stdlib; dans votre fichier module-info.java et devrait enlever toutes les erreurs présentes dans votre classe. Désormais, notre fichier module-info.java ressemble à cela :
Code Java : | Sélectionner tout |
1 2 3 4 5 | module test { requires javafx.graphics; requires kotlin.stdlib; exports test; } |
Dans l'arborescence du projet, cliquez avec le bouton de droite sur le nœud MainKt.kt et choisissez Run 'MainKt' pour voir s'afficher "Hello World!" sur votre sortie.
À noter que les variantes suivantes sont également valides pour décrire un point de lancement Kotlin :
- fun main(args: Array<String>) - cette fonction accepte un tableau de chaînes de caractères qui sont les paramètres passés sur la ligne de commande.
- fun main(vararg args: String) - cette fonction accepte un nombre variable de chaînes de caractères qui sont les paramètres passés sur la ligne de commande.
Le second soucis est un poil plus embêtant : il nous faut arriver à invoquer Application.launch(). Kotlin nous permet de simuler des méthodes statiques en ajoutant un membre compagnon dans une classe. Par exemple en faisant :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package test import javafx.application.Application import javafx.stage.Stage class MainKt : Application() { companion object { fun launch(args: Array<String>) { Application.launch(args) } } override fun start(stage: Stage) { stage.title = "Test Kt" stage.width = 800.0 stage.height = 600.0 stage.show() } } fun main(args: Array<String>) { MainKt.launch(args) } |
Mais l'IDE va immédiatement souligner la ligne Application.launch(args) en rouge en nous indiquant qu'elle ne peut pas trouver de surcharge de la méthode qui fonctionne avec les types des arguments que nous utilisons. Cela vient du fait que les varargs et tableaux en Kotlin ne sont pas tout à fait gérés de la même manière que leurs équivalents Java. On peut régler ce problème en utilisant le spread operator de Kotlin pour permettre de passer les éléments contenus dans la variable individuellement. Cet opérateur s’écrit sous la forme d'une * placée devant la variable. La ligne devient donc :
Code Kotlin : | Sélectionner tout |
1 2 3 | fun launch(args: Array<String>) { Application.launch(*args) } |
Cependant, notre solution ne fonctionne toujours pas car l’exécution le programme va échouer avec l'erreur suivante :
Code console : | Sélectionner tout |
1 2 3 4 5 | Exception in thread "main" java.lang.RuntimeException: Error: class test.MainKt$Companion is not a subclass of javafx.application.Application at javafx.graphics@21.0.1/javafx.application.Application.launch(Application.java:305) at test/test.MainKt$Companion.launch(MainKt.kt:9) at test/test.MainKtKt.main(MainKt.kt:20) at test/test.MainKtKt.main(MainKt.kt) |
En effet, la méthode statique Application.lauch() qui permet de démarrer une application JavaFX s'attend à ce que la classe dans laquelle elle est invoquée hérite de Application Or, l'objet compagnon est compilé dans une classe interne (la trace mentionne MainKt$Companion) qui n’hérite pas de Application. Heureusement, il existe une variante de Application.lauch() qui permet de spécifier quelle classe doit être utilisée pour initialiser l'app. On peut donc transformer le code en Application.launch(MainKt::class.java, *args) en utilisant la syntaxe MainKt::class.java qui permet de récupérer la référence sur la classe Java. Ce qui nous donne :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package test import javafx.application.Application import javafx.stage.Stage class MainKt : Application() { companion object { fun launch(vararg args: String) { Application.launch(MainKt::class.java, *args) } } override fun start(stage: Stage) { stage.title = "Test Kt" stage.width = 800.0 stage.height = 600.0 stage.show() } } fun main(vararg args: String) { MainKt.launch(*args) } |
En observant ce code, on se rend compte que notre appel est finalement totalement détaché de la classe MainKt (plus rien de nous oblige à initialiser le toolkit depuis l’intérieur de la classe) , ce qui nous permet de nous affranchir de la classe compagnon et de simplifier le code en :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package test import javafx.application.Application import javafx.stage.Stage class MainKt : Application() { override fun start(stage: Stage) { stage.title = "Test Kt" stage.width = 800.0 stage.height = 600.0 stage.show() } } fun main(vararg args: String) { Application.launch(MainKt::class.java, *args) } |
Vous pouvez à nouveau exécuter votre code Kotlin pour voir apparaître une fenêtre identique à celle produite par le code Java à l'exception du titre de la fenêtre. Félicitations !
Application avancée
C'est sympa mais on a juste une fenêtre vide à l'ecran. Nous allons maintenant modifier notre app pour afficher des contrôles et la rendre plus intéressante avec avec un graphique en ligne. Une fois de plus je vais commencer par modifier l'application Java pour donner une référence de ce que doit être l'application finale avant de passer sur la version Kotlin.
Dans l’arborescence de votre projet, cliquez avec le bouton de droite sur le nœud src -> resources puis choisissez New -> Directory et donnez à ce nouveau répertoire le même nom que notre package (ici test). Faites un clic droit sur ce nouveau répertoire et choisissez New -> File, puis nommez ce fichier strings.properties. Éditez ce fichier et collez ce contenu dedans :
Code Properties : | Sélectionner tout |
1 2 3 4 5 | app.title.fx = Test FX app.title.kt = Test Kt xaxis.label = abscisses yaxis.label = ordonnées series.label = y = x² |
Faites une seconde fois un clic droit sur ce nouveau répertoire et choisissez New -> File et nommez ce fichier app.css. Éditez ce fichier et collez ce contenu dedans :
Code CSS : | Sélectionner tout |
1 2 3 | .root { -fx-background-color: white; } |
Vous allez maintenant modifier le contenu de la méthode start() classe Java MainFX comme suit :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | final var bundle = ResourceBundle.getBundle("test/strings"); // Création du graphique. final var series = new LineChart.Series<Number, Number>(); series.setName(bundle.getString("series.label")); IntStream.rangeClosed(0, 10) .forEach(x -> { final double y = Math.pow(x, 2); final var data = new XYChart.Data<Number, Number>(x, y); series.getData().add(data); }); final var xAxis = new NumberAxis(0, 10, 5); xAxis.setLabel(bundle.getString("xaxis.label")); final var yAxis = new NumberAxis(0, 100, 10); yAxis.setLabel(bundle.getString("yaxis.label")); final var chart = new LineChart<>(xAxis, yAxis); chart.getData().add(series); // Mise en place de la scène. final var root = new StackPane(); root.getChildren().setAll(chart); var scene = new Scene(root); // Gestion des CSS. Optional.ofNullable(getClass().getResource("app.css")) .map(URL::toExternalForm) .ifPresent(scene.getStylesheets()::add); // Gestion de la fenêtre. stage.setTitle(bundle.getString("app.title.fx")); stage.setWidth(800); stage.setHeight(600); stage.setScene(scene); stage.show(); |
IntelliJ va sans doute se plaindre que le module javafx.controls est absent. Placez votre curseur sur une des lignes d'importation incriminées et faites ALT+ENTRÉE au clavier pour procéder à cette correction. Désormais, notre fichier module-info.java ressemble à cela :
Code : | Sélectionner tout |
1 2 3 4 5 6 | module test { requires javafx.graphics; requires javafx.controls; requires kotlin.stdlib; exports test; } |
Parmi les choses qui sont faites par ce code :
- On initialise un gestionnaire de ressources localisées qui va permettre d'extraire les chaînes de texte depuis le fichier strings.properties ;
- On crée un graphique en ligne qui permet d'afficher les valeurs du carré des nombres entiers entre 0 et 10 (inclus) ;
- On crée tous les composants et la scène nécessaire pour afficher ce graphique ;
- On charge le fichier CSS app.css et on l'applique sur la scène ;
- On place la scène dans notre fenêtre.
Le code bien que verbeux n'est pas très compliqué.
Maintenant, vous allez copier ce code Java et... le coller dans le code de la fonction start() dans la classe Kotlin MainKt ! En effet, IntelliJ va détecter que ce code est en Java et va vous proposer de traduire automatiquement ce code Java en code Kotlin ce qui prend juste quelques secondes. Et il est plutôt efficace à cette tache :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | val bundle = ResourceBundle.getBundle("test/strings") // Création du graphique. // Création du graphique. val series = Series<Number, Number>() series.name = bundle.getString("series.label") IntStream.rangeClosed(0, 10) .forEach { x: Int -> val y: Double = x.pow(2.0) val data = XYChart.Data<Number, Number>(x, y) series.data.add(data) } val xAxis = NumberAxis(0.0, 10.0, 5.0) xAxis.label = bundle.getString("xaxis.label") val yAxis = NumberAxis(0.0, 100.0, 10.0) yAxis.label = bundle.getString("yaxis.label") val chart = LineChart(xAxis, yAxis) chart.data.add(series) // Mise en place de la scène. // Mise en place de la scène. val root = StackPane() root.children.setAll(chart) val scene = Scene(root) // Gestion des CSS. // Gestion des CSS. Optional.ofNullable(javaClass.getResource("app.css")) .map { obj: URL -> obj.toExternalForm() } .ifPresent { e: String? -> scene.stylesheets.add(e) } // Gestion de la fenêtre. // Gestion de la fenêtre. stage.title = bundle.getString("app.title.fx") stage.width = 800.0 stage.height = 600.0 stage.scene = scene stage.show() |
Quelques soucis mineurs au final : tous mes commentaires ont été doublés et la fonction pow() semble être manquante. Pour les commentaires, c'est facilement corrigeable. Pour la fonction cela est du au fait que la fonction pow() est uniquement définie pour les nombres flottants et pas pour les nombres entiers. Il est possible de corriger le soucis en faisant : val y: Double = x.toDouble().pow(2.0). IntelliJ soulignera la ligne en rouge une dernière fois, il suffit de faire ALT+ENTRÉE au clavier sur cette ligne pour rajouter l'import manquant import kotlin.math.pow en début de fichier. Profitez-en pour changer la clé de ressource du titre de la fenêtre de stage.title = bundle.getString("app.title.fx") en stage.title = bundle.getString("app.title.kt"). Ce qui nous donne après un très court nettoyage du code :
Code Kotlin : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | package test import javafx.application.Application import javafx.scene.Scene import javafx.scene.chart.LineChart import javafx.scene.chart.NumberAxis import javafx.scene.chart.XYChart import javafx.scene.chart.XYChart.Series import javafx.scene.layout.StackPane import javafx.stage.Stage import java.net.URL import java.util.* import java.util.stream.IntStream import kotlin.math.pow class MainKt : Application() { override fun start(stage: Stage) { val bundle = ResourceBundle.getBundle("test/strings") // Création du graphique. val series = Series<Number, Number>() series.name = bundle.getString("series.label") IntStream.rangeClosed(0, 10) .forEach { x: Int -> val y: Double = x.toDouble().pow(2.0) val data = XYChart.Data<Number, Number>(x, y) series.data.add(data) } val xAxis = NumberAxis(0.0, 10.0, 5.0) xAxis.label = bundle.getString("xaxis.label") val yAxis = NumberAxis(0.0, 100.0, 10.0) yAxis.label = bundle.getString("yaxis.label") val chart = LineChart(xAxis, yAxis) chart.data.add(series) // Mise en place de la scène. val root = StackPane() root.children.setAll(chart) val scene = Scene(root) // Gestion des CSS. Optional.ofNullable(javaClass.getResource("app.css")) .map { obj: URL -> obj.toExternalForm() } .ifPresent { e: String? -> scene.stylesheets.add(e) } // Gestion de la fenêtre. stage.title = bundle.getString("app.title.kt") stage.width = 800.0 stage.height = 600.0 stage.scene = scene stage.show() } } fun main(vararg args: String) { Application.launch(MainKt::class.java, *args) } |
Parmi les choses qu'on peut remarquer : Kotlin utilise le mot-clé val qui est similaire à la combinaison final var en Java. Ces variables ne sont donc pas mutables après leur déclaration. Pour déclarer des variables mutables, il faut utiliser à la place le mot-clé var. D'ailleurs les paramètres des functions sont aussi non-mutables tant qu'on y est. De plus, Kotlin n'utilise pas le mot-clé new quand il faut créer de nouvelles instances. Le bloc lambda Kotlin ressemble à son ancêtre Java. Pour le reste, au niveau de la résolution des resources sur le CLASSPATH, et bien cela fonctionne exactement comme en Java basique et this.getClass() est devenu javaClass.
Au lancement, notre application Kotlin + JavaFX se présente et se comporte exactement comme son équivalent 100% Java. Il y a probablement des choses qui pourraient être simplifiées en utilisant plus de fonctions ou d'idiomes de Kotlin (pour, entre autres, la gestion des valeurs potentiellement null) mais c'est déjà un bon départ.
Fichier JAR
Nous allons maintenant créer un JAR pour permettre de distribuer plus facilement notre application. Allez dans le menu File -> Project Structure... ou cliquez avec le bouton de droite sur le nœud racine de votre projet et choisissez Open Module Settings. Déplacez-vous dans l'onglet Artifact et cliquez sur le bouton + et choisissez JAR -> From module with dependencies.... Dans Main Class, vous allez spécifier la classe qui sert à lancer le programme, celle qui contient la fonction main(). Or, souvenez-vous que cette fonction est globale et ne fait pas partie de la classe test.MainKt. Si vous dépliez la boite déroulante, vous verrez qu'il existe dans votre projet une 3e classe nommée test.MainKtKt (un second "Kt" a été ajouté à son nom) qui a été générée à la compilation. C'est cette classe qui contient la fonction main(). Sélectionnez-la et validez, puis créez votre artéfact sans changer d'autres options.
L'onglet Artifact détaillle désormais le contenu de votre nouvel artéfact. Commencez par changer son nom en MainKt. Puis sélectionnez le nœud racine qui correspond au fichier JAR et cliquez avec le bouton de droite puis choissiez l'action Rename et renommez-le en MainKt.jar et appuyez sur ENTRÉE pour valider. Dans la liste des JAR qui seront recopiés dans votre archive, suprimez tous les JAR de JavaFX (et uniquement ceux-ci). Conservez les autres JAR tierces comme les bibliothèques de Kotlin ou encore la bibliothèque des annotation Java. Validez en cliquant sur OK. En revenant vers votre projet, vous pouvez voir que l'IDE a ajouté un fichier manifeste dans vos ressources.
Vous pouvez maintenant compiler votre fichier JAR en allant dans le menu Build -> Build Artifacts... puis en choisissant MainKT -> Build. Au bout de quelques secondes, la nouvelle archive créée devrait se trouver dans le répertoire out/artifacts/MainKt de votre projet.
Une fois votre JAR obtenu, c'est le bon moment pour le signer avec un certificat numérique avant de procéder à sa distribution.
Lanceur natif
Il est temps maintenant de créer un lanceur natif pour votre programme. Cela permet d'éviter de devoir fournir des scripts de lancemnent a vos utilisateurs et ainsi eviter la presence parfois insolites de fenetres d'intepreteur de commande sur leur écran. Pour cela, nous allons utiliser l'outils jpackage qui est fourni dans le JDK.
Utilisez votre explorateur de fichier ou votre interpréteur de commandes favori suivant votre plateforme et déplacez-vous dans le répertoire racine de votre projet. Créez un sous-répertoire package et placez-y le contenu suivant. Vous pouvez utiliser des services en lignes pour convertir une images PNG en icones Windows ICO ou macOS ICNS :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | package ├── linux │ └── MainKt.png ├── macos │ └── MainKt.icns └── windows └── MainKt.ico |
Code : | Sélectionner tout |
1 2 3 4 | bin ├── package.bat ├── package_linux.sh └── package_macos.sh |
Code CMD : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @echo off set JAVA_HOME="C:\Program Files\java\jdk-21.0.1\" set PATH=%JAVA_HOME%\bin;%PATH% java -version set APP_NAME=MainKt set APP_TYPE=app-image set APP_ICON=./package/windows/%APP_NAME%.ico set MAIN_JAR=%APP_NAME%.jar set MODULES=javafx.graphics,javafx.controls set INPUT_DIR=out\artifacts\%APP_NAME% set OUTPUT_DIR=redist set FX_HOME=E:\fabriceb\Devel\Java\lib\JavaFX\javafx-sdk-21.0.1 set FX_LIBS=%FX_HOME%\lib set FX_JMODS=%FX_HOME%\jmods if exist %OUTPUT_DIR%\%APP_NAME% rmdir /s /q %OUTPUT_DIR%\%APP_NAME% if not exist %OUTPUT_DIR% mkdir %OUTPUT_DIR% REM Test the app works. REM java --module-path $FX_LIBS --add-modules $MODULES -jar $INPUT_DIR/$MAIN_JAR REM Create native launcher. jpackage --type %APP_TYPE% --input %INPUT_DIR% --name %APP_NAME% --main-jar %MAIN_JAR% --module-path %FX_JMODS% --add-modules %MODULES% --dest %OUTPUT_DIR% --icon %APP_ICON% |
La version Windows génère directement un lanceur natif exécutable. Il est possible de générer un installeur MSI, consultez la documentation de jpackage.
Pour Linux : package_linux.sh
Code bash : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #!/bin/sh JAVA_HOME=/usr/local/java/jdk-21.0.1 PATH=$JAVA_HOME/bin:$PATH java -version APP_NAME=TestKt APP_TYPE=app-image APP_ICON= MAIN_JAR=$APP_NAME.jar MODULES=javafx.graphics,javafx.controls INPUT_DIR=./out/artifacts/$APP_NAME OUTPUT_DIR=redist FX_HOME=/home/fabriceb/devel/java/lib/javafx/javafx-sdk-21.0.1 FX_LIBS=$FX_HOME/lib FX_JMODS=$FX_HOME/jmods if [[ -d $OUTPUT_DIR/$APP_NAME ]]; then rm -rf $OUTPUT_DIR/$APP_NAME fi mkdir -p $OUTPUT_DIR # Test the app works. #java --module-path $FX_LIBS --add-modules $MODULES -jar $INPUT_DIR/$MAIN_JAR # Create native launcher. jpackage --type $APP_TYPE --input $INPUT_DIR --name $APP_NAME --main-jar $MAIN_JAR --module-path $FX_JMODS --add-modules $MODULES --dest $OUTPUT_DIR |
La version Linux génère directement un lanceur natif exécutable ; cet exécutable ne prend pas d'icone mais il reste possible de fournir une image PNG pour personaliser un raccourci servant à lancer l'application dans votre interface graphique. Il est possible de générer des paquets RPM ou DEB pour votre distribution, consultez la documentation de jpackage.
Pour macOS : package_macos.sh
Code bash : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #!/bin/sh JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-21.0.1.jdk/Contents/Home/ PATH=$PATH:$JAVA_HOME/bin APP_NAME=MFCL-Viewer APP_TYPE=app-image APP_ICON=./package/macosx/$APP_NAME.icns MAIN_JAR=mfcl.viewer.jar MODULES=javafx.graphics,javafx.controls INPUT_DIR=./out/artifacts/mfcl_viewer_jar OUTPUT_DIR=redist FX_HOME=/Users/fabriceb/devel/lib/javafx/javafx-sdk-21.0.1 FX_LIBS=$FX_HOME/lib FX_JMODS=$FX_HOME/jmods if [[ -d $OUTPUT_DIR/$APP_NAME ]]; then rm -rf $OUTPUT_DIR/$APP_NAME fi mkdir -p $OUTPUT_DIR # Test the app works. #java --module-path $FX_LIBS --add-modules $MODULES -jar $INPUT_DIR/$MAIN_JAR # Create native launcher. jpackage --type $APP_TYPE --input $INPUT_DIR --name $APP_NAME --main-jar $MAIN_JAR --module-path $FX_JMODS --add-modules $MODULES --dest $OUTPUT_DIR --icon $APP_ICON |
La version macOS génère directement un répertoire d'application. Il est possible de générer un paquet PKG ou une image DMG si besoin, consultez la documentation de jpackage.
Nous utilisons l'outil jpackage du JDK pour créer un lanceur natif pour votre applicaiton sur votre systeme d'exploitation. Si vous souhaitez porter votre app vers un système d'exploitation différent, vous n'avez pas besoin de recompiler specialement votre projet Kotlin pour ce nouvel OS. Tout ce que vous avez besoin de faire est d'installer un JDK sur le nouvel OS ainsi que de récupérer les fichiers JMODS de JavaFX pour cet OS. Vous pouvez ensuite copier le fichier JAR, les scripts de packaging et les icones d'applications existants aux endroits appropriés, puis de modificer, si nécessaire, les chemins d'accès dans le script correspondant à cet OS, avant d'executer ce script.
La suite ?
Si vous êtes sous Windows, par exemple, c'est le bon moment de signer votre lanceur natif avec un certificat Microsoft Authenticode. Vous pouvez ensuite empaqueter tous les fichiers générés par jpackage avec un créateur d'installeur (InnoSetup, etc.).
De même, si vous êtes sur macOS, c'est également une bonne étape pour signer votre application ou votre image avec un certificat de développeur Apple.
Conclusion
Voila, vous avez créé une application JavaFX en Kotlin et vous pouvez désormais la distribuer à vos utilisateurs. Même si le code Kotlin prend moins de place et est un peu plus facile à lire que son equivalent Java, on peut se rendre compte qu'il ne sont pas si eloignés l'un de l'autre tant qu'on continue à utiliser des blibliothèques Java. Grace à la grande efficacité de la traduction automatique du code offerte par IntelliJ IDEA nous n'avons pas perdu énormément de temps en portant du code existant Java en code Kotlin. Le challenge serait maintenant de créer une application JavaFX dont le cœur de metier serait écrit en Kotlin pur. Cependant, concevoir et créer une nouvelle application JavaFX directement en Kotlin ne semble pas non-plus etre une tache totalement insurmontable une fois qu'on a appréhendé les bases du language et de son API.