Présentations

Kotlin, les bases

Logo de Kotlin
  • Qui suis-je.
  • Qui êtes-vous ? Quelles sont vos attentes.
  • Présentation du plan.

Qui suis-je

  • Développeur d'applications mobiles Android.
  • Formateur Android/Java et Kotlin, et Javacard (scolaires et pros).
  • Co-fondateur (1/7) de Startup Marseille.
    Logo Startup Marseille
  • Fondateur de Light4Events.
    Logo Light4events

Qui êtes vous ?

Tour de table :
  • Prénom.
  • Expérience (Java, Kotlin, Web, Mobile Android/iOS).
  • Attentes.

Kotlin, les bases

Introduction

Pourquoi le Kotlin ?

  • Kotlin et Java sont 100 % interopérables.
  • 2010 : idée (JetBrains).
  • 2015 : naissance, disponible sur GitHub.
  • Origine du nom : l'île de Kotlin.
  • Concis, sûr et pragmatique.
  • Statiquement typé, mais inférence de types.
  • C'est aussi un langage fonctionnel.

Dates importantes

  • 2010 : Lancement du projet Kotlin.
  • Juillet 2011 : communication officielle de JetBrain sur le projet Kotlin.
  • Février 2012 : mise à disposition du projet en open source.
  • 15 Février 2016 : Kotlin 1.0. Première version stable.
  • 28 Novembre 2017 : Kotlin 1.2. Partage du code entre la JVM et la plate-forme Javascript.
  • 29 Octobre 2018 : Kotlin 1.3. Coroutines.
  • 7 Mai 2019 : Google annonce que Kotlin est le langage préféré pour développer des applications Android.
  • 17 Août 2020 : Kotlin 1.4. Mix des paramètres nommés et non nommés, virgule de fin, ...
  • 22 Mars 2021 : Dernière version de Kotlin : 1.4.32.

Inférence de type


val number = 123
val message = "Hello world !"
fun sayHello() = "Hello world !"
					

Avantages

  • Performance.
  • Fiabilité.
  • Efficacité.

Langage fonctionnel

  • First-class function.
  • Immuabilité.
  • Aucun effet de bord.

Introduction à la JVM (Java Virtual Machine)

Schéma du compilateur Java

Lancer notre premier script

Nous avons différentes solutions pour lancer du code Kotlin :

En ligne de commande

Récupérons le compilateur sur GitHub, plus bas dans la catégorie assets, par exemple : kotlin-compiler-1.4.21.zip.
Décompressions le fichier.
Nous pouvons ajouter le répertoire /bin dans le path pour que cela soit plus pratique.
Écrivons maintenant notre premier programme : hello.kt :
notepad hello.kt

fun main() {
    println("Hello, World!")
}
Compilons notre code :

kotlinc hello.kt -include-runtime -d hello.jar
Nous pouvons le lancer maintenant :

java -jar hello.jar

En ligne de commande

Nous pouvons aussi récupérer le compilateur natif windows, toujours sur GitHub, par exemple kotlin-native-windows-1.4.21.zip.
Nous pouvons ajouter le répertoire /bin dans le path pour que cela soit plus pratique.
Reprenons notre premier programme : hello.kt :
notepad hello.kt

fun main() {
    println("Hello, World!")
}
Compilons notre code :

kotlinc hello.kt
Nous pouvons le lancer un executable maintenant :

program.exe
Le premier lancement est long, mais les suivants sont rapides.

En ligne de commande : REPL

Nous allons maintenant tester REPL (Read–Eval–Print Loop) qui permet de tester du code facilement.
Pour cela lançons la commande :kotlinc. Testons quelques commandes, par exemple :

40+2
"Hello, World!"
println("Hello, World!")
1
1.0

En ligne

Plus pratique : nous pouvons utiliser le compilateur en ligne à l'adresse :
https://play.kotlinlang.org
Nombreux exercices en ligne : https://try.kotlinlang.org.
Pour aller plus loin, vous pouvez utiliser Gradle pour compiler votre projet Kotlin.

La structure d'une application Kotlin

Les répertoires

La structure des répertoires suit la structure des packages.
Le package racine sera ignoré, par exemple :
si le projet est dans le package org.example.kotlin, alors les fichiers seront placés directement dans le répertoire racine contenant les sources.
Les fichiers dans le package org.example.kotlin.network.socket seront placés dans le sous répertoire : network/socket.

Les fichiers

Un fichier ne contenant qu'une seule classe, sera nommé du nom de celle-ci (en utilisant le camel case), suivit de l'extention .kt.
Si le fichier contient plusieurs classes, ou seulement des déclarations top niveau (top level declarations), alors, choisir un nom qui correspond le mieux au contenu du fichier.

Dans le fichier source

Généralement le contenu d'une classe est organisé de la manière suivante :
  • Déclaration des propriétés et des blocs d'initialiseurs.
  • Les constructeurs secondaires.
  • Les déclarations des méthodes.
  • L'objet compagnon.


Regrouper les méthodes (classiques et d'extention) ensembles.
Gardez une organisation cohérente sur tout le projet.
L'implémentation d'une interface gardera l'ordre de déclaration dans celle-ci.

Kotlin et IntelliJ IDEA

Installation

Commençons par installer InteliJ : Télécharger.
Optons pour la version Community .

Création du projet

Il est temps de créer notre première application : File / New / Project. Sélectionner Kotlin / JVM | IDEA.
Nouvelle version ici.
Création projet

Nommage

Nommons notre projet, par exemple HelloWorld :
Nommage du projet
Nous devrions obtenir le résultat suivant :
Projet vide
Créons un nouveau fichier dans le répertoire source. Click droit, New / Kotlin File/Class :
Création fichier
Nommons le app :
Nommage du fichier
Ajoutons la fonction principale main :
Taper main, puis la touche TAB, pour lancer l'autocomplétion.
Il reste juste à compléter le corps de la méthode pour obtenir le résultat suivant :

fun main() {
    println("Bonjour le monde")
}
Nommons notre projet, par exemple HelloWorld :
Création du projet
Gardons les paramètres par défaut :
Paramètres du projet
Nous obtenons :
Projet créé

Exécuter le code

Il y a plusieurs façons de lancer le code, la plus rapide est de cliquer sur le bouton vert Run :
Lancer le projet

Exécuter le code

Ou encore :
Lancer le projet

Exécuter le code

Ou bien :
Lancer le projet

Exécuter le code

Ou bien encore :
Lancer le projet

Exécuter le code

Ce qui permet d'avoir un nouveau raccourcit :
Lancer le projet

Exécuter le code

Ce qui nous donnera le résultat :
Résultat
Nous pouvons aussi directement lancer notre code dans :
  • Des Scratches : File | New | Scratch file et sélectionner le type Kotlin.
  • Une fenêtre REPL : Tools | Kotlin | Kotlin REPL (Control + Return pour valider).

Le scratch

Commençons par ouvrir un espace pour faire nos tests : un scratch.
Au choix :
  • Menu : File | New | Scratch file.
  • Ctrl + Alt + Shift + Inser.
  • Sur Android Studio : Click droit sur app dans notre projet (par exemple), puis : New/Scratch File.
New scratch
Choisissons le type de fichier : Kotlin :
New scratch
Pour obtenir le résultat suivant :
New scratch
Référence vers les chaînes de caractère.

Les conventions utilisées avec Kotlin

Règles de nommage

Elles sont identiques à celles en Java (nom des packages et des méthodes). A une différence prête : le nom des méthodes de fabrique est identique à celui de la classe :

abstract class Foo { ... }

class FooImpl : Foo { ... }

fun Foo(): Foo { return FooImpl(...) }
					

Règles de nommage des méthodes de test

On peut utiliser des noms avec des espaces entourés de `. (Note : cela ne fonctionnera pas en Android).
Les _ sont aussi autorisés :

class MyTestCase {
     @Test fun `ensure everything works`() { ... }

     @Test fun ensureEverythingWorks_onAndroid() { ... }
}					

Règles des espaces

Quelque unes des règles, qui sont nombreuses ; on utilise un espace :
  • Pour indenter (4 espaces).
  • Pour séparer un mot clé et une "(" (par exemple if, for ou catch.
  • Pour séparer un mot clé et une "{" (par exemple else.
  • Avant toute "{".
  • Avant et après tout opérateur binaire.
  • Avant et après la flèche ->.
  • Avant et après l'opérateur d'intervalle ...
  • Après une virgule ,.
  • Avant et après le signe de commentaire ligne simple //.

Nommage des propriétés

Les constantes (propriétés marquées avec un const, les propriétés top level ou propriétés val d'un objet sans fonction get custom, qui contient une valeur profondément immuable), doit utiliser des majuscules, et _ comme séparateur :

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"
Les fonctions top level ou les propriétés d'un objet dont les valeurs peuvent évoluer, utiliserons la notation camel-case :

val mutableCollection: MutableSet<String> = HashSet()
Le nom des propriétés qui contiennent une référence à un objet Singleton, peuvent utiliser la même rêgle de nommage :

val PersonComparator: Comparator<Person> = /*...*/
Pour les valeurs des enums, il est possible d'utiliser les majuscules séparés par des _, ou l'écriture camel-case, en commençant par une majuscule, selon l'usage.

Nommage des propriétés internes

Si une classe comporte deux propriétés qui conceptuellement parlant, représentent la même chose, mais une fait partis de l'API et l'autre est un détail de l'implémentation, dans ce cas, vous pouvez préfixer d'un _ la propriété privée :

class C {
    private val _elementList = mutableListOf<Element>()

    val elementList: List<Element>
         get() = _elementList
}

Choisir le bon nom

Le nom d'une classe est souvent un nom, qui définit la nature de la classe : List, PersonReader.
Le nom des méthodes est plus souvent un verbe, ou une phrase, décrivant son action : close, readPersons. Le nom doit aussi faire comprendre s'il modifie l'objet ou s'il en retourne un nouveau. Par exemple : sort modifie la collection, alors que sorted retournera une copie de la collection triée.

Les noms doivent être clairs, sur leur fonctionnement, ils doivent donc éviter de contenir des noms génériques tels que Manager, Wrapper, etc.

Quand vous utilisez des acronymes dans un nom, mettre en majuscules si sa taille est de 2 lettres (IOStream), mais ne mettez que la première lettre en majuscule s'il est plus long (XmlFormatter, HttpInputStream).

Ordre des modifieurs


public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation
companion
inline
infix
operator
data
					
Placer les annotations avant les modifieurs :

private val foo: Foo
					

typealias

Bases de Kotlin

Déclaration de variables en Kotlin

  • val : valeur
  • var : variable

val message = "Hello world !"
val message: String = "Hello world !"
var message: String = "Hello world !"
					
Le code :

val name: String = "Tristan"
val age: Int = 41
val isDeveloper: Boolean = true
					
Est équivalent à :

val name = "Tristan"
val age = 41
val isDeveloper = true
					
Une valeur peut être assignée dans le même bloc que sa déclaration :

import kotlin.random.Random

fun isUserHappy() = Random.nextInt(0, 100) % 2 == 0
fun main() {
    val message: String
    if (isUserHappy())
        message = "That's great"
    else
        message = "What's going on?"

    println(message)
}
					

Null safety

En Kotlin, c'est à nous de préciser qu'une variable peut prendre une valeur nulle.
Le code suivant, ne compilera pas.

var name: String = "Tristan"
name = null
					
Alors que le code suivant est correct.
Nous précisons que la variable peut prendre une valeur nulle avec le ?.

var name: String? = "Tristan"
name = null
					

Null safety (suite)

Pour utiliser une variable possiblement nulle nous ne pouvons pas l'utiliser simplement.
L'exemple suivant ne compilera pas :

var name: String? = "Tristan"
name.toUpperCase()
					
Il faut donc prendre une précaution en utilisant cette variable, en utilisant ? :

var name: String? = "Tristan"
name?.toUpperCase()
					
Si la valeur de la variable contient null, alors la méthode ne sera pas appelée.

Utilisation d'une variable dans une chaîne de caractère

Nous pouvons faire référence à une variable avec le symbole $, par exemple :

val name = "Tristan"
println("Hello $name")
					
Référence vers les chaînes de caractère.

Les constantes

Le mot clé static n'existe pas en Kotlin, donc pour déclarer une constante on utilise le mot clé const.
Le code Java suivant :

public static final String SERVER_URL = "http://my.api.com/";
					
Deviendra en Kotlin :

const val SERVER_URL = "http://my.api.com/"
					

Utilisation de variables "Basic Types" en Kotlin

Plusieurs types basiques sont disponibles en Kotlin :

Nombres

Type Size (bits) Min value Max value
Byte 8 -128 127
Short 16 -32768 32767
Int 32 -2,147,483,648 (-231) 2,147,483,647 (231 - 1)
Long 64 -9,223,372,036,854,775,808 (-263) 9,223,372,036,854,775,807 (263 - 1)

val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1
					

Nombres à virgule

Type Size (bits) Significant bits Exponent bits Decimal digits
Float 32 24 8 6-7
Double 64 53 11 15-16

val pi = 3.14 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817
					

Conversion automatique des types.


fun main() {
    fun printDouble(d: Double) { println(d) }

    val i = 1
    val d = 1.1
    val f = 1.1f

    printDouble(d)
//    printDouble(i) // Error: Type mismatch
//    printDouble(f) // Error: Type mismatch
}
					

Conversion explicite.

Pour convertir les types numériques, il faudra utiliser une des méthodes suivantes :
  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double
  • toChar(): Char

Exercice

Exercice : modifiez l'exemple, pour que l'on puisse afficher sa valeur, avec la fonction printDouble (sans toucher au code de la fonction).

fun main() {
    fun printDouble(d: Double) { println(d) }

    val i = 1
    val d = 1.1
    val f = 1.1f

    printDouble(d)
//    printDouble(i) // Error: Type mismatch
//    printDouble(f) // Error: Type mismatch
}
	

Solution


fun main() {
    fun printDouble(d: Double) { println(d) }

    val i = 1
    val d = 1.1
    val f = 1.1f

    printDouble(d)
    printDouble(i.toDouble())
    printDouble(f.toDouble())
}
	

Aller plus loin sur les numériques

Les valeurs :
  • Décimaux : 123
  • Long (marqués avec un L) : 123L
  • Hexadécimaux : 0x0F
  • Binaires : 0b10001001
  • Doubles par défaut : 123.5, 123.5e10
  • Floats (marqués avec un f ou F) : 123.5f
Le caractère "souligné" peut être utilisé pour améliorer la visibilité (depuis la version 1.1).

                val oneMillion = 1_000_000
                val creditCardNumber = 1234_5678_9012_3456L
                val socialSecurityNumber = 999_99_9999L
                val hexBytes = 0xFF_EC_DE_5E
                val bytes = 0b11010010_01101001_10010100_10010010
                    

Opérations

Caractères

Les caractères sont représentés par le type Char

fun check(c: Char) {
    if (c == 1) { // ERROR: incompatible types
        // ...
    }
}
					

Pour écrire un caractère, on utilise la simple "quote" '.
Exemple : 't'.
Les caractères spéciaux sont préfixés par \.
Exemple : \t, \b, \n, \r, \', \", \\ and \$.
Pour les autres caractères, on peut utiliser la syntaxe Unicode : '\uFF00'.

Le charactère peut être convertis explicitement :

fun decimalDigitValue(c: Char): Int {
    if (c !in '0'..'9')
        throw IllegalArgumentException("Out of range")
    return c.toInt() - '0'.toInt() // Explicit conversions to numbers
}
					

Les booléens

Les booléens sont représentés par le type Boolean qui peut prendre 2 valeurs : true et false.
Les opérations sur les booléens sont les suivantes :
  • || – opération logique OU
  • && – opération logique ET
  • ! - négation

Les tableaux

Les tableaux sont représentés par la classe Array qui dispose des méthodes get et set
(qui se transforment en [] grâce à la convention de surcharge des opérateurs),
et de la propriété size, entre autres.

class Array<T> private constructor() {
    val size: Int
    operator fun get(index: Int): T
    operator fun set(index: Int, value: T): Unit

    operator fun iterator(): Iterator<T>
        // ...
}
					
La méthode arrayOf(1, 2, 3) permet de créer des tableaux, arrayOfNulls() va créer un tableau de valeurs nulles.
Le constructeur Array prend en paramètre le nombre d'éléments, et la fonction pour remplir le tableau.

var array = arrayOf(1, 2, 3)
array.forEach { println(it) }
var arrayOfNull: Array<Int?> = arrayOfNulls(5)
arrayOfNull.forEach { println(it) }
val asc = Array(5) { i -> (i * i).toString() }
asc.forEach { println(it) }
					
L'opérateur [] est équivalent à l'appel des méthodes get() et set().

var array = arrayOf(1, 2, 3)
println(array[2])
array[0] = 4
array.forEach { println(it) }
					

Exercice

Déclarez un tableau contenant les chiffres pair de 0 à 20.

val oddArray = TODO()
	

Solution


fun main() {
    val oddArray = Array(11) { i -> (i * 2) }

    for(i in oddArray) {
        print("$i ")
    }
}
	

Exercice

Déclarez un tableau contenant toutes les lettres de l'alphabet.

val alphabetArray = TODO()
	

Solution


fun main() {
    val alphabetArray = Array(26) { i -> (i+'a'.toInt()).toChar() }

    for (i in alphabetArray) {
        print("$i ")
    }
}
	

Aller plus loin sur les tableaux

Kotlin propose des tableaux spécifiques contenant des types primitifs : ByteArray, ShortArray, IntArray, etc, sans la couche d'encapsulation. Ces classes n'ont aucune relation avec la classe Array, mais disposent des mêmes méthodes et propriétés.

val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]

// Tableau d'int de taille 5 contenant les valeurs [0, 0, 0, 0, 0]
val arr = IntArray(5)

// Exemple : initialisation des valeurs contenues dans le tableau avec une constante
// Tableau d'int de taille 5 contenant les valeurs [42, 42, 42, 42, 42]
val arr = IntArray(5) { 42 }

// Exemple : initialisation des valeurs contenues dans le tableau en utilisant une lambda (fonction)
// Tableau d'int de taille 5 contenant les valeurs [0, 1, 2, 3, 4] (valeurs égales à leur position dans le tableau)
var arr = IntArray(5, { it * 1 })
					

Les chaînes de caractères (String)

Les tableaux sont représentés par la classe String qui sont immutable. Les éléments d'une chaîne de caractère peuvent être accessibles avec l'opérateur [].
Il est possible de concaténer des chaînes avec l'opérateur +, même si l'on préférera les templates ($ dans les chaînes de caractères).

val s = "abc" + 1
println(s + "def")
					

La valeur d'une chaîne de caractère.

Kotlin dispose de deux méthodes pour définir les valeurs des chaînes de caractères :
  • avec échappement (avec ")
  • brutes (avec """)

val stringWithEscape = "Hello, world!\n"
println(stringWithEscape)

val stringRaw = """
    for (c in "foo")
        print(c)
"""
println(stringRaw)
                    
Il est possible de retirer les espaces au début des lignes, avec la méthode .trimMargin() qui utilise le symbole | par défaut.

val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
    |(Benjamin Franklin)
    """.trimMargin()
println(text)
					

Modèles (String templates)

Le chaînes de caractère peuvent contenir des expressions (des morceaux de code), qui sont préfixés par $.

val i = 10
println("i = $i") // affiche "i = 10"
					
Il est possible d'inclure une expression entre ${}, et d'afficher le symbole $ lui-même. Cela fonctionne aussi dans une chaîne brute (raw string).

println("$name.length is ${name.length}")
println("price : ${'$'}9.99")

val price = """
${'$'}9.99
"""
					

Exercice

Définir la fonction hello qui doit retourner : Hello, <NAME>

fun hello(name: String): String = TODO()

Solution


fun main() {
    fun hello(name: String): String = "Hello, ${name.toUpperCase()}"
    print(hello("Tristan"))
}

Les types "spéciaux"

Kotlin comporte deux types "spéciaux" :
  • Any qui correspond à Object en Java (n'importe quel type d'objet).
  • Unit qui correspond à void en Java. Autrement dit : rien.

Les commentaires

Il existe deux types de commentaires, comme en Java :

/*
Les commentaires sur plusieurs
lignes, pour pouvoir bien s'exprimer.
Vous pouvez écrire autant que vous voulez.
*/
// ceci est un commentaire uniligne.

Structures conditionnelles If et When

L'expression if

En Kotlin if est une expression : il retourne une valeur. De ce fait l'opérateur ternaire ? n'existe plus.

// Usage traditionnel
var max = a
if (a < b) max = b

// Avec else
var max: Int
if (a > b) {
    max = a
} else {
    max = b
}

// Comme une expression
val max = if (a > b) a else b

L'expression if (suite)

Les branches du if, peuvent être des blocs, dont la dernière expression est la valeur du bloc :

val max = if (a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}
Quand le if est utilisé comme expression (pour retourner une valeur ou pour assigner une valeur), l'expression doit obligatoirement comporter la branche else.

Selon le cas (when)

Le when est l'équivalent du switch en Java.

var apiReponse = 404
when (apiReponse) {
    200 -> "OK"
    404 -> "NOT FOUND"
    401 -> "UNAUTHORIZED"
    403 -> "FORBIDDEN"
    else -> "UNKNOWN"
}
Quand le when est utilisé comme expression (pour retourner une valeur ou pour assigner une valeur), l'expression doit obligatoirement comporter la branche else, sauf si tous les cas sont couverts (d'un enum par exemple).

Selon le cas (when) (aller plus loin)

Le when peut aussi être utilisé comme une expression

var apiReponse = 404
fun printResponse(apiReponse: Int) = when (apiReponse) {
    200 -> print("OK")
    404 -> print("NOT FOUND")
    401 -> print("UNAUTHORIZED")
    403 -> print("FORBIDDEN")
    else -> print("UNKNOWN")
}
printResponse(apiReponse)

Selon le cas (when) (aller plus loin)

Le when avoir plusieurs conditions pour une même branche, séparées par une virgule :

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}
Les conditions peuvent être des expressions :

when (x) {
    parseInt(s) -> print("s encodes x")
    else -> print("s does not encode x")
}
Ou encore des intervalles :

when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}
On peut aussi tester le type du paramètre :

fun hasPrefix(x: Any) = when(x) {
    is String -> x.startsWith("prefix")
    else -> false
}
On peut l'utiliser sans paramètre, les conditions sont alors obligatoirement des booléens,
et remplacent un if-else if :

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}
Depuis Kotlin 1.3 il est possible de capturer le paramètre du when dans une variable :

fun Request.getBody() =
        when (val response = executeRequest()) {
            is Success -> response.body
            is HttpError -> throw HttpException(response.status)
        }

Boucles et ranges en Kotlin

Tant que faire se peut (while)

En Kotlin, la syntaxe du while est exactement la même qu'en Java :

var isRaining = true
while (isRaining){
    println("I don't like rain.")
}

do {
    println("I don't like rain.")
} while (isRaining)
					

Boucle pour (for loop)

La boucle for, peut itérer sur tout ce qui fournit un itérateur, sa syntaxe est la suivante :

for (item in collection) print(item)
					
Par exemple sur une liste de chaînes de caractères, cela donne :

val names = listOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")

for(name in names) {
    println("This developer rocks : $name")
}
					

Les intervalles

Il est possible d'itérer sur des intervalles de valeur, qui sont définis avec la méthode rangeTo(), correspondant à l'opérateur ...
Cet opérateur est souvent complété par les fonctions in ou !in. Exemple :

if (i in 1..4) {  // equivalent à 1 <= i && i <= 4
    println(i)
}
					
Pour définir un intervalle, souvent utilisés dans les boucles for, la syntaxe est la suivante :

for (i in 1..4) println(i)
					
Pour utiliser l'ordre décroissant, la syntaxe sera :

for (i in 4 downTo 1) println(i)
					

Les intervalles (suite)

Il est possible d'itérer sur un des nombres dont l'intervalle n'est pas nécessairement 1,
en utilisant la méthode step :

for (i in 1..8 step 2) print(i)
println()
for (i in 8 downTo 1 step 2) print(i)
					
Pour ne pas inclure le dernier élément de la liste, utiliser la fonction until :

for (i in 1 until 10) {       // i in [1, 10), 10 is excluded
    print(i)
}
					

Boucle pour (for loop) (suite)

Il est possible d'itérer sur une liste en utilisant les indexes :

for (i in array.indices) {
    println(array[i])
}
					
Ou alors en utilisant le format suivant qui met en oeuvre la déstructuration (destructuring) :

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}
					

Break et continue

Ils fonctionnent de la même manière qu'en Java.

Exercice

Écrivez une boucle qui affiche séparément toutes les lettres d'une chaîne de caractères.

var stringValue = "Une chaîne de caractères"
	
Le résultat attendu est :
U n e   c h a î n e   d e   c a r a c t è r e s

Solution


var stringValue = "Une chaîne de caractères"
for (c in stringValue) {
    print("$c ")
}
	
Solution bis :

var stringValue = "Une chaîne de caractères"
stringValue.forEach { print("$it ")}
	
Solution ter :

var stringValue = "Une chaîne de caractères"
stringValue.toCharArray().joinToString(" ")
	
Référence joinToString.

Exercice

Afficher les nombres de 0 à 10, mais afficher Fizz si le nombre est divisible par 3, Buzz s'il est divisible par 5 et FizzBuzz s'il est divisible à la fois par 3 et par 5.

Solution


fun main(args: Array<String>) {
    for (x in 0 until 10) {
        when {
            (x % 3 == 0 && x % 5 == 0) -> println("FizzBuzz")
            (x % 3 == 0) -> println("Fizz")
            (x % 5 == 0) -> println("Buzz")
            else -> println(x)
        }
    }
}

Solution bis


fun main(args: Array<String>) {
    for (x in 0 until 10) {
        println( when {
            (x % 3 == 0 && x % 5 == 0) -> "FizzBuzz"
            (x % 3 == 0) -> "Fizz"
            (x % 5 == 0) -> "Buzz"
            else -> x
        })
    }
}

Collections en Kotlin

Kotlin propose plusieurs structures pour gérer des groupes d'objets en nombre variables (possiblement 0).
Si vous êtes familiers de ces concepts, passons à la suite. Sinon continuons ...

Une collection est une structure qui regroupe des objets de même type. Ces objets sont appelés des éléments, ou des items.

Il existe plusieurs types de collection :
  • Une liste (List), est une collection ordonnée d'objets auxquels nous pouvons accéder via leur position/index (un nombre entier). Un élément peut être présent une ou plusieurs fois dans la liste. Exemple d'une phrase qui comporte des mots, dont l'ordre est important, et qui peuvent se répéter.
  • Les ensembles (Set), est une collection d'éléments uniques. L'ordre dans un ensemble n'a pas d'importance. Par exemple les lettres de l'alphabet.
  • Les dictionnaires (Map), est un ensemble d'élements composés d'une paire (clé-valeur). Les clés ont des valeurs uniques qui désignent un seul objet de la collection. Les valeurs peuvent apparaître en plusieurs fois. Cette structure est employée pour stocker une connexion logique entre 2 objets, par exemple, un numéro d'employé et sa fiche descriptive.


Le comportement de chaque type de collection sera toujours le même, peu importe le type des objets stockés dans ces structures.
En Kotlin, il y a deux types principaux de collections :

  • En lecture seule (read-only/immutable).
  • En lecture/écriture (mutable) (ajout, retrait, modification des éléments).


Note : Une collection mutable peut être stockée dans une valeur (val) :

val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five")   // this is OK
//numbers = mutableListOf("six", "seven")      // compilation error
                    
Plusieurs exemples de collections List et Set :

val listOfNames = listOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")
listOfNames[0]
//listOfNames[0] = "Mathieu NEBRA" // Error: List is immutable

val mutableListOfNames = mutableListOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")
mutableListOfNames[0]
mutableListOfNames[0] = "Mathieu NEBRA" // OK

val setOfNames = setOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")
setOfNames.first()
//setOfNames.add("Mathieu NEBRA") // Error: Set is immutable

val mutableSetOfNames = mutableSetOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")
mutableSetOfNames.first()
mutableSetOfNames.add("Mathieu NEBRA") // OK
Exemple de tableau et de Map :

var arrayOfNames = arrayOf("Jake WHARTON", "Joe BIRCH", "Robert MARTIN")
var mapOfNames = mapOf(0 to "Jake WHARTON", 1 to "Joe BIRCH", 2 to "Robert MARTIN")
					
Logo de Kotlin

Packages et imports en Kotlin

Un ficher de code source peut commencer par la déclaration d'un package :

package org.example

fun printMessage() { /*...*/ }
class Message { /*...*/ }

// ...
					
Tout le contenu (tels que les classes et les fonctions) du fichier de code source appartiendront au package déclaré. Dans l'exemple ci-dessus, le nom complet de printMessage() est org.example.printMessage(), de la même manière, le nom complet de Message est org.example.Message.
Si le nom du package n'est pas précisé, le contenu du fichier appartient au package par défaut qui n'a pas de nom.

Imports par défaut

Kotlin importe par défaut les packages suivants :
  • kotlin.*
  • kotlin.annotation.*
  • kotlin.collections.*
  • kotlin.comparisons.* (depuis 1.1)
  • kotlin.io.*
  • kotlin.ranges.*
  • kotlin.sequences.*
  • kotlin.text.*

Imports par défaut (suite)

Selon la plate-forme cible, des packages supplémentaires sont importés :
  • JVM
    • java.lang.*
    • kotlin.jvm.*
  • JS
    • kotlin.js.*

Les bonnes pratiques

  • Ne pas utiliser le mot Utils pour nommer un fichier source, qui apporte peu d'informations sur son contenu.
  • Regrouper les classes qui ont un sens proche, dans un même fichier est recommandé, sous condition que le fichier ne soit pas trop gros (quelques centaines de lignes).
  • De même il est recommandé de regrouper les extensions correspondant aux même client, dans un unique fichier.
  • Utiliser des val ou des collections en lecture seule autant que possible.
  • Utiliser de préférence until au lieu d'un intervalle ouvert :
  • 
    for (i in 0..n - 1) { ... }  // bad
    for (i in 0 until n) { ... }  // good
    					
  • Utiliser les "string templates" au lieu de concaténations.

Les fonctions - Partie 1

Fonctions en Kotlin

Déclaration

On utilise le mot clé fun

fun double(x: Int): Int {
    return 2 * x
}
					

Utilisation

Appel traditionnel :

val result = double(2)
					
Appel d'une fonction membre d'un objet :

Stream().read() // créé une instance de la class Stream et appelle la fonction read()
					

Fonctions "expression seule"

Les fonctions "expression seule" (single-expression function)

Il n'est pas nécessaire d'utiliser les accolades, un simple = peut les remplacer :

fun double(x: Int): Int = x * 2
Il n'est même pas nécessaire de préciser le type de retour, qui est inféré par le compilateur :

fun double(x: Int) = x * 2

Paramètres des fonctions en Kotlin

Les paramètres simples

Chaque paramètre (explicitement typé), est séparé par une virgule :

fun powerOf(number: Int, exponent: Int) { /*...*/ }

Paramètres par défauts

Les paramètres peuvent avoir une valeur par défaut (exprimée avec =) quand un argument n'est pas précisé :

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { /*...*/ }

Paramètres par défauts : surcharge

Les fonctions surcharge utilisent la même valeur par défaut. Il ne faut pas répéter la valeur par défaut :

open class A {
    open fun foo(i: Int = 10) { /*...*/ }
}

class B : A() {
    override fun foo(i: Int) { /*...*/ }  // pas de valeur par défaut authorisée
}

Exercice

Écrivez une fonction qui prend en paramètre une chaîne de caractères, et retourne sa valeur en majuscule. Si aucune valeur n'est passée en paramètre, alors utiliser la valeur par défaut "default" :

fun upper(): String = TODO()
	

Solution


fun upper(str:String? = "default") = str?.toUpperCase()
	
Dans ce cas, si l'on précise une valeur null, c'est cette valeur qui est utilisée, et la méthode n'affichera rien.
Ou :

fun upper(str:String = "default") = str.toUpperCase()
	

Exercice

Écrivez la fonction add qui additionne 2 Int passés en paramètre :

fun add(a: Int, b: Int): Int = TODO()
	

Solution


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

Exercice

Écrivez les fonctions qui comparent 2 chaînes de caractères, en ignorant la casse ou pas.

fun strEq(s1: String, s2: String): Boolean = TODO()
fun strEq(s1: String, s2: String, ignoreCase: Boolean): Boolean = TODO()
	

Solution


fun strEq(s1: String, s2: String): Boolean = s1.equals(s2)
fun strEq(s1: String, s2: String, ignoreCase: Boolean): Boolean = if(ignoreCase) s1.toUpperCase().equals(s2.toUpperCase()) else s1.equals(s2)

fun main() {
    println(strEq("Tristan", "TRISTAN"))
    println(strEq("Tristan", "TRISTAN", true))
}
	
Solution bis :

fun strEq(s1: String, s2: String, ignoreCase: Boolean): Boolean = s.equals(p, ignoreCase)
	
Référence : equals.

Exercice

Écrivez la fonction qui affiche la liste des langages présents dans le tableau.

val languages = arrayOf("Java", "JavaScript", "Go", "Kotlin")
fun printLanguages(): Unit = TODO()
	

Solution


val languages = arrayOf("Java", "JavaScript", "Go", "Kotlin")
fun printLanguages(): Unit { for (language in languages) println(language) }

fun main() {
    printLanguages()
}
	
Solution bis :

fun printLanguages() { languages.forEach { println(it) } }
	

Exercice

Écrivez les fonctions suivantes :
  • tenFirstNumber() qui affiche les 10 premiers chiffres (0-9).
  • countdown() qui affiche les nombres de 10 à 0.
  • firstEvenNumbers() qui affiche les 10 premiers nombres pairs.
  • firstOddNumbers() qui affiche les 10 premiers nombres impairs.

fun tenFirstNumber(): Unit = TODO()
fun countdown(): Unit = TODO()
fun firstEvenNumbers(): Unit = TODO()
fun firstOddNumbers(): Unit = TODO()
	

Solution


fun tenFirstNumber(): Unit { for(i in 0 until 10) print("$i "); println()}
fun countdown() : Unit { for(i in 10 downTo 0) print("$i "); println()}
fun firstEvenNumbers(): Unit { for(i in 0 until 20 step 2) print("$i "); println()}
fun firstOddNumbers(): Unit { for(i in 1 .. 20 step 2) print("$i "); println()}

fun main() {
    tenFirstNumber()
    countdown()
    firstEvenNumbers()
    firstOddNumbers()
}
	

Paramètres nommés

Chaque paramètre peut être nommé explicitement. Cela est particulièrement pratique pour les fonctions qui ont beaucoup de paramètres, ou des paramètres par défauts :

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
/*...*/
}

Paramètres nommés utilisation

Nous pouvons utiliser les paramètres par défaut :

reformat(str)
Nous pouvons préciser tous les paramètres :

reformat(str, true, true, false, '_')
L'appel avec tous les paramètres nommés est bien plus clair :

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)

Paramètres par défauts en premier

Si le paramètre par défaut est en premier : il faudra appeler la fonction avec des paramètres nommés :

fun foo(bar: Int = 0, baz: Int) { /*...*/ }

foo(baz = 1) // La valeur par défaut bar = 0 est utilisée

Paramètres par défauts : lambda

Si le dernier paramètre est une lambda, elle peut être passée via un paramètre nommé, ou à l'extérieur des parenthèses :

fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /*...*/ }

foo(1) { println("hello") }     // Utilise la valeur par défaut baz = 1
foo(qux = { println("hello") }) // Utilise les valeurs par défaut bar = 0 et baz = 1
foo { println("hello") }        // Utilise les valeurs par défaut bar = 0 et baz = 1
Nous ne sommes pas obligé d'utiliser tous les paramètres :

reformat(str, wordSeparator = '_')
Il est possible de mixer les paramètres de position et les paramètres nommés :

f(1, y = 2)
Mais l'ordre est important : il est impossible d'utiliser un paramètre nommé puis un paramètre de position :

f(x = 1, 2)

Les paramètres variables

En Java 5 il est possible d'utiliser la notation ... pour définir un nombre variable de paramètres. En Kotlin, c'est le mot clé vararg qui est utilisé (pour le dernier paramètre généralement, autrement il faudra nommer les paramètres).

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}
Un seul paramètre peut être marqué variable.
Nous pouvons aussi utiliser le déstructuring (opérateur *), si nous avons déjà une variable contenant une liste.

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

Fonctions Infix en Kotlin

Les fonctions marquées avec le mot clé infix peuvent être appelées sans le point ni les parenthèses.

Les conditions sont :
  • La fonction doit être une fonction membre ou une fonction extension.
  • La fonction doit avoir un paramètre unique.
  • Le paramètre ne doit pas accepter un nombre variable de valeurs, et ne doit pas avoir de valeur par défaut.

infix fun Int.shl(x: Int): Int { ... }

// appel de la méthode en utilisant la notation infix
1 shl 2

// est la même chose que
1.shl(2)

Fonctions Anonyme en Kotlin

En Kotlin il est possible de définir des méthodes anonymes (sans leur donner de nom). Par exemple :

fun(x: Int, y: Int): Int = x + y
Une fonction anonyme ressemble à une fonction classique, sauf le que le nom est omis. Le corps de la fonction peut aussi être un bloc :

fun(x: Int, y: Int): Int {
    return x + y
}
Si le type des paramètres peut être inféré par le compilateur, il n'est pas nécessaire de le préciser, comme dans l'exemple :

ints.filter(fun(item) = item > 0)

Les fonctions récursives (Tail recursive functions)

Kotlin gère une façon de programmer appelée : "tail recursion".
Cela permet à certains algorithmes qui seraient écrits normalement avec des boucles for, d'être écrits en utilisant une fonction récursive, mais sans le risque de dépassement de pile (stack overflow). Quand une fonction est marquée avec le modifieur tailrec et répond au format requis, le compilateur optimise la récursion, en générant une boucle rapide et optimisée à la place :

val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
				
Référence : "Tail recursive functions".
Ce code calcule le point invariant de cosinus, qui est une constante mathématique.
Le code appelle la fonction Math.cos plusieurs fois, en partant de 1.0 jusqu'à ce que le résultat ne change plus, en arrivant au résultat de : 0.7390851331706995 pour la précision eps.

Le code est équivalent à la forme plus classique :

val eps = 1E-10 // "good enough", could be 10^-15

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps) return x
        x = Math.cos(x)
    }
}
					

Exercice

Modifions le code de la méthode findFixPoint pour visualiser le nombre d'appels récursifs effectués (par exemple en affichant un . à chaque itération.

val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
	

Solution

Nous modifions le code de la condition else, pour afficher un '.' à chaque appel.

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (Math.abs(x - Math.cos(x)) < eps) x else {
    print('.')
    findFixPoint(Math.cos(x))
}
	

Returns en Kotlin

Retour d'une fonction

Une fonction qui ne retourne rien, retourne implicitement un type Unit.
Il n'est pas besoin de retourner explicitement la valeur :

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // "return Unit" or "return" is optional
}
Il n'est même pas obligatoire de préciser ce type retour :

fun printHello(name: String?) {
    ...
}

Types de retours explicites

Les fonctions définies avec des accolades doivent explicitement préciser le type de retour
(sauf si elles ne retournent rien, comme vu précédemment).

En effet il n'est pas forcément très clair pour lecteur (et parfois même pour le compilateur) de déterminer le type de retour, en fonction de l'implémentation.

Fonctions locales

Kotlin gère les fonctions locales, c'est à dire des fonctions à l'intérieur d'autres fonctions :

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}
					
Une fonction locale a accès aux variables locales définies à l'extérieur de la fonction, donc dans le cas, ci-dessus, la variable visited peut être définie comme variable locale :

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}
					

Exercice

Écrivez une fonction qui permet de déterminer si une valeur est paire en utilisant une fonction locale
fun isMultiple(operand: Int): Boolean :

fun isEven(n: Int): Boolean = TODO()
	

Solution


fun isEven(n: Int): Boolean {
    fun isMultiple(operand: Int): Boolean = n % operand == 0
    return isMultiple(2)
}

fun main() {
    println(isEven(2))
    println(isEven(3))
}
	

Classes en Kotlin

La POO : Programmation Orientée Objet

L'objectif de la Programmation Orientée Objet est d'organiser le code afin de mieux pouvoir le réutiliser.
Pour cela nous utilisons l'encapsulation qui permet de :
  • Rassembler dans une même structure :
    • Attributs (données) ou "variables membres".
    • Méthodes (fonctions).
  • Garantir l’intégrité des données en :
    • Ne laissant visible que ce qui doit être réellement utilisé (visibilité).
    • N’accédant aux données que par des méthodes.


Classe = description abstraite d’un objet.
Instancier une classe = créer un objet sur son modèle (grâce au constructeur).
Propriété = attribut accessible par un getter et/ou setter.

Une classe

Déclaration

On utilise le mot clé class

class Invoice { /*...*/ }
					
Une déclaration de classe consiste en : un nom, un entête (qui spécifie le type des paramètres, le constructeur primaire, etc.) et le corps de la classe, le tout entouré d'accolades. L'entête et le corps de la classe sont optionnels. Si la classe n'a pas de corps, les accolades sont optionnelles :

class Empty
					

Le constructeur

Une classe peut avoir un constructeur primaire et un ou plusieurs constructeurs secondaires. Le constructeur primaire fait partie intégrante de l'entête : il est placé juste après le nom de la classe.

class Person constructor(firstName: String) { /*...*/ }
					
Si le constructeur n'a pas d'annotations, ou de modificateurs de visibilité, le mot clé constructor n'est pas obligatoire :

class Person(firstName: String) { /*...*/ }
					
Le constructeur ne comporte aucun code. Si nécessaire, il est possible de définir un bloc d'initialisation qui est préfixé par le mot clé init. Les blocs sont exécutés dans l'odre d'apparition dans le code :

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}
					
Les paramètres du constructeur peuvent être utilisés dans les blocs d'initialisation ou pour initialiser les propriétés déclarées dans le corps de la classe :

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}
					

La forme concise du constructeur

C'est cette forme que l'on utilisera de préférence :

class Person(val firstName: String, val lastName: String, var age: Int) { /*...*/ }
					

Les propriétés peuvent être :
  • En lecture seule : val
  • En lecture ET écriture (mutable) : var

Exemple de classe User

En Java une classe User serait écrite comme suit :

public class User {

    // PROPERTIES
    private String email;
    private String password;
    private int age;

    // CONSTRUCTOR
    public User(String email, String password, int age) {
        this.email = email;
        this.password = password;
        this.age = age;
    }

    // GETTERS
    public String getEmail() { return email; }
    public String getPassword() { return password; }
    public int getAge() { return age; }

    // SETTERS
    public void setEmail(String email) { this.email = email; }
    public void setPassword(String password) { this.password = password; }
    public void setAge(int age) { this.age = age; }
}
					
En Kotlin son équivalent est :

class User(var email: String, var password: String, var age: Int)
					

Les constructeurs secondaires

Les constructeurs secondaires sont préfixés par constructor :

class Person {
    var children: MutableList<Person> = mutableListOf<Person>();
    constructor(parent: Person) {
        parent.children.add(this)
    }
}
					
Si la classe comporte une constructeur primaire, chaque constructeur secondaire doit faire appel au constructeur primaire, directement ou indirectement via un autre constructeur secondaire. La délégation à un autre constructeur de la même classe est effectué en utilisant le mot clé this :

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<Person>();
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}
					
Note : le code des blocs d'init font partis du constructeur primaire. La délégation au constructeur primaire est la première action effectuée dans un constructeur secondaire, donc le code de tous les blocs d'initialiseurs est exécuté avant le corps du constructeur secondaire, même si la classe n'a pas de constructeur primaire, la délégation est implicite :

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor")
    }
}
					
Une classe non abstraire qui ne déclare pas de constructeur en aura un public, par défaut, ne comportant pas de paramètre.
Si vous ne voulez pas de constructeur public, alors il faut en déclarer un privé :

class DontCreateMe private constructor () { /*...*/ }
					

Les valeurs par défaut

En Kotlin, pas besoin de définir plusieurs constructeurs pour gérer les valeurs des paramètres par défaut, il suffit de préciser leur valeur avec le signe = dans le constructeur :

class Customer(val customerName: String = "")
					

Modification des accesseurs par défaut

Kotlin permet de modifier le comportement par défaut des getters et des setters générés pour les propriétés :

class User(email: String, var password: String, var age: Int) {

    var email: String = email
        get() { println("User email read access done."); return field}
        set(value) { println("User email write access done."); field = value}
}
					

Propriété privée

Par défaut en Kotlin les propriétés sont publiques, pour changer cela, il suffit d'ajouter le modifieur de visibilité :

class User(var email: String, private var password: String, var age: Int)
					

Utilisation (instantiation) d'une classe

En Kotlin, il n'y a pas de new, on utilise directement le nom de la classe :

val user = User("hello@gmail.com", "azerty", 41)
					
Pour accéder aux champs, la notation à . est utilisée :

val user = User("hello@gmail.com", "azerty", 41)
println(user.email) // Getter
user.email = "my_new_email@gmail.com"
println(user.email) // Getter
					

Exercice

Écrivez une classe Person avec les propriétés firstName et lastName dont les valeurs par défaut sont la chaîne vide :"". La propriété lastName est en lecture seule. Vous afficherez le nom complet (prénom nom) après avoir modifié le prénom (affectation d'une nouvelle valeur pour le prénom de notre objet créé) et vérifiée que le nom est bien en lecture seule (en essayant de modifier sa valeur).

class Person
	

Solution


class Person (var firstName: String = "", val lastName: String = "")

fun main() {
    var father = Person("Anakin", "Skywalker")
    println(father)
    println( "${father.firstName} ${father.lastName}")

    me.firstName = "Luke"
//    me.lastName = "Organa d'Alderaan"

    println( "${me.firstName} ${me.lastName}")
}
	

Exercice

Testez la classe User, création d'une instance, changement de la valeur de l'email, et affichage de cette valeur.

Solution


class User(email: String, var password: String, var age: Int) {

    var email: String = email
        get() { println("User email read access done."); return field}
        set(value) { println("User email write access done."); field = value}
}

fun main() {
    var currentUser = User("tristan.salaun.pro@gmail.com", "azerty", 41)
    currentUser.email = "test@test.com"
    println(currentUser.email)
}
	

Les attributs

Les propriétés en Kotlin peuvent être déclarées en lecture/écriture (mutable) en utilisant le mot clé var, ou en lecture seule (immutable) en utilisant le mot clé val :

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "221b Baker Street"
    var city: String = "London"
    var state: String? = null
    var zip: String = "NW1 6XE"
}
					
Pour accéder aux propriétés, il suffit d'utiliser son nom :

fun copyAddress(address: Address): Address {
    val result = Address() // there's no 'new' keyword in Kotlin
    result.name = address.name // accessors are called
    result.street = address.street
    // ...
    return result
}
					

Méthodes (Functions Members)

Une méthode membre d'une classe, est définie comme suit :

class Sample() {
    fun foo() { print("Foo") }
}
Comme déjà vu, pour appeler une telle méthode il suffit d'utiliser la notation à point :

Sample().foo() // creates instance of class Sample and calls foo

Visibilité des membres en Kotlin

En Kotlin la visibilité par défaut est public, les modifieurs de visibilité disponibles sont :
  • private : Un membre déclaré comme private sera visible uniquement dans la classe où il est déclaré.
  • protected : Un membre déclaré comme protected sera visible uniquement dans la classe où il est déclaré ET dans ses sous-classes (via l’héritage).
  • internal : Un membre déclaré comme internal sera visible par tous ceux du même module. Un module est un ensemble de fichiers compilés ensemble (comme une librairie Gradle ou Maven, par exemple).
  • public : Un membre déclaré comme public sera visible partout et par tout le monde.

Héritage en Kotlin

Toutes les classes en Kotlin héritent de la super class Any (équivalent à Object en Java), qui est la superclasse par défaut de toutes les classes qui n'ont pas déclaré de super type :

class Example // Implicitly inherits from Any
					
La classe Any à 3 méthodes : equals(), hashCode() et toString().
Pour déclarer un super type, la syntaxe est la suivante :

open class Base(p: Int)

class Derived(p: Int) : Base(p)
					
Si la classe enfant comporte un constructeur primaire, la classe parente doit être initialisée à cet endroit, en utilisant les paramètres du constructeur primaire.
Si la classe enfant n'à pas de constructeur primaire, alors chaque constructeur secondaire doit initialiser la classe parente en utilisant le mot clé super, ou déléguér à un constructeur secondaire qui le fera :::

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
					

La surcharge

La surcharge des méthodes

En Kotlin, tout doit être explicite, une méthode qui peut être surchargée sera marquée avec le modifieur open :

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}
					
Une méthode qui redéfinit une autre de la classe parente est elle même redéfinissable, à moins de la marquer comme final :

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}
					

La surcharge des propriétés

La surcharge d'une propriété fonctionne de la même manière que la surcharge des méthodes :

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}
					
Il est possible de redéfinir une propriété de type val avec une autre de type var, mais pas l'inverse.
En effet la propriété val déclare une méthode get, en la redéfinissant par une var, on déclare une méthode set de plus dans la classe dérivée.

interface Shape {
    val vertexCount: Int
}

class Polygon : Shape {
    override var vertexCount: Int = 0  // Can be set to any number later
}
					
Kotlin permet de redéfinir la propriété directement dans le constructeur primaire avec le mot clé override :

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 vertices

class Polygon : Shape {
    override var vertexCount: Int = 0  // Can be set to any number later
}
					

Ordre d'exécution

Pendant la construction de la nouvelle instance, la classe de base est initialisée en première, et ensuite l'initialisation de la classe enfant :

open class Base(val name: String) {

    init { println("Initializing Base") }

    open val size: Int =
        name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
    name: String,
    val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
					
Cela signifie que lorsque la classe primaire est créée, les propriétés déclarées ou redéfinies dans la classe dérivée, ne sont pas encore initialisées.

L'appel à la classe parente

Comme en Java, le mot clé pour appeler le parent est super :

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }

    val fillColor: String get() = super.borderColor
}
					

L'appel à la classe parent à partir d'une classe interne (inner class)

Dans une classe interne (inner class) l'accès à la classe englobante (outer class) est effectué avec le mot clé super qualifié par le nom de cette classe : super:@Outer :

class FilledRectangle: Rectangle() {
    fun draw() { /* ... */ }
    val borderColor: String get() = "black"

    inner class Filler {
        fun fill() { /* ... */ }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
        }
    }
}
					

Règle de réécriture

En Kotlin, l'implémentation de l'héritage est régie par la règle suivante : si une classe hérite de plusieurs implémentation du même membre, en même temps, des classes parentes immédiates, elle doit redéfinir ce membre et fournir sa propre implémentation. Pour identifier l'origine du constructeur, nous utilisons le mot clé super qualifié du nom de la classe parente avec des chevrons super<Base>  :

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // The compiler requires draw() to be overridden:
    override fun draw() {
        super<Rectangle>.draw() // call to Rectangle.draw()
        super<Polygon>.draw() // call to Polygon.draw()
    }
}
					

Classes abstraites en Kotlin

Une classe et quelques membres peuvent êtres déclarés abstract. Un membre abstrait n'a pas d'implémentation dans la classe. Le mot clé open n'est pas nécessaire, dans le cas d'une classe ou méthode abstraite, car cela est évident.
Note : il est possible de surcharger un membre non abstrait par un abstrait :

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    override abstract fun draw()
}
					

L'objet compagnon

Interface en Kotlin

Les interfaces en Kotlin peuvent contenir des méthodes abstraites, mais aussi des méthodes implémentées. Ce qui les différencies d'une classe abstraite, est le fait qu'elles ne contiennent pas d'état.
Elles peuvent comporter des propriétés, mais elles doivent être abstraites ou fournir une implémentation pour les accesseurs.
Une interface est définie en utilisant le mot clé interface :

interface MyInterface {
    fun bar()
    fun foo() {
      // optional body
    }
}
					
Implémenter une interface :

class Child : MyInterface {
    override fun bar() {
        // body
    }
}
					
Il est possible de déclarer des propriétés dans les interfaces. Elles peuvent être abstraites, ou fournir une implémentation pour les accesseurs.

interface MyInterface {
    val prop: Int // abstract

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}
					

Héritage d'interfaces

Une interface peut hériter d'une autre interface, qui va compléter la première interface. Elle peut fournir une implémentation pour ses membres, et déclarer des nouvelles fonctions et propriétés. Les classes implémentant ce genre d'interface n'a besoin de définir que les implémentations manquantes :

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String

    override val name: String get() = "$firstName $lastName"
}

data class Employee(
    // implementing 'name' is not required
    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person
					

Résolution des conflits de surcharge

Quand plusieurs types sont déclarés en tant que super-type, il peut y avoir un conflit si deux (ou plus) interfaces déclarent la même méthode, par exemple :

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        supe<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}
					
Les interfaces A et B déclarent toutes les deux les fonctions foo() et bar(). Note : par défaut les méthodes dans une interface, sans corps, sont marquées comme abstraites, c'est pour cela que la méthode bar(')) de la classe A, n'est pas explicitement marquée.
La classe concrète Cdoit surcharger la méthode bar() et l'implémenter.

Si nous dérivons D depuis A et B, nous devons implémenter toutes les méthodes héritées de ces interfaces et spécifier exectement comment D doit les implémenter. Cette règle s'applique aux méthodes héritées avec une implémentation simple ( tel que bar() ) ou multimples ( foo() ).

Polymorphisme en Kotlin

Le principe est le même qu'en Java : nous pouvons utiliser par exemple une variable d'un type parent, contenant des sous types. Par exemple :

open class User (var name: String, var firstName: String, var age: Int) {
    open fun displayInformations() = println("$firstName $name is $age old")
}
class Admin(name: String, firstName: String, age: Int, var phoneNumber: String) : User(name, firstName, age) {
    override fun displayInformations() = println("$firstName $name is $age old, phone number : $phoneNumber")
}

fun main() {
    var currentUser = User(firstName = "Tristan", name = "SALAUN", age = 41)
    currentUser.displayInformations()
    currentUser = Admin(firstName = "Tristan", name = "SALAUN", age = 41, phoneNumber = "0123456789")
    currentUser.displayInformations()
}

Exercice

Écrivez les classes Dog, Bird, Duck et Snake avec les cris respectifs : "Waf", "Cui cui", "Coin coin" et "Ssssssss". Validez en appellant la méthode speak sur les différentes instances d'animaux référencées par un même objet de type Animal.

interface Animal {
  fun speak()
}

class Dog: Animal
class Bird: Animal
class Duck: Animal
class Snake: Animal
	

Solution


interface Animal {
    fun speak()
}

class Dog: Animal { override fun speak() = println("Waf") }
class Bird: Animal { override fun speak() = println("Cui cui") }
class Duck: Animal { override fun speak() = println("Coin coin") }
class Snake: Animal { override fun speak() = println("Ssssssss") }

fun main() {
    var myPet: Animal = Dog()
    myPet.speak()
    myPet = Bird()
    myPet.speak()
    myPet = Duck()
    myPet.speak()
    myPet = Snake()
    myPet.speak()
}
	

Surcharge de fonctions

Les fonctions définies ci-dessous, diffèrent uniquement par leur signature :

fun printNumber(n : Number){
    println("Using printNumber(n : Number)")
    println(n.toString() + "\n")
}

fun printNumber(n : Int){
    println("Using printNumber(n : Int)")
    println(n.toString() + "\n")
}

fun printNumber(n : Double){
    println("Using printNumber(n : Double)")
    println(n.toString() + "\n")
}

Surcharge de fonctions

Quelles seront les méthodes appelées lors de l'exécution ?

fun main() {
    val a : Number = 99
    val b = 1
    val c = 3.1

    printNumber(a) //Which version of printNumber is getting used?
    printNumber(b) //Which version of printNumber is getting used?
    printNumber(c) //Which version of printNumber is getting used?
}

Data Classes en Kotlin

Nous écrivons régulièrement des classes, dont le rôle est de contenir de l'information. Dans ce genre de classes, des fonctionnalités, et des fonctions utilitaires sont souvent déduites mécaniquement des données. En Kotlin, ces classes sont appelées data class, et sont donc marquées data :

data class User(val name: String, val age: Int)
					
Le compilateur va générer les membres suivant, en utilisant toutes les propriétés déclarées dans le constructeur principal :
  • equals()/hashCode().
  • toString() sous la forme "User(name=Tristan, age=40)".
  • Les fonctions componentN() correspondant aux propriétés dans leur ordre de déclaration.
  • La fonction copy().

Les conditions

Afin d'assurer la consistance et le sens du code généré, la classe data doit se conformer aux conditions suivantes :
  • Le constructeur primaire doit avoir au moins un paramètre.
  • Tous les paramètre du constructeur primaire doivent êtres marqués var ou val.
  • La classe data ne doit pas être abstraite (abstract), ouverte (open), scélée (sealed) ou interne (inner).
  • (avant 1.1) Les classes data, peuvent seulement implémenter des interfaces.
De plus, les membres générés, suivent les règles suivantes :
  • S'il y a une implémentation explicite des méthodes equals(), hashCode() ou toString() dans le corps de la classe, ou une implémentation marquée final dans la classe parente, alors ces fonctions ne sont pas générés : c'est le code existant qui est utilisé.
  • Si un type parent comporte les fonctions componentN() qui sont open et retournent des types compatibles, la fonction correspondante est générée pour la classe data et surcharge celles du type parent. Si les fonctions du parent ne peuvent pas êtres surchargées, à cause d'une signature incompatible, ou si elles sont finales, alors une erreur est signalée.
  • Etendre une classe qui comporte déjà une fonction copy(...) avec une signature correspondante est déprécié en Kotlin 1.2 et interdit en Kotlin 1.3.
  • Fournir une implémentation explicite pour les fonctions componentN() et copy() n'est pas autorisé.
Depuis la version 1.1, les classes data peuvent étendre d'autres classes.
Sur la JVM, si les classes générées ont besoin d'un constructeur par défaut (sans paramètre) alors il faut spécifier les valeurs par défaut de toutes les propriétés.

data class User(val name: String = "", val age: Int = 0)
					

Les propriétés déclarées dans le corps de la classe

Le compilateur utilise seulement les propriétés définies dans le constructeur primaire pour générer automatiquement les fonctions. Pour exclure une propriété de la génération de code, il suffit de la déclarer dans le corps de la classe :

data class Person(val name: String) {
    var age: Int = 0
}
					
Seule la propriété name sera utilisée dans les fonctions toString(), equals(), hashCode() et copy()n et il n'y aura qu'une seule fonction component1(). De ce fait, deux objets de type Person qui auraient des valeurs pour la propriété age différentes, seront considérés comme égales.

data class Person(val name: String) {
    var age: Int = 0
}
fun main() {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
    println("person1 == person2: ${person1 == person2}")
    println("person1 with age ${person1.age}: ${person1}")
    println("person2 with age ${person2.age}: ${person2}")
}
					

La copie

Il arrive régulièrement de devoir copier un objet, et de modifier quelques unes de ses propriétés, tout en gardant le reste intact. C'est le but de la fonction générée copy(). Pour la classe User ci-dessous, l'implémentation serait la suivante :

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
					
Ce qui nous permet d'écrire :

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
					

La destructuration

Les fonctions du composant générées permettent le "destructuring" :

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"
					

Classes standards

La librairie standard met à disposition les classes Pair et Triple. Dans la majorité des cas, les classes data sont plus appropriées, car elles permettent de nommer les propriétés, ce qui rend le code beaucoup plus compréhensible.

Exercice

Reprenez la classe Person, ajoutez le modifieur data et utiliser la copie pour changer uniquement le prénom (pour déclarer un parent par exemple) :

data class Person (var firstName: String = "", var lastName: String = "")
	

Solution


data class Person (var firstName: String = "", var lastName: String = "")

fun main() {
    var personTristan = Person("Tristan", "SALAUN")
    var personMelody = personTristan.copy(firstName = "Mélody")
    println(personTristan)
    println(personMelody)
}
	

Les listes dans les data classes

Ajoutons un attribut de classe qui sera de type List, et regardons comment l'affichage va se comporter.

data class Person (var firstName: String = "", var lastName: String = "", var list: List<Int>)

fun main() {
    val listValue = listOf(1,2,3,4,5,6)
    val personTristan = Person("Tristan", "SALAUN", listValue)
    val personMelody = personTristan.copy(firstName = "Mélody")
    println(personTristan)
    println(personMelody)
}
	

Solution


Person(firstName=Tristan, lastName=SALAUN, list=[1, 2, 3, 4, 5, 6])
Person(firstName=Mélody, lastName=SALAUN, list=[1, 2, 3, 4, 5, 6])
	

Enum Classes en Kotlin

L'usage le plus simple d'une classe enum est d'implémenter une énumération sûre :

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}
					
Chaque constante de l'énumération est un objet. Les constantes sont séparées par une virgule.

Initialisation

Chaque valeur de l'énumération est une instance de la classe enum, elles peuvent êtres initialisées de la façon suivante :

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}
					

Les classes anonymes

Les constantes peuvent déclarer leur propre classe anonyme, avec leurs valeurs correspondantes, mais aussi surcharger les méthodes de base :

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}
					
Si une classe enum définie plusieurs membres, il faut séparer la définition des constantes de la définition des membres, par un point virgule.

Les valeurs d'une énumération, ne peut pas contenir d'imbrications autre que des inner classes (déprécié en Kotlin 1.2).

Implémenter les interfaces dans les classes enum

Une classe enum peut implémenter une interface (mais ne dérive pas d'une classe), fournir une unique implémentation de l'interface pour toutes les valeurs, ou une implémentation défférente pour chaque, via sa classe anonyme. Cela est fait en ajoutant une interface à la déclaration de la classe enum, comme ci-dessous :

import java.util.function.BinaryOperator
import java.util.function.IntBinaryOperator

enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
    PLUS {
        override fun apply(t: Int, u: Int): Int = t + u
    },
    TIMES {
        override fun apply(t: Int, u: Int): Int = t * u
    };

    override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}

fun main() {
    val a = 13
    val b = 31
    for (f in IntArithmetics.values()) {
        println("$f($a, $b) = ${f.apply(a, b)}")
    }
}
					

Travailler avec des constantes de la classe enum

Les classe enum, en Kotlin, disposent de fonctions qui permettent de lister les valeurs des constantes définies dans la classe, et d'obtenir un enum avec son nom. La signature de ces méthodes sont (en supposant que le nom de la classe est : EnumClass) :

EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>
					
La méthode valueOf() lève une IllegalArgumentException si le nom passé en paramètre ne correspond pas à une valeur définie dans la classe.

Depuis Kotlin 1.1, il est possible d'accéder à une constante d'une classe enum via une méthode générique, en utilisant les fonctions ; enumValues<T>() et enumValueOf<T>() :

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // prints RED, GREEN, BLUE
					
Chaque constante de la classe enum possède des propriétés pour obtenir son nom et sa position dans la déclaration de la classe :

val name: String
val ordinal: Int
					
Les constantes de la classe enum implémentes aussi l'interface Comparable, avec un ordre naturel qui est l'ordre de déclaration dans la classe.

Exercice

Utilisez la classe enumération Direction précédente, et utilisez un "switch" (when) pour afficher en clair la direction prise.

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}
fun main() {
    val direction = Direction.NORTH
    when{
        true -> TODO()
    }
}
	

Solution


enum class Direction {
    NORTH, SOUTH, WEST, EAST
}
fun main() {
    val direction = Direction.NORTH;
    when(direction){
        Direction.NORTH -> println("On va au Nord.")
        Direction.SOUTH -> println("On va au Sud.")
        Direction.EAST -> println("On va à l'Est.")
        Direction.WEST -> println("On va à l'Ouest.")
    }
}
	

Nested Classes en Kotlin

Les classes peuvent être imbriquées dans d'autres classes :

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2
					

Classes imbriquées

Une classe peut être marquée comme inner pour avoir la possibilité d'accéder aux membres de la classe englobante (outer class). Une classe imbriquée peut référencer un objet de la classe englobante :

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1
					

Classes imbriquées anonymes

Les classes imbriquées anonymes sont crées en utilisant une expression object

window.addMouseListener(object : MouseAdapter() {

    override fun mouseClicked(e: MouseEvent) { ... }

    override fun mouseEntered(e: MouseEvent) { ... }
})
					
Note : dans la JVM, si l'objet est une instance d'une interface fonctionnelle Java (c'est-à-dire, une interface avec une seule méthode abstraite), vous pouvez la créer en utilisant une expression lambda, préfixée du type de l'interface :

val listener = ActionListener { println("clicked") }
					

Sealed Classes en Kotlin

Les classes scellées sont utilisées pour représenter une hiérarchie restreinte. Quand une valeur peut avoir qu'un type parmi un nombre limité de types. C'est, dans un sens, une extension des classes enum : les différentes valeurs d'un enum sont aussi limitées ; il n'existe qu'une seule instance pour chaque constante de l'enum, alors qu'une sous-classe scellée peut avoir plusieurs instances qui peuvent contenir un état.

Pour déclarer une classe scellée, il suffit d'utiliser le modifieur sealed avant le nom de la classe. Une classe scellée, peut avoir des sous classes, mais toutes doivent être déclarées dans le même fichier où celle-ci est déclarée. (Avant Kotlin 1.1, la règle était encore plus stricte : les classes devaient être des classes imbriquées dans la déclaration de la classe scellée).

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
					
(L'exemple ci-dessus utilise la possibilité d'une fonctionnalité de Kotlin 1.1 : la possibilité pour les classes data d'étendre une autre classe, incluant les classes scellées.)
Une classe scellée est abstraite, elle ne peut pas être instanciée directement, et peut avoir des membres abstraits.

Les classes scellées ne peuvent pas avoir de constructeurs non privés (private) : leurs constructeurs sont privés par défaut).

Notez que les classes qui étendent d'une sous-classe d'une classe scellée (héritage indirect), peuvent êtres déclarées n'importe où : pas obligatoirement dans le même fichier.
L'avantage principal de l'utilisation de ces classes scellées, est lorsque nous utilisons l'expression when. Si il est possible de vérifier que les cas, couvrent toutes les possibilités, alors il n'est pas nécessaire d'ajouter la clause else. toutefois, cela ne fonctionne que si vous utilisez when en tant qu'expression et non comme une déclaration :

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // the `else` clause is not required because we've covered all the cases
}
					

Exemple sans classe scellée

Résolvons l'exercice situé ici.
Que pouvons nous remarquer concernant l'usage de l'interface Expr ?
Ajoutons maintenant de nouvelles expressions, par exemple

data class Substract(val e1: Expr, val e2: Expr) : Expr
data class Multiply(val e1: Expr, val e2: Expr) : Expr
data class Divide(val e1: Expr, val e2: Expr) : Expr
	

Que constatons-nous, et que devons nous corriger ?

Solution


fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    is Substract -> eval(expr.e1) - eval(expr.e2)
    is Multiply -> eval(expr.e1) * eval(expr.e2)
    is Divide -> eval(expr.e1) / eval(expr.e2)
    NotANumber -> Double.NaN
    // the `else` clause is not required because we've covered all the cases
}

Les fonctions - Partie 2

Surcharge d'opérateurs en Kotlin

Kotlin permet de fournir le code pour une liste prédéfinie d'opérateurs sur notre propre type. Ces opérateurs ont une représentation figée (exemple + ou *) et des règles de priorité figées. Pour définir un opérateur, il suffit de coder la méthode membre, ou une fonction d'extension, correspondant au nom de l'opérateur à définir. Les fonctions doivent être marquées avec le modifieur operator.

Les opérations unaires

Expression Traduites en
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
Ce tableau nous dit que quand le compilateur traite, par exemple, une expression +a, il suit les étapes suivante :
  • Détermine le type de a, par exemple le type T.
  • Il cherche une fonction unaryPlus()marquée avec le modifieur operator sans paramètre, pour le type T.
  • Si la fonction est introuvable, ou s'il y a une ambiguité, alors c'est une erreur de compilation.
  • Si la fonction est présente et que le type de la valeur retournée est R, alors l'expression +a sera de type R.
Note : ces opérations ainsi que les autres opérations, sont optimisées pour les types basics, et n'introduisent pas de surcharge sur l'appel de ces fonctions.
Exemple d'implémentation pour la classe Point :

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
    println(-point)  // prints "Point(x=-10, y=-20)"
}
                

Les opérations incrément et décrément

Expression Traduites en
a++ a.inc()
a-- a.dec()
Les méthodes a.inc() et a.dec() doivent retourner une valeur, qui sera assignée à la variable sur laquelle l'opérateur ++ ou -- à été utilisé. Il ne faut pas changer la valeur de l'objet original.
Le compilateur effectue les opérations suivantes, pour déterminer comment interpréter une opération postfixée, par exemple a++ :
  • Détermine le type de a, par exemple le type T.
  • Il cherche une fonction inc()marquée avec le modifieur operator sans paramètre, pour le type T.
  • Vérifie que le type de retour de la fonction est bien un sous type de T.
L'exécution de l'expression aura les effets suivants :
  • Stocker la valeur initiale de a dans une variable temporaire a0.
  • Affecter le résultat de a.inc() à a
  • Retourner la valeur de a0 comme résultat de l'expression.
Pour a++ les étapes sont identiques.
Pour les formes préfixées ++a et --a, la résolution fonctionne de la même manière, et l'effet est le suivant :
  • Affecter le résultat de a.inc() à a
  • Retourner la valeur de a comme résultat de l'expression.

Exercice

Écrivez l'opérateur ++ et -- pour la classe Point.

data class Point(var x: Int, var y: Int)
	
Testez la différence entre point++ et ++point.

Solution


data class Point(var x: Int, var y: Int) {
    operator fun inc() : Point {
        return Point(this.x + 1, this.y + 1)
    }
}
fun main() {
    var point1 = Point(1,1)
    println(point1)
    point1++
    println(point1)
}
	

Les opérations binaires

On parle ici d'opérations qui nécessitent deux valeurs.

Les opérations arithmétiques

Expression Traduites en
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b), a.mod(b) (deprecated)
a..b a.rangeTo(b)
Pour les opérations présentes dans ce tableau, le compilateur résout l'expression en utilisant tout simplement la colonne de droite.

Exemple

Implémentation de l'opérateur + pour la classe Counter.

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}
Exercice : proposer un exemple d'utilisation.

fun main() {
    var counter: Counter = Counter(0)

    println(counter)
    println(counter + 3)
}

Exercice

Écrivez l'opérateur + pour la classe Point qui permet d'additionner 2 points (on additionne le x1 avec le x2 et le y1 avec le y2).

data class Point(var x: Int, var y: Int)
	

Solution


data class Point(var x: Int, var y: Int) {
    operator fun plus(other: Point) : Point {
        return Point(this.x + other.x, this.y + other.y)
    }
}
	
Ou de manière équivalente :

data class Point(var x: Int, var y: Int)
operator fun Point.plus(other: Point) : Point { return Point(this.x + other.x, this.y + other.y) }
	

L'opérateur 'In'

Expression Traduites en
a in b b.contains(a)
a !in b !b.contains(a)
Notez que l'ordre des paramètres est inversé pour in et !in par rapport au précédent opérateurs.

Opérateur d'accès indexé

Expression Traduites en
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)
Les crochets, sont traduits en appels aux fonctions get et set avec le nombre d'arguments correspondant.

Opérateur d'invocation

Expression Traduites en
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)
Les parenthèses sont traduites par l'appel aux méthodes invoke avec le nombre de paramètres approprié.

Opérateur d'affectation étendus

Expression Traduites en
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b), a.modAssign(b) (deprecated)
Pour les opérateurs d'affectation étendus, par exemple a += b, le compilateur effectue les étapes suivantes :
  • Si la fonctions de la colonne de droite est disponible :
    • Si la fonction binaire correspondante est aussi disponible (par exemple plus() pour plusAssign(), alors remonter une erreur (car il y a une ambiguïté).
    • Vérifier que le type de retour est bien Unit.
    • Générer le code pour a.plusAssign(b).
  • Sinon, essayer de générer le code pour a = a + b (avec une vérification du type de a + b qui doit retourner un sous type de a.
Note : les affectations, ne sont pas des expressions en Kotlin.

Opérateurs d'égalité et d'inégalité

Expression Traduites en
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
Ces opérateurs ne fonctionnent qu'avec la fonction equals(other: Any?): Boolean qui peut être surchargée pour fournir une fonction personnalisée de comparaison d'égalité. Toute autre fonction avec le même nom (par exemple equals(other: Foo)) ne sera pas appelée.
Note : Il n'est pas possible de surcharger les fonctions === et !== (tests d'identité), donc aucune convention n'existe pour ces opérateurs.
L'opération == est spéciale : elle est traduite en une expression complexe qui recherche les valeurs nulles. null == null est toujours vrai, et x == null est toujours faux, et n'invoque pas x.equals().
Attention à la définition de l'égalité :

fun main() {
    val first = Integer(10)
    val second = Integer(10)

    println(first == second)       // 1
    println(first.equals(second))  // 2
    println(first === second)      // 3
}
A votre avis, quel sera l'affichage du code ci-dessus ?
  • 1 => true
  • 2 => true
  • 3 => false

Opérateurs de comparaison

Expression Traduites en
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0
Toutes les comparaisons sont traduites en appels à la fonction compareTo, qui doit nécessairement retourner un Int.
Property delegation operators provideDelegate, getValue and setValue operator functions are described in Delegated properties. https://kotlinlang.org/docs/reference/delegated-properties.html Infix calls for named functions We can simulate custom infix operations by using infix function calls. https://kotlinlang.org/docs/reference/functions.html#infix-notation

Lambda expression en Kotlin

Les fonctions en Kotlin sont des fonctions "première classe" (first-class), c'est à dire qu'elles peuvent êtres stockées dans des variables, des structures de donnée, passées en argument et retournées depuis des fonctions d'ordre supérieur (higher-order functions). Vous pouvez utiliser les fonctions, comme n'importe quel autre type classique.

Fonctions d'ordre supérieur

Une fonction d'ordre supérieur est une fonction qui prend en paramètre, ou retourne une fonction.
Un très bon exemple est la fonctionnalité fold pour les collections, qui prend une valeur initiale pour l'accumulateur, et une fonction pour combiner les items. Le résultat est obtenu en appliquant la fonction sur l'item courant, et l'accumulateur, pour en faire changer la valeur. Exemple :

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
    ): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}
Dans le code ci-dessus, le paramètre combine à un type de fonction (R, T) -> R, donc il accepte une fonction qui prend 2 arguments, de types R et T et retourne une valeur de type R. Cette fonction est appelée dans une boucle for, et la valeur de retour est ensuite assignée à l'accumulateur.
Pour appeler la méthode fold, nous devons passer une instance de type fonction en argument, et les expression lambdas sont couramment utilisées à cet usage :

val items = listOf(1, 2, 3, 4, 5)

// Lambdas are code blocks enclosed in curly braces.
items.fold(0, {
    // When a lambda has parameters, they go first, followed by '->'
    acc: Int, i: Int ->
    println("acc = $acc, i = $i, ")
    val result = acc + i
    println("result = $result")
    // The last expression in a lambda is considered the return value:
    result
})

// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

// Function references can also be used for higher-order function calls:
val product = items.fold(1, Int::times)

Les types fonctions

Instantiation des types fonctions

Invoquer un type fonction

La valeur d'un type fonction peut être appelé en utilisant son opérateur invoke(...) : par exemple f.invoke(x) ou simplement f(x).

Si la valeur à un type receveur, alors, l'objet receveur doit être passé en premier paramètre. Une autre façon d'invoquer la valeur de type fonction avec un type de receveur est de préfixer avec l'objet receveur, comme s'il s'agissait d'une fonction d'extention, exemple : 1.foo(2).

fun main() {
    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus

    println(stringPlus.invoke("<-", "->"))
    println(stringPlus("Hello, ", "world!"))

    println(intPlus.invoke(1, 1))
    println(intPlus(1, 2))
    println(2.intPlus(3)) // extension-like call
}

Expression lambda et fonctions anonymes

Une expression lambda, ou une fonction anonyme, sont des fonctions littérales, c'est à dire des fonctions qui ne sont pas déclarées, mais qui sont passées directement comme expression. Dans l'exemple suivant :

max(strings, { a, b -> a.length < b.length })
La fonction max est une fonction d'ordre supérieur, qui prend une fonction en second paramètre. Cette argument est une expression, qui est elle même une fonction : une fonction littérale qui correspond à la fonction nommée suivante :

fun compare(a: String, b: String): Boolean = a.length < b.length

Syntaxe de l'expression lambda

La syntaxe complète d'une expression lambda est la suivante :

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
Une expression lambda est toujours entourée d'accolades. Les paramètres dans la syntaxe complète, sont déclarés dans ces accolades, et leur typage est optionnel. Le corp est situé après la ->. Si le type de retour inféré de la lambda n'est pas Unit, la dernière (et possiblement seule) expression dans le corps de la lambda est considéré comme la valeur de retour.
En retirant toutes les annotations optionnelles, le code devient :

val sum = { x: Int, y: Int -> x + y }

Passer une lambda en paramètre

En Kotlin, il y a une convention : si le dernier paramètre d'une fonction est une fonction, alors l'expression lambda, passée en paramètre peut être placée à l'extérieur des parenthèses :

val product = items.fold(1) { acc, e -> acc * e }
Cette notation est appelée lambda de fin (trailing lambda).
Si la lambda, est le seul argument, alors les parenthèses peuvent être complètement omises :

run { println("...") }

it : le nom implicite du paramètre unique

Il est courant qu'une expression lambda ait un unique paramètre. Si le compilateur peut déterminer la signature de lui même, alors il n'est pas obligatoire de déclarer le paramètre unique, et omètre par la même occasion ->. Le paramètre sera implicitement déclaré avec le mon it :

var ints = listOf(0, 1,-2,3,-1)
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

Retourner une valeur depuis une expression lambda

Caractère souligné (underscore) pour les paramètres non utilisés

Si un paramètre de la lambda est inutilisé, alors on peut utiliser _ à la place du paramètre :

map.forEach { _, value -> println("$value!") }

Destructuration dans une lambda (depuis 1.1)

Exercice

Écrivez la lambda qui permet d'aditionner deux Int et stockez la dans une variable. Appelez cette lambda stockée à partir de la variable avec la fonction invoke().

val sum = TODO()
	

Solution


fun main() {
    val sum = { x: Int, y: Int -> x + y }
    println(sum.invoke(3, 5))
}
	
Ou de manière plus concise :

fun main() {
    val sum = { x: Int, y: Int -> x + y }
    println(sum(3, 5))
}
	

Exercice

Supposons que vous ayez besoin de développer une calculatrice. Commencez par écrire les lambdas correspondant aux opérations de base (addition, soustraction, multiplication et division), stockez les dans des variables, et tester leur usage avec la fonction executeOperation écrite ci-dessous.

inline fun executeOperation(x: Int, y: Int, operation: (Int, Int) -> Int) = operation(x, y)
	
inline permet d'optimiser l'appel de la méthode (il est optionnel).

Exercice


val addition: (Int, Int) -> Int = TODO()
val subtraction: (Int, Int) -> Int  = TODO()
val multiplication: (Int, Int) -> Int  = TODO()
val division: (Int, Int) -> Int  = TODO()

inline fun executeOperation(x: Int, y: Int, operation: (Int, Int) -> Int) = operation(x, y)

fun main() {
    println(executeOperation(10, 5, addition))
    println(executeOperation(10, 5, subtraction))
    println(executeOperation(10, 5, multiplication))
    println(executeOperation(10, 5, division))
}
	

Solution


val addition = { x: Int, y: Int -> x + y }
val subtraction = { x: Int, y: Int -> x - y }
val multiplication = { x: Int, y: Int -> x * y }
val division = { x: Int, y: Int -> x / y }

inline fun executeOperation(x: Int, y: Int, operation: (Int, Int) -> Int) = operation(x, y)

fun main() {
    println(executeOperation(10, 5, addition))
    println(executeOperation(10, 5, subtraction))
    println(executeOperation(10, 5, multiplication))
    println(executeOperation(10, 5, division))
}
	

Simplification d'une méthode Java en Kotlin

Exemple de code en Java :

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d(TAG, "User clicked button");
    }
});
                        
Qui va afficher un message de log dans la console, de type DEBUG :

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d(TAG, "User clicked button");
    }
});
                        
La version utilisant les lambdas Java :

button.setOnClickListener ( v -> {
    Log.d(TAG, "User clicked button");
});
                        
L'équivalent en Kotlin :

button.setOnClickListener( { v: View -> Log.d(TAG, "User clicked button"); })
                        
Le ; en fin de ligne n'est pas nécessaire :

button.setOnClickListener( { v: View -> Log.d(TAG, "User clicked button"); } )
                        
La lambda peut être sortie des parenthèses de la méthode :

button.setOnClickListener( { v: View -> Log.d(TAG, "User clicked button")  } )
                        
Les parenthèses de la méthode ne sont pas nécessaires :

button.setOnClickListener() { v: View -> Log.d(TAG, "User clicked button")  }
                        
Le type du paramètre est inféré par le compilateur :

button.setOnClickListener   { v: View -> Log.d(TAG, "User clicked button")  }
                        
Le paramètre est unique, et n'est pas utilisé dans notre expression, donc inutile :

button.setOnClickListener   { v       -> Log.d(TAG, "User clicked button")  }
                        
Ce qui nous donne au final le code suivant ...

button.setOnClickListener   {            Log.d(TAG, "User clicked button")  }
                        
Avec les espaces superflu en moins, cela donne :

button.setOnClickListener{ Log.d( TAG, "User clicked button" ) }
                        
Qui correspond au code Java :

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d(TAG, "User clicked button");
    }
});
                        
Ou encore :

button.setOnClickListener ( v -> {
    Log.d(TAG, "User clicked button");
});
                        

Extensions

Kotlin permet d'étendre les fonctionnalités d'une classe, sans avoir besoin d'en hériter ni d'utiliser le design pattern du décorateur. Il faut utiliser la déclaration spéciale appelée : extensions. Par exemple, il est possible d'ajouter des nouvelles fonctions à une classe d'une librairie externe, dont le code source n'est pas disponible. Il est possible d'utiliser ces fonctions comme si elles faisaient partie intégrante du code source original. Ce mécanisme est appelé extension de fonctions. Nous verrons par la suite, le mécanisme d'extension de propriétés, qui permet d'ajouter des propriétés à une classe existante.

Extensions de fonctions en Kotlin

Pour déclarer une fonction d'extention, nous devons la préfixer par le nom du type receveur, c'est à dire le type qui va être étendu. L'exemple ci-dessous ajoute la fonction swap à MutableList<Int>

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}
Le mot clé this, dans la fonction d'extension, fait référence à l'objet receveur (qui est juste avant le point). Maintenant, nous pouvons appeler cette fonction sur tous les objets de type MutableList<Int>.

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'list'
Testez ce code pour en comprendre la puissance.

Exercice

Écrivez une extension à la classe String qui permet de mettre la première lettre d'une chaîne de caractère en majuscule et le reste en minuscule (pour afficher par exemple un prénom, non composé).

fun String.firstUpper(): Any = TODO()

// Exemple d'appel :
fun main() {
    println("tristan".firstUpper()) // => Tristan
    println("TRISTAN".firstUpper()) // => Tristan
}
	

Solution


fun String.firstUpper() = this.first().toUpperCase() + this.substring(1).toLowerCase()

fun main() {
    println("tristan".firstUpper())
    println("TRISTAN".firstUpper())
}
	
Autre solution :

fun String.firstUpper(): Any = this.toLowerCase().capitalize()
	
Encore plus concis (sans utiliser le this) :

fun String.firstUpper() = toLowerCase().capitalize()
	
Bien entendu cette fonction à du sens, pour tout type de MutableListMutableList <T>, et peut donc être générique :

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}
Le type du receveur est déclaré avant le nom de la fonction, pour qu'il soit disponible à ce moment la.

Les extensions sont résolues de manière statique

Les extensions ne modifient pas les classes qu'elles étendent. En définissant une extension, vous n'ajoutez pas de membres à une classe, mais vous déclarez uniquement des nouvelles fonctions que l'on peut appeler avec la notation point.
Les fonctions sont distribuées de manière statique. De ce fait, la fonction d'extention appelée, est déterminée par le type de l'expression sur laquelle la fonction est appelée, et non pas par le type de l'évaluation de l'expression lors de l'exécution du code. Par exemple :

fun main() {
    open class Shape

    class Rectangle: Shape()

    fun Shape.getName() = "Shape"

    fun Rectangle.getName() = "Rectangle"

    fun printClassName(s: Shape) {
        println(s.getName())
    }

    printClassName(Rectangle())
}
L'exemple précédent affiche "Shape", car la fonction d'extension appelée dépend uniquement du type du paramètre de s, qui est de type Shape.
Si une classe comporte une fonction membre, et qu'une fonction d'extension est définie, avec le même type de receveur, le même nom et les mêmes types d'arguments, alors la fonction membre gagne toujours. Par exemple :

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType()
Le code affiche "Class method".
Par contre il est tout à fait correct de définir des fonctions d'extensions dont le nom est identique, mais avec une signature différente :

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType(i: Int) { println("Extension function") }

Example().printFunctionType(1)
Le code affiche "Extension function".

Receveur pouvant être null

Note : une extension peut être définie pour un receveur étant de type pouvant être null. Ce genre d'extension peut être appelée sur un objet, même si sa valeur est nulle, et peut tester this == null dans le corps de la fonction. C'est ce qui nous permet de toujours pouvoir faire appel à la méthode toString() en Kotlin, sans avoir besoin de tester la nullité. La vérification s'effectue dans la fonction d'extension.

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}
Le code affiche "Extension function".

Extensions de propriétés en Kotlin

De manière similaire, Kotlin permet l'extention de propriétés. Mais il faut garder en tête que nous ne pouvons pas vraiment ajouter des propriétés à un objet. En pratique c'est plus un moyen de récupérer et/ou modifier un élément de la classe que l'on souhaite étendre. Exemple 

val <T> List<T>.lastIndex: Int
    get() = size - 1

Exercice

Écrivez une propriété d'extension firstLetter pour la classe StringBuilder qui permet d'accéder à la première lettre contenue dans l'objet.

var StringBuilder.firstLetter: Char
    get() = TODO()
    set(value) = TODO()
	

Solution


import java.lang.StringBuilder

var StringBuilder.firstLetter: Char
    get() = get(0)
    set(value) = this.setCharAt(0, value)

fun main() {
    val message = StringBuilder("hello world !")
    println("${message.firstLetter} is the first letter of $message")
    message.firstLetter = 'H'
    println("${message.firstLetter} is the first letter of $message")
}
	
A noter, que le this dans this.setCharAt(0, value) est optionnel.

Closures en Kotlin

Une expression lambda, ou une fonction anonyme (ou une fonction locale, ou encore une expression object) peuvent accéder à leur environnement (closure), c'est à dire aux variables définies en dehors de leur périmètre (scope). Exemple :

var ints = listOf(1, 2, -2, 3, 4, -15, 5, 6, -1)
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
println(sum)

Exercice

Reprenez l'exemple précédent, et tester l'accès de la lambda à la variable sum située en dehors des accolades.
N'oubliez pas de déclarer la liste d'Ints contenus dans la variable ints.

var ints = listOf(1, 2, -2, 3, 4, -15, 5, 6, -1)
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
println(sum)
	

Solution


fun main() {
    var ints = listOf(1, 2, -2, 3, 4, -15, 5, 6, -1)
    var sum = 0
    ints.filter { it > 0 }.forEach {
        sum += it
    }
    println(sum)
}
	

Délégation

Concept de délégation en Kotlin

Le principe de délégation en informatique est le fait d'assigner le traitement d'une action d'une instance à une autre. La délégation peut être faite de manière definitive, ou temporaire. L'héritage est une délégation statique immuable. Par exemple, supposons que nous voulons que la classe B ait les même fonctionnalités que la classe A et que la classe B soit une classe A. Dans ce cas là, vous pouvez utiliser l'héritage. Cela donne une relation permanente entre les classes. En utilisant la délégation, vous pouvez passer en paramètre un autre objet d'un autre type, un sous type de la classe A, par exemple, à l'instance de B. Ce qui rend le mécanisme de délégation extrêmement puissant.

Délégation de fonctions en Kotlin

Exemple en Java :

interface Showable {
    void show();
}

class View implements Showable {

    @Override
    public void show() {
        System.out.println("View.show()");
    }

}

class CustomView implements View {

    @Override
    public void show() {
        System.out.println("CustomView.show()");
    }

}

class Screen implements Showable {

    private Showable showable;

    Screen(Showable showable) {
        this.showable = showable;
    }

    @Override
    public void show() {
        showable.show();
    }

}

Showable view = new View();
Showable customView = new CustomView();
new Screen(view).show(); //View.show()
new Screen(customView).show(); //CustomView.show()

Exemple en Kotlin


interface Nameable {
    var name: String
}

class JackName : Nameable {
    override var name: String = "Jack"
}

class LongDistanceRunner: Runnable {
    override fun run() {
        println("long")
    }
}

class Person(name: Nameable, runner: Runnable): Nameable by name, Runnable by runner

fun main(args: Array<String>) {
    val person = Person(JackName(), LongDistanceRunner())
    println(person.name) //Jack
    person.run() //long
}
Ce mécanisme permet d'étendre les fonctionnalités d'une classe, facilement. Pas besoin d'implémenter les méthodes de l'interface manuellement, c'est le compilateur qui s'en charge.
Il est alors possible de faire de l'héritage multiple, tel que dans l'exemple précédent.
Pour cela, il suffit d'utiliser le mot clé by.

Exercice

Reprenez l'exemple précédent, implémentez une nouvelle classe class TristanName : Nameable et class ShortDistanceRunner: Runnable qui seront utilisées par la classe Person :

fun displayPerson(person: Person){
    println(person.name)
    person.run()
}
	

Solution


interface Nameable {
    var name: String
}

class JackName : Nameable {
    override var name: String = "Jack"
}
class TristanName : Nameable {
    override var name: String = "Tristan"
}

class LongDistanceRunner : Runnable {
    override fun run() {
        println("long")
    }
}
class ShortDistanceRunner : Runnable {
    override fun run() {
        println("short")
    }
}

class Person(name: Nameable, runner: Runnable) : Nameable by name, Runnable by runner

fun displayPerson(person: Person){
    println(person.name)
    person.run()
}
fun main(args: Array<String>) {
    val person = Person(JackName(), LongDistanceRunner())
    displayPerson(person)
    val person2 = Person(TristanName(), ShortDistanceRunner())
    displayPerson(person2)
}
	

Exercice

Reprenez l'exemple précédent, ajoutez des attributs à l'interface Nameable, par exemple firstName, companyName et cityName, il faut implémenter ces nouveaux attributs dans les classes dérivées, et tester que notre objet Person à bien hérité de ces nouvelles propriétés .

Solution


interface Nameable {
    var name: String
    var firstName: String
    var companyName: String
    var cityName: String
}

class JackName : Nameable {
    override var name: String = "Jack"
    override var firstName: String = "Terence"
    override var companyName: String = "IBM"
    override var cityName: String = "London"
}
class TristanName : Nameable {
    override var name: String = "SALAUN"
    override var firstName: String = "Tristan"
    override var companyName: String = "STDev"
    override var cityName: String = "Marseille"
}

class LongDistanceRunner : Runnable {
    override fun run() {
        println("long")
    }
}
class ShortDistanceRunner : Runnable {
    override fun run() {
        println("short")
    }
}

class Person(name: Nameable, runner: Runnable) : Nameable by name, Runnable by runner

fun displayPerson(person: Person){
    println("${person.firstName} ${person.name} of ${person.companyName} from ${person.cityName}")
    person.run()
}
fun main(args: Array<String>) {
    val person = Person(JackName(), LongDistanceRunner())
    displayPerson(person)
    val person2 = Person(TristanName(), ShortDistanceRunner())
    displayPerson(person2)
}
	

Patience

Je vois dans vos regards, que vous n'êtes pas encore convaincu par cette fonctionnalité.

Patience, les prochains exemples seront plus convaincants (je l'espère en tout cas).

Délégation de propriétés en Kotlin

Comme vous le savez maintenant, pour utiliser une propriété en Kotlin, on utilise simplement son nom, préfixé d'un . :

class Foo {
    var prop: String? = null
}

val foo = Foo()
foo.prop = "something"
val another = foo.prop
Vous pouvez aussi écrire des getters et des setters sur mesure :

var str: String
    get() = this.toString()
    set(value) {
        println(value)
        field = value
    }
Parfois nos méthodes getters et setters contiennent le même code. Pour éviter la duplication du code, ou simplement encapsuler la logique de ces fonctions, vous pouvez utiliser la délégation de propriétés :

import kotlin.reflect.KProperty

class Example {
    val someName by NameDelegate()
    val otherName by NameDelegate()
}

class NameDelegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return property.name
    }
}

fun main() {
    val example = Example()
    println(example.someName)
    println(example.otherName)
}
Que va afficher le code ci-dessus ?
Est-il possible d'affecter une valeur à l'attribut someName, et pourquoi ?
Autre exemple de délégation :

import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}
Reprenez l'exemple précédent, sans changer le code de la fonction, main, et remplacer la délégation NameDelegate par la délégation Delegate ci-dessus. Que constatez vous ?

Patience

C'est un peu mieux maintenant ?

Mais vous n'êtes pas encore convaincu, pas de soucis, on continue.

Delegate standards

Kotlin fournit plusieurs classes de délégations standards :

Delegate lazy

Exemple de mise en oeuvre de la délégation lazy :

val myVar: String by lazy {
    println("Lazy init")
    "Hello"
}
println("myVar is not initialized yet")
println(myVar + " My dear friend")
De manière plus classique/concise, nous utiliserons :

val myString by lazy { "Some Value" }
Que va afficher le code ci-dessus ?

Delegate observable

Comme toutes les délégations standards, la classe de délégation observable se trouve dans la classe Delegates. Elle prend en paramètre une valeur initiale et une lambda qui sera exécutée à chaque fois que la valeur du champ sera modifiée, sa signature est la suivante :

inline fun <T> observable(
    initialValue: T,
    crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
): ReadWriteProperty<Any?, T>

Exemple d'utilisation


var updated = false
var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
    updated = true
}

println(max) // 0
println("updated is ${updated}")

max = 10
println(max) // 10
println("updated is ${updated}")
	
Que va afficher le code ci-dessus ?

Exercice

Écrivez une délégation by observable pour la variable maxCount qui affichera le nom de la propriété modifiée, son ancienne valeur et la nouvelle.

fun main() {
    var maxCount: Int = 0

    maxCount = 5
    maxCount = 10
    maxCount = 20
}
	
Qu'affiche le code ci-dessus ?

Solution


import kotlin.properties.Delegates

fun main() {
    var maxCount: Int by Delegates.observable(initialValue = 0) { property, oldValue, newValue ->
        println("${property.name} is being changed from $oldValue to $newValue")
    }

    maxCount = 5
    maxCount = 10
    maxCount = 20
}
	

Delegate vetoable

La syntaxe est pratiquement la même que observable, sauf que la lambda, doit retourner un Boolean, qui indique si la valeur doit être modifiée, on non.
Cette délégation est parfaite pour garantir qu'une valeur est comprise dans un interval cohérent, ou pour implémenter un framework de validation simplement.

 var age: Int by vetoable(initialValue = 0) { property, oldValue, newValue ->
   newValue > 0
}

Exercice

Essayez d'affecter les valeurs suivantes à la variable age déclarée comme vu précédemment :
  • 10
  • -1
  • 30
  • 0
Affichez la valeur de la variable à chaque affectation.

 var age: Int by vetoable(initialValue = 0) { property, oldValue, newValue ->
   newValue > 0
}
	

Solution


import kotlin.properties.Delegates.vetoable

fun main() {
    var age: Int by vetoable(initialValue = 0) { property, oldValue, newValue ->
        newValue > 0
    }

    age = 10
    println(age)
    age = -1
    println(age)
    age = 30
    println(age)
    age = 0
    println(age)
}
	

Exercice

Modifiez la condition de test pour afficher un message quand la valeur est rejetée.

Solution


import kotlin.properties.Delegates

fun main() {
    var age: Int by Delegates.vetoable(initialValue = 0) { property, oldValue, newValue ->
        if (newValue > 0) true else {println("${property.name} rejected value $newValue staying at value $oldValue"); false}
    }

    age = 10
    println(age)
    age = -1
    println(age)
    age = 30
    println(age)
    age = 0
    println(age)
}
	

Delegate notNull

C'est le plus simple des délégations standards. Il fonctionne comme lateinit, dans le sens ou il lève une IllegalStateException si la variable est accédé avant d'être initialisée.

var age by notNull<Int>()
fun main() = println(age)

Exercice

Modifiez le code pour le rendre valide (indiquez que nous allons initialiser la variable tardivement :

var person1: Person // Erreur sur cette ligne. à corriger.

fun main(args: Array<String>) {
    // initializing variable lately
    person1 = Person("Ted",28)
    print(person1.name + " is " + person1.age.toString())
}

data class Person(var name:String, var age:Int)
	

Solution


var person1 by notNull<Person>()

fun main(args: Array<String>) {
    // initializing variable lately
    person1 = Person("Ted",28)
    print(person1.name + " is " + person1.age.toString())
}

data class Person(var name:String, var age:Int)
	
Une remarque sur les bonnes pratiques utilisées (ou pas) dans cet exemple ?

Exercice

Écrivez une classe de délégation, qui permet d'afficher un message, quand une propriété est accédé, ou modifiée. Héritez de la classe ReadWriteProperty. Pour tester son usage nous écrirons une classe Demo contenant un attribut var name qui sera déléguée à notre class LogDelegate<T> :

class LogDelegate<T> : ReadWriteProperty<Any, T?> {}
	

Solution


import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class LogDelegate<T> : ReadWriteProperty<Any, T?> {
    private var value: T? = null
    override fun getValue(thisRef: Any, property: KProperty<*>): T? {
        println("LOG get $value")
        return value
    }
    override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
        println("LOG set : $value")
        this.value = value
    }
}

class Demo {
    var name by LogDelegate<String>()
}

val d = Demo()

fun main() {
    d.name = "Tristan"
    println(d.name)
}
	

Generics

Generics en Kotlin

Tout comme en Java, en Kotlin les classes peuvent avoir des paramètres typés :

class Box<T>(t: T) {
    var value = t
}
En général pour créer une instance de ce genre de classe, nous devons fournir le type de l'argument :

val box: Box<Int> = Box<Int>(1)
Mais si le type du paramètre peut être inféré, par exemple depuis le type des paramètres du constructeur, ou par un autre moyen, alors il est possible d'omètre le type des arguments :

val box = Box(1) // 1 est de type Int, donc le compilateur peut en déduire que nous utilisons un type : Box<Int>

Generics et invariance en Kotlin

Covariance en Kotlin

Invariance

Quand on utilise des classes simples, étendre de ces classes est simple. Mais quand on commence à utiliser des classes génériques, alors les règles se compliquent un peu.
L'invariance exprime le fait que bien que 2 classes aient une relation de hiérarchie, un type complexe (un généric) ne suit pas cette même hiérarchie. Pour que cela soit plus clair, prenons un exemple :

open class A
open class B : A()
Considérons les types suivants :

MutableList<A>
MutableList<B>

Relation ?

Quelle est la relation entre les deux MutableList ?
Quelle est la relation entre les types complexes ?

Testons


open class A
open class B : A()

fun main() {
    var objectA = A()
    var objectB = B()
    var listofA: MutableList<A> = mutableListOf<A>()
    var listofB: MutableList<B> = mutableListOf<B>()
    println("objectA is A " + (objectA is A))
    println("objectB is B " + (objectB is B))
    println("objectA is B " + (objectA is B))
    println("objectB is A " + (objectB is A))
    println("listofA is MutableList<A> " + (listofA is MutableList<A>))
    println("listofB is MutableList<B> " + (listofB is MutableList<B>))
    println("listofA is MutableList<B> " + (listofA is MutableList<B>))
    println("listofB is MutableList<A> " + (listofB is MutableList<A>))
}

Réponse

Comme nous avons pu le constater, les 2 listes n'ont aucune liaison entre elles, la MutableList est invariante.

Aller plus loin sur l'invariance

Covariance en Kotlin

La covariance exprime la relation entre deux jeux de données dont les deux sous-types vont dans la même direction, reprenons notre exemple :

open class A
open class B : A()
Considérons les types en lecture seule suivants :

List<A>
List<B>

Relation ?

Quelle est la relation entre les deux List ?
Quelle est la relation entre les types complexes ?

Testons


open class A
open class B : A()

fun main() {
    var objectA = A()
    var objectB = B()
    var listofA: List<A> = listOf<A>()
    var listofB: List<B> = listOf<B>()
    println("objectA is A " + (objectA is A))
    println("objectB is B " + (objectB is B))
    println("objectA is B " + (objectA is B))
    println("objectB is A " + (objectB is A))
    println("listofA is List<A> " + (listofA is List<A>))
    println("listofB is List<B> " + (listofB is List<B>))
    println("listofA is List<B> " + (listofA is List<B>))
    println("listofB is List<A> " + (listofB is List<A>))
}

Réponse

Comme nous avons pu le constater, le type List est covariant : List<B> est un sous-type de List<A>.
Cela fonctionne car la List<T> étant immutable, lors de l'affectation de List<B> dans une variable de type List<A>, une copie de la liste est effectuée.

Exercice

Prenons les classes suivantes :

abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Spider(val terrorFactor: Int): Animal(1)
Hierarchie animaux

Validons la hiérarchie

Vérifions que Dog et Spider sont bien des sous types de Animal :

val dog: Dog = Dog(10)
val spider: Spider = Spider(9000)
var animal: Animal = dog
animal = spider
Tout est OK.

Test avec les List

Faisons le même test que précédemment, en utilisant des List :

val dogList: List<Dog> = listOf(Dog(10), Dog(20))
val animalList: List<Animal> = dogList
La liste étant immuable (impossible de changer sa valeur après initialisation), cela fonctionne bien, car une copie de la liste est réalisée.
Covariance avec les animaux

Aller plus loin sur la covariance

Contravariance en Kotlin

Reprenons l'exemple des animaux, et supposons que nous souhaitions comparer ces animaux. Créons pour cela une interface Compare<T> avec une méthode compare(T item1, T item2), qui permet de comparer deux items.
Comparons les chiens du plus mignon au moins mignon. Le code du comparateur sera :

val dogCompare: Compare<Dog> = object: Compare<Dog> {
  override fun compare(first: Dog, second: Dog): Int {
    return first.cuteness - second.cuteness
  }
}
Essayons de l'affecter à une variable qui permet de comparer les Animal :

val animalCompare: Compare<Animal> = dogCompare // Compiler error

Comparaison d'animaux

Si nous voulons comparer tous les animaux, le mécanisme doit fonctionner pour tous les chiens et les araignées :

val animalCompare: Compare<Animal> = object: Compare<Animal> {
  override fun compare(first: Animal, second: Animal): Int {
    return first.size - second.size
  }
}
val spiderCompare: Compare<Spider> = animalCompare // Works nicely!
La relation entre le type et le type complexe sont inversés, on parle de contravariance.
Contravariance avec les animaux

Autre exemple avec des fruits

Prenons un autre exemple avec des fruits

Schéma des classes

Définition des fruits


open class Fruit
open class Apple: Fruit() // Apple extends Fruit
class Gala: Apple() // Gala extends Apple

Expression des variances


class Variance{
    val fruitProducer: () -> Fruit = ::Fruit
    val appleProducer: () -> Apple = ::Apple
    val galaProducer: () -> Gala = ::Gala
    val fruitConsumer: (Fruit) -> Unit = ::eatFruit
    val appleConsumer: (Apple) -> Unit = ::eatApple
    val galaConsumer: (Gala) -> Unit = ::eatGala
    val newFruitProducer1: () -> Fruit = appleProducer
    val newFruitProducer2: () -> Fruit = galaProducer
    val newAppleProducer: () -> Apple = galaProducer
    val newGalaConsumer1: (Gala) -> Unit = appleConsumer
    val newGalaConsumer2: (Gala) -> Unit = fruitConsumer
    val newAppleConsumer: (Fruit) -> Unit = fruitConsumer
}

Fonctions utilitaires


fun eatFruit(fruit: Fruit) {}
fun eatApple(apple: Apple) {}
fun eatGala(fruit: Gala) {}

Autres fonctionnalités

Casting de types en Kotlin

Il est possible de tester le type d'une variable :

fun foo(x: Any) {
    if (x is Person) {
        println("${x.name}") // This wouldn't compile outside the if
    }
}
Notez que nous n'avons pas besoin de caster l'objet x, pour quelle raison, à votre avis ?

Pour changer le type d'une variable, il est possible de le faire explicitement :

val p = x as Person
Si l'objet n'est pas vraiment de type Person (ou d'une sous classe), alors l'exception ClassCastException sera levée.
Si vous n'êtes pas sûr du type, mais que vous pouvez vous satisfaire d'un null, si l'instance n'est pas de type Person, vous pouvez utiliser as?. Notez que le type de retour sera Person? :

val p = x as? Person
Vous pouvez utiliser x as Person? pour caster un type qui peut être null. La différence entre cette solution et la précédente as? est que celle-ci échouera si x est une instance non nulle d'un autre type que Person :

                    val p = x as Person?

Exercice

Tester le cast de la variable x avec les 2 méthodes (as et as?).
Une première fois avec x de type Any contenant une instance de Person, et ensuite contenant une instance de Other.

class Person(var name: String)
class Other()
	
Vous testerez ensuite la différence entre les 2 autres types de cast (String en Int) :

val value = "Message"
println(value as? Int)
println(value as Int?)
	

Solution


class Person(var name: String)
class Other()

fun main() {
    fun foo(x: Any) {
        if (x is Person) {
            println("${x.name}") // This wouldn't compile outside the if
        }
    }

    var x: Any = Person("SALAUN")
    var p = x as Person
    var pNull = x as? Person

    x = Other()
    p = x as Person
    pNull = x as? Person


    val value = "Message"
    println(value as? Int)
    println(value as Int?)
}
	

Exercice

Prenons le code en Java, et convertissons le en Kotlin :

import java.util.ArrayList;
import java.util.List;

public class Test {

    private static int getDefaultSize(Object object){
        if(object instanceof String) {
            return ((String) object).length();
        } else if(object instanceof List) {
            return ((List) object).size();
        }
        return 0;
    }

    public static void main(String[] args) {

        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);

        System.out.println(getDefaultSize(list));
        System.out.println(getDefaultSize("list"));
    }
}
	

Solution


private fun getDefaultSize(anyObject: Any): Int {
    if (anyObject is String) {
        return anyObject.length
    } else if (anyObject is List<*>) {
        return anyObject.size
    }
    return 0
}
	
Ou encore plus concis :

private fun getDefaultSize(anyObject: Any) = when (anyObject) {
    is String -> anyObject.length
    is List<*> -> anyObject.size
    else -> 0
}

Tuples

Les types standards

Kotlin propose dans la librairie standard des Tuples :
  • Pair : qui contient 2 éléments.
  • Triple : qui contient 3 éléments.

Pair

Tous les détails sur la page officielle.
La classe peut contenir 2 éléments. Pratique lorsque l'on doit retourner deux valeurs dans une fonction.
  • first contient la première valeur.
  • second contient la seconde valeur.

Triple

Tous les détails sur la page officielle.
La classe peut contenir 3 éléments. Pratique lorsque l'on doit retourner trois valeurs dans une fonction.
  • first contient la première valeur.
  • second contient la seconde valeur.
  • third contient la troisième valeur.

Exercice

Écrivez une variable référençant une Pair de valeur, et les afficher séparément.
Procédez de la même manière avec Triple.
Les signatures des classes sont rappelées ci-dessous :

data class Pair<out A, out B> : Serializable
data class Triple<out A, out B, out C> : Serializable
	

Solution


fun main() {

    val pairValue= Pair(1, "x")
    val tripleValue = Triple("Tristan","SALAUN", 40)

    println(pairValue.first)
    println(pairValue.second)
    println(tripleValue.first)
    println(tripleValue.second)
    println(tripleValue.third)
}
	

Aller plus loin sur les Tuples

Tuples multiples

Deconstructing (Destructuring) Values

Parfois il est pratique de déstructurer un objet en plusieurs variables, par exemple :

data class Person(var name:String, var age: Int)
val tristan = Person("Tristan", 40)
val (name, age) = tristan
Cette syntaxe est appelée "destructuring declaration". Cette déclaration crée plusieurs variables en une seule fois. Dans notre exemple, les 2 variables déclarées, peuvent être utilisées de manière indépendantes :

println(name)
println(age)
Le code généré correspond à :

val name = person.component1()
val age = person.component2()
Les fonctions component1() et component2() est un nouvel exemple des conventions utilisées dans Kotlin ( tout comme +, *, ...).
Il est aussi possible d'utiliser le "destructuring" dans une boucle for :

for ((a, b) in collection) { ... }
Il peut y avoir n'importe quel objet à droite de la déclaration de déstructuration, tant que le nombre de fonctions composantes correspond. Et bien entendu leur nombre peut être plus important : component3(), component4(), etc.

Exemple : retourner deux valeurs depuis une fonction

Vous avez deux valeurs à retourner depuis une fonction, par exemple un objet result et un status. Une façon pratique de le faire, en Kotlin, est de déclarer une classe data, et retourner une instance de cet objet.

data class Result(val result: Int, val status: Status)
fun function(...): Result {
    // computations

    return Result(result, status)
}

// Now, to use this function:
val (result, status) = function(...)
Les classes data déclarent automatiquement les fonctions componentN(), donc la déstructuration fonctionne directement dans notre cas.

Exercice

Reprenons l'exemple avec les tuples et destructurez les Pair dans des variables a et b.
Faisons de même pour Triple avec c, d et e.

Solution


fun main() {

    val (a, b) = Pair(1, "x")
    val (c, d, e) = Triple("Tristan","SALAUN", 40)

    println(a)
    println(b)
    println(c)
    println(d)
    println(e)

    println("a = $a, b = $b, c = $c, d = $d and e = $e")
}
	

Destructuring des Map

Ignorer des valeurs avec _

Destructuring dans les lambdas

Gestion des exceptions

Les classes d'exception

Toutes les classes d'exceptions sont des descendantes de la classe Throwable. Toutes les exceptions ont un message, une pile d'exécution (stack trace), et une cause optionnelle.
Pour lever une exception, il faut utiliser l'expression throw :

throw Exception("Hi There!")
Pour attraper une exception, il faut utiliser l'expression try :

try {
    // some code
}
catch (e: SomeException) {
    // handler
}
finally {
    // optional finally block
}
Il peut y avoir 0 ou plus blocs de catch, le block finally est optionnel. Toutefois il doit y avoir au moins un block catch ou un block finally.

Try est une expression

Try est une expression, et en tant que telle, elle doit avoir une valeur de retour.

val numValue: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
La valeur retournée par l'expression est, soit la dernière expression du block try, ou la dernière expression du bloc catch. Le contenu du block finally n'affecte pas le résultat de l'expression.

Exercice

Testez différentes valeurs d'input ( "2005" et "azerty") pour affecter la valeur à numValue :

val numValue: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
	

Solution


import java.lang.Integer.parseInt
fun main() {
    var input = "2005"
    var numValue: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
    println(numValue)
    input = "azerty"
    numValue = try { parseInt(input) } catch (e: NumberFormatException) { null }
    println(numValue)
}
	

Exceptions vérifiées

Kotlin n'a pas d'exceptions vérifiées

Déclaration de constantes

Une propriété qui est connue au moment de la compilation est annotée avec le mot clé const.
Elle peut être utilisée dans les annotations :

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
					

L'objet compagnon

Pour les méthodes

Étudions l'exemple situé à l'adresse : https://odelia-technologies.com/blog/object-companion-kotlin.html

Une autre utilisation de l'objet companion est pour déclarer nos constantes, étant donné que le mot clé static n'existe pas en Kotlin :

companion object {
    private const val TAG = "ClassName"
}

Initialisation tardive

Les valeurs déclarées comme n'acceptant pas de valeur nulle, doivent être initialisées dans le constructeur, toutefois cela est parfois peu pratique. Par exemple des propriétés qui peuvent êtres initialisées via une injection de dépendance, ou dans une méthode setup dans un test unitaire. Nous ne voulons toutefois pas que la variable puisse avoir une valeur nulle, pour ce faire, nous pouvons utiliser le modifieur lateinit :

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}
					
Ce modifieur peut être utilisé sur des variables de type var, déclarées dans le corps de la classe (pas dans le constructeur primaire, et seulement si la propriété n'a pas d'accesseurs sur mesures (custom getter or setter).
Accéder à cette propriété avant son initialisation, lève une exception :

lateinit var allByDefault: String // error: explicit initializer required, default getter and setter implied
print(allByDefault) // KO, Exception : kotlin.UninitializedPropertyAccessException: lateinit property allByDefault has not been initialized
					

Vérification initialisation tardive

Depuis Kotlin 1.2, pour vérifier qu'une variable lateinit var à bien été initialisée, on peut utiliser .isInitialized :

if (foo::bar.isInitialized) {
    println(foo.bar)
}
					

Utilisation de lateinit

Étant donné le code suivant, que ce passe-il quand nous l'exécutons, pourquoi, et comment corriger le problème ?

class MyTest() {
    lateinit var subject: String

    fun displaySubject() {
        println(subject)
    }
}

fun main() {
    var myTest = MyTest()
    myTest.displaySubject()
}
	

Solution

Respectons notre contrat : initialisons la variable, comme promis :

fun main() {
    var myTest = MyTest()
    myTest.subject = "The best subject"
    myTest.displaySubject()
}
	

Annotation en Kotlin

Définition

Les annotations sont utilisées pour attacher des méta-données aux classes, interfaces, paramètres, etc., lors de la compilation. Ces annotations peuvent avoir un impact sur l'exécution du code.

Méta annotation en Kotlin

Lors de la déclaration d'une annotation, il est possible d'ajouter des méta-informations. Ci-dessous en voici quelques unes :
Nom de l'annotation Usage
@Target Liste de tous les types d'éléments possibles qui peuvent être annotés avec cette annotation.
@Retention Définie si l'annotation est stockée dans la fichier de la classe compilée, et s'il est visible via la réflexion lors de l'éxécution du programme.
@Repeatable Définie si l'annotation est applicable plusieurs fois sur un même bloc de code.
@MustBeDocumented Spécifie que l'annotation fait partis d'une API publique et doit donc être inclue dans la classe ou méthode.

Exemple d'annotation


@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class MyClass

Déclaration d'une annotation

L'annotation est déclarée en utilisant le modifieur annotation avant la class.

annotation class MyClass

Annoter un constructeur

Il est possible d'annoter un constructeur d'une classe, pour cela il faut préciser le mot clé constructor lors de la déclaration, et placer l'annotation avant ce mot clé.

class MyClass @Inject constructor( dependency: MyDependency){
    //. . .
}

Exemple d'une annotation

Déclaration de l'annotation :

import java.lang.annotation.ElementType
import java.lang.annotation.RetentionPolicy

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Ann(val value: Int)
Utilisation de l'annotation :

@Ann(value = 10)
class MyClass

fun main(args: Array<String>) {
    val c = MyClass()
    val x = c.javaClass.getAnnotation(Ann::class.java)
    println("Value:${x?.value}")
}
Que va afficher le code ci-dessus ?

Bonnes et mauvaises pratiques

Au lieu d'utiliser des tuples standards, il est recommandé d'utiliser des objets qui nommerons les champs pour leur donner plus de sens.

Exercice récapitulatif

Nous allons reprendre et mettre en pratique les notions que nous avons apprises jusqu'à présent. Pour cela nous allons développer un système de gestion d'utilisateurs.

1 - Définition des structures de données

Dans un premier temps, nous allons déclarer nos structures de données :
Une classe parente Person :
  • Un champ texte, firstName de type chaîne de caractère en lecture/écriture.
  • Un champ texte, lastName de type chaîne de caractère en lecture.

Classes filles

Une classe fille Friend qui hérite de Person qui contiens en plus :
  • Un champ de type Short : age en lecture/écriture.

Une classe fille Colleague qui hérite de Person qui contiens en plus :
  • Un champ role qui ne peux prendre que les valeurs suivantes : MANAGER, CEO, WORKER.

Une classe fille Contact qui hérite de Person qui contiens en plus :
  • Un champ texte, phoneNumber de type chaîne de caractère en lecture.
  • Un champ texte, email de type chaîne de caractère en lecture.

Solution proposée


open class Person(var firstName: String, var lastName: String)

class Friend(firstName: String, lastName: String, var age: Short) : Person(firstName, lastName)
class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName)
class Contact(firstName: String, lastName: String, var phoneNumber: String, var email: String) : Person(firstName, lastName)

enum class Role {
    MANAGER, CEO, WORKER
}
	

2 - Gestion avancée du numéro de téléphone

Ajoutons un peu plus de détails pour la gestion du numéro de téléphone : il sera composé maintenant deux champs :
  • Le type du numéro, qui ne peux prendre que les valeurs suivantes : MOBILE, FIX, FAX.
  • Un champ texte, value de type chaîne de caractère, avec une valeur initiale de "0000000000", qui ne peut prendre comme valeur qu'une chaîne de caractère, composée de 10 chiffres (utilisation de vetoable).

Solution proposée


import kotlin.properties.Delegates

open class Person(var firstName: String, var lastName: String)

class Friend(firstName: String, lastName: String, var age: Short) : Person(firstName, lastName)
class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName)
class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, var email: String) : Person(firstName, lastName)

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(type: PhoneType, value: String) {
    var value: String by Delegates.vetoable("0000000000") { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}
	

3 - Gestion avancée de l'age

Modifions notre classe Friend afin de ne pouvoir définir un age qu'avec une valeur valide :
  • Un nombre positif.
  • Inférieur à une constante MAX_AGE définie avec la valeur : 150.
Note, pour la gestion de la valeur, nous utiliserons un interval.

Solution


import kotlin.properties.Delegates

open class Person(var firstName: String, var lastName: String)

class Friend(firstName: String, lastName: String, age: Short) : Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }
}

class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName)
class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, varemail: String) : Person(firstName, lastName)

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(type: PhoneType, value: String) {
    var value: String by Delegates.vetoable("0000000000") { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150
	

4 - Affichage

Afin d'afficher nos objets, redéfinissons l'affichage pour les 3 classes filles.
Pour cela  :
  • Ajoutons une méthode toString abstraite à notre classe Person.
  • Implémentons la méthode dans les classes filles.

Au lieu d'afficher l'age, nous allons afficher le stade de la vie, correspondant à l'age :
  • Bébé : de la naissance à 2 ans.
  • Enfant : de 2 ans à 10 ans.
  • Adolescent : de 10 ans à 18 ans.
  • Adulte : de 18 ans à 70 ans.
  • Personne âgée : à partir de 70 ans.

Solution proposée


import kotlin.properties.Delegates

abstract class Person(var firstName: String, var lastName: String) {
    abstract override fun toString(): String
}

class Friend(firstName: String, lastName: String, age: Short) : Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }
    override fun toString() = "$firstName $lastName ${age.lifeStep()}"
}

class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"
}

class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, var email: String) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(type: PhoneType, value: String) {
    var value: String by Delegates.vetoable("0000000000") { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }
	

5 - Affichage liste

  • Définissons une liste qui contiendra plusieurs Person.
  • Affichons cette liste.
  • Préfixons l'affichage de l'objet, par son type ("Amis, Collègue ou Contact").
  • Préfixons l'affichage de l'objet par sa position dans la liste.

Solution proposée


import kotlin.properties.Delegates

abstract class Person(var firstName: String, var lastName: String) {
    abstract override fun toString(): String
}

class Friend(firstName: String, lastName: String, age: Short) : Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }
    override fun toString() = "$firstName $lastName ${age.lifeStep()}"
}

class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"
}

class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, var email: String) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(val type: PhoneType, value: String) {
    var value: String by Delegates.vetoable(value) { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
    override fun toString() = "$type $value"
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }

fun getType(person:Person) = when (person) {
    is Friend -> "Amis"
    is Colleague -> "Collègue"
    is Contact -> "Contact"
    else -> "Inconnu"
}

fun main() {

    val person: Person = Friend("Tristan", "SALAUN", 42.toShort())
    val person2: Person = Friend("Melody", "SALAUN", 25.toShort())
    val person3: Person = Colleague("Nicolas", "DUPOND", Role.MANAGER)
    val person4: Person = Colleague("Jean-Christophe", "ANDRE", Role.WORKER)
    val person5: Person = Contact("John", "DOE", PhoneNumber(PhoneType.MOBILE, "0612345678"), "john.doe@test.com")

    val personList = listOf(person, person2, person3, person4, person5)

    var counter = 0
    personList.forEach { println("${counter++} ${getType(it)} $it") }
}
	

6 - Sécurisation du code

Nous remarquons que dans la méthode getType, qui nous permet d'afficher le type d'objet en français, nous avons dû utiliser le else. Si nous avons besoin d'implémenter une nouvelle classe fille, il est fort probable, que nous oublions d'ajouter ce cas dans cette méthode. Pour fiabiliser le code, nous allons mettre en place une classe scellée pour la classe Person.

Solution proposée


import kotlin.properties.Delegates

sealed class Person(var firstName: String, var lastName: String) {
    abstract override fun toString(): String
}

class Friend(firstName: String, lastName: String, age: Short) : Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }
    override fun toString() = "$firstName $lastName ${age.lifeStep()}"
}

class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"
}

class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, var email: String) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(val type: PhoneType, value: String) {
    var value: String by Delegates.vetoable(value) { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
    override fun toString() = "$type $value"
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }

fun getType(person:Person) = when (person) {
    is Friend -> "Amis"
    is Colleague -> "Collègue"
    is Contact -> "Contact"
}

fun main() {

    val person: Person = Friend("Tristan", "SALAUN", 42.toShort())
    val person2: Person = Friend("Melody", "SALAUN", 25.toShort())
    val person3: Person = Colleague("Nicolas", "DUPOND", Role.MANAGER)
    val person4: Person = Colleague("Jean-Christophe", "ANDRE", Role.WORKER)
    val person5: Person = Contact("John", "DOE", PhoneNumber(PhoneType.MOBILE, "0612345678"), "john.doe@test.com")

    val personList = listOf(person, person2, person3, person4, person5)

    var counter = 0
    personList.forEach { println("${counter++} ${getType(it)} $it") }
}
	

7 - Gestion des valeurs nulles

Ajoutons une classe Address qui sera une propriété optionnelle de la classe Friend, avec les propriétés suivantes :
  • Un champ texte line1 de type chaîne de caractère, obligatoire.
  • Un champ texte line2 de type chaîne de caractère, qui peut prendre une valeur nulle.
  • Un champ texte cp de type chaîne de caractère, au format similaire au numéro de téléphone : longueur de 5, ne contenant que des chiffres.
  • Un champ texte city de type chaîne de caractère, obligatoire.
  • Un champ texte country de type chaîne de caractère, prenant la valeur "France", par défaut.

Solution proposée


import kotlin.properties.Delegates

sealed class Person(var firstName: String, var lastName: String) {
    abstract override fun toString(): String
}

class Friend(firstName: String, lastName: String, age: Short, var address: Address? = null) : Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }
    override fun toString() = "$firstName $lastName ${age.lifeStep()}${if (address != null) "\n$address" else ""}"
}

class Colleague(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"
}

class Contact(firstName: String, lastName: String, var phoneNumber: PhoneNumber, var email: String) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(val type: PhoneType, value: String) {
    var value: String by Delegates.vetoable(value) { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }
    override fun toString() = "$type $value"
}

class Address(val line1: String, val line2: String? = null, cp: String, val city: String, val country: String = "France") {
    var cp: String by Delegates.vetoable(cp) { property, oldValue, newValue ->
        newValue.length == 5 && isNumber(newValue)
    }
    override fun toString() = "$line1\n${if (!line2.isNullOrEmpty()) "$line2\n" else ""}$cp $city\n$country"
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }

fun getType(person:Person) = when (person) {
    is Friend -> "Amis"
    is Colleague -> "Collègue"
    is Contact -> "Contact"
}

fun main() {

    val person: Person = Friend("Tristan", "SALAUN", 42.toShort())
    val person2: Person = Friend("Melody", "SALAUN", 25.toShort())
    val person2b: Person = Friend("Jean", "DUPONT", 55.toShort(), Address("36, quai des Orfèvres", cp = "75001", city = "Paris"))
    val person3: Person = Colleague("Nicolas", "DUPOND", Role.MANAGER)
    val person4: Person = Colleague("Jean-Christophe", "ANDRE", Role.WORKER)
    val person5: Person = Contact("John", "DOE", PhoneNumber(PhoneType.MOBILE, "0612345678"), "john.doe@test.com")

    val personList = listOf(person, person2, person3, person4, person5, person2b)

    var counter = 0
    personList.forEach { println("${counter++} ${getType(it)} $it") }
}
	

8 - La fabrique

Afin de créer des instances de personnes, plus facilement, nous allons mettre en place une fabrique :
  • Nous allons ajouter une méthode "statique" à notre classe Person, qui retourne une instance d'une des classes fille.
  • Nous allons passer le constructeur des classes filles en private.
  • Nous allons ajouter une méthode statique, qui retourne une instance de la classe fille dans chaque classe fille.
  • Nous allons utiliser une classe de génération de valeurs aléatoires pour générer des prénoms, noms, ...

import kotlin.random.Random

class RandomValues {
    companion object {
        val firstNameList = listOf("Jean", "Pierre", "Clément")
        val lastNameList = listOf("DUPOND", "DUPONT", "MARTIN")

        fun getFirstName(): String {
            return firstNameList[Random.nextInt(0, firstNameList.size)]
        }

        fun getLasrName(): String {
            return lastNameList[Random.nextInt(0, lastNameList.size)]
        }

        fun getRole(): Role {
            return Role.values()[Random.nextInt(0, Role.values().size)]
        }

        fun getPhoneNumber(): PhoneNumber {
            return PhoneNumber(PhoneType.MOBILE, "0000000000")
        }

        fun getEmail(): String {
            return "test@test.com"
        }
    }
}
	

Solution proposée


import kotlin.properties.Delegates
import kotlin.random.Random

sealed class Person(var firstName: String, var lastName: String) {
    abstract override fun toString(): String

    companion object {
        fun getPerson(): Person {
            val typeId = Random.nextInt(0, 3)
            return when (typeId) {
                0 -> Friend.getFriend()
                1 -> Colleague.geColleague()
                2 -> Contact.geContact()
                else -> Friend.getFriend()
            }
        }
    }
}

class Friend private constructor(firstName: String, lastName: String, age: Short, var address: Address? = null) :
    Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }

    override fun toString() = "$firstName $lastName ${age.lifeStep()}${if (address != null) "\n$address" else ""}"

    companion object {
        fun getFriend() =
            Friend(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                age = Random.nextInt(0, MAX_AGE).toShort()
            )
    }
}

class Colleague private constructor(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"

    companion object {
        fun geColleague() =
            Colleague(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                role = RandomValues.getRole()
            )
    }
}

class Contact private constructor(
    firstName: String,
    lastName: String,
    var phoneNumber: PhoneNumber,
    var email: String
) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"

    companion object {
        fun geContact() =
            Contact(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                phoneNumber = RandomValues.getPhoneNumber(),
                email = RandomValues.getEmail()
            )
    }
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(val type: PhoneType, value: String) {
    var value: String by Delegates.vetoable(value) { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }

    override fun toString() = "$type $value"
}

class Address(
    val line1: String,
    val line2: String? = null,
    cp: String,
    val city: String,
    val country: String = "France"
) {
    var cp: String by Delegates.vetoable(cp) { property, oldValue, newValue ->
        newValue.length == 5 && isNumber(newValue)
    }

    override fun toString() = "$line1\n${if (!line2.isNullOrEmpty()) "$line2\n" else ""}$cp $city\n$country"
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }

fun getType(person: Person) = when (person) {
    is Friend -> "Amis"
    is Colleague -> "Collègue"
    is Contact -> "Contact"
}

fun main() {
    val randomPersonList = mutableListOf<Person>()
    for (i in 0..10) {
        randomPersonList.add(Person.getPerson())
    }
    var counter = 0
    randomPersonList.forEach { println("${counter++} ${getType(it)} $it") }
}
	

9 - Interface

Ajoutons une interface JsonExport qui comporte une méthode toJson qui va nous permettre d'exporter nos contacts en JSON.

interface JsonExport {
    fun toJson(): String
}

sealed class Person(var firstName: String, var lastName: String) : JsonExport {
....
}
	

Solution proposée


import kotlin.properties.Delegates
import kotlin.random.Random

interface JsonExport {
    fun toJson(): String
}

sealed class Person(var firstName: String, var lastName: String) : JsonExport {
    abstract override fun toString(): String

    companion object {
        fun getPerson(): Person {
            val typeId = Random.nextInt(0, 3)
            return when (typeId) {
                0 -> Friend.getFriend()
                1 -> Colleague.geColleague()
                2 -> Contact.geContact()
                else -> Friend.getFriend()
            }
        }
    }
}

class Friend private constructor(firstName: String, lastName: String, age: Short, var address: Address? = null) :
    Person(firstName, lastName) {
    val age: Short by Delegates.vetoable(age) { property, oldValue, newValue ->
        newValue in 0..MAX_AGE
    }

    override fun toString() = "$firstName $lastName ${age.lifeStep()}${if (address != null) "\n$address" else ""}"

    companion object {
        fun getFriend() =
            Friend(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                age = Random.nextInt(0, MAX_AGE).toShort()
            )
    }

    override fun toJson() = """
{
    "type": "Friend",
    "firstName": "$firstName",
    "lastName": "$lastName",
    "age": "$age"
    ${if (address != null) """, "line2": "${address?.toJson()}\n" """ else ""  }
}
        """.trimIndent()
}

class Colleague private constructor(firstName: String, lastName: String, var role: Role) : Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $role"

    companion object {
        fun geColleague() =
            Colleague(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                role = RandomValues.getRole()
            )
    }

    override fun toJson() = """
{
    "type": "Colleague",
    "firstName": "$firstName",
    "lastName": "$lastName",
    "role": "$role"
}
        """.trimIndent()
}

class Contact private constructor(
    firstName: String,
    lastName: String,
    var phoneNumber: PhoneNumber,
    var email: String
) :
    Person(firstName, lastName) {
    override fun toString() = "$firstName $lastName $phoneNumber $email"

    companion object {
        fun geContact() =
            Contact(
                firstName = RandomValues.getFirstName(),
                lastName = RandomValues.getLasrName(),
                phoneNumber = RandomValues.getPhoneNumber(),
                email = RandomValues.getEmail()
            )
    }

    override fun toJson() = """
{
    "type": "Contact",
    "firstName": "$firstName",
    "lastName": "$lastName",
    "phoneNumber": "$phoneNumber",
    "email": "$email"
}
        """.trimIndent()
}

enum class Role {
    MANAGER, CEO, WORKER
}

class PhoneNumber(val type: PhoneType, value: String) : JsonExport {
    var value: String by Delegates.vetoable(value) { property, oldValue, newValue ->
        newValue.length == 10 && isNumber(newValue)
    }

    override fun toString() = "$type $value"
    override fun toJson() = """
"phone": {
"type": "$type",
"value": "$value"
}
        """.trimIndent()
}

class Address(
    val line1: String,
    val line2: String? = null,
    cp: String,
    val city: String,
    val country: String = "France"
) : JsonExport {
    var cp: String by Delegates.vetoable(cp) { property, oldValue, newValue ->
        newValue.length == 5 && isNumber(newValue)
    }

    override fun toString() = "$line1\n${if (!line2.isNullOrEmpty()) "$line2\n" else ""}$cp $city\n$country"
    override fun toJson() = """
"address": {
"line1": "$line1",
${if (!line2.isNullOrEmpty()) """ "line2": "$line2\n" """ else ""  }
"cp": "$cp",
"city": "$city",
"country": "$country"
}
        """.trimIndent()
}

fun isNumber(s: String?): Boolean {
    return if (s.isNullOrEmpty()) false else s.all { Character.isDigit(it) }
}

enum class PhoneType {
    MOBILE, FIX, FAX
}

const val MAX_AGE = 150

fun Short.lifeStep() =
    when (this) {
        in 0..2 -> "Bébé"
        in 2..10 -> "Enfant"
        in 10..18 -> "Adolescent"
        in 18..70 -> "Adulte"
        else -> "Personne âgée"
    }

fun getType(person: Person) = when (person) {
    is Friend -> "Amis"
    is Colleague -> "Collègue"
    is Contact -> "Contact"
}

fun main() {
    val randomPersonList = mutableListOf<Person>()
    for (i in 0..10) {
        randomPersonList.add(Person.getPerson())
    }
    var counter = 0
    randomPersonList.forEach { println("${counter++} ${getType(it)} $it") }

    println("""{ "personList" : [ """)
    randomPersonList.forEach { println("${it.toJson()},") }
    println(""" ] }""")
}
	

10 - Fold

Nous allons maintenant utiliser la méthode fold, pour obtenir des statistiques sur notre liste d'éléments :
  • La taille moyenne du prénom (en nombre de lettres).
  • L'age cumulé des Friend.

Solution proposée


    val firstNameCumulatedSize = randomPersonList.fold(0) { acc, person ->
        acc + person.firstName.length
    }
    println("$firstNameCumulatedSize / ${randomPersonList.size} = ${firstNameCumulatedSize.toFloat() / randomPersonList.size.toFloat()}")

    val ageCumulated = randomPersonList.fold(0) { acc, person ->
        if(person is Friend) {
            acc + person.age
        } else {
            acc
        }
    }
    println("$ageCumulated")
	

11 - Opérateur

Nous allons maintenant définir l'opérateur +, pour notre classe Person qui :
  • Prend un prénom aléatoire.
  • Concatène les deux noms.
  • Définis un age à 0.
Pour l'exemple, nous allons rendre public de nouveau le constructeur de l'objet Friend.

Solution proposée


operator fun Person.plus(other:Person) = Friend(RandomValues.getFirstName(), "${this.lastName}-${other.lastName}", 0 )
	

Interopérabilité

Interopérabilité avec Java

Kotlin est conçu pour être interopérable avec le Java. Le code existant Java peut être appelé en Kotlin, naturellement, et du code Kotlin peut être appelé assez facilement en Java. Par exemple :

import java.util.*

fun demo(source: List<Int>) {
    val list = ArrayList<Int>()
    // 'for'-loops work for Java collections:
    for (item in source) {
        list.add(item)
    }
    print(list)

    // Operator conventions work as well:
    for (i in 0..source.size - 1) {
        list[i] = source[i] // get and set are called
    }
    print(list)
}
                

fun main() {
    demo(listOf(1,3,5,74,1,-2,5,98))
}

Getters et Setters

Les méthodes qui suivent la convention Java de nomage pour les getters et les setters (pas d'argument, avec un nom commençant par get et un seul argument avec un nom commençant par set) sont représentées comme des propriétés en Kotlin. Les accesseurs pour les valeurs de type Boolean (le nom du getter commence par is et le nom du setter commence par set) sont représentés aussi comme des propriétés. Par exemple :

import java.util.Calendar

fun calendarDemo() {
    val calendar = Calendar.getInstance()
    if (calendar.firstDayOfWeek == Calendar.SUNDAY) {  // call getFirstDayOfWeek()
        calendar.firstDayOfWeek = Calendar.MONDAY      // call setFirstDayOfWeek()
    }
    if (!calendar.isLenient) {                         // call isLenient()
        calendar.isLenient = true                      // call setLenient()
    }
}

fun main() {
    calendarDemo()
}

Un autre exemple

Écrivez la classe JAVA suivante (en ajoutant les getters/setters avec le menu) :

public class Customer {

    private String firstName;
    private String lastName;
    private int age;

    //standard setters and getters
}

Utilisation en Kotlin

Nous pouvons utiliser la classe Customer directement dans notre code en Kotlin :

fun main() {
    val customer = Customer()

    customer.firstName = "Frodo"
    customer.lastName = "Baggins"

    println("${customer.firstName} ${customer.lastName}")
}

Remarques

Nous devons nous rappeler que si une classe Java ne comporte que des méthodes setter, la propriété ne sera pas accessible car Kotlin ne prend pas en charge les propriétés en écriture seule (set-only).
Si une méthode retourne void, alors quand elle est appelée depuis Kotlin elle retournera Unit.

De Kotlin au Java

Appeler du Kotlin en Java

Il est assez facile d'appeler du code écrit en Kotlin depuis du code Java. Il y a cependant certains points qui méritent une attention particulière, nous allons développer certains points ci-dessous.

Les propriétés

Une propriété Kotlin sera compilée en Java, de la manière suivante :
  • Une méthode getter, sera formatée en préfixant le nom de la propriété par get.
  • Une méthode setter, sera formatée en préfixant le nom de la propriété par set(pour les propriétés de type var).
  • Un champ privé aura le même nom que le nom de la propriété.

Par exemple : var firstName: String sera compilée en Java de la manière suivante :

private String firstName;

public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

Propriétés (suite)

Si le nom de la propriété commence par is, alors la règle diffère : le nom du getter sera le même que la propriété, et le nom du setter sera obtenu en replaçant le is par set. Par exemple, une propriété isOpen, le getter sera isOpen() et le setter : setOpen. Cette règle est valable pour toutes les propriétés, peu importe leur type, pas seulement pour les propriétés de type Boolean.

Exercice

Déclarer les 2 variables suivantes en Kotlin, dans un fichier séparé.

var firstName = ""
var isOpen = false
	
Obtenez le bytecode Kotlin avec le menu : Tools, Kotlin, Show Kotlin Bytecode.
Cliquez ensuite sur le bouton Decompile, pour obtenir le code Java équivalent.
Que constatez vous ?

Les fonctions au niveau du package

Toutes les fonctions et propriétés déclarées dans un fichier app.kt dans un package org.example, incluant toutes les fonctions d'extention, sont compilées dans des méthodes statiques d'une classe nomée org.example.AppKt. Exemple :

// app.kt
package org.example
class Util

fun getTime(): Int { println("10h21"); return 10 }

// Java
import org.example.AppKt;
import org.example.Util;

public class Test {

   Util utilVar = new Util();
   int timeValue = AppKt.getTime();
}

Nom de la classe Java générée

Nous pouvons changer le nom de la classe Java généré en utilisant l'annotation @JvmName :

@file:JvmName("DemoUtils")

package org.example

class Util

fun getTime() { /*...*/ }

// Java
new org.example.Util();
org.example.DemoUtils.getTime();

Multiples noms de classes identiques

Avoir plusieurs fichiers avec le même nom de classe Java généré (même package, et même nom, ou la même annotation @JvmName) est normalement une erreur. Toutefois, nous pouvons indiquer au compilateur de générer une classe facade qui aura le nom correspondant, et contiendra toutes les déclarations dans une même fichier. Pour cela, nous devons utiliser l'annotation @JvmMultifileClass dans tous les fichiers.

// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getTime() { /*...*/ }

// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package org.example

fun getDate() { /*...*/ }

// Java
org.example.Utils.getTime();
org.example.Utils.getDate();

Les champs d'instances

Si vous voulez exposer une propriété Kotlin en tant que champs d'instance en Java, il faut l'annoter avec @JvmField. Le champ aura alors la même visibilité que la propriété d'origine.

class User(id: String) {
    @JvmField val ID = id
}

// Java
class JavaClient {
    public String getID(User user) {
        return user.ID;
    }
}
Une propriété modifiée avec lateinit sera aussi exposée en tant que champs d'instance. La visibilité du champ sera la même que la visibilité du setter de la propriété.

Champs statiques (statics)

Les propriétés en Kotlin, déclarées dans un objet nommé, ou un objet compagnon sont par défaut privées, mais peuvent être rendues publiques en Java en utilisant une de ces méthodes :
  • L'annotation @JvmField
  • le modifieur lateinit
  • le modifieur const

statics / @JvmField

En annotant la propriété avec @JvmField, cela donnera un champ "static" avec la même visibilité que la propriété elle même :

class Key(val value: Int) {
    companion object {
        @JvmField
        val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
    }
}

// Java
Key.COMPARATOR.compare(key1, key2);
// public static final field in Key class

statics / lateinit

En modifiant la propriété avec lateinit, cela donnera un champ "static" avec la même visibilité que la propriété elle même :

object Singleton {
    lateinit var provider: Provider
}

// Java
Singleton.provider = new Provider();
// public static non-final field in Singleton class

statics / const

Une propriété déclarée avec const (dans une classe ou au top niveau (top level) ), seront traduites en champs statiques en Java :

// file example.kt
object Obj {
    const val CONST = 1
}

class C {
    companion object {
        const val VERSION = 9
    }
}

const val MAX = 239

// Java
int const = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;

Les méthodes statiques

Les méthodes default dans les interfaces

La visilibité

Visibilité en Kotlin Visibilité en Java Commentaire
private (members)
private (top level)
protected
internal
public public RAS

Surcharges

Normalement, si vous écrivez une fonction Kotlin avec des paramètres par défauts, en Java, c'est la version avec tous les paramètres qui sera disponible. Si vous voulez exposer de multiples méthodes (surcharge) avec des signatures différentes, vous pouvez utiliser l'annotation @JvmOverloads.
Cette annotation fonctionne de partout : constructeurs, méthodes statiques, etc. Toutefois elle ne peut pas être utilisée sur des méthodes abstraites, ni dans dans interfaces.
Exemple :

class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
    @JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}
Pour chaque paramètre avec une valeur par défaut, une méthode de surcharge sera générée. Dans notre exemple, cela donnera :

// Constructors:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)

// Methods
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }

Nulls de Java

Kotlin est bien connu pour sa fonctionnalité de sécurité null, mais comme nous le savons, ce n’est pas le cas pour Java, ce qui le rend peu pratique pour les objets qui en proviennent. Un exemple très simple permet de mettre cela en relief. Prenez le code suivant :

// Java
public class Nullable {

    public String get(){
        return null;
    }
}

fun main() {
    Nullable().get().length
}
Que se passe t'il quand on lance le code ?
Que pouvons nous faire pour éviter l'erreur ?

Solution


fun main() {
    Nullable().get()?.length
}
	

Le Kotlin dans Java

La classe Kotlin

Dans cette section nous allons voir comment appeler du Kotlin en Java. Créons une class Shape, en Kotlin, avec des propriétés : height, width et area, et deux fonctions : shapeMessage et draw :

// Shape.kt
class Shape(var width: Int, var height: Int, val shape: String) {
    var area: Int = 0
    fun shapeMessage() {
        println("Hi i am $shape, how are you doing")
    }
    fun draw() {
        println("$shape is drawn")
    }
    fun calculateArea(): Int {
        area = width * height
        return area
    }
}

L'appel en Java

Vous pouvez instancier la classe Kotlin de la même manière que si vous instanciez une classe en Java. Par exemple :

public class FromKotlinClass {
    public static void callShapeInstance() {
        Shape shape = new Shape(5,5,"Square");
        shape.shapeMessage();
        shape.setHeight(10);
        System.out.println(shape.getShape() + " width " + shape.getWidth());
        System.out.println(shape.getShape() + " height " + shape.getHeight());
        System.out.println(shape.getShape() + " area " + shape.calculateArea());
        shape.draw();
    }
    public static void main(String[] args) {
        callShapeInstance();
    }
}

Appel d'un Singleton Kotlin

Il est possible d'appeler une classe Singleton Kotlin en Java, en utilisant le mot clé object :

// Kotlin
object Singleton {
    fun happy() {
        println("I am Happy")
    }
}
Pour appeler le singleton en Java, il faudra utiliser le mot clé INSTANCE :

// Java
public static void main(String args[]) {
    Singleton.INSTANCE.happy();
}

Appel d'un Singleton (suite)

Il est possible d'éviter l'utilisation du mot clé INSTANCE en utilisant l'annotation @JvmStatic :

object Singleton {

    fun happy() {
        println("I am Happy")
    }

    @JvmStatic fun excited() {
        println("I am very Excited")
    }
}
Ce qui donnera l'appel en Java :

public static void main(String args[]) {
    Singleton.INSTANCE.happy();
    Singleton.excited();
}

Appel de fonctions top level

Les fonctions en Kotlin, n'ont pas besoin d'être déclarées dans une Classe, comme c'est le cas en Java. Nous allons voir en détail comment appeler ce genre de fonctions.
Prenons par exemple un fichier utils.kt :

fun logD(message: String) {
    Log.d("", message)
}
fun logE(message: String) {
    Log.e("", message)
}
En Java, il sera possible d'appeler simplement ces fonctions comme suit :

UtilsKt.logD("Debug");
UtilsKt.logE("Error");

Extensions de fonctions à partir du Java

Exemple d'extension

Supposons que nous avons le code suivant :

fun String.firstUpper() = this.first().toUpperCase() + this.substring(1).toLowerCase()
Que l'on pourrait appeler en Kotlin :

println("tristan".firstUpper())
println("TRISTAN".firstUpper())
En Java cela deviendra :

System.out.println(ExtensionKt.firstUpper("Tristan"));
Comme vous pouvez le voir, l'objet sur lequel est appliqué la fonction (le receveur/"reveiver") est ajouté en paramètre de la fonction. De plus les paramètres optionnels deviennent obligatoires, car Java ne gère pas cette fonctionnalité.

Interopérabilité avec Java 7 et Java 8

Kotlin et un langage récent et le langage Java comporte de nombreuses fonctionnalités et est en constante évolution. De ce fait, toutes les fonctionnalités de Java ne sont pas encore supportés en Kotlin. Par exemple les méthodes default dans les interfaces, ne sont disponibles que pour la JVM 1.8, et l'annotation @JvmDefault correspondante est expérimentale en Kotlin 1.3.

Java Réflexion avec Kotlin

Exercice

Reprenons notre classe Java Customer :

public class Customer {

    private String firstName;
    private String lastName;
    private int age;

    //standard setters and getters
}
La reflexion fonctionne à la fois sur les classes Kotlin et Java.
Testons la classe Customer avec les méthodes de réflexion Kotlin :
  • <ClassName>::class.java permet de récupérer la classe.
  • la propriété constructors de Class<T> contient le tableau des constructeurs.
  • la propriété name du Constructor contient le non du constructeur.
Afficher le nombre et le nom du/des constructeur(s).

Solution


fun main() {
    val type = Customer::class.java
    val constructors = type.constructors

    println(constructors.size)
    println(constructors[0].name)
}
	

Appel d'instances, et champs statiques

Appel d'une data class

Appel d'une sealed class

Les fonctions inline

Kotlin Réflexion

reflexion Java en Kotlin

La réflexion fonctionne de manière équivalente en Java et en Kotlin. Prenons un exemple :

MyClass::class.java.methods
Qui permet de lister les méthodes d'une classe. Décomposons cette construction :
  • MyClass::class nous donne une représentation de la class MyClass
  • .java nous donne l'équivalent de java.lang.Class
  • .methods appelle la méthode java.lang.Class.getMethods()
Prenons un exemple concret :

data class ExampleDataClass(
    val name: String, var enabled: Boolean)

fun main() {
    ExampleDataClass::class.java.methods.forEach(::println)
}

Reflexion avancée en Kotlin

Correspondance des types

Kotlin n'utilise pas les types Java directement, ils sont convertis en leur équivalent Kotlin :
Java type Kotlin type
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Types non primitifs

Java type Kotlin type
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.Annotation kotlin.Annotation!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Types encapsulés

Java type Kotlin type
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Character kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

Standard Library

Kotlin Standard Library et collections dans Kotlin

La librairie standard de Kotlin fournit l'essentiel des méthodes pour développer tels que :
  • Les fonctions d'ordre supérieur, pour gérer les cas classiques (let, apply, use, synchronized, ...).
  • Des fonctions d'extension, qui fournissent des opérations pour interroger des collections et des séquences.
  • Des outils pour travailler avec les chaînes de caractères et les caractères.
  • Des extensions pour les classes du JDK pour que cela soit plus pratique de travailler avec des fichiers, les Entrés/Sorties (IO), et les fils d'exécutions (threading).

Les collections

Diagramme des interfaces des collections en Kotlin
Collection<T> est parente de toute la hiérarchie des collections. Elle définie le comportement commun d'une collection en lecture seule : la récupération de la taille de la liste, la vérification d'appartenance d'un objet à la collection, etc.
Collection hérite d'Iterable<T> qui définie les opérations pour itérer sur les éléments. C'est le type à utiliser pour gérer les différents types de collections. Dans les cas plus précis, préférer List ou Set.

fun printAll(strings: Collection<String>) {
    for(s in strings) print("$s ")
    println()
}

fun main() {
    val stringList = listOf("one", "two", "one")
    printAll(stringList)

    val stringSet = setOf("one", "two", "three")
    printAll(stringSet)
}
					
MutableCollection est une Collection avec les opérateurs d'écriture tels que add et remove.

fun List<String>.getShortWordsTo(shortWords: MutableList<String>, maxLength: Int) {
    this.filterTo(shortWords) { it.length <= maxLength }
    // throwing away the articles
    val articles = setOf("a", "A", "an", "An", "the", "The")
    shortWords -= articles
}

fun main() {
    val words = "A long time ago in a galaxy far far away".split(" ")
    val shortWords = mutableListOf<String>()
    words.getShortWordsTo(shortWords, 3)
    println(shortWords)
}
					

Différences et similitudes entre List<T> et Array<T>

Exemples d'utilisation :

// Initializing array and list
val array = arrayOf(1, 2, 3)
val list = listOf("apple", "ball", "cow")
val mixedArray = arrayOf(true, 2.5, 1, 1.3f, 12000L, 'a') // mixed Array
val mixedList = listOf(false, 3.5, 2, 1.4f, 13000L, 'b') // mixed List
					


Les deux semblent similaires, ...

Définition de List<T>

Une liste est une interface. C'est une collection générique et ordonnée d'éléments. Les méthodes dans cette interface permettent un accès en lecture seule aux éléments.
Les opérations de lecture et écriture sont définies dans l'interface MutableList.

Définition de Array<T>

Les tableaux sont un conteneur d'objets qui regroupe un nombre fixe d'éléments d'un seul et même type.
La taille d'un tableau est établie de manière définitive lors de sa création.

Les tableaux peuvent être concaténés pour donner un nouveau tableau :

val array1 = arrayOf(1, 2, 3, 4, 5, 6)
val array2 = arrayOf(7, 8, 9, 10, 11, 12)
val newArray = array1 + array2
for(element in newArray) {
    println("$element - ")
} // 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12
					

Les similitudes

  1. Les 2 comportent un nombre finit d'éléments. Il n'est pas possible d'ajouter des éléments après initialisation. Leur taille est fixe et ne peut pas augmenter ni diminuer :
    
    array.add(1) // cannot be expanded or shrank
    list.add("dog") // cannot be expanded or shrank
    					
    Notons que les listes mutables peuvent changer de taille.
  2. Les 2 permettent de stocker plusieurs éléments et font partis de la famille des collections :
    
    val array = arrayOf(1, 2, 3)
    val list = listOf("apple", "ball", "cow")
    					

Les différences

  • Les Array sont modifiables (mutable), leur contenu peut changer de valeur, contrairement aux List qui sont en lecture seule (immutable) :
    
    val array = arrayOf(1, 2, 3)
    array[2] = 4 // OK
    for (element in array){
      println("$element, ") // 1, 2, 4
    }
    val list = listOf("apple", "ball", "cow")
    list[2] = "cat" //will not compile, lists are immutable
    // println(list) = {apple, ball, cow}
    
    val m = mutableListOf(1, 2, 3)
    m[0] = m[1] // OK
    					

Les différences (suite)

  • Les tableaux sont optimisés pour les types primitifs (IntArray, DoubleArray, CharArray, etc) : qui utilisent directement les tableaux Java primitifs (int[], double[], char[], etc.)
    Les listes en général n'ont pas d'implémentations optimisées pour les types primitifs.
    
    val optimisedIntegerArray = intArrayOf(1, 2, 3, 4, 5, 6) // optimised for Integer Array
    val optimisedDoubleArray = doubleArrayOf(1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8) // Optimised for Double Array
    val optimisedCharacterArray = charArrayOf('a', 'b', 'c', 'd') // Optimised for Character Array
    //List does not have intListOf, charListOf as such it is not optimised for primitive arrays
    					

Les différences (suite)

  • Array<T> est une classe dont l'implémentation est connue : c'est une région de mémoire séquentielle de taille fixe qui stocke les éléments. Dans la JVM elle est représenté par Java array
    List<T> et MutableList<T> sont des interfaces qui peuvent avoir plusieurs implémentations : ArrayList<T>, LinkedList<T>, etc.
  • Array<T> est invariant (Array<Int> n'est pas un Array<Number>, idem pour MutableList<T>
    Par contre List<T> est covariant (List<Int> est une List<Number>).
    
    val a: Array<Number> = Array<Int>(0) { 0 } // won't compile
    val l: List<Number> = listOf(1, 2, 3) // OK
    					

Les différences (suite)

  • Au niveau de l'interopérabilité JAVA, les 2 ne sont pas gérés de la même manière.
  • Certains types de tableaux sont utilisés dans les annotations, alors que pour les listes et autres collections cela n'est pas possible.
  • L'usage veut que l'on utilise de préférence les liste au lieu des tableaux, sauf dans le cas ou la performance est un critère critique.

Filtering, Mapping et Flatmapping en Kotlin

Entraînement en ligne

Plutôt que de réinventer la roue, je vous propose de vous entraîner sur les collections directement en ligne ICI.

Kotlin lazy evaluation

Ci-dessous, un exemple d'appel d'une fonction en mode "lazy".

fun main() {
    printValue(getValue())
}

private fun printValue(value: Lazy<Int>) = println("Nothing")

private fun getValue() = lazy {
    println("Returning 5")
    return@lazy 5
}
Que remarquez vous ?
Changez la valeur du println en :

"Nothing ${value.value}"
Quel changement observez vous, et pourquoi ?
FIXME : Ajouter le let .... https://medium.com/@fatihcoskun/kotlin-scoping-functions-apply-vs-with-let-also-run-816e4efb75f5 https://blog.engineering.publicissapient.fr/2019/02/15/pepite-5-comprendre-les-fonctions-standard-de-kotlin-les-fonctions-de-transformation/ https://blog.engineering.publicissapient.fr/2019/02/22/pepite-6-comprendre-les-fonctions-standard-de-kotlin-les-fonctions-de-mutation/ https://blog.mindorks.com/using-scoped-functions-in-kotlin-let-run-with-also-apply https://typealias.com/guides/understanding-let-also-run-apply/ https://webdevdesigner.com/q/example-of-when-should-we-use-run-let-apply-also-and-with-on-kotlin-40123/

Programmation asynchrone

Le problème de la programmation asynchrone

Depuis des décennies, les développeurs sont confrontés à un problème à résoudre : comment faire pour que les applications ne se bloquent pas. Que l'on développe pour un ordinateur, un mobile ou un serveur, nous voulons éviter que l'utilisateur attende, ou encore pire, des goulots d'étranglements qui empêcherait à l'application de passer à l'échelle.

Plusieurs approches sont possibles pour résoudre ce problème :
  • Traitements en parallèle (Threading).
  • Les "callbacks".
  • Futures, Promises et al.
  • Les extensions réactives.
  • Coroutines.
https://kotlinlang.org/docs/tutorials/coroutines/async-programming.html#threading
https://kotlinlang.org/docs/tutorials/coroutines/async-programming.html#callbacks
https://kotlinlang.org/docs/tutorials/coroutines/async-programming.html#futures-promises-et-al
https://kotlinlang.org/docs/tutorials/coroutines/async-programming.html#reactive-extensions
https://kotlinlang.org/docs/tutorials/coroutines/async-programming.html#coroutines

Coroutines en Kotlin

Mise en place du projet

Créez un nouveau projet : File / New / Project. Sélectionner Gradle / Kotlin / JVM.
Création projet

Nommage du module

Création projet

Nommage du projet

Création projet
Ouvrez le fichier build.gradle
Création projet

Ajoutez la dépendance


dependencies {
    ...
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
}
Une coroutine pourrait être vue comme un thread léger, car une coroutine peut être lancée en parallèle, s'attendre les unes les autres et communiquer ensemble. Mais la grosse différence est le coût de celles-ci : pratiquement rien. Il est possible d'en créer des centaines, sans impacter les performances, contrairement aux thread classiques.

Pour lancer une coroutine, le point de départ est la fonction launch {} :

launch {
//    ...
}
Les coroutines, utilisent un pool de threads pour fonctionner, mais un thread peut faire tourner plusieurs coroutines, donc il n'est pas nécessaire d'avoir beaucoup de threads lancés.
Lançons notre première coroutine :

println("Start")

// Start a coroutine
GlobalScope.launch {
    delay(1000)
    println("Hello")
}

Thread.sleep(2000) // wait for 2 seconds
println("Stop")
La fonction delay() fonctionne comme la fonctions Thread.sleep(), mais elle ne bloque pas le thread, elle suspend simplement la coroutine. Le thread retourne dans le pool de thread. Et la coroutine, reprendra son fonctionnement sur un thread disponible.
Q1 - Que se passe-t-il si l'on supprime la ligne Thread.sleep(2000) ?
Q2 - Que se passe-t-il si l'on remplace Thread.sleep(2000) par delay(2000) ?

Blocage du thread principal

Pour pouvoir utiliser la fonction delay(...), nous allons l'appeler dans une fonction runBlocking {} :

runBlocking {
    delay(2000)
}
Ce qui nous donnera au final :

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() {
    println("Start")

// Start a coroutine
    GlobalScope.launch {
        delay(1000)
        println("Hello")
    }

    // Step 1
    //Thread.sleep(2000) // wait for 2 seconds

    // Step 2
    //    delay(2000)

    // Step 3
    runBlocking {
        delay(2000)
    }
    println("Stop")
}

Lançons beaucoup de thread

Nous allons comparer les thread et les coroutines, en lançant, disons 1 million de processus en parallèle
Commençons avec les threads :

val c = AtomicLong()

for (i in 1..1_000_000L)
    thread(start = true) {
        c.addAndGet(i)
    }

println(c.get())
Que remarquez vous, concernant la charge de la machine ?

Lançons beaucoup de thread

Écrivez le même code avec des coroutines& :

val c = AtomicLong()

for (i in 1..1_000_000L)
    GlobalScope.launch {
        c.addAndGet(i)
    }

println(c.get())
Que constate-t-ons ?
Toutefois, le résultat n'est pas correct, toutes les coroutines n'ont pas terminé leur traitement avant que la fonction main() affiche le résultat. Nous allons corriger cela.

Async et Await en Kotlin

Une autre manière de lancer les coroutines, et d'utiliser async {}. C'est comme launch {} mais cela retourne une instance de Deferred<T> qui comporte une fonction await() qui retourne le résultat de la coroutine. Deferred<T> est une sorte de future très basique.
Nous allons donc maintenant lancer de nouveau le million de coroutines, et attendre leur retour. La variable de type AtomicLong n'est plus nécessaire 

val deferred = (1..1_000_000).map { n ->
    GlobalScope.async {
        n
    }
}
val sum = deferred.sumBy { it.await() }
println("Sum: $sum")
Quel est le problème ici ?
Il faut donc lancer la fonction await() dans un contexte de coroutine, nous utilisons de nouveau runBlocking {} 

val deferred = (1..1_000_000).map { n ->
    GlobalScope.async {
        n
    }
}
runBlocking {
    val sum = deferred.sumBy { it.await() }
    println("Sum: $sum")
}
Nous devrions obtenir le résultat suivant :

Sum: 1784293664

Parallèle

Cela fonctionne vraiment en parallèle ? Pour en être convaincu, ajoutons un délai à notre traitement 

val deferred = (1..1_000_000).map { n ->
    GlobalScope.async {
        delay(1000)
        n
    }
}
runBlocking {
    val sum = deferred.sumBy { it.await() }
    println("Sum: $sum")
}
Allons nous devoir attendre 1 million de secondes (11,5 jours) pour obtenir notre résultat ?

fonctions suspendues

Nous voulons extraire le code fonctionnel dans une fonction :

fun workload(n: Int): Int {
    delay(1000)
    return n
}
Le compilateur, n'est pas content, car delay ne peut être utilisé que dans une cadre de coroutine. Marquons la fonctions avec le mot clé suspend :

suspend fun workload(n: Int): Int {
    delay(1000)
    return n
}

Code final

Nous obtenons le code final, suivant :

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking

fun main() {

    suspend fun workload(n: Int): Int {
        //delay(3000)
        return n
    }

    val deferred = (1..1_000_000).map { n ->
        GlobalScope.async {
            workload(n)
        }
    }
    runBlocking {
        val sum = deferred.sumBy { it.await() }
        println("Sum: $sum")
    }
}

Aller plus loin sur les coroutines

Le guide officiel
Les exercices
https://blog.ineat-conseil.fr/2018/05/kotlin-les-coroutines/
https://code.i-harness.com/fr/docs/kotlin/docs/reference/coroutines
Voir le mécanisme de Channel.

Yield en Kotlin

Construction de séquences

Nous allons construire une séquence, de manière paresseuse, par exemple la suite de fibonacci en utilisant la fonction sequence qui utilise yield :

 fun main(args: Array<String>) {
    val fibonacciSeq = sequence {
        var a = 0
        var b = 1

        yield(1)

        while (true) {
            yield(a + b)

            val tmp = a + b
            a = b
            b = tmp
        }
    }

    // Print the first five Fibonacci numbers
    println(fibonacciSeq.take(8).toList())
}
Ce qui devrait nous afficher :

[1, 1, 2, 3, 5, 8, 13, 21]
Nous venons de définir une séquence, potentiellement infinie, générée par une coroutine. Validons ensemble que cette génération est bien paresseuse :

fun main(args: Array<String>) {
    val lazySeq = sequence {
        print("START ")
        for (i in 1..5) {
            yield(i)
            print("STEP ")
        }
        print("END")
    }

    // Print the first three elements of the sequence
    lazySeq.take(3).forEach { print("$it ") }
}
Combien de fois est affiché le message END ?
Que faudrait-il faire pour qu'il s'affiche ?
Que se passe-t'il si l'on demande par exemple 10 éléments de la séquence ?

Generation complète de la séquence

Si nous voulons générer la séquence d'un coup, nous utiliserons alors yieldAll, par exemple :

fun main(args: Array<String>) {
    val lazySeq = sequence {
        yield(0)
        yieldAll(1..10)
    }

    lazySeq.forEach { print("$it ") }
}

Génération filtrée de la séquence

Il est possible de décomposer notre génération de séquence :

suspend fun SequenceScope<Int>.yieldIfOdd(x: Int) {
    if (x % 2 != 0) yield(x)
}

val lazySeq = sequence<Int> {
    for (i in 1..10) yieldIfOdd(i)
}

fun main(args: Array<String>) {
    lazySeq.forEach { print("$it ") }
}

Filtrer une séquence

Nous pouvons filter une séquence en utilisant la méthode filter. Par exemple prenons les 8 premières valeurs de la suite de fibonacci qui sont supérieures à 20 :

fun main(args: Array<String>) {
    val fibonacciSeq = sequence {
        var a = 0
        var b = 1

        yield(1)

        while (true) {
            yield(a + b)

            val tmp = a + b
            a = b
            b = tmp
        }
    }

    // Print the first five Fibonacci numbers
    println(fibonacciSeq.filter { it > 20 }.take(8).toList())
}

Exercice

  • Reprenons l'exercice sur les personnes.
  • Créons une suite (sequence) de personnes, en appelant la fabrique.
  • Affichons les 10 premières personnes.
  • Avec un prénom de 4 lettres.

Solution proposée


val personSequence = sequence {
    while (true)
        yield(Person.getPerson())
}

fun main() {
    println(personSequence.filter { it.firstName.length == 4 }.take(10).toList())
}
	

Reactive extension en Kotlin

Librairies

Il existe plusieurs projets permettant de gérer l'asynchronisme plus facilement :

Bonnes et mauvaises pratiques

  • N'oubliez pas de stopper toute les coroutines en quittant votre programme. Exemple en Android :
    
     override fun onDestroy() {
        super.onDestroy()
        async.cancelAll()
    }
    
  • Utilisez les coroutines, uniquement quand cela est nécessaire.

Annexes

Codes source

Projet IntelliJ des exercices
Projet IntelliJ des exercices coroutines

Scripts en Kotlin

Kotlin peut être utilisé pour écrire des scripts. Exemple :

// list_folders.kts
import java.io.File
val folders = File(args[0]).listFiles { file -> file.isDirectory() }
folders?.forEach { folder -> println(folder) }
Pour lancer le script, il faut ajouter l'option -script et passer le chemin du script à lancer :

$ kotlinc -script list_folders.kts "path_to_folder_to_inspect"

Les fonctions inline

Utiliser des fonctions d'ordre supérieur, imposent des contraintes lors de l'exécution du programme : chaque fonction est un objet, qui comporte un contexte (closure), c'est à dire des variables accessibles depuis le corps de la fonction. L'allocation de mémoire (pour les objets fonction et les classes) et les appels virtuels engendrent une surcharge.
Dans plusieurs cas, il et possible d'éliminer cette contrainte en mettant en ligne (inlining) une expresion lambda. Voici un très bon exemple de fonction à "inliner" : la fonction lock()

lock(l) { foo() }
				
Au lieu de créer un objet de type fonction en paramètre, et de générer un appel à celle-ci, le compilateur pourrait produire le code suivant :

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}
Pour indiquer au compilateur de produire ce code, nous devons marquer la fonction lock() avec le modifieur inline:

inline fun <T> lock(lock: Lock, body: () -> T): T { ... }
Ce modifieur va impacter la fonction, en elle même, mais aussi la lambda passée en paramètre : tout sera mis en ligne à l'endroit de l'appel.
Utiliser cette technique pourra augmenter la taille du code compilé, il faut donc faire attention à son usage (éviter d'inliner des grosses fonctions), mais l'on gagnera en performances, particulièrement lors d'appel dans des boucles.

noinline

Non-local returns

Reified type parameterss

LiveTemplates

Mise en place

Les lives Templates, sont des raccourcis qui permettent d'écrire rapidement des bouts de code. Je vous fournis une série de Live Templates que nous utiliserons dans la suite de la formation. Procédons comme suit :
  • Récupération des templates :
  • Les copier dans : C:\Users<your_user>\AppData\Roaming\Google\AndroidStudio4.1\templates.
  • File/Invalidate caches/Restart...
  • Just Restart.
  • File/Settings.
  • Editor/LiveTemplates.
  • AndroidTristanJava et AndroidTristanKotlin.

Utilisation

Pour utiliser les lives templates il suffit de taper le début de l'abréviation, lancer l'autocomplétion, et le LiveTemplate sera exécuté.

Réalisation d'une application Android simple en Kotlin

L'application IMC

Nous allons suivre les étapes suivantes :

Installer et lancer Android Studio

Il suffit d'avoir une bonne connexion internet, et d'aller sur https://developer.android.com/studio.

Créer un projet : IMC

Mettre en place le view binding

Pour cela, nous allons suivre les indications de la page : View Binding.

Définir une première interface graphique

Application IMC

Implémenter les méthodes de base

  • Ajouter un tag aux boutons avec des valeurs différentes.
  • Définit une callback qui sera appelée par le setOnClickListener.
  • Assigner la callback à nos 3 boutons en utilisant with.

Solution proposée

Ajoutons un tag aux boutons :

<Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="Salut"
        android:text="btn2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/button3"
        app:layout_constraintStart_toEndOf="@+id/button1" />
	

Solution proposée

Définissons la callback : buttonCallback :

val buttonCallback: (View) -> Unit = { localView ->
    if (BuildConfig.DEBUG) {
        Log.d(TAG, "onCreate ${localView.tag}")
    }
}
	

Solution proposée

Assignons la callback aux boutons en utilisant le mot clé with :

with(binding) {
    button1.setOnClickListener(buttonCallback)
    button2.setOnClickListener(buttonCallback)
    button3.setOnClickListener(buttonCallback)
}
	

Définir notre interface graphique

Application IMC

Implémenter le code métier

Solution proposée

Pour écouter les changements de valeur des champs textes, nous allons utiliser la méthode addTextChangedListener :

binding.editTextHeight.addTextChangedListener { v ->
    calcIMC()
}
	

Solution proposée

Nous allons implémenter la méthode calcIMC() :

fun calcIMC() {
    val weight = try {
        binding.editTextWeight.text.toString().toInt()
    } catch (e: NumberFormatException) {
        binding.textViewResult.text = "ERROR"
        0
    }
    val height: Float = try {
        binding.editTextHeight.text.toString().toFloat()
    } catch (e: NumberFormatException) {
        binding.textViewResult.text = "ERROR"
        0f
    }
    if (weight != 0 && height != 0f) {
        binding.textViewResult.text = (weight / (height * height)).toString()
    }
}
	

Solution proposée

Pour afficher la page web, nous allons utiliser le LiveTemplate : android_webview.

Solution proposée

Pour enregistrer et stocker la valeur du poids, nous allons utiliser les 2 LiveTemplates :
  • android_SharedPreferences_load
  • android_SharedPreferences_save
       

Les accès aux Web Services

Il existe plusieurs façons de faire des appels à des web services, par exemple : La solution la plus populaire, semble être Retrofit, c'est celle-là que nous allons utiliser. Il est à noter que Ktor gagne en popularité.

Application de news

Nous allons écrire une application nous permettant d'afficher des nouvelles, en provenance d'un site proposant une API gratuitement (pour les développeurs). Les étapes que nous allons suivre sont :
  • Création du compte sur le site.
  • Ajout de la librairie à notre projet.
  • Écriture des fichiers POJO.
  • Écriture de l'interface avec l'API.
  • Instantiation du client.
  • Lancement d'une requête.
  • Bonus : mise en place de logs HTTP.
La source du sujet est inspiré de : Live data, ViewModel, Retrofit Android Architecture Component.

Points techniques

Dans cette application nous verrons les points suivants :
Android Studio main screen
NewsKotlin com.example.newskotlin D:\NewsKotlin Sorry, your browser does not support inline SVG.

Ajout des librairies à notre projet


implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
implementation 'com.squareup.picasso:picasso:2.71828'

implementation "androidx.recyclerview:recyclerview:1.1.0"

implementation 'android.arch.lifecycle:viewmodel:1.1.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'
annotationProcessor 'android.arch.lifecycle:compiler:1.1.1'

Écriture des fichiers POJO

Pour écrire les fichiers qui correspondent à notre API de news, nous allons suivre les étapes suivantes :
  • Nous allons installer le plugin JSON To Kotlin.
  • Nous allons utiliser le JSON renvoyé par le site web.
  • Nous allons le copier/coller dans le plugin JSON To Kotlin.
  • Renseigner le champ Class Name: avec : NewsResponse.
  • Il ne reste qu'à cliquer sur Generate.

Installons le plugin JSON to Kotlin

Menu Settings

Installons le plugin JSON to Kotlin

Menu Settings

Utilisons JSON to Kotlin

Menu Settings

Création du compte sur le site de news

Afin d'accéder à l'API de News, nous allons suivre les étapes suivantes :
  • Créer un compte (gratuit) sur le site News API : https://newsapi.org/.
  • Aller dans la section : "News sources" (en haut), puis France.
  • Copier l'URL de l'API (par exemple : http://newsapi.org/v2/top-headlines?country=fr&apiKey=API_KEY)
  • Récupérer le contenu du message (click droit, code source de la page => view-source:http://newsapi.org/v2/top-headlines?country=fr&apiKey=API_KEY (sur chrome).
  • Générer les POJO (détail page suivante).

Génération

Menu Settings

Paramétrage avancé

Il est possible de configurer le générateur avec le bouton "Advanced". Nous allons par exemple activer l'Annotation Gson.
Nous avons donc maintenant 3 nouvelles classes :
  • NewsResponse.
  • Article.
  • Source.

Écriture de l'interface avec l'API

Créons une nouvelle interface : NewsApi contenant le code suivant :

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApi {
    @GET("top-headlines")
    fun getNewsList(
        @Query("country") newsSource: String?,
        @Query("apiKey") apiKey: String?
    ): Call<NewsResponse?>?
}

Creation de la classe de service


import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class RetrofitService {
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://newsapi.org/v2/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun <S> createService(serviceClass: Class<S>?): S {
        return retrofit.create(serviceClass)
    }
}

Écrivons notre répository

Notre Repository étant un Singleton, mettons en place une structure pour gérer cela :

open class SingletonHolder<out T: Any, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg: A): T {
        val checkInstance = instance
        if (checkInstance != null) {
            return checkInstance
        }

        return synchronized(this) {
            val checkInstanceAgain = instance
            if (checkInstanceAgain != null) {
                checkInstanceAgain
            } else {
                val created = creator!!(arg)
                instance = created
                creator = null
                created
            }
        }
    }
}

NewsRepository


import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

open class NewsRepository private constructor(context: Context) {

    private var newsApi: NewsApi? = null

    init {
        newsApi = RetrofitService.createService(NewsApi::class.java)
    }

    fun getNews(country: String?, key: String?): MutableLiveData<NewsResponse?>? {
        val newsData = MutableLiveData<NewsResponse?>()
        newsApi?.getNewsList(country, key)?.enqueue(object : Callback<NewsResponse?> {
            override fun onResponse(call: Call<NewsResponse?>?, response: Response<NewsResponse?>) {
                if (response.isSuccessful()) {
                    newsData.setValue(response.body())
                } else {
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "onResponse $response")
                    }
                }
            }

            override fun onFailure(call: Call<NewsResponse?>?, t: Throwable?) {
                newsData.setValue(null)
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "onFailure $t")
                }
            }
        })
        return newsData
    }

    companion object : SingletonHolder<NewsRepository, Context>(::NewsRepository) {
        private const val TAG = "NewsRepository"
    }
}

NewsViewModel


import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel


class NewsViewModel: ViewModel() {

    private var mutableLiveData: MutableLiveData<NewsResponse?>? = null
    private var newsRepository: NewsRepository? = null

    fun init(context: Context) {
        if (mutableLiveData != null) {
            return
        }
        newsRepository = NewsRepository.getInstance(context)
        mutableLiveData = newsRepository?.getNews("fr", "96c6792fdad6434fbc3a893daba40e0f")
    }

    fun getNewsRepository(): LiveData<NewsResponse?>? {
        return mutableLiveData
    }
}

Modification du layout

Nous allons ajouter une RecyclerView dans notre layout principal :

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvNews"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

Création du layout pour afficher une news

Nous allons par exemple écrire un layout : item_news.xml :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                               xmlns:app="http://schemas.android.com/apk/res-auto"
                               xmlns:tools="http://schemas.android.com/tools"
                               android:layout_width="match_parent"
                               android:layout_height="wrap_content"
                               android:layout_margin="5dp">

    <TextView
            android:id="@+id/item_news_textView_title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:transitionName="title"
            app:layout_constraintEnd_toStartOf="@id/item_news_imageView_image"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Title title title title title title title title title title title title title title title title title title title title title title title title title title title title title" />

    <TextView
            android:id="@+id/item_news_textView_description"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:transitionName="description"
            app:layout_constraintEnd_toStartOf="@id/item_news_imageView_image"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/item_news_textView_title"
            tools:text="description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description description" />

    <ImageView
            android:id="@+id/item_news_imageView_image"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:scaleType="fitCenter"
            android:src="@mipmap/ic_launcher"
            android:transitionName="image"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.widget.Barrier
            android:id="@+id/barrier"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="bottom"
            app:constraint_referenced_ids="item_news_textView_description,item_news_imageView_image" />

    <TextView
            android:id="@+id/item_news_textView_publishedAt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/barrier"
            tools:text="14 minutes ago" />

    <TextView
            android:id="@+id/item_news_textView_author"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/barrier"
            tools:text="by author" />
</androidx.constraintlayout.widget.ConstraintLayout>

Création de l'adapter : NewsAdapter


import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide

class NewsAdapter(val context: Context, val myDataset: List<Article>) :
    RecyclerView.Adapter<NewsAdapter.MyViewHolder>() {
    private var mDataset: List<Article>? = null
    private var mInflater: LayoutInflater? = null

    init {
        mInflater = LayoutInflater.from(context)
        mDataset = myDataset
    }

    class MyViewHolder(v: View) : ViewHolder(v) {
        // each data item is just a string in this case
        var tvTitle: TextView
        var tvDescription: TextView
        var tvPublishedAt: TextView
        var tvAuthor: TextView
        var imageView: ImageView

        init {
            tvTitle = v.findViewById(R.id.item_news_textView_title)
            tvDescription = v.findViewById(R.id.item_news_textView_description)
            tvPublishedAt = v.findViewById(R.id.item_news_textView_publishedAt)
            tvAuthor = v.findViewById(R.id.item_news_textView_author)
            imageView = v.findViewById(R.id.item_news_imageView_image)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        // create a new view
        val v = mInflater!!.inflate(R.layout.item_news, parent, false)

        return MyViewHolder(v)
    }

    override fun getItemCount(): Int = if(mDataset == null) 0 else mDataset!!.size

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        if(mDataset != null){
            holder.tvTitle.setText(mDataset!![position].title)
            holder.tvDescription.setText(mDataset!![position].description)
            holder.tvPublishedAt.setText(mDataset!![position].publishedAt)
            holder.tvAuthor.setText(mDataset!![position].author)

            Glide.with(holder.imageView).load(mDataset!![position].urlToImage)
                .into(holder.imageView)
        }
    }
}

Appel dans notre activity

Déclarons les variables de classes suivantes :

var articleArrayList: ArrayList<Article> = ArrayList()
var newsAdapter: NewsAdapter? = null
var rvHeadline: RecyclerView? = null
var newsViewModel: NewsViewModel? = null

Appel dans notre activity

Déclarons la fonction suivante :

private fun setupRecyclerView() {
    if (newsAdapter == null) {
        newsAdapter = NewsAdapter(this@MainActivity, articleArrayList);
        rvHeadline?.setLayoutManager(LinearLayoutManager(this));
        rvHeadline?.setAdapter(newsAdapter)
        //rvHeadline?.setItemAnimator(DefaultItemAnimator())
        //rvHeadline?.setNestedScrollingEnabled(true)
    } else {
        newsAdapter?.notifyDataSetChanged();
    }
}

Appel dans notre activity

Complétons notre fonction onCreate avec :

        rvHeadline = findViewById(R.id.rvNews);

        newsViewModel = ViewModelProviders.of(this).get(NewsViewModel::class.java)
        newsViewModel?.init(this@MainActivity)
        newsViewModel?.getNewsRepository()?.observe(this, Observer<NewsResponse?> {
            var newsArticles = it?.articles
            if (newsArticles != null) {
                articleArrayList.addAll(newsArticles)
                newsAdapter?.notifyDataSetChanged()
            }
        })

        setupRecyclerView()

Erreurs possibles

Si vous avez l'erreur :

java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles
Il faut activer la compatibilité JVM 1.8 en ajoutant dans le build.gradle, le code suivant :

 android {
...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Erreurs possibles

Si vous avez l'erreur, sur l'émulateur :

java.net.SocketException: socket failed: EPERM (Operation not permitted)
Il suffit que nous désinstallions l'application, pour la réinstaller.

Erreurs possibles

Si nous avons une SocketTimeoutException, il suffit d'allonger la durée du Timeout.
Dans la classe : RetrofitService remplaçons 

private val retrofit = Retrofit.Builder()
    .baseUrl("https://newsapi.org/v2/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
Par :

var client = OkHttpClient.Builder()
    .connectTimeout(100, TimeUnit.SECONDS)
    .readTimeout(100, TimeUnit.SECONDS).build()
private val retrofit = Retrofit.Builder()
    .baseUrl("https://newsapi.org/v2/").client(client)
    .addConverterFactory(GsonConverterFactory.create(Gson()))
    .build()
Cela nous permet d'attendre un peu plus de temps avant d'avoir une erreur. Il faut dans ce cas là, afficher une indication à l'utilisateur pour qu'il comprenne qu'il doit attendre.

Bonus : mise en place de logs HTTP

Nous allons utiliser la librairie :chuck.

Annexe

Mode développeur

Pour mettre son téléphone Android en mode développeur :
  • Allons dans les paramètres.
  • A propos du téléphone.
  • Cliquons plusieurs fois sur le numéro de build.
  • Une fois débloqué, nous pouvons activer le mode débugger, et la connexion USB.
Si au lieu de cliquer sur la version du build, nous cliquons sur la version d'Android, une surprise nous attend :-)

Erreur classique

Une erreur : Unsupported major.minor version 52.0 nous indique, que nous avons compilé le code avec une version de JDK 1.8 et que nous essayons de le lancer avec une version antérieure .

Références

Les références

Cours en ligne

  • Sur Openclassrooms : https://openclassrooms.com/fr/courses/5353106-initiez-vous-a-kotlin
  • Sur CodinGame : https://www.codingame.com/playgrounds/28826/formation-kotlin/les-bases-de-kotlin
  • Kotlin Koans : https://play.kotlinlang.org/koans/overview
  • Tests/Challenges

  • https://github.com/Kotlin/kotlin-koans-edu
  • https://tech.io/explore
  • https://github.com/SK1dev/KotlinChallenges/blob/master/README.md
  • https://github.com/SK1dev/KotlinChallengesPart2
  • https://github.com/igorwojda/kotlin-coding-puzzle
  • Articles sur des points précis

  • https://typealias.com/
  • http://zetcode.com/all/
  • Pourquoi adopter Kotlin : https://medium.com/videdressing-engineering/pourquoi-je-suis-pass%C3%A9-%C3%A0-kotlin-7d40d79054a4
  • Livres

  • https://kotlinlang.org/docs/books.html
  • https://dzone.com/articles/2018s-top-6-book-recommendations-to-learn-kotlin
  • Liste des mots clés

  • https://kotlinlang.org/docs/reference/keyword-reference.html
  • Kotlin pour les applications Backend

  • https://soat.developpez.com/tutoriels/kotlin-application-backend/
  • Kotlin pour Android

  • https://www.udacity.com/course/ud9012
  • https://blog.ippon.fr/2017/12/11/introduction-a-kotlin-pour-android/
  • https://kotlinlang.org/docs/reference/android-overview.html
  • https://developer.android.com/kotlin
  • https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0
  • https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#0