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 (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, pragmatique et 100 % interopérable avec Java.
  • 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 opent 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 plafeforme 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.

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 :
Récupérons le compilateur sur GitHub, 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
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.
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
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.
    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")
    }
    

    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

    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 de 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 :
    
    @Named("Foo")
    private val foo: Foo
    					

    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")
    					

    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 : les nombres, les lettres, les booléens, les tableaux et les chaînes de caractères.

    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 fauddra 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étateur [].
    Il est possible de concaténer des chaînes avec l'opérateur +, même si l'on pé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 morceau 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 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
        print(i)
    }
    					
    Pour définir un intervalle, souvent utilisés dans les boucles for, la syntaxe est la suivante :
    
    for (i in 1..4) print(i)
    					
    Pour utiliser l'ordre décroissant, la syntaxe sera :
    
    for (i in 4 downTo 1) print(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"
    	

    Solution

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

    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) -> print("FizzBuzz")
                (x % 3 == 0) -> print("Fizz")
                (x % 5 == 0) -> print("Buzz")
                else -> print(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
    • Paramètres des fonctions en Kotlin
    • Tail recursion en Kotlin

    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()
    					

    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()
    	
    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))
    }
    	

    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()
    }
    	

    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 appellé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))
    				
    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)
        }
    }
    					

    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édement).

    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))
    }
    	

    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

    Classes en Kotlin

    • La POO.
    • Une classe.
    • Les attributs.
    • Méthodes (Functions Members).
    • Visibilité des membres en Kotlin.
    • Héritage en Kotlin.
    • Abstract Classes en Kotlin.
    • Interface en Kotlin.
    • Polymorphisme en Kotlin.
    • Data Classes en Kotlin.
    • Enum Classes en Kotlin.
    • Nested Classes en Kotlin.
    • Sealed 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 et vérifiée que le nom est bien en lecture seule.
    
    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'annimaux 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)
    }
    	

    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

    • Operator Overloading en Kotlin
    • Lambda expression en Kotlin
    • Extensions de fonctions en Kotlin
    • Extensions de propriétés en Kotlin
    • Closures en Kotlin
    • Bonnes et mauvaises pratiques

    Operator Overloading 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.
    
    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 ->
        print("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 }
        print(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).

    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()
    	

    Solution

    
    fun String.firstUpper() = this.first().toUpperCase() + this.substring(1).toLowerCase()
    
    fun main() {
        println("tristan".firstUpper())
        println("TRISTAN".firstUpper())
    }
    	
    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")
    }
    	

    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 sum = 0
    ints.filter { it > 0 }.forEach {
        sum += it
    }
    print(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 sum = 0
    ints.filter { it > 0 }.forEach {
        sum += it
    }
    print(sum)
    	

    Solution

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

    Délégation

    • Concept de délégation en Kotlin
    • Délégation de fonctions en Kotlin
    • Délégation de propriétés en Kotlin
    • Bonnes et mauvaises pratiques

    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 :
    • "lazy properties" : les valeurs sont calculées lors du premier accès.
    • "observable properties" : les listeners sont notifiés lors des changements de la propriété.
    • "vetoable properties" : il est possible de mettre un veto sur le changement d'une valeur.
    • "notNull properties" : permet de vérifier que la valeur est définie avant un premier accès.

    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 observed = false
    var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
        observed = true
    }
    
    println(max) // 0
    println("observed is ${observed}")
    
    max = 10
    println(max) // 10
    println("observed is ${observed}")
    	
    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

    Le 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
    
    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

    
    lateinit var person1: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)
    }
    	

    Bonnes et mauvaises pratiques

    • utilisez la délégation pour factoriser le code pour adhérer à la bonne pratique "DRY" (Don't Repeat Yourself).
    • Utiliser la délégation notNull pour les types primitifs, car lateinit ne peut pas s'appliquer
      (essayez lateinit var age: Int)

    Generics

    • Generics en Kotlin
    • Generics et invariance en Kotlin
    • Covariance en Kotlin
    • Contravariance en Kotlin
    • Bonnes et mauvaises pratiques

    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
    • Tuples
    • Deconstructing Values
    • Gestion des exceptions
    • Déclaration de constantes
    • Annotation en Kotlin
    • Bonnes et mauvaises pratiques

    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)
    }
    					
    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), et depuis Kotlin 1.2 pour les propriétés top level et les variables locales.
    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
    					

    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 auc 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.

    Interopérabilité

    • Interopérabilité avec Java.
    • De Kotlin au Java.
    • Nulls de Java.
    • Le Kotlin dans Java.
    • Java Réflexion avec Kotlin.
    • Kotlin Réflexion.

    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 parmè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
    • Filtering, Mapping et Flatmapping en Kotlin
    • Kotlin lazy evaluation

    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 chaines 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 sypes 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 entrainer 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 ?

    Programmation asynchrone

    • Le problème de la programmation asynchrone
    • Coroutines en Kotlin et l'implémentation des coroutines
    • Async et Await en Kotlin
    • Yield en Kotlin
    • Reactive extension en Kotlin
    • Bonnes et mauvaises pratiques

    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 tread disponible.
    Que se passe-t-il si l'on supprime la ligne Thread.sleep(2000) ?
    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

    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 finonacci en utilisant la fonction sequence :
    
     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 ") }
    }
    

    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

    La présentation en PDF

    Cette présentation en PDF

    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 cors 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

    Réalisation d'une application Android simple en Kotlin

    L'application IMC

    Nous allons suivre les étapes suivantes :
    • Installer et lancer Android Studio.
    • Créér un projet : IMC.
    • Mettre en place le view binding.
    • Définir notre interface graphique.
    • Implémenter le code métier.

    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 notre interface graphique

    Application IMC

    Implémenter le code métier

    La plate-forme Android

    • L'architecture Android, Linux. Historiques et fonctionnalités.
    • Les terminaux cibles.

    Android dans le monde

    Os forecast
    Source : https://www.idc.com/promo/smartphone-market-share/os
    Os forecast
    Source : https://www.idc.com/promo/smartphone-market-share/vendor

    L'histoire d'Android

    Histoire versions Android 4.0
    Histoire versions Android 4.1
    Histoire versions Android 4.2
    Histoire versions Android 4.3
    Histoire versions Android 4.4
    Histoire versions Android 5.0
    Histoire versions Android 5.1
    Histoire versions Android 6.0
    Histoire versions Android 7.0
    Histoire versions Android 7.1
    Histoire versions Android 8.0
    Histoire versions Android 8.1
    Histoire versions Android 9.0
    Histoire versions Android 10.0

    Les terminaux cibles

    Pour obtenir la distribution des versions, vous devez créer un nouveau projet Android, puis sur l'écran "Configure Your Project" cliquons sur "Help me choose".
    Create new project
    Select template
    Configure project

    Développement Android

    • Les concepts de base. Le cycle développement.
    • Les classes de base du framework.
    • Le projet sous Android Studio.
    • L'émulateur du SDK. Les outils du SDK, SDK manager, AVD manager.
    • L'utilisation des outils sous Android Studio : debugger, profiler, etc.
    • Les paramètres du manifest.
    • La production de l'application, la publication.

    Les concepts de base

    Android Stack
    Source : https://developer.android.com/guide/platform

    Optimisation

    Les applications sur smartphone doivent veiller à optimiser tous les points, tels quel :
    • Mémoire
    • Processeur
    • Radio (WiFi, 3G, BT)
    • Graphique
    En effet tous ces points vont avoir une répercussion sur la consommation d'énergie, et donc sur la batterie.

    Le cycle développement

    Le développement d'une application peut suivre plusieurs schémas, nous allons en détailler une possibilité :
    1. Lancement : toutes les applications commencent par une idée. Cette idée est généralement affinée jusqu’à constituer une base solide pour une application.
    2. Conception : la phase de conception consiste à définir l’expérience utilisateur de l’application, comme la présentation générale, son fonctionnement, etc., ainsi que la conversion de cette expérience utilisateur en une interface utilisateur appropriée, généralement avec l’aide d’un infographiste.
    3. Développement : généralement la phase la plus consommatrice de ressources, c’est la phase de création réelle de l’application.
    4. Stabilisation : quand le développement est suffisamment avancé, l’assurance qualité commence généralement à tester l’application et les bogues sont corrigés. Le plus souvent, une application passe par une phase bêta limitée, durant laquelle un public plus large a la possibilité de l’utiliser, de fournir des commentaires et d’obtenir des modifications.
    5. Déploiement

    Conception des interfaces utilisateur

    Afin de définir notre interface utilisateur, Google fournit des guides pour bien réaliser cette tâche :
    https://developer.android.com/design/index.html
    Il nous appartiendra à bien qualifier notre cible (type de terminaux : smartphone, tablette, TV, montre, ordinateur de voiture, ...).

    Phases de développement

    Nous pouvons passer par plusieurs phases pour réaliser l'application finale :
    • Prototype/MVP : l’application est toujours en phase de preuve de concept, et seules les fonctionnalités principales ou des parties spécifiques de l’application fonctionnent. Des bogues majeurs y sont présents.
    • Alpha : les fonctionnalités principales sont généralement entièrement présentes dans le code (qui est généré, mais pas entièrement testé). Des bogues majeurs sont encore présents, des fonctionnalités périphériques peuvent ne pas encore être présentes.
    • Bêta : la plupart des fonctionnalités sont maintenant terminées, une partie des tests et de la correction des bogues a été effectuée. Des problèmes majeurs connus peuvent encore être présents.
    • Version Release Candidate : toutes les fonctionnalités sont terminées et testées. Sauf si de nouveaux bogues apparaissent, l’application est candidate à la publication.

    Les classes de base du framework

    Plusieurs classes de bases sont disponible dans le Framework, nous les détaillerons par la suite :
    • Activity
    • Fragment (3.0+)
    • Service
    • Content providers
    • Broadcast receivers
    • Intent

    Le projet sous Android Studio

    Android Studio main screen
    1 2 3 1 2 3 4 5 6 1 1 2 3 4 5 1 2 3 4 5 Sorry, your browser does not support inline SVG.
    Les grandes parties en violet :
    1. La liste des fichiers.
    2. La zone d'édition.
    3. Les logs.
    Détail des points en rouge :
    1. Le code de notre application.
    2. Le répertoire des ressources.
    3. Le répertoire contenant les mises en forme des écrans (les layouts).
    4. Le répertoire contenant les versions des icônes de l'application.
    5. Le répertoire contenant les autres ressources.
    Détail des points en orange :
    1. Le chemin complet du fichier courant sélectionné.
    Détail des points en vert :
    1. Le choix de la cible pour lancer le programme.
    2. Relancer l'application.
    3. Relancer l'activity.
    4. Relancer en mode débug.
    5. Attacher à la volée le debugger.
    Détail des points en bleu, il s'agit du Logcat :
    1. Nous pouvons choisir le device sur lequel se connecter.
    2. Sélectionner le process pour filtrer les messages.
    3. Sélectionner le type de log des messages.
    4. Filtrer les messages.
    5. Activer le filtre.
    Android Studio main screen
    1 2 3 4 5 6 7 8 9 Sorry, your browser does not support inline SVG.
    Les fichiers principaux rouge :
    1. Le manifest (AndroidManifest.xml), qui contient la carte d'identité de notre application.
    2. Le code de l'application.
    3. Le code des test graphiques.
    4. Le code des test unitaires.
    5. Les ressources graphiques.
    6. Les écrans (layouts) de notre application.
    7. Les ressources (texte, couleurs, dimensions, styles, ...) de notre application.
    8. Le fichier de configuration global du système de compilation (gradle).
    9. Le fichier de configuration de notre projet (celui qui sera le plus souvent modifié).

    Émulateur

    Laçons l'écran de gestion des machines virtuelles, avec le menu :
    Tools/AVD Manager
    Tools menu

    Choix de la machine virtuelle

    Sur cet écran, nous pouvons choisir la machine virtuelle à lancer/configurer/supprimer.
    Nous pouvons aussi en créer une nouvelle, en cliquant sur le bouton "Create Virtual Device..."
    AVD
    Choisissons le modèle de device que nous voulons émuler :
    AVD
    Choisissons la version d'Android :
    AVD
    En cas de besoin, nous pouvons directement télécharger une image :
    AVD
    Donnons un nom à notre machine virtuelle, nous laissons les paramètres par défaut :
    AVD
    Notre machine virtuelle est maintenant disponible, lançons la :
    AVD
    L'émulateur permet de faire fonctionner nos applications sur des versions d'Android que nous n'avons pas forcément sur notre smartphone :
    AVD
    Cliquons sur les "..."
    Nous pouvons :
    Gérer la position du device directement sur une carte (nouvelle fonctionnalité Android Studio 3.6) : AVD
    Gérer la réception réseau :
    AVD
    Définir le niveau de batterie :
    AVD
    Définir ce que "voit" la/les caméra(s) :
    AVD
    Lancer des appels téléphoniques
    et envoyer des SMS :
    AVD
    Émuler le pad :
    AVD
    Émuler la gestion du micro :
    AVD
    Gérer les empruntes tactiles :
    AVD
    Émuler les capteurs (rotations, et accélération) :
    AVD
    Envoyer un rapport de bug :
    AVD
    Définir des points de restauration :
    AVD
    Enregistrer l'écran :
    AVD
    Définir certains paramètres :
    AVD
    Obtenir des informations sur l'émulateur :
    AVD

    SDK

    Laçons l'écran de gestion du SDK :
    Tools/SDK Manager
    Tools menu
    Nous pouvons choisir les versions de l'OS à télécharger :
    SDK
    Et les outils à installer :
    SDK

    Le Manifest

    Le manifest est le fichier qui décrit l'application, pour les outils de compilation, pour le système d'exploitation et pour Google Play. On y retrouve principalement :
    • Le nom du package, qui doit être unique sur le Google Play.
    • Les interfaces avec l'extérieur : les activities, les services, ...
    • Les permissions que nous allons utiliser.
    • Le matériel nécessaire à notre application (NFC, Bluetooth, ...).
    Tous les détails ici.

    Exemple

    
                        
                        <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapplication">
    
        <application
                android:allowBackup="true"
                android:icon="@mipmap/ic_launcher"
                android:label="@string/app_name"
                android:roundIcon="@mipmap/ic_launcher_round"
                android:supportsRtl="true"
                android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    

    La production de l'application, la publication

    La production de l'application

    Une fois développée, principalement via Android Studio (il existe d'autres moyens, par exemple via des outils de développement cross plate-forme), l'application est enregistrée dans un APK, qui contient :
    • Le code de l'application.
    • Les ressources.
    • Les assets.
    • Les certificats/signatures
    • Le Manifest
    Le fichier APK est un fichier ZIP qu'il est possible d'ouvrir avec n'importe quel utilitaire sachant traiter ce type de fichier (7Zip, Winzip, ...).

    La publication

    Une fois signée, l'application est le plus souvent publiée sur la boutique de Google : Google Play. Il existe d'autres boutiques, des stores alternatifs, tels que : Il peut aussi s'agir de stores de constructeurs, tels que :

    Les interfaces utilisateurs

    • Organisation générale du layout.
    • Exemple de layouts : LinearLayout, RelativeLayout, ConstraintLayout.
    • Les ressources : drawables, string. Les styles.
    • La gestion événementielle.

    Organisation générale du layout

    ViewGroup
    Source : https://developer.android.com/guide/topics/ui/declaring-layout
    A chaque élément déclaré dans le layout nous pouvons associer un Id qui nous permettra de le retrouver/référencer dans le code (Java/Kotlin/XML).
    Pour chaque layout, des contraintes particulières peuvent êtres appliquées (nous allons voir plusieurs layouts dans les slides suivantes).
    Layout parameters
    Source : https://developer.android.com/guide/topics/ui/declaring-layout

    Exemple de layouts

    LinearLayout Exercice 1

    Les 2 boutons sont positionnés l'un au dessous de l'autre, et prennent toute la largeur :
    LinearLayout exercice 1

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Premier button" />
    
        <Button
                android:id="@+id/button2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Second button" />
    </LinearLayout>
    	

    LinearLayout Exercice 2

    Le bouton prend toute la hauteur de l'écran :
    LinearLayout exercice 2

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="Premier button" />
    
    </LinearLayout>
    	

    LinearLayout Exercice 3

    Les 2 boutons sont positionnés l'un au dessous de l'autre, et prennent la largeur nécessaire à leur affichage :
    LinearLayout exercice 3

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Premier button" />
    
        <Button
                android:id="@+id/button2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Second button" />
    </LinearLayout>
    	

    LinearLayout Exercice 4

    Les 2 boutons sont positionnés l'un à côté de l'autre, centré verticalement, le premier prend la hauteur nécessaire à son affichage, et le second prend toute la hauteur :
    LinearLayout exercice 4

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="horizontal"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:text="Premier button" />
    
        <Button
                android:id="@+id/button2"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:text="Second button" />
    </LinearLayout>
    	

    LinearLayout Exercice 5

    Les 3 boutons sont affichés les uns à côté des autres. Le bouton 1 est en bas, de largeur fixe : 120dp, celui du milieu prend la place disponible, et son texte est aligné en bas à droite. Le 3ième bouton est de largeur fixe : 120dp :
    LinearLayout exercice 5 LinearLayout exercice 5

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="horizontal"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="120dp"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom"
                android:text="button 1" />
    
        <Button
                android:id="@+id/button2"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_weight="1"
                android:gravity="end|bottom"
                android:text="button 2" />
    
        <Button
                android:id="@+id/button3"
                android:layout_width="120dp"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:text="button 3" />
    </LinearLayout>
    	

    LinearLayout Exercice 6

    Les 2 boutons sont affichés l'un à côté de l'autre, et prennent exactement la moitié de la largeur de l'écran, avec 2 labels de taille très différente :
    LinearLayout exercice 6

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="horizontal"
                  tools:context=".MainActivity">
    
        <Button
                android:id="@+id/button"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginRight="4dp"
                android:layout_weight="1"
                android:text="btn 1" />
    
        <Button
                android:id="@+id/button2"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_weight="1"
                android:text="button 2 big label" />
    
    </LinearLayout>
    	

    Afficher un contenu très grand

    Dans le cas de l'utilisation d'une listview, verticale, permettant d'ajouter beaucoup d'éléments sur notre écran, il est préférable d'englober notre LinearLayout dans une ScrollView, par exemple :
    
    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
            <Button
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="BTN" />
    
        </LinearLayout>
    </ScrollView>
    

    RelativeLayout Exercice 1

    Les 3 TextView sont alignés de la manière suivante :
    • Centré horizontalement
    • Centré verticalement
    • Centré dans le parent
    Que remarquez vous d'inapproprié ?
    RelativeLayout exercice 1

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:context=".MainActivity">
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:text="Centré horizontalement" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:text="Centré verticalement" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="Centré dans le parent" />
    
    </RelativeLayout>
    	

    RelativeLayout Exercice 2

    Nous allons positionner les TextView suivant les paroles de la chanson (dans le même ordre dans le XML) :
    • En haut
    • En bas
    • A gauche
    • A droite
    • Ces soirées là ! (centrée dans le parent)
    RelativeLayout exercice 2

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:context=".MainActivity">
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:text="En haut" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:text="En bas" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:text="A gauche" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:text="A droite" />
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="Ces soirées là !" />
    </RelativeLayout>
    	

    RelativeLayout Exercice 3

    Nous allons maintenant positionner les TextView suivant les consignes suivantes :
    • [I] En haut a gauche par défaut
    • [II] En dessous de [I]
    • [III] En dessous et à droite de [I]
    • [IV] au dessus de [V], bord aligné avec le bord gauche de [II]
    • [V] En bas à droite
    RelativeLayout exercice 3

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:context=".MainActivity">
    
        <TextView
                android:id="@+id/ex_rel_3_textView_1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:text="[I] En haut a gauche par défaut" />
    
        <TextView android:layout_marginLeft="30dp"
                  android:id="@+id/ex_rel_3_textView_2"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_below="@id/ex_rel_3_textView_1"
                  android:text="[II] En dessous de [I]" />
    
        <TextView
                android:id="@+id/ex_rel_3_textView_3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/ex_rel_3_textView_1"
                android:layout_toRightOf="@id/ex_rel_3_textView_1"
                android:text="[III] En dessous et à droite de [I]" />
    
        <TextView
                android:id="@+id/ex_rel_3_textView_4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_above="@id/ex_rel_3_textView_5"
                android:layout_alignLeft="@id/ex_rel_3_textView_2"
                android:gravity="start"
                android:text="[IV] au dessus de [V], bord aligné avec le bord gauche de [II]" />
    
        <TextView
                android:id="@+id/ex_rel_3_textView_5"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_alignParentBottom="true"
                android:text="[V] En bas à droite" />
    </RelativeLayout>
    	

    TableLayout Exercice 1

    Le table layout nous permet de faire des alignements de type tableaux :
    TableLayout exercice 1

    Solution

    
    <?xml version="1.0" encoding="utf-8"?>
    <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:padding="8dp"
                 android:stretchColumns="1"
                 tools:context=".MainActivity">
    
        <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_span="3"
                android:text="Les items précédés d'un V ouvrent un sous-menu" />
    
        <TableRow>
    
            <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_span="3"
                    android:background="@color/colorPrimaryDark" />
        </TableRow>
    
        <TableRow>
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_column="1"
                    android:layout_weight="1"
                    android:text="N'ouvre pas un sous-menu" />
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Non !" />
        </TableRow>
    
        <TableRow>
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="8dp"
                    android:text="V" />
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
    
                    android:text="Ouvre pas un sous-menu" />
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Là oui !" />
        </TableRow>
    
        <TableRow>
    
            <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_span="3"
                    android:background="@color/colorPrimaryDark" />
        </TableRow>
    
    
        <TableRow>
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="V" />
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Ouvre pas un sous-menu" />
        </TableRow>
    
        <TableRow>
    
            <View
                    android:layout_width="match_parent"
                    android:layout_height="2dp"
                    android:layout_span="3"
                    android:background="@color/colorPrimaryDark" />
        </TableRow>
    
        <TableRow>
    
            <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_column="1"
                    android:layout_span="2"
                    android:text="Cet item s'étend sur 2 colonnes, il faut un très long texte." />
        </TableRow>
    
    </TableLayout>
    	

    ConstraintLayout Exercice 1

    Le constraint layout est le dernier disponible. Il permet de réaliser des mises en forme complexes, tout en gardant une structure de layout plate (pas de layout imbriqué dans un autre layout) :
    ConstraintLayout exercice 1

    Solution

    
    <?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="match_parent"
                                                       tools:context=".MainActivity">
    
        <androidx.constraintlayout.widget.Guideline
                android:id="@+id/activity_edit_guideline_picture"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:layout_constraintGuide_percent="0.2" />
    
        <androidx.constraintlayout.widget.Guideline
                android:id="@+id/activity_edit_guideline_col1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_percent="0.5" />
    
        <ImageButton
                android:id="@+id/activity_edit_imageButton_favorit"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/vertical_margin"
                android:layout_marginRight="@dimen/horizontal_margin"
                android:src="@android:drawable/btn_star_big_off"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:src="@android:drawable/btn_star_big_on" />
    
        <ImageView
                android:id="@+id/activity_edit_imageView_picture"
                android:layout_width="100dp"
                android:layout_height="0dp"
                android:layout_marginTop="@dimen/vertical_margin"
                android:scaleType="centerCrop"
                app:layout_constraintBottom_toBottomOf="@id/activity_edit_guideline_picture"
                app:layout_constraintDimensionRatio="1:1"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="@id/activity_edit_guideline_picture"
                app:srcCompat="@mipmap/ic_launcher" />
    
        <ImageView
                android:id="@+id/activity_edit_imageView_edit_picture"
                android:layout_width="40dp"
                android:layout_height="40dp"
                app:layout_constraintBottom_toBottomOf="@id/activity_edit_imageView_picture"
                app:layout_constraintEnd_toEndOf="@id/activity_edit_imageView_picture"
                app:srcCompat="@android:drawable/ic_menu_camera" />
    
    
        <EditText
                android:id="@+id/activity_edit_editText_firstname"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="@dimen/horizontal_margin"
                android:hint="FirstName"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="@id/activity_edit_guideline_col1"
                app:layout_constraintTop_toBottomOf="@id/activity_edit_imageView_picture" />
    
        <EditText
                android:id="@+id/activity_edit_editText_name"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/horizontal_margin"
                android:hint="Name"
                app:layout_constraintLeft_toLeftOf="@id/activity_edit_editText_firstname"
                app:layout_constraintRight_toRightOf="@id/activity_edit_guideline_col1"
                app:layout_constraintTop_toBottomOf="@id/activity_edit_editText_firstname" />
    
        <RadioGroup
                android:id="@+id/activity_edit_RadioGroup_gender"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:layout_constraintLeft_toLeftOf="@id/activity_edit_editText_firstname"
                app:layout_constraintRight_toRightOf="@id/activity_edit_guideline_col1"
                app:layout_constraintTop_toBottomOf="@id/activity_edit_editText_name">
    
            <RadioButton
                    android:id="@+id/activity_edit_radioButton_male"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="H" />
    
            <RadioButton
                    android:id="@+id/activity_edit_radioButton_female"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="F" />
        </RadioGroup>
    
        <EditText
                android:id="@+id/activity_edit_editText_birthday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginRight="@dimen/horizontal_margin"
                android:editable="false"
                android:hint="Date naissance"
                android:inputType="none"
                app:layout_constraintLeft_toLeftOf="@id/activity_edit_guideline_col1"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="@id/activity_edit_editText_firstname" />
    
        <TextView
                android:id="@+id/activity_edit_textView_age"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintRight_toRightOf="@id/activity_edit_editText_birthday"
                app:layout_constraintTop_toBottomOf="@id/activity_edit_editText_birthday"
                tools:text="41 ans" />
    
        <Spinner
                android:id="@+id/activity_edit_spinner_phone_type"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/horizontal_margin"
                app:layout_constraintBottom_toBottomOf="@id/activity_edit_editText_phone"
                app:layout_constraintLeft_toLeftOf="@id/activity_edit_editText_firstname"
                app:layout_constraintRight_toRightOf="@id/activity_edit_guideline_col1"
                app:layout_constraintTop_toTopOf="@id/activity_edit_editText_phone" />
    
        <EditText
                android:id="@+id/activity_edit_editText_phone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="@dimen/horizontal_margin"
                android:hint="Phone number"
                app:layout_constraintLeft_toRightOf="@id/activity_edit_guideline_col1"
                app:layout_constraintTop_toBottomOf="@id/activity_edit_RadioGroup_gender" />
    
        <Button
                android:id="@+id/activity_edit_button_save"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="SAVE"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    	

    Les ressources

    Les différentes ressources

    Les ressources peuvent êtres de plusieurs types :
    • anim : des animations que l'on peut utiliser pour animer des éléments de notre interface graphique.
    • drawable : des images, bitmap ou vectorielles.
    • layout : l'organisation de nos écrans.
    • menu : la description des menus (des activités, des fragments, ...)
    • mipmap : les icônes de notre application.
    • raw : des données brutes (mp3 par exemple).
    • xml : des données en XML.

    Les différentes ressources

    • values
      • arrays : des tableaux (de chaînes de caractères ou d'entiers)
      • colors : la définition de nos couleurs.
      • bools : des boolééns.
      • dimens : des dimensions (tailles d'images, de texte, de marges, ...).
      • integers : des valeurs numériques.
      • plurials : des chaînes de caractères prenant en compte le pluriel.
      • strings : nos chaînes de caractères.
      • styles : pour afficher nos éléments.

    Accéder aux valeurs en Kotlin

    Nous allons voir plus en détail l'utilisation de ces différentes ressources, dans un premier temps, pour les récupérer, en Kotlin, nous utiliserons la syntaxe suivante :
    
    [package.]R.type.nom
    
    // Par exemple
    R.string.app_name
    

    Accéder aux valeurs en XML

    En XML la syntaxe est similaire :
    
    @[package:]type/nom
    
    // Par exemple :
    @string/app_name
    

    Internationalisation

    Voyons le fichier de ressource strings.xml et son utilisation pour internationaliser notre application.
    • Ajouter une langue.
    • Ajouter des clés.
    • Tester dans l'éditeur.
    • Tester sur l'émulateur.
    • Tester sur le téléphone.

    Apostrophes et guillemets

    Le format XML nous impose des contraintes par rapport aux caractères ' et ", regardons comment nous pouvons gérer ces cas :
    
    <string name="good_example">"This'll work"</string>
    <string name="good_example_2">This\'ll also work</string>
    <string name="bad_example">This doesn't work</string>
    <string name="bad_example_2">XML encodings don't work</string>
    

    Formatage valeurs

    Nous pouvons ajouter des variables dans notre texte, qui seront remplacées par les valeurs passées en paramètre, par exemple :
    
    <string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>
    
    
    Resources res = getResources();
    String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);
    

    Le pluriel

    Les ressources permettent aussi de gérer le pluriel :
    
    <?xml version="1.0" encoding="utf-8"?>
                    <resources>
                        <plurals name="numberOfSongsAvailable">
                    <!– zero, one, two, few, many, other -->
                            
                            <item quantity="one">%d song found.</item>
                            <<item quantity="other">%d songs found.</item>
                        </plurals>
                    </resources>
    

    Exemple d'utilisation du pluriel

    Il s'agit d'une valeur dynamique, nous utilisons donc du code pour définir la valeur de la chaîne de caractère, par exemple :
    
    int count = getNumberOfsongsAvailable();
    Resources res = getResources();
    String songsFound = res.getQuantityString(R.plurals.numberOfSongsAvailable, count, count);
    

    Mise en forme

    Nous avons à disposition quelques outils de mise en forme de notre texte :
    
    <b> for bold text.
    <i> for italic text.
    <u> for underline text.
                    
    Par exemple :
    
    <string name="welcome">Welcome to <b>Android</b>!</string>
                    

    Tableaux

    Nous pouvons aussi définir des tableaux, par exemple :
    
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string-array name="planets_array">
            <item>Mercury</item>
            <item>Venus</item>
            <item>Earth</item>
            <item>Mars</item>
        </string-array>
    </resources>
                    
    Que l'on utilisera :
    
    val planets = resources.getStringArray(R.array.planets_array)
                    

    Dimens

    Le fichier dimens.xml contiendra des définitions de taille, par exemple la taille d'une image, d'une marge, d'un texte, ....
    Nous avons plusieurs type de dimentsions disponibles :
    • px : mesure en pixel, à banir.
    • dp : mesure qui s'adapte à la densité de pixel de l'écran.
    • sp : mesure utilisée pour les textes, car elle prend en compte le paramétrage de l'utilisateur relatif à la taille de la police de caractère.

    Colors

    Nous pouvons définir toutes nos couleurs dans ce fichier. Par exemple :
    
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <color name="colorPrimary">#6200EE</color>
        <color name="colorPrimaryDark">#3700B3</color>
        <color name="colorAccent">#03DAC5</color>
    </resources>
                    

    Styles

    Les styles nous permettent de regrouper des caractéristiques d'affichage d'éléments (des titres, des entêtes, ...). Par exemple :
    
    <style name="Counter" parent="AppTheme">
        <item name="android:textColor">@color/l4e_counter_text</item>
        <item name="android:background">@drawable/rounded_corner</item>
        <item name="android:padding">@dimen/l4e_counter_padding</item>
    </style>
    
    <style name="DialogTitle" parent="AppTheme">
        <item name="android:padding">@dimen/l4e_dialog_title_text_padding</item>
        <item name="android:textSize">@dimen/l4e_dialog_title_text_size</item>
        <item name="android:textStyle">bold</item>
    </style>
                    

    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 (clé usb).
    • File/Import Settings.
    • Settings.jar.
    • File/Invalidate caches/Restart...
    • Just Restart.
    • File/Settings.
    • Editor/LiveTemplates.
    • 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é.

    La gestion événementielle

    Ajout d'un bouton

    Nous allons ajouter un bouton à notre interface, via l'éditeur graphique, ou via l'éditeur texte.
    Afin de réagir aux click de n'utilisateur, nous allons brancher un View.OnClickListener, ou un View.OnLongClickListener.
    Pour cela nous avons 4 options que nous allons détailler :
    • Par héritage
    • Classe anonyme
    • Attribut
    • XML
    Voyons tout cela en détail.

    Par héritage

    Suivons les étapes suivantes :
    • Faisons étendre notre MainActivity de View.OnClickListener.
    • Implémentons la méthode en utilisant l'ampoule rouge proposée par l'éditeur.
    • Remplissons la méthode onClick générée.
    • Associons notre listener à un bouton

    Contenu de la méthode onClick :
    
    when(v?.id){
        R.id.activity_main_button_test -> Log.d(TAG, "onCreate click")
    }
                    
    Association du listener avec notre classe.
    
    findViewById<Button>(R.id.activity_main_button_test).setOnClickListener(this@MainActivity)
                    

    Par attribut

    Déclarons un attribut de classe :
    
    private var btnListener: View.OnClickListener = View.OnClickListener {
        when (it?.id) {
            R.id.activity_main_button_test -> Log.d(TAG, "onCreate click")
        }
    }
                    
    Attribuons notre listener à notre bouton :
    
    findViewById<Button>(R.id.activity_main_button_test).setOnClickListener(btnListener)
                    

    Par lambda

    C'est la méthode la plus courante :
    
    findViewById<Button>(R.id.activity_main_button_test).setOnClickListener(){ Log.d(TAG, "onCreate click") }
                    

    Par XML

    Dans le layout de notre activity, sur le bouton, ajoutons l'attribut onClick :
    
    android:onClick="handleClick"
                    
    Créons la méthode en utilisant l'aide de l'éditeur, il nous reste plus qu'à implémenter la méthode.

    Les éléments graphiques

    Plusieurs éléments graphiques de base sont à notre disposition, nous allons en voir plusieurs, et la correspondance entre la partie graphique et la partie texte.

    Éditeur code

    Ide design
    Choix du mode d'édition. Sorry, your browser does not support inline SVG.

    Éditeur code/design

    Ide design

    Éditeur code/design

    Ide design

    Button

    Button
    
    <Button
        android:id="@+id/activity_main_button_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="handleClick"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Texte affiché

    TextView
    
    <TextView
        android:id="@+id/activity_main_textView_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="handleClick"
        android:text="Hello world."
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Champ texte

    EditText
    
    <EditText
        android:id="@+id/activity_main_editText_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="handleClick"
        android:hint="Enter your first name"
        android:inputType="textPersonName"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Case à cocher

    CheckBox
    
    <CheckBox
        android:id="@+id/activity_main_checkBox_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="handleClick"
        android:hint="Is activated ?"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    

    Boutons de choix

    RadioButton et RadioGroup
    
    <RadioGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    
        <RadioButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Thé" />
    
        <RadioButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Café" />
    
    </RadioGroup>
                    

    Liste de choix

    Dans la méthode onCreate de notre Activity utilisons le live template : android_spinner_string_array.

    Barre de progression

    ProgressBar
    
    <ProgressBar
        android:id="@+id/activity_main_progressBar_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Notation (étoiles)

    RatingBar
    
    <RatingBar
        android:id="@+id/activity_main_ratingBar_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:rating="4"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Date

    
                        DatePicker
    <DatePicker
        android:id="@+id/activity_main_datePicker_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
                    

    Time

    
                        TimePicker
    <TimePicker
        android:id="@+id/activity_main_timePicker_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
                    

    Vue Web

    WebView
    
    <WebView
            android:id="@+id/activity_main_webView_test"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
                    

    Vue Web (suite)

    Pour fonctionner nous allons préciser l'URL que nous souhaitons affichier dans la méthode onCreate de notre Activity :
    
    var webView = findViewById<WebView>(R.id.activity_main_webView_test)
    webView.loadUrl("http://light4events.fr")
                    
    Quel est le problème qui empèche la page de s'afficher ? Comment résoudre le problème ?
    Que remarque-t-on quand on clique sur un lien ? Comment résoudre le problème ?

    Solution

    Pour accéder à internet, nous devons demander la permission dans le manifest en ajoutant (en dessous de la balise manifest, mais avant la balise application) :
    
    <uses-permission android:name="android.permission.INTERNET" />
    	
    Pour empêcher l'ouverture d'un navigateur externe, nous allons ajouter la ligne suivante (avant de charger l'URL) :
    
    webview.setWebViewClient(new WebViewClient());  
                    
    Pour aller plus loin, nous pouvons utiliser le LiveTemplate : android_webview dans notre méthode onCreate.

    Le modèle de composants

    • La relation activité mère-fille.
    • Les fragments, les services, les IntentServices.
    • Les Intents et leur gestion par l'activité.

    Les Intents et leur gestion par l'activité

    Les différents modules applicatifs ne sont pas directement instanciés par le développeur, un bus de messages permet au système de choisir le composant à monter en mémoire.
    Les messages sont de type Intent.
    Les valeurs envoyées dans l'Intent sont contenues dans un Bundle (optionnel).
    Les intentions décrivent quelle est l'opération qui devra être effectuée.
    Plusieurs composants applicatifs peuvent répondre à un même Intent, dans ce cas, l'utilisateur aura alors le choix.
    Nous allons tester plusieurs Intent simple pour mettre en oeuvre ces principes.

    LiveTemplates Intent

    Nous avons à notre disposition plusieurs intents, entre autres :
    • android_intent_mail : pour envoyer un email avec les champs pré-remplis.
    • android_intent_map : pour ouvrir l'application Maps affichant une coordonnée.
    • android_intent_phone_call : pour lancer le numéroteur téléphonique.
    • android_intent_sms : pour envoyer un SMS (pour lancer l'application de SMS, prête à envoyer le SMS).
    • android_intent_web : pour lancer un navigateur web pour afficher une URL.
    De la même manière il est possible de déclarer dans le manifest des capacités de nos Activity à gérer des Intents. Par défaut l'activity principale (MainActivity en général) gère l'appel par le Launcher. Dans le AndroidManifest.xml, nous voyons les lignes suivantes :
    
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
                    

    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 cliquez sur la version du build, nous cliquez sur la version d'Android, une surprise nous attend :)

    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