Publication de Kotlin 1.4-M1

Nous avons le plaisir de vous présenter Kotlin 1.4-M1, la Preview de la prochaine version majeure de Kotlin.

Il y a quelques mois, nous vous avions informé de Ce que Kotlin 1.4 allait vous réserver. À l’approche de sa sortie officielle, nous vous proposons une version Preview pour vous permettre d’essayer certaines de ses nouveautés.

m1_twitter-01

Dans cet article, nous présentons les nouvelles fonctionnalités et principales améliorations apportées par la version 1.4-M1 :

  • Un nouvel algorithme d’inférence de type plus puissant est activé par défaut.
  • Les contrats sont désormais disponibles pour les fonctions finales des membres.
  • Le compilateur Kotlin/JVM génère désormais des annotations de type dans le bytecode pour les cibles Java 8+.
  • Un nouveau backend pour Kotlin/JS apporte des améliorations majeures aux artefacts résultants.
  • Changements évolutifs dans la bibliothèque standard : achèvement de cycles de dépréciation et dépréciation de certaines parties supplémentaires.

Vous trouverez la liste complète des modifications dans le journal des modifications. Comme toujours, nous sommes extrêmement reconnaissants envers nos contributeurs externes.

Nous vous encourageons vivement à tester la Preview et attendons impatiemment les commentaires que vous ajouterez dans notre outil de suivi de tickets.

Algorithme d’inférence de type plus puissant

Kotlin 1.4 utilise un nouvel algorithme d’inférence de type plus puissant. Vous pouviez déjà essayer ce nouvel algorithme avec Kotlin 1.3 en spécifiant une option de compilateur. Il est maintenant utilisé par défaut. Vous trouverez la liste complète des problèmes corrigés par le nouvel algorithme dans YouTrack. Dans cet article de blog, nous mettrons en lumière certaines des améliorations les plus notables.

Conversion SAM pour les fonctions et interfaces Kotlin

La conversion SAM vous permet de passer une lambda lorsqu’une interface avec une “méthode abstraite unique” est attendue. Auparavant, vous ne pouviez appliquer la conversion SAM que lorsque vous travailliez avec des méthodes et des interfaces Java de Kotlin. Vous pouvez désormais l’utiliser aussi avec des fonctions et des interfaces Kotlin.

Kotlin prend désormais en charge la conversion SAM pour les interfaces Kotlin. Notez qu’il fonctionne différemment qu’en Java : vous devez marquer explicitement les interfaces fonctionnelles. Après avoir marqué une interface avec le mot clé fun, vous pouvez passer une lambda comme argument chaque fois qu’une telle interface est attendue comme paramètre :

Vous pouvez en apprendre davantage à ce sujet dans cet article.

Kotlin prend en charge les conversions SAM pour les interfaces Java depuis le début, mais il y avait un cas qui n’était pas pris en charge, ce qui pouvait être ennuyeux lorsque vous travailliez avec des bibliothèques Java existantes. Si vous appeliez une méthode Java prenant deux interfaces SAM comme paramètres, les deux arguments devaient être des lambdas ou des objets ordinaires. Il n’était pas possible de passer un argument en tant que lambda et un autre en tant qu’objet. Le nouvel algorithme corrige ce problème et vous pouvez dans tous les cas passer une lambda au lieu d’une interface SAM, ce qui correspond au fonctionnement qui serait naturellement attendu.

Inférence automatique du type dans plus de cas d’utilisation

Le nouvel algorithme d’inférence infère des types dans de nombreux cas où l’ancienne inférence vous obligeait à les spécifier explicitement. Par exemple, dans l’exemple suivant, le type du paramètre lambda it infère correctement à String? :

Dans Kotlin 1.3, vous deviez introduire un paramètre lambda explicite ou remplacer to par un constructeur Pair avec des arguments génériques explicites pour que cela fonctionne.

Conversions intelligentes pour la dernière expression de la lambda

Dans Kotlin 1.3, la dernière expression à l’intérieur de la lambda n’est pas une conversion intelligente, sauf si vous spécifiez le type attendu. Ainsi, dans l’exemple suivant, Kotlin 1.3 infère String? comme le type de la variable result :

Dans Kotlin 1.4, grâce au nouvel algorithme d’inférence, la dernière expression à l’intérieur de la lambda bénéficie d’une conversion intelligente, et ce nouveau type plus précis est utilisé pour inférer le type obtenu pour la lambda. Ainsi, le type de la variable result devient String.

Dans Kotlin 1.3, vous deviez souvent ajouter des conversions explicites (soit !!, soit des conversions de type comme as String) pour que de tels cas fonctionnent. Vous n’avez plus besoin de le faire désormais.

Conversions intelligentes pour les références appelables

Dans Kotlin 1.3, vous ne pouviez pas accéder à une référence de membre d’un type de conversion intelligente. Vous pouvez maintenant :

Vous pouvez utiliser différentes références de membre animal::miaou and animal::wouf après que la variable animal a été convertie intelligemment en types spécifiques Chat et Chien. Après les vérifications de type, vous pouvez accéder aux références de membre correspondant aux sous-types.

Meilleure inférence pour les références appelables

L’utilisation de références appelables sur des fonctions avec des valeurs d’argument par défaut a été améliorée. Par exemple, la référence appelable sur la fonction foo suivante peut être interprétée à la fois comme prenant un argument Int ou comme ne prenant aucun argument :

Meilleure inférence pour les propriétés déléguées

Le type d’une propriété déléguée n’a pas été pris en compte lors de l’analyse de l’expression de la déléguée qui suit le mot clé by. Par exemple, le code suivant ne compilait pas auparavant, mais à présent le compilateur infère correctement les types des paramètres old et new comme String? :

Changements du langage

La plupart des changements apportés au langage ont déjà été décrits dans des articles de blog précédents :

Dans cet article, nous allons aborder quelques petites améliorations concernant les contrats.

Prise en charge des contrats

La syntaxe permettant de définir les contrats personnalisés reste expérimentale, mais nous avons pris en charge quelques nouveaux cas pour lesquels les contrats pourraient être utiles.

Vous pouvez désormais utiliser des paramètres de type générique réifiés pour définir un contrat. Par exemple, vous pouvez implémenter le contrat suivant pour la fonction assertIsInstance :

Le paramètre de type T étant réifié, vous pouvez vérifier son type dans le corps de la fonction. Désormais c’est également possible à l’intérieur du contrat. Une fonction similaire avec le message d’assertion sera ajoutée à la bibliothèque kotlin.test ultérieurement.

De plus, vous pouvez désormais définir des contrats personnalisés pour les membres final. La définition de contrats pour les fonctions membres était auparavant totalement interdite car la définition de contrats sur certains des membres de la hiérarchie implique une hiérarchie des contrats correspondants, et cela reste une question de conception ouverte à la discussion. Toutefois, si une fonction membre est final et ne remplace aucune autre fonction, cela ne pose pas de problème de lui définir un contrat.

Modifications de la bibliothèque standard

Exclusion des coroutines expérimentales obsolètes

L’API kotlin.coroutines.experimental a été remplacée par kotlin.coroutines dans la version 1.3.0. Dans la version 1.4-M1, nous finalisons le cycle d’obsolescence de kotlin.coroutines.experimental en le supprimant de la bibliothèque standard. Pour ceux qui l’utilisent encore sur la JVM, nous fournissons un artefact de compatibilité kotlin-coroutines-experimental-compat.jar avec toutes les API de coroutines expérimentales. Nous allons le publier sur maven et l’inclure dans la distribution Kotlin aux côtés de la bibliothèque standard. Pour le moment, nous l’avons publié dans le référentiel bintray avec les artefacts 1.4-M1.

Suppression de l’opérateur mod obsolète

Autre fonction obsolète : l’opérateur mod sur les types numériques qui calcule le reste après une opération de division. Dans Kotlin 1.1, il a été remplacé par la fonction rem(). Nous le supprimons maintenant complètement de la bibliothèque standard.

Obsolescence des conversions de types flottants en Byte et Short

La bibliothèque standard contient des fonctions pour convertir des nombres à virgule flottante en types entiers : toInt(), toShort(), toByte(). Les conversions de nombres à virgule flottante en types Short et Byte pourraient entraîner des résultats inattendus en raison de la plage de valeurs étroite et de la taille plus petite des variables. Pour éviter de tels problèmes, à partir de la version 1.4-M1, nous démarrons l’obsolescence des fonctions toShort() et toByte() appelées sur des types Double et Float. Si vous avez encore besoin de convertir des nombres à virgule flottante en Byte ou Short, utilisez la conversion en deux étapes : vers le type Int puis vers le type cible.

API de réflexion commune

Nous avons remanié l’API de réflexion commune. Désormais, elle ne contient que les membres disponibles sur les trois plateformes cibles (JVM, JS, Native) afin de vous garantir que le même code fonctionne sur toutes les plateformes.

Nouveaux contrats pour la fonction use() et les fonctions de mesure du temps

Nous élargissons l’utilisation des contrats dans la bibliothèque standard. Dans 1.4-M1, nous avons ajouté des contrats déclarant une exécution unique d’un bloc de code pour la fonction use() et pour les fonctions de mesure du temps measureTimeMillis() et measureNanoTime().

Configurations Proguard pour les réflexions Kotlin

Nous avons intégré des configurations Proguard/R8 pour Kotlin Reflection dans kotlin-reflect.jar dans la version 1.4-M1. Grâce à cela, la plupart des projets Android utilisant R8 ou Proguard devraient fonctionner avec kotlin-reflect sans avoir à recourir à aucun tour de magie pour la configuration. Vous n’avez plus besoin de copier-coller les règles de Proguard pour les composants internes kotlin-reflect. Mais notez que vous devez encore répertorier explicitement toutes les API impliquées dans vos réflexions.

Kotlin/JVM

Depuis la version 1.3.70, Kotlin a pu générer des annotations de type dans le bytecode JVM (version cible 1.8+), afin qu’elles soient disponibles au moment de l’exécution. Cette fonctionnalité était demandée par la communauté depuis un certain temps car elle facilite l’utilisation de certaines bibliothèques Java existantes et donne plus de pouvoir aux auteurs de nouvelles bibliothèques.

Dans l’exemple suivant, l’annotation @ Foo sur le type String peut être émise dans le bytecode puis utilisée par le code de la bibliothèque :

Pour en savoir plus sur l’émission d’annotations de type dans le bytecode, reportez-vous à la section correspondante de l’article de blog sur la sortie de Kotlin 1.3.70.

Kotlin/JS

Pour Kotlin/JS, ce jalon comprend quelques modifications de Gradle DSL, et c’est la première version à inclure le nouveau backend du compilateur IR, qui permet des optimisations et de nouvelles fonctionnalités.

Modifications de Gradle DSL

Dans les plugins Gradle kotlin.js et multiplatform, un nouveau paramètre important a été introduit. Dans le bloc cible de votre fichier build.gradle.kts, le paramètre produceExecutable() est désormais disponible, et nécessaire si vous souhaitez générer des artefacts .js pendant votre build :

Vous pouvez omettre produceExecutable() si vous rédigez une bibliothèque Kotlin/JS. Lorsque vous utilisez le nouveau backend du compilateur IR (plus de détails à ce sujet ci-dessous), l’omission de ce paramètre signifie qu’aucun fichier JS exécutable ne sera généré (et, dans ces conditions, le processus de build s’exécutera plus rapidement). Un fichier klib est généré dans le dossier build/libs. Il peut être utilisé à partir d’autres projets Kotlin/JS, ou en tant que dépendance dans le même projet. Si vous ne spécifiez pas explicitement produceExecutable(), ce comportement se produit par défaut.

L’utilisation de produceExecutable() générera du code exécutable à partir de l’écosystème JavaScript, soit avec son propre point d’entrée, soit en tant que bibliothèque JavaScript. Cela générera les fichiers JavaScript, qui peuvent s’exécuter dans un interpréteur de nœuds, être intégrés dans une page HTML et exécutés dans le navigateur, ou utilisés comme dépendances à partir de projets JavaScript.

Notez que lors du ciblage du nouveau backend du compilateur IR (plus de détails ci-dessous), produceExecutable() générera toujours un seul fichier .js indépendant par cible. Actuellement, il n’y a pas de support pour la déduplication ou le fractionnement de code entre plusieurs artefacts générés. Vous pouvez vous attendre à ce que le comportement de produceExecutable() change dans les prochains jalons. Le nom de cette option est également susceptible de changer à l’avenir.

Nouveau backend

Kotlin 1.4-M1 est la première version à inclure le nouveau backend du compilateur IR pour la cible Kotlin/JS. Ce backend est le fondement d’optimisations considérablement améliorées et le facteur déterminant pour certains changements dans les interactions entre Kotlin/JS et JavaScript/TypeScript. Les fonctionnalités mises en évidence ci-dessous ciblent toutes le nouveau backend du compilateur IR. Bien qu’il ne soit pas encore activé par défaut, nous vous encourageons à l’essayer avec vos projets, à commencer à préparer vos bibliothèques pour le nouveau backend, et bien sûr à nous faire part de vos commentaires et à nous rapporter les problèmes que vous pourriez rencontrez.

Utiliser le nouveau backend

Pour commencer à utiliser le nouveau backend, définissez l’indicateur suivant dans votre fichier gradle.properties :

Si vous devez créer des bibliothèques pour le backend du compilateur IR et le backend par défaut, vous pouvez également définir cet indicateur sur both. L’action de cet indicateur est présentée dans la section de cet article de blog intitulée “Mode both”. L’indicateur est nécessaire car le nouveau backend du compilateur et le backend par défaut ne sont pas compatibles avec les binaires.

Pas de compatibilité binaire

L’un des changements majeurs apportés par le nouveau backend du compilateur IR est l’absence de compatibilité binaire avec le backend par défaut. Cette absence de compatibilité entre les deux backends pour Kotlin/JS signifie qu’une bibliothèque créée avec le nouveau backend du compilateur IR ne peut pas être utilisée à partir du backend par défaut, et vice versa.

Si vous souhaitez utiliser le backend du compilateur IR pour votre projet, vous devez mettre à jour toutes les dépendances de Kotlin vers des versions qui prennent en charge ce nouveau backend. Les bibliothèques publiées par JetBrains pour Kotlin 1.4-M1 ciblant Kotlin/JS contiennent déjà tous les artefacts requis pour une utilisation avec le nouveau backend du compilateur IR. S’ils dépendent d’une telle bibliothèque, les artefacts corrects sont automatiquement sélectionnés par Gradle (c’est-à-dire qu’il n’est pas nécessaire de spécifier une coordonnée spécifique à l’IR). Veuillez noter que certaines bibliothèques, telles que kotlin-wrappers, rencontrent des problèmes avec le nouveau backend du compilateur IR car elles s’appuient sur des caractéristiques spécifiques du backend par défaut. Nous en sommes conscients et cherchons à améliorer cette fonctionnalité.

Si vous êtes un auteur de bibliothèque et que vous cherchez à assurer une compatibilité avec le backend du compilateur actuel, ainsi qu’avec le nouveau backend du compilateur IR, consultez également la section “Mode both” de cet article de blog.

Dans la section suivante, nous présentons de manière plus détaillée certains des avantages et différences du nouveau compilateur.

DCE optimisé

Le nouveau backend du compilateur IR est capable de réaliser des optimisations beaucoup plus importantes que celles du backend par défaut. Le code généré fonctionne mieux avec les analyseurs statiques et il est même possible de l’exécuter à partir du nouveau backend du compilateur IR à l’aide du Closure Compiler de Google et d’utiliser ses optimisations de mode avancé (notez toutefois que le plugin Kotlin/JS Gradle ne fournit pas de prise en charge spécifique pour cela).

Le changement le plus visible ici concerne la taille du code des artefacts générés. Une méthode améliorée d’élimination du code mort permet une réduction considérable des artefacts. Par exemple, cela réduit un programme “Hello, World!” en Kotlin/JS à un peu moins de 1,7 Ko. Pour les projets (démo) plus complexes, tels que dans cet exemple utilisant kotlinx.coroutines, les chiffres ont également radicalement changé et parlent d’eux-mêmes :

Backend par défaut Backend IR
Après compilation 3,9 Mio 1,1 Mio
Après DCE JS 713 Kio 430 Kio
Après bundle 329 Kio 184 Kio
Après ZIP 74 Kio 40 Kio

Si vous n’êtes pas encore convaincu, essayez vous-même. DCE et le regoupement sont activés par défaut pour les deux backends dans Kotlin 1.4-M1 !

Exporter des déclarations vers JavaScript

Lorsque vous utilisez le backend du compilateur IR, les déclarations marquées comme publiques ne sont plus exportées automatiquement (pas même une version avec un nom modifié). En effet, le modèle de monde clos du compilateur IR suppose que les déclarations exportées sont spécifiquement annotées. C’est l’un des facteurs qui facilite les optimisations telles que celle mentionnée ci-dessus.

Pour rendre une déclaration top-level disponible en externe sur JavaScript ou TypeScript, utilisez l’annotation @JsExport. Dans l’exemple suivant, nous rendons KotlinGreeter (et ses méthodes) et farewell() disponibles à partir de JavaScript, mais réservons l’accès à secretGreeting() à Kotlin :

Preview : définitions de TypeScript

Autre fonctionnalité du nouveau compilateur IR Kotlin/JS que nous sommes ravis de vous présenter : la génération de définitions TypeScript à partir de code Kotlin. Ces définitions peuvent être utilisées par les EDI et outils JavaScript lorsque vous travaillez sur des applications hybrides pour fournir la saisie semi-automatique, prendre en charge les analyseurs statiques et faciliter l’inclusion de code Kotlin dans les projets JS et TS.

Pour les déclarations dtop-level annotées par @JsExport (voir ci-dessus) dans un projet configuré pour utiliser produceExecutable(), un fichier .d.ts avec les définitions TypeScript sera généré. Pour le snippet ci-dessus, elles ressemblent à ceci :

Dans Kotlin 1.4-M1, ces déclarations se trouvent dans build/js/packages //kotlin à côté du code JavaScript Web non compressé correspondant. Veuillez noter que puisqu’il ne s’agit que d’une preview, ils ne sont pas ajoutés au dossier distributions par défaut pour l’instant. Ce comportement devrait changer à l’avenir.

Mode both

Pour faciliter l’adoption du nouveau backend du compilateur IR pour les responsables de bibliothèque, nous avons introduit un paramètre supplémentaire pour l’indicateur kotlin.js.compiler dans gradle.properties :

En mode both, le backend du compilateur IR et le backend du compilateur par défaut sont tous deux utilisés lors de la création d’une bibliothèque à partir de vos sources (d’où le nom). Cela signifie que les fichiers klib avec Kotlin IR mais aussi les fichiers js pour le compilateur par défaut seront générés. S’il est publié sous la même coordonnée Maven, Gradle choisira automatiquement le bon artefact en fonction du cas d’utilisation : js pour l’ancien compilateur, klib pour le nouveau. Vous pouvez donc compiler et publier votre bibliothèque avec le nouveau backend du compilateur IR pour les projets qui ont déjà été mis à niveau vers Kotlin 1.4-M1 et qui utilisent l’un des deux backends du compilateur. Cela permet de garantir que vous ne romprez pas l’expérience pour ceux de vos utilisateurs qui utilisent toujours le backend par défaut – parce qu’ils ont mis à niveau leur projet vers la version 1.4-M1.

Veuillez noter qu’un problème persiste actuellement, qui empêche l’EDI de résoudre correctement les références de bibliothèque lorsque la dépendance et votre projet actuel sont générés en mode both. Nous sommes conscients de ce problème et le corrigerons prochainement.

Kotlin/Native

Prise en charge des génériques Objective-C par défaut

Les versions précédentes de Kotlin fournissaient une prise en charge expérimentale pour les génériques dans l’interopérabilité Objective-C. Pour générer un en-tête de framework avec des génériques à partir du code Kotlin, vous avez dû utiliser l’option de compilateur -Xobjc-generics. Dans la version 1.4-M1, ce comportement devient la valeur par défaut. Dans certains cas, cela peut casser un code Objective-C ou Swift existant s’il appelle les frameworks Kotlin. Pour que l’en-tête du framework soit écrit sans génériques, ajoutez l’option de compilateur -Xno-objc-generics.

Veuillez noter que toutes les spécificités et limitations répertoriées dans la documentation restent valables.

Changements de la gestion des exceptions dans l’interopérabilité Objective-C/Swift

Dans la version 1.4, nous modifierons légèrement l’API Swift générée à partir de Kotlin en ce qui concerne la traduction des exceptions. Il existe une différence fondamentale entre Kotlin et Swift concernant la gestion des erreurs . Toutes les exceptions Kotlin sont non vérifiées, tandis que Swift n’a que des erreurs vérifiées. Ainsi, pour rendre le code Swift conscient des exceptions attendues, les fonctions Kotlin doivent être signalées par une annotation @Throws spécifiant une liste de classes d’exceptions potentielles.
Lors de la compilation vers Swift ou le framework Objective-C, les fonctions qui comportent des annotations @Throws ou en héritent sont représentées comme des méthodes produisant NSError* dans Objective-C et comme des méthodes throws dans Swift.
Auparavant, toute exception autre que RuntimeException et Error étaient converties en NSError. Dans la version 1.4-M1, nous avons changé ce comportement. Désormais, NSError est levée uniquement pour les exceptions qui sont des instances de classes spécifiées en tant que paramètres de l'annotation @Throws (ou de leurs sous-classes). Les autres exceptions Kotlin qui atteignent Swift/Objective-C sont considérées comme non gérées et provoquent l'arrêt du programme.

Améliorations des performances

Nous nous efforçons d’améliorer continuellement les performances globales de la compilation et de l’exécution de Kotlin/Native. Dans la version 1.4-M1, nous vous proposons le nouvel allocateur d’objets, jusqu’à deux fois plus rapide sur certains comparateurs. Actuellement, le nouvel allocateur est expérimental et n’est pas utilisé par défaut ; vous pouvez l’activer en utilisant l’option du compilateur -Xallocator=mimalloc.

Compatibilité

Notez que Kotlin 1.4 n’est pas rétrocompatible avec la version 1.3 dans certains cas particuliers. Tous ces cas ont été soigneusement examinés par le comité du langage et seront répertoriés dans le “guide de compatibilité” (similaire à celui-ci). Pour le moment, vous pouvez trouver cette liste dans YouTrack.

Les règles de résolution des surcharges peuvent changer légèrement. Si vous avez plusieurs fonctions avec les mêmes noms et des signatures différentes, celle qui est appelée dans Kotlin 1.4 peut être différente de celle qui a été choisie dans Kotlin 1.3. Toutefois, cela ne se produit que dans des cas très particuliers et nous nous attendons à une très faible occurrence en pratique. Nous supposons également qu’en pratique, les fonctions surchargées se comportent de manière similaire, s’appelant éventuellement les unes les autres. C’est pourquoi ces modifications ne devraient pas affecter le comportement du programme. Mais faites attention à cela si vous aimez écrire du code complexe avec des génériques et de nombreuses surcharges à différents niveaux. Tous les cas de ce type seront répertoriés dans le guide de compatibilité mentionné ci-dessus.

Notes de pré-version

Notez que les garanties de rétrocompatibilité ne concernent pas les versions préliminaires. Les fonctionnalités et l’API peuvent changer dans les versions ultérieures. Lorsque nous atteindrons un RC final, tous les binaires produits par les versions préliminaires seront interdits par le compilateur et vous devrez recompiler tout ce qui a été compilé par la version 1.4 ‑ Mx.

Comment essayer

Comme toujours, vous pouvez essayer Kotlin en ligne sur play.kotl.in.

Dans IntelliJ IDEA et Android Studio, vous pouvez mettre à jour le plugin Kotlin vers la version 1.4-M1. Voir comment procéder.

Si vous souhaitez travailler sur des projets existants qui ont été créés avant d’installer la version préliminaire, vous devez configurer votre build pour la version Prewiew dans Gradle ou Maven.

Vous pouvez télécharger le compilateur de ligne de commande sur la page de la version sur Github.

Vous pouvez utiliser les versions suivantes des bibliothèques publiées avec cette version du langage :

Les détails de la version et la liste des bibliothèques compatibles sont également disponibles ici.

Faites-nous part de vos commentaires

Nous vous serions très reconnaissants de nous signaler les bugs sur notre outil de suivi de tickets YouTrack. Nous ferons de notre mieux pour résoudre tous les problèmes importants avant la version finale, afin que vous n’ayez pas à attendre la prochaine version de Kotlin.

Si vous avez des questions et souhaitez participer aux discussions, nous vous invitons à rejoindre le canal #eap du Slack Kotlin (demandez une invitation ici). Vous pouvez également recevoir des notifications concernant les nouvelles builds de la Preview sur ce canal.

Faison évoluer Kotlin ensemble !

Contributions externes

Nous tenons à remercier particulièrement Zac Sweers pour sa contribution à l’intégration des configurations Proguard dans kotlin-reflect.

Nous remercions tous nos contributeurs externes dont les requêtes pull ont été incluses dans cette version :

About Delphine Massenhove

Marketing Manager France
This entry was posted in New features, News, Release Announcements and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.