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 :
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.
Nommage
Nommons notre projet, par exemple HelloWorld :
Nous devrions obtenir le résultat suivant :
Créons un nouveau fichier dans le répertoire source. Click droit, New / Kotlin File/Class :
Nommons le app :
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 :
Exécuter le code
Ou encore :
Exécuter le code
Ou bien :
Exécuter le code
Ou bien encore :
Exécuter le code
Ce qui permet d'avoir un nouveau raccourcit :
Exécuter le code
Ce qui nous donnera le 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.
Choisissons le type de fichier : Kotlin :
Pour obtenir le résultat suivant :
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
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 Booleanqui 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 Arrayqui 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().
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 Stringqui 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.
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")
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 :
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 :
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 :::
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 :
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) :
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 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))
}
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 Bsoit 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
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 ?
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 ?
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)
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.
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.
Autre exemple avec des fruits
Prenons un autre exemple avec des fruits
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 :
É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 :
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 :
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){
//. . .
}
@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.
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()
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
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 :
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.
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 :
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 {} :
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")
}
}
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 :
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
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".
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 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é :
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.
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.
Développement : généralement la phase la plus consommatrice de ressources, c’est la phase de création réelle de l’application.
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.
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
Les grandes parties en violet :
La liste des fichiers.
La zone d'édition.
Les logs.
Détail des points en rouge :
Le code de notre application.
Le répertoire des ressources.
Le répertoire contenant les mises en forme des écrans (les layouts).
Le répertoire contenant les versions des icônes de l'application.
Le répertoire contenant les autres ressources.
Détail des points en orange :
Le chemin complet du fichier courant sélectionné.
Détail des points en vert :
Le choix de la cible pour lancer le programme.
Relancer l'application.
Relancer l'activity.
Relancer en mode débug.
Attacher à la volée le debugger.
Détail des points en bleu, il s'agit du Logcat :
Nous pouvons choisir le device sur lequel se connecter.
Sélectionner le process pour filtrer les messages.
Sélectionner le type de log des messages.
Filtrer les messages.
Activer le filtre.
Les fichiers principaux rouge :
Le manifest (AndroidManifest.xml), qui contient la carte d'identité de notre application.
Le code de l'application.
Le code des test graphiques.
Le code des test unitaires.
Les ressources graphiques.
Les écrans (layouts) de notre application.
Les ressources (texte, couleurs, dimensions, styles, ...) de notre application.
Le fichier de configuration global du système de compilation (gradle).
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
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..."
Choisissons le modèle de device que nous voulons émuler :
Choisissons la version d'Android :
En cas de besoin, nous pouvons directement télécharger une image :
Donnons un nom à notre machine virtuelle, nous laissons les paramètres par défaut :
Notre machine virtuelle est maintenant disponible, lançons la :
L'émulateur permet de faire fonctionner nos applications sur des versions d'Android que nous n'avons pas forcément sur notre smartphone :
Cliquons sur les "..."
Nous pouvons :
Gérer la position du device directement sur une carte (nouvelle fonctionnalité Android Studio 3.6) :
Gérer la réception réseau :
Définir le niveau de batterie :
Définir ce que "voit" la/les caméra(s) :
Lancer des appels téléphoniques et envoyer des SMS :
Émuler le pad :
Émuler la gestion du micro :
Gérer les empruntes tactiles :
Émuler les capteurs (rotations, et accélération) :
Envoyer un rapport de bug :
Définir des points de restauration :
Enregistrer l'écran :
Définir certains paramètres :
Obtenir des informations sur l'émulateur :
SDK
Laçons l'écran de gestion du SDK : Tools/SDK Manager
Nous pouvons choisir les versions de l'OS à télécharger :
Et les outils à installer :
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, ...).
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 :
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 :
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 :
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 :
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 :
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
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 :
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) :
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 :
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 :
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.
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.
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) :
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 :