Pré-requis

Adrien Pessu Gautier de Saint Martin Lacaze

Installation de Java 8

Linux x64

RPM

Accéder au site de téléchargement Oracle pour le jdk 8.

Récupérer le RPM suivant jdk-8u161-linux-x64.rpm

Suivez les instructions de votre gestionnaire de paquet.

Changez la version de java à utiliser par votre système avec la commande suivante

alternatives --config java

Sélectionner la version JDK 8 installé précédement

Éventuellement changer également la version de de javac avec la commande suivante

alternatives --config javac
Il se peut que vous soyez obligé pour la commande alternative d’utiliser sudo
Autre

Accéder au site de téléchargement Oracle pour le jdk 8.

Récupérer l’archive suivante jdk-8u161-linux-x64.tar.gz

Désarchiver votre archive où vous le souhaitez. Par exemple dans /usr/java/jdk1.8.0_161/

Changez la version de java à utiliser avec la commande suivante

alternatives --config java

Sélectionner la version JDK 8 installé précédement

Éventuellement changer également la version de de javac avec la commande suivante

alternatives --config javac
Il se peut que vous soyez obligé pour la commande alternative d’utiliser sudo

Mac

Accéder au site de téléchargement Oracle pour le jdk 8.

Récupérer le fichier suivant jdk-8u161-macosx-x64.dmg

Utiliser le fichier dmg et suivre les instructions d’installation.

Windows

Accéder au site de téléchargement Oracle pour le jdk 8.

Récupérer l’exécutable suivant jdk-8u161-windows-x64.exe

Lancer l’exécutable et suivre les instructions

Vous pouvez par la suite changer votre path pour rajouter une JAVA_HOME. Plus d’information sur ce site

Installation d’un IDE

Linux

Par simplicité, nous allons installer Jetbrains Intellij IDEA Community.

Accéder au site de téléchargement

Récupérer l’archive de l’édition community.

Désarchiver l’archive dans un répertoire de votre choix.

Pour lancer l’IDE, il vous suffira d’accéder au répertoire où vous avez installé l’IDE et de lancer le fichier `idea.sh`présent dans le répertoire bin.

Mac

Par simplicité, nous allons installer Jetbrains Intellij IDEA Community.

Accéder au site de téléchargement

Récupérer l’archive dmg de l’édition community.

Utiliser le fichier dmg et suivre les instructions d’installation.

Windows

Par simplicité, nous allons installer Jetbrains Intellij IDEA Community.

Accéder au site de téléchargement

Récupérer l’exécutable de l’édition community.

Exécuter l’installeur et suivre les instructions.

Récupération du projet

En ligne de commande :

git clone https://github.com/bttf-kotlin/kotlin-codelab-bttf.git
git checkout step0

Builder l’application

Accéder au répertoire du projet et lancer la commande suivante :

./gradlew build

Les bases

Adrien Pessu Gautier de Saint Martin Lacaze

Contexte

Afin de bien démarrer ce codelab, nous vous avons déjà généré un squelette de projet avec le site https://start.spring.io

Ce site permet d’avoir un projet prêt à l’emploi et nous évitera de perdre du temps dans ce codelab. Nous vous invitons si vous ne le connaissez pas à découvrir ce site après le codelab.

Un peu de théorie

val vs var

En Kotlin, il existe deux mot clés pour déclarer des variables. Le premier s’appelle val et le second `var

val est immutable alors que var est mutable.

val name = "kotlin"

Dans ce premier cas, on doit obligatoirement initialiser name et on ne pourra pas changer sa valeur. Si l’on a besoin de le faire, il faudra utiliser le mot clé var.

var name: String
name = "kotlin"

Dans ce second cas, grâce au mot clé var, on peut initialiser name dans la suite du programme.

On peut remarquer que dans le cas de val le compilateur est capable d’inférer le type de name. Il n’est pas nécessaire de le préciser. Dans le cas de var c’est différent. L’initialisation tardive de name empêche le compilateur d’inférer le type. Il est donc nécessaire de le préciser.
Comme vous pouvez le remarquer il n’y a pas besoin de ; à la fin des instructions.

Qu’est ce qu’une Class ?

Une classe Kotlin ressemble à une classe Java à quelques différences prêt. On la déclare avec le mot clé class

class User {
}

On peut lui déclarer un ou plusieurs contructeurs. Le premier constructeur sera appelé constructeur primaire. Il est déclaré dans l’entête de la classe.

class User constructor(firstName: String) {
}

Dans le cas où le constructeur n’aurait pas d’annotation, il est possible de réduire la syntaxe du constructeur.

class User (firstName: String) {
}

Vous trouverez plus d’informations si besoin sur le site officiel de Kotlin.

Qu’est ce qu’une Data Class ?

Contrairement à une Class, une Data Class représente un POJO contenant de la donnée. Il a donc pour but de stocker des données dans des attributs et éventuellement de fournir des méthodes pour manipuler le contenu de notre objet.

Pour déclarer une Data Class, il suffit d’utiliser le mot clé data conjointement au mot clé class.

data class User(val name: String, val age: Int)
Comme pour les class, il n’est pas nécessaire de mettre les accolades lorsqu’il n’y a pas de méthodes dans la classe.

Là où la Data Class est très intéressante, c’est que le compilateur va nous aider pour l’avenir en fournissant automatiquement les éléments suivants à notre objet.

  • Les méthodes equals() et hashCode()

  • La méthode toString() renvoyant une chaîne de charactères de la forme suivante User(name=John, age=42)

  • Une méthode de copy (évoqué plus tard dans ce codelab)

  • Les méthodes dites componentN() qui sont utile lors de l’utilisation du destructuring.

Vous trouverez plus d’informations si besoin sur le site officiel de Kotlin.

Exercice 1

Passons maintenant à la pratique.

Si ce n’est pas déjà fait, faites un checkout du step0.

git checkout step0

Aller dans la Data Class Event et supprimer l’annotation @JsonIgnoreProperties.

Lancer l’application via la classe KotlinCodelabBttfApplication.

Corriger le problème que vous rencontrez.

Comme vous pouvez le remarquer, Event est une Data Class. La syntaxe est très concise pour créer des POJOs.

Un peu de théorie

Les fonctions

Maintenant que nous avons joué rapidement avec notre Data Class Event, nous allons présenter les fonctions en Kotlin.

Une fonction Kotlin se déclare de la manière suivant :

fun sum(a: Int, b: Int): Int {
    return a + b
}
1 fun est le mot clé permettant de définir une fonction. Contrairement à Java, une fonction Kotlin a la portée publique par défaut.
2 sum est le nom de la fonction. Nous reviendrons sur les noms juste après.
3 On peut déclarer ensuite les paramètres d’entrée. Ici a et b tous les deux de type Int.
4 Enfin on indique le type de retour Int

Comme vous pouvez le remarquer cette fonction est très simple. Il est donc possible en Kotlin de raccourcir la syntaxe d’une telle fonction à une seule ligne. Le code aura alors la forme suivante.

fun sum(a: Int, b: Int) = a + b

Vous trouverez plus d’informations sur les fonctions sur le site officiel de Kotlin.

Il est possible de déclarer une méthode avec des espaces dans le nom. C’est une pratique à réserver aux tests mais qui s’avère bien pratique pour écrire des tests explicites.
class MyTestCase {
     @Test fun `ensure everything works`() {
     }

     @Test fun ensureEverythingWorks_onAndroid() {
     }
}s

Vous trouverez plus d’informations sur les conventions de nommage des fonctions sur cette page

Exercice 2

Dans ce second exercice, nous allons filter automatiquement les événements qui n’ont pas de date dans notre source de données.

Pour cela, accéder à la classe EventController et ajouter un filtre sur le retour de repository. findAll().

Pour information, l’API collection de Kotlin fourni de nombreuses méthodes utilitaires sur les classes.

Les companion, vos meilleurs amis

Adrien Pessu Gautier de Saint Martin Lacaze

Un peu de théorie

Static method

Contrairement à Java, Kotlin ne supporte pas les méthodes statiques pour une classe. En fait, les méthodes statiques n’appartiennent pas à l’instance de l’objet mais plutôt au type lui-même. De ce fait, il est conseillé en Kotlin de définir les méthodes, que l’on veut statique, directement au niveau du package.

fun toUpperCase(input:String):String{
    return if(input.isEmpty()) "" else input.toUpperCase()
}

La méthode précédente pourra être appelé directement (sans instance d’un objet) de la façon suivante :

fun main(args: Array<String>) {
    val message = "Kotlin : le back du futur"
    println("Initial case: $message")
    println("Upper case: " + toUpperCase(message))
}

Puisque l’on vient de voir les méthodes statiques, on peut se demander comment déclarer un singleton. Pour rappel, le singleton est un pattern de programmation limitant l’instanciation d’une classe à une unique instance. Une fois l’instance créée, elle "vivra" tout au long de la durée de vie de votre programme.

Voici un singleton en Kotlin :

object MonSingleton {
    private var count = 0
    fun callMe():Unit {
        println("Ça fait $count fois que je suis appelé !")
    }
}

Avec l’exemple précédent, on peut appeler directement notre méthode via MonSingleton.callMe().

companion

Maintenant que l’on a présenté la façon de déclarer une méthode statique et un singleton, on peut se demander comment peut on faire pour avoir une méthode statique dans une classe comme en Java. C’est à ce moment là que le companion (ou companion object) est utile.

Ce sera notamment utile dans le cas où l’on veut créer une factory.

class User private constructor(val login: String) {
    companion object UserFactory {
        fun create(login: String): User {
            return User(login)
        }
    }
}
Comme vous pouvez le remarquer le constructeur de User est privé. Il n’est donc pas possible d’instancier User en dehors de sa classe.
Le companion a également accès à toutes les méthodes ainsi que tous les membres de la classe User.
Il est possible de "nommer" un companion en utilisant la syntaxe suivante : companion object MonCompanion {}. Il sera alors possible d’appeler le companion dans la classe principale via MonCompanion.

Pour appeler la méthode de création d’un User, vous devez utiliser le code suivant :

User.Companion.create("myLogin")

Comme c’est un peu long et que Kotlin pense aux développeurs, vous pouvez utiliser le raccourci suivant :

User.create("myLogin")

Vous trouverez plus d’informations sur les companion sur cette page

Exercice 3

Après autant de théorie, repassons à la pratique. Nous allons créer notre premier companion dans la classe EventController.

Afin de commancer l’exercice suivant, faites un checkout du step1.

git checkout step1

Ce compagnon aura la charge à la fois de remplacer le filtre utilisé dans la méthode get, mais également de rendre les données plus "jolie".

  • Ajouter le companion Letter à la classe EventController

  • Ajouter une méthode nommée prettier au companion

    • la méthode prendra en paramètre une liste d'`Event` et retournera le type Any

    • la méthode permettra de filter les événements sans date

  • Remplacer le filtre actuellement dans la méthode get`par votre nouveau `companion

Pour vous aider, il y a des tests unitaires sur step1.

Un peu de théorie

Paramètre nommé

Contrairement à Java, Kotlin fourni le support des paramètres nommés. Ceux-ci nous permettent notamment d’être explicites sur le nommage des arguments lorsqu’ils sont passés à une fonction. Cela à l’avantage d’expliciter les appels notament lorsqu’il y a de nombreux arguments.

Dans l’exemple suivant, il n’est pas aisé de savoir à quoi correspond chaque valeur passé en paramètre.

val string = "Kotlin is a great language"
string.endsWith("Great Language", true)

Avec les paramètres nommés cela devient beaucoup plus lisible.

val string = "Kotlin is a great language"
string.endsWith(suffix = "Great Language", ignoreCase = true)

Un autre bénéfice des paramètres nommés est la possibilité pour le code appelant la fonction de réorganiser l’ordre des paramètres. Le code suivant est toujours valide.

val string = "Kotlin is a great language"
string.endsWith(ignoreCase = true, suffix = "Great Language")
Les paramètres nommés ne peuvent être utilisés que sur des fonctions définies par Kotlin et non sur des fonctions définies par Java. Cela est dû au fait que le code Java, lorsqu’il est compilé en bytecode, ne conserve pas toujours les noms des paramètres.

Vous trouverez plus d’informations sur les paramètre nommé sur cette page

Paramètre par défaut

Il est souvent utile de définir des paramètres par défaut pour nos méthodes. En Java, cela se traduit par l’overloading. On a alors un code au format suivant

public void myFunction(String string, boolean ignoreCase) {
    // do stuff
}

public void myFunction(String string) {
    myFunction(string, false);
}

Kotlin fournit une alternative très intéressante. Il est possible dans la définition d’une fonction de préciser des valeurs par défaut. L’équivalent du code Java précédent serait le suivant en Kotlin.

fun myFunction(string: String, ignoreCase: Boolean = false): Unit {
    // do stuff
}

On peut alors appeler notre code de la façon suivante.

myFunction("call without default parameter", true)
myFunction("call with default parameter")

Vous trouverez plus d’informations sur les paramètres par défaut sur cette page

Copy

Comme promis au début de ce codelab, nous allons aborder rapidement la méthode copy des Data class. Pour rappel, lorsque l’on déclare une Data class, on obtient une méthode de copie prête à l’emploi. Cette méthode vous permet de créer une nouvelle instance de votre type tout en sélectionnant les champs que vous souhaitez modifier. Par exemple, vous pouvez décider que vous souhaitez obtenir une nouvelle instance d’une classe Event à partir d’une instance existante dont vous souhaitez simplement modifier les champs de date et de lieu.

event.copy(location = "Tours", date = "2018-02-23")

String templates

En tant que développeurs Java, nous sommes familiers avec l’utilisation de la concaténation de chaînes de caractères. Si l’on garde la pattern appris en Java cela donnerait le code suivant en Kotlin.

val name = "TouraineTech"
print("Hello " + name)

Les String templates sont un moyen simple et efficace d’incorporer des valeurs, des variables ou même des expressions dans une chaîne sans avoir besoin d’utiliser la concaténation précédente.

Les String templates améliorent l’expérience du développeur lors de l’utilisation de plusieurs variables dans un seul littéral. En effet, ils conservent la chaîne plus courte et plus lisible. L’utilisation est extrêmement simple. Une valeur ou une variable peut être intégrée simplement par préfixer avec un symbole dollar ($):

val name = "TouraineTech"
print("hello $name")

Là où ça devient intéressant pour le templating, c’est qu’il existe plusieurs format de String. Pour le moment nous avons vu le String correspondant à celui en Java avec la forme suivante val name = "TouraineTech". Il existe également le raw String. Il se déclare de la façon suivante.

val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
    |(Benjamin Franklin)
    """.trimMargin()
Par défaut le "|" est utilisé comme préfixe indiquant la position de la marge gauche du raw string. On peut néanmois choisir un autre caractère et le passer en paramètre de la méthode trimMargin. Par exemple, trimMargin(">").

Vous trouverez plus d’informations sur les String template sur cette page

Exercice 4

Dans l’exercice 3, nous avons créé notre companion avec une méthode nommé prettier. Nous allons maintenant enrichir cette méthode pour transformer les dates du format chiffre vers le format lettres.

  • Ajouter un tableau de valeurs contenant les chaines de caractères pour les chiffres 0 à 9 en anglais

    • Pour 1, on utilisera "one"

    • …​

  • Dans votre méthode prettier, pour chaque date, remplacer chaque chiffre par son équivalent en chaîne de charactères

Pour vous aider, il y a des tests unitaires sur step1.

Corrigé

Nous vous invitons à regarder notre solution pour la comparer avec la votre. Notamment la contruction de la chaîne de caractères.

Gestion de la nullité et initialisation tardive

Adrien Pessu Gautier de Saint Martin Lacaze

Un peu de théorie

La mort de l’opérateur ternaire

Bon ok, c’est peut être un peu pour le buzz.

Pour le moment, on n’a pas encore parlé des conditions. Les fameux if. Tout comme en Java il est possible d’utiliser le if en tant que gestionnaire du flux d’éxécution (désolé, nous n’avons pas trouvé mieux comme traduction française de flow control).

val a = 10
val b = 11

if (a < b) {
    print ("Youhou a est inférieur à b")
}

// Version avec un else
if (a < b) {
    print ("Youhou a est inférieur à b")
} else {
    print ("Youhou b est inférieur à a")
}

Il existe une autre façon d’utiliser le if et c’est pourquoi on peut parle plus ou mois de la fin de l’opérateur ternaire (le fameux = ? :). En Kotlin, if est une expression. Il est donc possible de s’en servir pour remplacer l’opérateur ternaire.

val max = if (a > b) a else b

On peut même imaginer plus complexe avec l’exemple suivant.

val max = if (a > b) {
    print("Youhou a est le plus grand")
    a
} else {
    print("Youhou b est le plus grand")
    b
}

Vous trouverez plus d’informations sur les String template sur cette page

Gestion du null

Le système de type de Kotlin vise à éliminer le danger des références nulles à partir du code. Ce problème est également connu sous le nom de The Billion Dollar Mistake. Bien entendu on parle de la fameuse NullPointerException de Java (aka NPE).

Pour régler ce problème, Kotlin ne permet pas par défaut d’avoir des références nulles. Le code suivant est donc invalide.

var kotlin: String = "Kotlin for te win !"
kotlin = null // Le code ne compile pas

Il est possible de déclarer une variable nulle. vDans ce cas nous utiliserons une précision syntaxique pour indiquer que l’on autorise le null.

var java: String? = "Java for the NPE"
java = null // Dans ce cas le code compile
Notez bien l’utilisation du ? dans la déclaration de la variable java.

A part la déclaration explicite d’une variable nulle, il existe d’autres cas pouvant générer une NPE :

  • un appel à throw NullPointerException()

  • l’utilisation de l’opérateur !! (non présenté dans cette partie cf. le site officiel de Kotlin)

  • un problème de données inconsistantes lors de l’initialisation

  • l’intéraction avec du code Java

Alors qu’est ce qui change entre nos deux morceaux de code précédent.

Pour le premier, c’est simple l’appel suivant sera valide.

val length = kotlin.length

Pour le second, ce n’est pas le cas.

val length = java.length // Erreur de compilation

Le compilateur va nous forcer à gérer les cas de la nullité. Et clairement c’est un gros progrès par rapport au compilateur Java. Pour la gestion de la nullité, on peut alors utiliser le if else traditionnel, ou bien le safe call operator ?..

val length: Int? = java?.length // Ici ça compile

La valeur de length sera soit la taille de java si java est valorisé, ou alors null.

On peut remarquer que le type de length sera alors Int? et non pas Int

Là où cela devient très intéressant c’est que l’on peut chaîner les opérateurs ?..

val profileLabel = user?.profile?.label

Dans le cas précédent, profileLabel sera null si user est null ou si profile est null.

Vous trouverez plus d’informations sur les String template sur cette page

Exercice 5

Afin de commancer l’exercice suivant, faites un checkout du step2.

git checkout step2

Reprenons notre projet et enrichissons un peu notre code. Nous allons maintenant permettre à notre endpoint d’API de gérer deux cas d’affichage. Le premier sera l’affichage sans transformation de la date des événements, le second sera celui que l’on a mis en place dans l’exercice précédent.

  • Ajouter un paramètre optionnel dans la méthode get de EventController

  • En fonction du paramètre d’entrée, utiliser ou non le companion

    • on vous laisse choisir les valeurs permettant ou non d’utiliser le companion

Pour vous aider, il y a des tests unitaires sur step2.

Un peu de théorie

Depuis la version 1.2 de Kotlin il est possible d’initialiser plus tard une val dans le code. Pour cela il faut utiliser le modifier lateinit. Cela est notamment utile lors de l’utilisation d’une lambda dans un constructeur.

// A cycle of three nodes:
lateinit var third: Node<Int>

val second = Node(2, next = { third })
val first = Node(1, next = { second })

third = Node(3, next = { first })

Pour éviter les problèmes lors de l’utilisation des variables déclarée avec le modifier lateinit, il est possible d’utiliser la méthode isInitialized

println("isInitialized before assignment: " + this::lateinitVar.isInitialized)
lateinitVar = "value"
println("isInitialized after assignment: " + this::lateinitVar.isInitialized)
Le modifier lateinit est régulièrement utilisé dans les projets Spring. L’injection de dépendance pourra toujours être effectuée sans problème et le code compilera.

Vous trouverez plus d’informations sur le lateinit sur cette page.

Exercice 6

Ici nous n’allons pas coder, mais simplement découvrir l’utilisation du lateinit dans le cadre d’un projet Spring.

Regarder le code de la classe EventController.

Flow control

Adrien Pessu Gautier de Saint Martin Lacaze

Une peu de théorie

Pour le moment nous avons vu les rapidement les éléments suivants de Kotlin :

  • val et var

  • class et data class

  • méthode statique

  • companion

  • paramètre nommé

  • paramètre par défaut

  • copy

  • string templates

  • if / else

  • gestion de la nullité avec l’opérateur Elvis

Nous allons passer maintenant à deux autres éléments nécessaire pour écrire la majorité des algorithmes d’un programme.

range

Les range sont des expressions permettant de créer rapidement une plage de valeur très rapidement. Pour les utiliser, on peut utiliser la méthode rangeTo.

1.rangeTo(10)

Il existe une écrire plus courte via l’opérateur ...

1..10
Il est possible d’utiliser également les range sur des lettres. Par exemple val aToZ = "a".."z"

On peut tester la présence d’un élément dans une plage de valeur de la manière suivante.

if (i in 1..10) { // équivalent à 1 <= i && i <= 10. Avouez que c'est plus agréable à lire non ?
    println(i)
}

Vous trouverez plus d’informations sur les range sur cette page.

When

L’expression when est un équivalent du switch présent dans de nombreux langages de programmation. C’est également une expression fonctionnelle permettant de faire du pattern matching.

Il existe deux façon d’utiliser le when. Soit il prend un argument. Soit on ne met pas d’argument. Dans ce second cas, le when permet de remplacer facilement une série de if / else.

L’une des écritures les plus simple du when est la suivante :

fun whatNumber(x: Int) {
    when (x) {
        0 -> println("x is zero")
        1 -> println("x is 1")
        else -> {
            println("X is neither 0 or 1")
        }
    }
}
Dans ce premier cas, on prend un paramètre en entrée. Le when permet alors de vérifier si l’on "matche" une des possibilités du when.

L’autre façon d’écrire le when est de ne pas passer d’arguments. Dans ce second cas, la syntaxe est la suivante.

fun whichOneIsBigger(x: Int, y: Int) {
    when {
        x < y -> println("x is less than y")
        x > y -> println("X is greater than y")
        else -> println("X must equal y")
    }
}

A plusieurs moment, on a indiqué que when était une expression. Celle-ci est donc assignable à une variable. La syntaxe suivante est valide.

fun startsWith(any: Any, prefix: String): Boolean {
    val result = when (any) {
        is String -> any.startsWith(prefix)
        else -> false
    }
    return result
}

On peut même simplifier le code précédent.

fun startsWith(any: Any, prefix: String): Boolean {
    return when (any) {
        is String -> any.startsWith(prefix)
        else -> false
    }
}

Vous trouverez plus d’informations sur l’expression when sur cette page.

Exercice 7

git checkout step3

Dans l’exercice 4, nous avons remplacé chaque chiffre de la date par son équivalent en chaîne de charactères. Comme vous pouvez le remarquer ce n’est pas comme cela que l’on doit lire une date.

Nous allons donc modifier notre companion pour gérer les cas des milliers, des centaines et des dizaines.

Pour vous aider, il y a des tests unitaires sur step3.

Pour aller plus loin

Adrien Pessu Gautier de Saint Martin Lacaze

Exercice 8

Isoler la partir conversion de chiffre en lettre et déclarer ces instructions dans une fonction en tant qu’Extension de la classe String.

Exercice 9

Passer la fonction de conversion de chiffre en lettre en tant que paramètre de la fonction de la coroutine.

Smart casts (pas besoin de recaster si tu fais un is)
Interface
function (single expression + member function)
function literals