Bases
Variables et types
Préférez val (immuable) à var (mutable). Kotlin infère les types mais permet les annotations explicites. Utilisez 'is' pour les vérifications de type (smart-casts automatiquement). Tout est un objet—pas de primitives dans la syntaxe Kotlin.
val name = "Alice" // immutable (preferred)
var age = 30 // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class) // class kotlin.String
println(age is Int) // trueTemplates de chaînes
$var pour les variables simples, ${expr} pour les expressions. Les chaînes à triple guillemets préservent les nouvelles lignes—utilisez trimIndent() pour un formatage propre. Les templates de chaînes rendent la construction de chaînes concise et lisible sans format strings.
val name = "Alice"
val age = 30
println("Name: $name, Age: $age") // simple var
println("Length: ${name.length}") // expression
println("Upper: ${name.uppercase()}")
val multiline = """
Hello, $name
Age: $age
""".trimIndent()Sécurité null
La sécurité null de Kotlin : ? marque les types nullables, ?. appel sûr (retourne null si null), !! asserte non-null (lance NPE si null), ?: elvis (valeur par défaut si null). Cela élimine NullPointerException à la compilation—fonctionnalité centrale de Kotlin.
var name: String? = null // nullable type
println(name?.length) // null (safe call)
name = "Alice"
println(name!!.length) // 5 (non-null assertion)
val len: Int = name?.length ?: 0 // elvis operator
// Non-null types can never be null
val s: String = "hi" // not nullableConversion de type
Kotlin nécessite une conversion de type explicite (pas de widening implicite comme Java). Utilisez toInt()/toDouble()/toString() pour la conversion. toIntOrNull() retourne null en cas d'échec—plus sûr que toInt() qui lance une exception. Gérez toujours null depuis l'analyse.
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull() // null
println(d, s, fromStr, invalid)Vérifications de type et smart cast
is vérifie le type et smart-cast la variable dans cette branche—pas de cast explicite nécessaire. !is est la négation. Utilisez as pour les casts non sûrs (lance ClassCastException), as? pour les casts sûrs (retourne null en cas d'échec). Le smart cast est une commodité majeure de Kotlin.
fun describe(x: Any): String =
when (x) {
is Int -> "Int: ${x + 1}" // x smart-cast to Int
is String -> "String of length ${x.length}"
is List<*> -> "List with ${x.size} items"
else -> "Unknown"
}
println(describe(42)) // Int: 43
println(describe("hello")) // String of length 5Chaînes de caractères
Méthodes courantes de chaînes
Les chaînes Kotlin ont de riches fonctions d'extension de la bibliothèque standard. La plupart retournent de nouvelles chaînes (immuables). Utilisez-les au lieu de boucles manuelles. Des fonctions comme uppercase() sont locale-aware—utilisez uppercase(Locale.ROOT) pour des résultats cohérents.
val s = "Hello, World"
println(s.length) // 12
println(s.uppercase()) // HELLO, WORLD
println(s.lowercase()) // hello, world
println(s.split(", ")) // [Hello, World]
println(s.replace("o", "0")) // Hell0, W0rld
println(s.reversed()) // dlroW ,olleH
println(s.startsWith("Hello")) // trueChaînes multilignes et brutes
Les chaînes à triple guillemets sont brutes—pas de séquences d'échappement nécessaires (sauf triple double-guillemets). trimMargin('|') ne garde que le texte après | pour une indentation propre. trimIndent() supprime les espaces communs. Idéal pour SQL, JSON, HTML dans le code.
val text = """
|Hello,
|World!
""".trimMargin() // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json) // no escape needed for quotes
// trimIndent() removes common leading whitespaceConstruction de chaînes
joinToString est la manière idiomatique de joindre des collections avec séparateurs, préfixe, postfixe. buildString fournit une portée StringBuilder pour une construction concise. Évitez + répété dans les boucles—utilisez joinToString ou StringBuilder pour l'efficacité.
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", ")) // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
parts.forEach { append(it).append(";") }
}Chaîne vers nombre
toInt()/toDouble() lancent NumberFormatException sur entrée invalide. toIntOrNull() retourne null—associez avec ?: pour des valeurs par défaut sûres. toBooleanStrict() n'accepte que 'true'/'false'. Utilisez les variantes OrNull pour l'entrée utilisateur ou les données non fiables.
val n = "42".toInt() // 42 (throws if invalid)
val d = "3.14".toDouble() // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0 // 0
val bool = "true".toBooleanStrict() // true
println(n, d, safe, withDefault)Regex
La classe Regex enveloppe le Pattern de Java. find retourne MatchResult? (première correspondance), findAll retourne toutes. matches vérifie la correspondance complète, containsMatchIn vérifie partielle. Utilisez les chaînes à triple guillemets pour les patterns afin d'éviter le double-escaping des backslashes.
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value) // [email protected]
println(email.matches("[email protected]")) // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text)) // trueStructures de données
List
listOf crée des listes immuables, mutableListOf pour mutables. Les listes sont indexées à partir de zéro. Utilisez first()/last() pour les extrémités (lancent si vide), firstOrNull() pour la sécurité. contains vérifie l'appartenance. Préférez les listes immuables sauf si vous devez modifier.
val nums = listOf(1, 2, 3) // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size) // 3
println(nums.first()) // 1
println(nums.last()) // 3
println(nums.contains(2)) // true
println(nums.indexOf(2)) // 1Map
mapOf crée des maps immuables, mutableMapOf pour mutables. 'to' infix crée des paires. [] access retourne V? nullable (null si manquant). Utilisez getOrDefault ou getValue (lance si manquant). Itérez avec déstructuration (k, v).
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"]) // 30
println(ages.getOrDefault("Eve", 0)) // 0
println(ages.containsKey("Alice")) // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2 // add/updateSet
setOf crée des sets immuables, mutableSetOf pour mutables. union/intersect/subtract retournent de nouveaux sets. Les sets imposent l'unicité—add retourne false si l'élément existe. Utilisez pour la déduplication et les opérations ensemblistes. LinkedHashSet préserve l'ordre d'insertion.
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b) // [1,2,3,4,5]
println(a intersect b) // [3]
println(a subtract b) // [1,2]
println(a.contains(2)) // true
val mutable = mutableSetOf(1, 2)
mutable.add(3) // true if addedArray et tableaux primitifs
arrayOf crée Array<T> (boxé pour les primitives). Utilisez intArrayOf/doubleArrayOf pour les tableaux de primitives unboxed (meilleure performance). Array(size) { init } crée avec une fonction d'init. Les arrays sont mutables ; préférez List pour la plupart des usages.
val arr = arrayOf(1, 2, 3) // Array<Int>
arr[0] = 10
println(arr.size) // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it } // [0,1,4,9,16]
println(squares.toList())Pair et Triple
Pair et Triple groupent 2-3 valeurs. 'to' infix crée des Pairs (courant pour les maps). Déstructurez avec val (a, b) = pair. Pour plus de 3 valeurs ou des champs nommés, utilisez les data classes—elles sont plus claires et plus maintenables.
val pair = "Alice" to 30 // Pair<String, Int>
println(pair.first) // Alice
println(pair.second) // 30
val (name, age) = pair // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
Pair(list.min(), list.max())Flux de contrôle
If / Else (expression)
Dans Kotlin, if/else est une expression qui retourne une valeur. Cela remplace l'opérateur ternaire. Les deux branches doivent retourner des types compatibles. Utilisez pour une affectation conditionnelle concise. Comme instruction (sans else), retourne Unit.
val score = 85
val grade = if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
println(grade) // B
// if returns a value—no ternary needed
// Both branches must have compatible typesWhen (switch)
when est le switch puissant de Kotlin—supporte les littéraux, ranges (in), négation (!in), valeurs multiples (virgule) et is-checks. Doit être exhaustif lorsqu'utilisé comme expression avec des types sealed. Sans sujet, c'est une chaîne if/else plus propre.
val n = 2
val label = when (n) {
0 -> "zero"
1, 2, 3 -> "small"
in 4..10 -> "medium"
!in 1..100 -> "out of range"
else -> "large"
}
println(label) // small
// When without subject = multi-condition if
when {
n > 0 -> println("positive")
n < 0 -> println("negative")
else -> println("zero")
}Boucles for et ranges
.. est un range inclusif, until est exclusif, downTo compte à rebours, step définit l'incrément. withIndex() donne l'itération indexée. for-in fonctionne avec n'importe quel Iterable. Ces ranges sont concis et lisibles pour les boucles de comptage.
for (i in 0 until 5) print(i) // 01234 (excludes 5)
for (i in 1..5) print(i) // 12345 (includes 5)
for (i in 5 downTo 1) print(i) // 54321
for (i in 1..10 step 2) print(i) // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
println("$index: $value")
}While et do-while
while vérifie avant, do-while vérifie après (s'exécute au moins une fois). Les deux nécessitent un état mutable. Préférez les boucles for ou les opérations fonctionnelles (map, filter) pour l'immutabilité. Utilisez while pour les boucles pilotées par condition où le compte n'est pas connu.
var count = 0
while (count < 3) {
println(count)
count++
}
var j = 0
do {
println(j)
j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possibleBreak et continue (labels)
continue passe à l'itération suivante, break sort de la boucle. Les labels (@outer) permettent de sortir des boucles imbriquées—définissez avec label@ avant la boucle, break/continue avec @label. Utilisez avec parcimonie ; le refactoring en fonctions est souvent plus propre.
for (i in 1..5) {
if (i == 3) continue // skip 3
if (i == 5) break // stop at 5
println(i) // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2 && j == 2) break@outer
println("$i,$j")
}
}Fonctions et lambdas
Définition de fonction
Les fonctions utilisent fun name(params): ReturnType. Corps d'expression (= expr) pour les expressions uniques. Paramètres par défaut et arguments nommés supportés. Type de retour Unit = effet de bord uniquement. Les params par défaut réduisent le besoin de surcharge.
fun add(a: Int, b: Int): Int = a + b // expression body
fun greet(name: String, greeting: String = "Hello"): String {
return "$greeting, $name!"
}
println(add(3, 4)) // 7
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", greeting = "Hi")) // named arg
fun log(msg: String): Unit = println(msg) // Unit = voidLambdas et ordre supérieur
Lambdas : { params -> body }. 'it' est le raccourci pour un paramètre unique. Passez les lambdas aux fonctions d'ordre supérieur comme map/filter/reduce. Syntaxe trailing lambda : si le dernier param est une fonction, il peut aller en dehors de (). C'est du Kotlin idiomatique.
val square: (Int) -> Int = { x -> x * x }
println(square(5)) // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 }) // [2, 4, 6]
println(nums.filter { it > 1 }) // [2, 3]
println(nums.reduce { a, b -> a + b }) // 6
// 'it' is the implicit single parameterFonctions d'extension
Les fonctions d'extension ajoutent des méthodes aux classes existantes (même de Java) sans héritage ni modification. Elles sont du sucre syntaxique pour des fonctions statiques. 'this' fait référence au récepteur. Utilisez pour rendre les APIs fluentes. Résolu à la compilation (pas de dispatch dynamique).
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout()) // HELLO!
println(4.isEven()) // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any typeInline et infix
inline copie le corps de la fonction aux sites d'appel (élimine l'overhead des lambdas). Utilisez pour les fonctions d'ordre supérieur dans les chemins chauds. infix permet les appels de fonction sans point/parenthèses (a op b)—utilisez pour les DSL et opérations lisibles comme 'to', 'in', 'and'.
inline fun measure(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab") // abababVararg et spread
vararg accepte des arguments variables (collectés dans un tableau). * spread un tableau en vararg. Les params nommés peuvent suivre vararg. Utilisez pour des APIs flexibles comme listOf(), fonctions style printf. L'opérateur spread est l'équivalent Kotlin du spread JS.
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4)) // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr)) // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))Classes et POO
Classe et constructeur
Le constructeur primaire est dans l'en-tête de classe. Les params val/var deviennent des propriétés (avec getters/setters). Sans val/var, ce sont juste des params de constructeur (non accessibles). Le bloc init s'exécute pendant la construction. Kotlin génère getters/setters automatiquement.
class Person(val name: String, val age: Int) {
fun greet(): String = "Hi, I'm $name"
fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet()) // Hi, I'm Alice
println(p.name) // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto propertyData class
data class auto-génère equals, hashCode, toString, copy et componentN (pour la déstructuration). Utilisez pour les conteneurs de données. Doit avoir au moins un param val/var. copy() crée un clone modifié—excellent pour les mises à jour immuables. Déstructurez avec val (a, b) = point.
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2) // true (value equality)
println(p1.copy(x = 5)) // Point(x=5, y=4)
println(p1) // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1 // destructuring via componentN()Sealed class
Les sealed classes restreignent les sous-types au même fichier/module kotlin. Les expressions when sont exhaustives (le compilateur vérifie tous les cas). Utilisez pour les ADT (Algebraic Data Types) représentant des états finis. Combiné avec when, cela permet du pattern matching sûr et vérifié à la compilation.
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
is Success -> "Got ${r.value}"
is Failure -> "Error: ${r.error}"
Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all casesObject et companion
object déclare un singleton (une instance, initialisé paresseusement). companion object dans une classe contient des membres 'statiques' (accessibles via ClassName.member). Utilisez object pour les singletons, companion pour les méthodes factory et constantes. Companion peut implémenter des interfaces.
object Config { // singleton
val version = "1.0"
fun load() = mapOf("key" to "value")
}
println(Config.version) // 1.0
class User(val name: String) {
companion object {
fun create(name: String) = User(name)
const val MAX = 100
}
}
val u = User.create("Alice") // like static methodHéritage et interface
Les classes sont final par défaut—utilisez 'open' pour permettre l'héritage. 'override' est requis. Les interfaces peuvent avoir des implémentations par défaut. Une classe étend une classe mais implémente plusieurs interfaces. Utilisez abstract pour l'implémentation partielle. Préférez la composition à l'héritage.
open class Animal(val name: String) {
open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
override fun speak() = "Woof"
}
interface Greetable {
val name: String
fun greet(): String // can have default impl
}
class Person(override val name: String) : Greetable {
override fun greet() = "Hi, $name"
}Sécurité null et fonctions de portée
Appels sûrs et elvis
?. appel sûr (retourne null si le récepteur est null), ?: elvis (fournit une valeur par défaut). Chaînez ?. pour l'accès profond (user?.address?.city). Utilisez ?: return/throw pour les sorties anticipées. Cela rend la gestion null concise et sûre—pas de vérifications null explicites nécessaires.
var name: String? = "Alice"
println(name?.length) // 5
name = null
println(name?.length) // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return) // early return
// Safe call chain
val city: String? = user?.address?.citylet (vérification null)
let exécute un bloc uniquement si la valeur est non-null. 'it' est la valeur non-null dans le bloc. Utilisez pour les opérations gardées par null au lieu des vérifications if-null. Pattern courant : value?.let { ... } pour un traitement sûr. Retourne le résultat du bloc.
var name: String? = "Alice"
name?.let {
println("Name is $it") // only runs if not null
println("Length: ${it.length}") // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
process(it) // it: String (non-null)
} ?: "default"apply et also
apply configure un objet (retourne l'objet, récepteur 'this')—idéal pour les builders. also effectue des effets de bord (retourne l'objet, param 'it')—bon pour le logging/debugging dans les chaînes. Les deux retournent l'objet original, permettant des chaînes fluentes.
val list = mutableListOf<Int>().apply {
add(1); add(2); add(3) // 'this' = the list
println("Built $size items")
}
val config = Config().also {
it.timeout = 30 // 'it' = config
println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)run et with
run exécute un bloc sur un objet (retourne le résultat du bloc, récepteur 'this')—utilisez pour transformer un objet. with est comme run mais prend l'objet comme paramètre (pas chaînable sur nullables). Utilisez pour grouper des opérations sur le même objet.
val result = "Hello".run {
length // 'this' = string, returns last expr
} // 5
val r2 = with(StringBuilder()) {
append("a"); append("b")
toString() // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)TakeIf et TakeUnless
takeIf retourne l'objet si le prédicat est vrai, null sinon. takeUnless est l'opposé. Utilisez pour le filtrage conditionnel dans les chaînes—évite les vérifications if séparées. Combinez avec ?: pour les valeurs par défaut. Élégant pour les pipelines de validation.
val age = 25
val valid = age.takeIf { it >= 18 } // 25 (if true)
val invalid = age.takeIf { it < 18 } // null (if false)
val adult = age.takeUnless { it < 18 } // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)Collections et fonctionnel
Map / Filter / Fold
map transforme, filter sélectionne, reduce/fold agrège. 'it' est l'élément implicite. fold prend une graine ; reduce non (lance si vide). Ce sont le cœur du traitement fonctionnel des collections—utilisez au lieu des boucles pour la clarté.
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 }) // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 }) // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum()) // 15
println(nums.joinToString(", ")) // 1, 2, 3, 4, 5FlatMap et GroupBy
flatten supprime un niveau d'imbrication. flatMap map et aplatit en une étape—essentiel pour les transformations imbriquées. groupBy partitionne par clé dans un Map. Ce sont puissants pour le traitement de données et les pipelines d'analyse.
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten()) // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } }) // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)Tri
sorted/sortedDescending trient naturellement. sortedBy/sortedByDescending trient par un sélecteur de clé. Ils retournent de nouvelles listes (immuables). Pour les listes mutables, utilisez sort/sortBy (sur place). Utilisez les sélecteurs de clé pour trier par un champ spécifique.
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted()) // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second } // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)Sequences (paresseux)
Les sequences sont paresseuses—les opérations sont différées jusqu'à une opération terminale (toList, sum, count). Évite les collections intermédiaires pour une meilleure performance sur les grandes données. Utilisez asSequence() pour les pipelines multi-étapes sur de grandes collections. Comme Java Streams.
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.toList() // forces evaluation
println(lazy) // [4, 8, 12, 16, 20]Partition et Chunked
partition sépare en deux listes par un prédicat (retourne Pair). chunked divise en listes de taille fixe. windowed crée des fenêtres glissantes. Utiles pour le batching, la pagination et les algorithmes à fenêtre glissante. Tous retournent de nouvelles collections.
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens) // [2, 4]
println(odds) // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked) // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed) // [[1,2,3], [2,3,4], [3,4,5]]Coroutines et async
Launch (fire and forget)
launch démarre une coroutine qui ne retourne pas de résultat (fire-and-forget). delay est non-bloquant (contrairement à Thread.sleep). runBlocking fait le pont sync/async (utilisez dans main/tests). Les coroutines sont légères—des milliers peuvent s'exécuter sur quelques threads.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000)
println("World!") // after 1s
}
println("Hello") // immediately
// launch doesn't return a result
// Use for side-effect coroutines
}
// Output: Hello, then World! after 1sAsync et await
async démarre une coroutine qui retourne un Deferred<T>. await() suspend jusqu'à ce que le résultat soit prêt. Démarrez plusieurs async avant d'await pour le parallélisme. Utilisez pour les calculs concurrents qui produisent des résultats. Comme Promise/Future dans d'autres langages.
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(1000)
42 // return value
}
val result = deferred.await()
println(result) // 42
// Parallel execution
val a = async { computeA() }
val b = async { computeB() }
println(a.await() + b.await()) // concurrent
}Fonctions suspend
Les fonctions suspend peuvent se mettre en pause et reprendre sans bloquer les threads. Elles ne peuvent être appelées que depuis des coroutines ou d'autres fonctions suspend. coroutineScope fournit une portée structurée (attend tous les enfants). Utilisez pour les APIs async—rend le code async synchrone en apparence.
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
delay(500) // simulates network
return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
val a = async { fetchUser(1) }
val b = async { fetchUser(2) }
listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutinesFlow (flux froid)
Flow est le flux async froid de Kotlin (comme RxJava Observable). Les valeurs sont produites à la collection. Utilisez les opérateurs map/filter/reduce. emit produit, collect consomme. Idéal pour le streaming de données, événements ou APIs paginées. Les flux chauds utilisent SharedFlow/StateFlow.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // produce value
}
}
fun main() = runBlocking {
numbers().collect { println(it) } // 1,2,3,4,5
// Transform
numbers().map { it * 2 }.filter { it > 4 }.collect {
println(it) // 6, 8, 10
}
}Dispatchers et contexte
Les Dispatchers choisissent le thread pool : Main pour l'UI, IO pour réseau/fichier (grand pool), Default pour le travail CPU (nombre de cœurs). withContext change de contexte dans une coroutine. Utilisez le bon dispatcher pour éviter de bloquer l'UI ou d'affamer les thread pools.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(Dispatchers.Main) { /* UI thread */ }
launch(Dispatchers.IO) {
// Network/file I/O (thread pool)
val data = fetchData()
}
launch(Dispatchers.Default) {
// CPU-intensive work
val result = heavyCompute()
}
// withContext switches dispatcher
val data = withContext(Dispatchers.IO) { readFile() }
}Gestion d'erreurs et I/O
Try / Catch / Finally
try/catch/finally est comme Java mais try est une expression retournant une valeur. Pas d'exceptions checked—toutes sont unchecked. Attrapez des exceptions spécifiques pour un traitement ciblé. Utilisez finally pour le nettoyage. Préférez Result ou les retours nullables pour les échecs attendus.
val result = try {
"abc".toInt()
} catch (e: NumberFormatException) {
0 // fallback
} finally {
println("cleanup")
}
println(result) // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptionsType Result
Result<T> enveloppe succès ou échec (comme Try dans Scala). runCatching convertit les exceptions en Result. getOrNull/getOrElse pour un accès sûr. onSuccess/onFailure pour les callbacks. Utilisez pour les erreurs attendues au lieu des exceptions—gestion fonctionnelle des erreurs plus propre.
fun parse(s: String): Result<Int> =
runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull()) // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
.onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull()) // nullExceptions personnalisées
Les exceptions personnalisées étendent Exception (ou une sous-classe). Passez un message pour le débogage. Attrapez les exceptions spécifiques avant les génériques (l'ordre compte). Utilisez les exceptions pour les cas vraiment exceptionnels ; pour les échecs attendus, préférez Result ou les retours nullables.
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
setAge(-5)
} catch (e: InvalidAgeException) {
println("Caught: ${e.message}")
} catch (e: Exception) {
println("Other: ${e.message}")
}I/O fichier
Kotlin utilise le File de Java avec des fonctions d'extension. writeText/readText pour les cas simples. useLines diffuse les lignes (ferme automatiquement, économe en mémoire). Pour les gros fichiers, utilisez bufferedReader(). Fermez toujours les ressources—utilisez le bloc use { } pour la fermeture automatique.
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content) // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
lines.forEach { println(it) }
}JSON (kotlinx.serialization)
kotlinx.serialization est la bibliothèque JSON officielle de Kotlin—sûre à la compilation, pas de réflexion. Annototez les data classes avec @Serializable. encodeToString/decodeFromString pour JSON. Utilisez @SerialName pour les noms de clés personnalisés, @Optional pour les valeurs par défaut. Alternative : Gson/Moshi.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json) // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name) // Alice
// Requires @Serializable annotationCoroutines en profondeur
Concurrence structurée
La concurrence structurée lie les coroutines enfants à une portée parent—le parent ne se termine pas tant que tous les enfants ne sont pas finis, et un échec d'enfant annule les frères. Préférez coroutineScope à GlobalScope. Cela prévient les fuites de coroutine et rend l'annulation prévisible.
import kotlinx.coroutines.*
fun main() = runBlocking {
// coroutineScope waits for all children
coroutineScope {
launch { delay(1000); println("A") }
launch { delay(500); println("B") }
}
println("Done") // after both complete
// If a child fails, parent cancels siblings
// Never use GlobalScope unless necessary
}Annulation et annulation coopérative
L'annulation est coopérative—les coroutines doivent la vérifier aux points de suspension (delay, yield, await). Les fonctions suspend annulables lancent CancellationException. Utilisez ensureActive() ou isActive pour vérifier. Les boucles CPU non-suspendantes ne répondront pas à l'annulation sauf si vous appelez yield() ou vérifiez isActive.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(10) { i ->
delay(300)
yield() // explicit suspension point
println("Working $i")
}
}
delay(700)
job.cancelAndJoin() // cancels and waits
println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation stateGestion des exceptions
Les exceptions non gérées dans launch se propagent au parent (annulant les frères) sauf si un CoroutineExceptionHandler est installé. async diffère les exceptions à await(). Utilisez try/catch autour de await. Le handler fonctionne uniquement pour les exceptions non attrapées dans launch—utilisez SupervisorJob pour isoler les échecs.
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("Caught: $e")
}
val job = launch(handler) {
throw RuntimeException("Boom!")
}
job.join()
// async exceptions surface on await()
val deferred = async { throw IllegalStateException() }
try { deferred.await() } catch (e: Exception) {
println("Async failed: $e")
}
}Supervision (supervisorScope)
supervisorScope (et SupervisorJob) crée une portée où l'échec d'un enfant n'annule PAS ses frères—chaque enfant échoue indépendamment. Utilisez-le pour des opérations indépendantes (ex. multiples appels API indépendants). Le coroutineScope régulier annule les frères au premier échec (fail-fast).
import kotlinx.coroutines.*
fun main() = runBlocking {
// supervisorScope: child failures don't cancel siblings
supervisorScope {
launch { delay(100); throw RuntimeException("A fails") }
launch { delay(200); println("B still runs") }
}
// vs coroutineScope: A's failure cancels B
// Use SupervisorJob for long-lived services
val scope = CoroutineScope(SupervisorJob())
}Channels (style CSP)
Les Channels passent des valeurs entre coroutines (comme les channels Go). Rendezvous (capacité 0) synchronise émetteur et récepteur ; buffered permet la file d'attente. send suspend quand plein, receive quand vide. Fermez toujours les producteurs avec close(). Pour le fan-out, utilisez BroadcastChannel ou SharedFlow. Préférez Flow pour la plupart des besoins de streaming.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
val channel = Channel<Int>(3) // buffered capacity 3
launch {
for (i in 1..5) {
channel.send(i)
println("Sent $i")
}
channel.close()
}
// receive() blocks until available
for (x in channel) println("Got $x")
// Produce-consume pattern
// Rendezvous (cap 0) syncs sender/receiver
}Flow en profondeur
Opérateurs Flow
Les opérateurs Flow sont froids—ils s'exécutent uniquement lorsqu'ils sont collectés. map/filter/take sont comme Stream/Sequence. transform est le plus flexible (peut émettre plusieurs valeurs par entrée). Les variantes flatMap gèrent les flows imbriqués : concat (séquentiel), merge (concurrent), latest (annule le précédent). Choisissez selon les besoins d'ordonnancement/concurrence.
import kotlinx.coroutines.flow.*
fun nums() = flow {
for (i in 1..5) emit(i)
}
suspend fun main() {
nums().map { it * it } // 1,4,9,16,25
.filter { it > 5 } // 9,16,25
.take(2) // 9,16
.collect { println(it) }
// transform: emit multiple values
nums().transform { x ->
emit(x)
emit(x * 10)
}.collect { println(it) }
// flatMapConcat / flatMapMerge / flatMapLatest
}Buffer et concurrence
buffer découple le producteur et le consommateur avec une file à capacité fixe—utile lorsque les vitesses de production et consommation diffèrent. conflate ne garde que la dernière valeur (drop les intermédiaires) pour les mises à jour UI/état. collectLatest annule le collecteur précédent lorsqu'une nouvelle valeur arrive—idéal pour search-as-you-type.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
for (i in 1..3) {
delay(100); emit(i)
}
}
fun main() = runBlocking {
// Without buffer: producer & consumer alternate (slow)
events().buffer() // decouple producer/consumer
.collect { delay(200); println(it) }
// conflate: drop intermediate values
events().conflate().collect { println(it) }
// collectLatest: cancel previous collection
events().collectLatest { delay(200); println(it) }
}StateFlow et SharedFlow (flux chauds)
StateFlow est un flux chaud tenant une seule valeur à jour—utilisez-le pour l'état UI (remplace LiveData). Il a toujours une valeur et conflate. SharedFlow est un flux de diffusion chaud (multiple collecteurs, pas de valeur initiale requise)—utilisez-le pour les événements. SharedFlow avec replay=1 se comporte comme un bus d'événements en cache.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// StateFlow: holds one value, stateful
val state = MutableStateFlow(0)
launch {
state.collect { println("State: $it") }
}
state.value = 1 // update synchronously
state.value = 2
// SharedFlow: broadcasts to multiple collectors
val events = MutableSharedFlow<String>()
launch { events.collect { println("A: $it") } }
launch { events.collect { println("B: $it") } }
events.emit("Hello") // both receive
}Gestion des exceptions Flow
catch gère les exceptions en amont et peut émettre des valeurs de repli. Il n'attrape que les exceptions émises avant lui dans la chaîne. retry/retryWhen recollectent le flux en cas d'échec (utile pour les appels réseau). Pour les exceptions en aval, enveloppez collect dans un try/catch. N'attrapez jamais d'exceptions dans le constructeur de flux—laissez-les se propager.
import kotlinx.coroutines.flow.*
fun numbers() = flow {
emit(1)
throw RuntimeException("fail")
emit(2) // never reached
}
suspend fun main() {
// catch operator (upstream only)
numbers().catch { e -> emit(-1) }
.collect { println(it) } // 1, -1
// retry on failure
numbers().retry(3) { e ->
println("Retry: $e"); true
}.collect { println(it) }
// retryWhen for custom logic
}flowOn & Contexte
flowOn change le répartiteur pour le flux en amont (producteur + opérateurs au-dessus de lui). C'est essentiel lorsque le flux fait des E/S bloquantes—enveloppez-le avec flowOn(Dispatchers.IO). L'aval (collect) s'exécute sur le contexte de l'appelant. Plusieurs appels flowOn créent des contextes séparés pour chaque segment.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
for (i in 1..3) {
Thread.sleep(100) // blocking I/O
emit(i)
}
}
suspend fun main() {
// flowOn changes upstream context
diskFlow().flowOn(Dispatchers.IO)
.collect { println(it) }
// Without flowOn, runs on collector's dispatcher
// flowOn applies to all operators above it
diskFlow().map { it * 2 }
.flowOn(Dispatchers.IO)
.filter { it > 2 }
.collect { println(it) }
}Classes sealed & ADT
Bases des classes sealed
Les classes sealed restreignent les sous-classes à un ensemble connu (même fichier/module). Le compilateur connaît tous les types possibles, permettant des expressions when exhaustives sans else. Idéal pour modéliser des états finis (Result, UiState, réponses réseau). Combinées avec les data classes, elles forment les Types de Données Algébriques.
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
is Result.Success -> println("Got ${r.value}")
is Result.Failure -> println("Error: ${r.error}")
Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/moduleInterfaces sealed (Kotlin 1.5+)
Les interfaces sealed (Kotlin 1.5+) étendent sealed aux interfaces, permettant à une classe d'implémenter plusieurs types sealed—plus flexible que les classes sealed. La restriction même fichier/même module s'applique. Utile pour la modélisation de domaine où un type appartient à plusieurs catégories.
sealed interface Shape {
fun area(): Double
}
data class Circle(val r: Double) : Shape {
override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
override fun area() = 0.0
override fun click() = println("Clicked")
}When exhaustif
When utilisé comme expression (et non instruction), le compilateur exige tous les cas sealed—pas de else nécessaire. Ajouter une nouvelle sous-classe produit des erreurs de compilation sur les sites when non gérés, rendant le refactoring sûr. C'est la fonctionnalité clé des classes sealed pour les machines à états et le rendu UI.
sealed class NetworkState {
object Loading : NetworkState()
data class Success(val data: String) : NetworkState()
data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
NetworkState.Loading -> "Spinner"
is NetworkState.Success -> "Data: ${state.data}"
is NetworkState.Error -> "Error: ${state.message}"
// No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle itModélisation ADT (Result/Either)
Les classes sealed modélisent des types somme (Either/Result) pour la programmation orientée railway. fold gère les deux branches. Contrairement aux exceptions, les erreurs sont explicites dans la signature de type. Arrow-kt fournit un Either/Validated plus riche. Utilisez-les pour une gestion d'erreurs prévisible et composable sans try/catch.
sealed class Either<out L, out R> {
data class Left<out L>(val value: L) : Either<L, Nothing>()
data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
ifLeft: (L) -> Unit,
ifRight: (R) -> Unit
) = when (this) {
is Either.Left -> ifLeft(value)
is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
if (b == 0) Either.Left("Divide by zero")
else Either.Right(a / b)Classes sealed avec récursion
Les classes sealed peuvent être récursives, modélisant des structures arboresques (AST, JSON, expressions). Le pattern matching avec when + récursion les évalue élégamment. C'est l'usage classique des ADT fonctionnels—type-safe, exhaustif et adapté au refactoring. Utilisé dans les compilateurs, parseurs et DSL de configuration.
sealed class Expr {
data class Num(val value: Int) : Expr()
data class Add(val left: Expr, val right: Expr) : Expr()
data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
is Expr.Num -> e.value
is Expr.Add -> eval(e.left) + eval(e.right)
is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr)) // 14Propriétés déléguées
Délégué lazy
lazy diffère l'initialisation jusqu'au premier accès et met en cache le résultat. Par défaut thread-safe (double-checked locking). Utilisez-le pour les ressources coûteuses (configs, connexions DB, singletons) qui pourraient ne pas être nécessaires. Passez le mode NONE pour les contextes mono-thread afin d'éviter le coût de synchronisation.
val heavyConfig: Config by lazy {
println("Initializing...")
loadConfigFromFile() // runs once, first access
}
fun main() {
println("Before access")
println(heavyConfig) // initializes here
println(heavyConfig) // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threadedobservable & vetoable
observable déclenche un callback après chaque changement (journalisation, effets de bord). vetoable peut rejeter les changements en retournant false (validation). Les deux prennent une valeur initiale et un lambda. Utilisez-les pour l'état réactif, la validation ou le déclenchement de mises à jour UI. Pour des scénarios plus complexes, utilisez des délégués personnalisés.
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
new >= 0 // reject negative values
}
fun main() {
count = 1 // prints "Changed: 0 -> 1"
count = 2 // prints "Changed: 1 -> 2"
age = 25 // accepted
age = -5 // rejected, stays 25
}Délégué de propriété personnalisé
Les délégués personnalisés implémentent getValue (et setValue pour les vars) via ReadOnlyProperty/ReadWriteProperty. Ils encapsulent un comportement de propriété réutilisable (validation, formatage, cache, mapping DB). thisRef est le propriétaire, prop est les métadonnées de la propriété. Puissant pour les frameworks ORM/sérialisation.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
private var value: String = ""
override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
this.value = value.trim()
}
}
class User {
var name: String by TrimmedString()
}
fun main() {
val u = User()
u.name = " Alice "
println(u.name) // "Alice"
}Propriétés basées sur Map
Déléguer à une Map vous permet de lier des propriétés à des clés dynamiques—utile pour analyser JSON, configs ou lignes ORM sans boilerplate. Le nom de la propriété doit correspondre à la clé de la map. Utilisez MutableMap pour les propriétés inscriptibles. C'est ainsi que fonctionnent kotlinx.serialization et de nombreux ORM en interne.
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
val email: String? by map
}
fun main() {
val u = User(mapOf(
"name" to "Alice",
"age" to 30,
"email" to "[email protected]"
))
println(u.name) // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
var name: String by map
}notNull & Délégué Singleton
notNull() est comme lateinit mais pour tout type (y compris les primitifs) et fonctionne avec val/var. Il lève une exception si accédé avant initialisation. Pour les singletons, préférez object (eager) ou by lazy (lazy). lateinit est pour les vars dans les classes uniquement ; le délégué notNull est plus flexible mais a un léger coût.
import kotlin.properties.Delegates
class Service {
// Late-init for non-null, set once
var config: String by Delegates.notNull()
// Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
val s = Service()
// println(s.config) // IllegalStateException
s.config = "prod"
println(s.config) // prod
}Construction de DSL
Builders type-safe
Les builders type-safe utilisent des types de fonction avec receveur (lambda avec receveur) pour créer des DSL imbriqués. Le lambda s'exécute dans le contexte du receveur (this), vous appelez donc ses méthodes directement. C'est ainsi que fonctionnent le DSL HTML de Kotlin, les scripts de build Gradle et kotlinx.html—déclaratif et vérifié à la compilation.
class Table {
private val rows = mutableListOf<Row>()
fun row(init: Row.() -> Unit) {
val r = Row(); r.init(); rows.add(r)
}
fun build() = rows
}
class Row {
private val cells = mutableListOf<String>()
fun cell(text: String) { cells.add(text) }
fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
row { cell("A"); cell("B") }
row { cell("C"); cell("D") }
}@DslMarker (Contrôle de portée)
@DslMarker empêche la fuite implicite du receveur—dans un bloc DSL imbriqué, vous ne pouvez appeler que les méthodes du receveur le plus interne. Sans lui, Kotlin permettrait d'appeler les méthodes des receveurs externes, conduisant à des DSL confus/buggés. Annotete toutes les classes DSL avec la même annotation marqueur.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
body {
// p() is unambiguous—Body's method
// body() would be an error (not Body's method)
p("Hello")
}
}Fonctions infix
Les fonctions infix permettent une syntaxe en langage naturel (a to b, 1 until 10). Elles doivent être des fonctions membre ou d'extension avec un seul paramètre. Utilisées massivement dans les DSL, frameworks de test et bibliothèques mathématiques. Exemples intégrés : to (Pair), until/step (intervalles) et opérations de collection.
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
println(3 times "ab") // "ababab"
// Equivalent to: 3.times("ab")
// No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.Surcharge d'opérateurs
La surcharge d'opérateurs permet aux objets d'utiliser +, -, *, [], (), etc. avec une syntaxe naturelle. Marquez les fonctions avec operator. Surchargz de manière sensée—les types mathématiques (Vec, Matrix, Money) en bénéficient ; la surcharge arbitraire nuit à la lisibilité. get/set permettent l'indexation ; invoke rend les objets appelables. Les opérateurs d'intervalle (rangeTo, contains) alimentent les boucles for.
data class Vec(val x: Int, val y: Int) {
operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
operator fun times(s: Int) = Vec(x * s, y * s)
operator fun unaryMinus() = Vec(-x, -y)
operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
val a = Vec(1, 2)
val b = Vec(3, 4)
println(a + b) // Vec(x=4, y=6)
println(a * 2) // Vec(x=2, y=4)
println(-a) // Vec(x=-1, y=-2)
println(a[0]) // 1
}Types de fonction avec receveur
Les types de fonction avec receveur (A.(B) -> C) permettent aux lambdas d'accéder au receveur comme 'this'. C'est le fondement des DSL Kotlin et des fonctions de portée (apply, run, with). apply retourne le receveur ; run retourne le résultat du lambda. Maîtrisez cela pour construire des DSL internes expressifs et type-safe.
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
println("Hello".greet(3)) // Hello!!!Hello!!!Hello!!!
// 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
private val parts = mutableListOf<String>()
fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
StringBuilder().apply(init)
val sb = build { add("a"); add("b") }Tests (JUnit 5, MockK, Turbine)
Bases de JUnit 5
JUnit 5 (Jupiter) est le framework de test standard pour Kotlin. @Test marque les tests ; @BeforeEach/@AfterEach exécutent le setup/teardown par test. Utilisez des noms avec backticks pour la lisibilité. @ParameterizedTest + @ValueSource exécutent un test avec plusieurs entrées. assertThrows vérifie les exceptions. @Disabled ignore des tests.
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
lateinit var calc: Calculator
@BeforeEach fun setup() { calc = Calculator() }
@Test fun `add two numbers`() {
assertEquals(5, calc.add(2, 3))
}
@Test fun `divide by zero throws`() {
assertThrows<ArithmeticException> { calc.divide(10, 0) }
}
@ParameterizedTest
@ValueSource(ints = [1, 2, 3])
fun `positive numbers`(n: Int) { assertTrue(n > 0) }
@Disabled("TODO") @Test fun skip() {}
}MockK (Simulation)
MockK est la bibliothèque de simulation idiomatique de Kotlin (gère les classes final, coroutines, fonctions d'extension). every { } returns/throws stubbe ; verify { } vérifie les appels. Utilisez relaxed = true pour ignorer le stubbing des méthodes void. MockK supporte nativement les fonctions suspend via coEvery/coVerify—essentiel pour les tests de coroutines.
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
val mock = mockk<UserService>()
@Test fun test() {
every { mock.find(1) } returns "Alice"
every { mock.find(any()) } returns null
every { mock.save(any()) } throws RuntimeException("fail")
verify { mock.find(1) } // called once
verify(exactly = 2) { mock.find(any()) }
confirmVerified(mock)
}
}
// relaxUnitFun / relaxed = true for void methodsTurbine (Tests de Flow)
Turbine est la bibliothèque standard pour tester Flow. test { } collecte dans un scope contrôlé ; awaitItem() vérifie la prochaine émission, awaitComplete()/awaitError() vérifient la terminaison. Il gère le timing et le temps virtuel correctement. Combinez avec runTest pour des tests Flow rapides, déterministes et adaptés aux coroutines.
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
@Test fun testFlow() = runTest {
flowOf(1, 2, 3).test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
@Test fun testError() = runTest {
flow<Int> { throw RuntimeException("x") }.test {
assertTrue(awaitError() is RuntimeException)
}
}
}Tests de coroutines (runTest)
runTest utilise le temps virtuel—les délais sont instantanément sautés, rendant les tests async rapides et déterministes. Utilisez StandardTestDispatcher + advanceUntilIdle() pour un contrôle manuel. Injectez un TestDispatcher dans le code de production (au lieu de Dispatchers.Main) pour la testabilité. Évitez runBlocking dans les tests—runTest est la méthode moderne.
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
@Test fun fetch() = runTest {
// Virtual time: delay(1000) completes instantly
val result = async { delay(1000); "data" }.await()
assertEquals("data", result)
}
@Test fun withDispatcher() = runTest {
val scheduler = StandardTestDispatcher()
val scope = CoroutineScope(scheduler)
// advanceUntilIdle() runs pending coroutines
scope.launch { /*...*/ }
scheduler.advanceUntilIdle()
}
}Assertions et styles Kotest
Kotest offre des assertions expressives (shouldBe, shouldContain) et plusieurs styles de spec (StringSpec, FunSpec, BehaviorSpec pour BDD). Il s'intègre aux tests basés sur les propriétés et possède des matchers riches. Choisissez-le plutôt que JUnit pour des tests lisibles et idiomatiques Kotlin. Il peut fonctionner aux côtés de JUnit 5.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
"addition works" {
1 + 1 shouldBe 2
}
"list contains" {
listOf(1, 2, 3) shouldContain 2
}
"string prefix" {
"hello" shouldStartWith "he"
}
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpecProgrammation fonctionnelle avancée
Fonctions de portée (let, run, with, apply, also)
Les fonctions de portée diffèrent par le receveur (this vs it) et la valeur de retour (this vs résultat). apply/also retournent le receveur (chaînage, builders) ; let/run/with retournent le résultat du lambda (transformations). Utilisez apply pour la configuration, let pour les vérifications de null/transformations, also pour les effets de bord. N'en abusez pas—la lisibilité d'abord.
data class User(var name: String, var age: Int)
fun main() {
val u = User("A", 1)
// apply: configure & return this (builders)
val configured = u.apply { age = 30 }
// let: transform, it = receiver, returns lambda result
val name = u.let { it.name.uppercase() }
// run: transform, this = receiver, returns result
val s = u.run { "$name:$age" }
// with: like run but called as function
val str = with(u) { "$name:$age" }
// also: side effects, returns this (logging/chaining)
u.also { println("Created $it") }
}Séquences (Évaluation paresseuse)
Les séquences s'évalent paresseusement—chaque élément traverse tout le pipeline avant que le suivant commence (comme Java Streams). Cela évite les collections intermédiaires et court-circuite (take/find). Utilisez-les pour les grands jeux de données ou les pipelines multi-étapes. Pour les petites listes, une List eager est souvent plus rapide (moins de surcoût). generateSequence construit des flux infinis.
fun main() {
// Eager: List processes each step fully
val eager = (1..10).toList()
.map { it * 2 }
.filter { it > 5 }
// Lazy: Sequence processes element-by-element
val lazy = (1..10).asSequence()
.map { println("map $it"); it * 2 }
.filter { println("filter $it"); it > 5 }
.take(2)
.toList() // only processes until 2 found
// Use Sequence for large/pipelined data
// generateSequence for infinite streams
val naturals = generateSequence(1) { it + 1 }
}Fonctions inline & reified
inline élimine la surcoût d'allocation d'objets lambda en inlinant le bytecode—critique pour les boucles chaudes. Cela permet aussi les retours non locaux (return depuis la fonction externe dans un lambda). Les paramètres de type reified rendent l'info de type générique disponible à l'exécution (T::class, is T), mais nécessitent inline. Utilisez-les pour des assistants type-safe comme filterIsInstance.
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()Fonctions d'ordre supérieur
Les fonctions d'ordre supérieur prennent ou retournent des fonctions. Elles sont l'épine dorsale du Kotlin fonctionnel—map, filter, fold, compose. Les types de fonction comme (Int) -> Int sont de première classe. compose construit des pipelines. Utilisez-les pour abstraire des patterns, permettre la réutilisation de code et écrire du code déclaratif. Marquez inline pour les chemins critiques en performance.
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) result.add(transform(item))
return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
{ x -> f(g(x)) }
fun main() {
val double = { x: Int -> x * 2 }
val inc = { x: Int -> x + 1 }
val doubleThenInc = compose(inc, double)
println(doubleThenInc(3)) // 7
listOf(1, 2, 3).mapTo { it * it }
}Récursion & tailrec
tailrec convertit les fonctions tail-récursives (où la récursion est la dernière opération) en boucles, empêchant le débordement de pile. L'appel récursif doit être en position tail—pas de multiplication/addition en attente. Utilisez un paramètre accumulateur pour rendre les fonctions tail-récursives. Essentiel pour les boucles de style fonctionnel sur de grandes entrées.
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
println(factorialTail(10000)) // no stack overflow
}Génériques & Variance
Classes & fonctions génériques
Les génériques permettent un code type-safe et réutilisable. Les classes utilisent <T> ; les fonctions déclarent <T> avant le type de retour. Les contraintes (T : Entity) restreignent la borne du type. Contrairement à Java, les génériques de Kotlin sont reified pour les fonctions inline et ont une variance au site de déclaration, rendant les APIs génériques plus sûres et ergonomiques.
class Box<T>(val value: T) {
fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
val intBox = Box(42) // Box<Int> inferred
val strBox = Box<String>("hi")
val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }Variance (in/out)
La variance contrôle les relations de sous-type des génériques. out (covariant) : un Producer<Dog> est un Producer<Animal>—sûr car vous ne lisez que T. in (contravariant) : un Sink<Animal> est un Sink<Dog>—sûr car vous n'écrivez que T. Les collections mutables sont invariantes (lecture + écriture). Utilisez out/in pour concevoir des APIs sûres et flexibles.
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
fun add(item: T) {}
fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>() // OK (out)
val sink: Sink<Dog> = Sink<Animal>() // OK (in)Variance en pratique
Les types de fonction utilisent la variance automatiquement : les paramètres sont 'in', les types de retour sont 'out'. C'est pourquoi (Dog) -> Unit est assignable à (Animal) -> Unit. Le principe PECS (Producer Extends, Consumer Super) correspond à out/in de Kotlin. Concevez des interfaces génériques avec out quand elles ne produisent que T, in quand elles ne consomment que T.
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo() // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumerProjections de type & projection étoile
Les projections de type rendent temporairement un type variant au site d'usage. Array<out Any> signifie 'Array d'un sous-type d'Any, lisible'. Array<in Any> signifie 'inscriptible comme Any'. La projection étoile (*) signifie 'un type inconnu'—utile quand vous n'avez besoin que de size/contains, pas du type d'élément. Comme les wildcards de Java.
fun copy(from: Array<out Any>, to: Array<in Any>) {
// from: only read (covariant projection)
// to: only write (contravariant projection)
for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
println(list.size) // OK (no type needed)
// list[0] // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknownParamètres de type reified
reified préserve l'info de type générique à l'exécution (normalement effacée sur JVM). Il nécessite inline (le type est connu à chaque site d'appel). Permet les vérifications is T, T::class et filterIsInstance. Sans reified, vous devriez passer un paramètre Class<T> manuellement. Limitation : les types reified ne peuvent être utilisés dans les fonctions non-inline ou comme paramètres de type de classe.
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
val list = listOf(1, "a", 2, "b", 3)
val strs: List<String> = list.filterIsInstance()
println(strs) // [a, b]
println(typeOf<String>()) // String
// Without reified, T is erased at runtime
// reified makes T::class and 'is T' available
}Sécurité null avancée
Types nullables
Kotlin distingue les types nullables (T?) et non-null (T) à la compilation. ?. l'appel sûr retourne null si le receveur est null. ?: Elvis fournit une valeur par défaut. !! lève une NPE (à éviter). Le compilateur impose les vérifications de null, éliminant NullPointerException dans le code Kotlin.
var name: String = "Alice" // Non-null
// name = null // Error
var nickname: String? = null // Nullable
// Safe call
println(nickname?.length) // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0 // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length // NPE if nulllet & run
let exécute un bloc si la valeur est non-null (it = la valeur). run appelle un bloc avec l'objet comme receveur, retourne le résultat. apply configure un objet, retourne l'objet. also effectue des effets de bord, retourne l'objet. Ces fonctions de portée réduisent les vérifications de null et améliorent la lisibilité.
val name: String? = "Alice"
name?.let {
println("Length: ${it.length}") // Only if not null
}
// run: object + block
val result = "Hello".run {
length // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
add(1); add(2); add(3)
}lateinit
lateinit diffère l'initialisation des propriétés non-null. Ne peut être utilisé avec les primitifs ou types nullables. Lève UninitializedPropertyAccessException si accédé avant init. ::prop.isInitialized vérifie. Utile pour l'injection de dépendances et les propriétés gérées par cycle de vie.
class Service {
lateinit var dependency: Database
fun init() {
dependency = connectDatabase()
}
fun use() {
if (::dependency.isInitialized) {
dependency.query()
}
}
}Collections nullables
Les collections peuvent contenir des éléments nullables (List<String?>) ou être nullables (List<String>?). filterNotNull supprime les null. firstOrNull retourne null au lieu de lever une exception. Gérez les éléments nullables avec des appels sûrs. Soyez explicite sur la nullabilité dans les types de collection.
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull() // ["a", "b"]
val lengths = list.map { it?.length } // [1, null, 1]
val first = list.firstOrNull() // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }Types plateforme
Les types plateforme proviennent de l'interop avec Java où la nullabilité est inconnue. Kotlin ne peut pas imposer la sécurité null pour eux. Déclarez toujours explicitement les types nullables pour les valeurs de retour Java. Utilisez les annotations @Nullable/@NotNull en Java. Les annotations JSR-305 aident Kotlin à inférer la nullabilité.
// Java interop: platform type (no null info)
val name: String = javaObject.getName() // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in JavaConstruction de DSL
Builder avec receveur lambda
Le lambda avec receveur (T.() -> Unit) permet la syntaxe DSL. Dans le lambda, this est l'objet receveur. Les méthodes peuvent être appelées sans qualification. C'est ainsi que Kotlin construit des DSL type-safe comme Gradle, HTML et les builders SQL.
class StringBuilder {
private val parts = mutableListOf<String>()
fun line(s: String) { parts.add(s) }
fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.init() // Execute lambda with sb as receiver
return sb.build()
}DSL HTML
Le DSL HTML utilise des builders imbriqués avec receveurs lambda. Chaque balise est une fonction qui crée un builder enfant. Le lambda configure l'enfant. Produit du HTML type-safe et composable. kotlinx.html est une implémentation réelle. Le même pattern fonctionne pour toute structure hiérarchique.
fun html(init: HTML.() -> Unit): HTML {
val h = HTML(); h.init(); return h
}
class HTML {
fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
body {
// this: Body
p("Hello")
}
}@DslMarker
@DslMarker empêche l'accès implicite au receveur des portées externes. Sans lui, les méthodes de HTML et Body sont toutes accessibles, causant de la confusion. L'annotation restreint l'accès au receveur le plus interne. Rend les DSL type-safe et non ambigus. Essentiel pour les DSL complexes.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
body {
// p() // OK: in Body scope
// body() // Error: in Body scope, not HTML
}
}DSL Gradle
Le DSL Kotlin de Gradle utilise le même pattern de builder. plugins, dependencies sont des fonctions avec lambdas receveur. implementation, testImplementation sont des fonctions de configuration de dépendances. Type-safe : le compilateur vérifie les noms de fonction et les types de paramètres. Bien meilleur que Groovy pour le refactoring.
plugins {
kotlin("jvm") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
// All are function calls with lambda receiversAnko SQL
Anko (et Exposed) fournissent des DSL SQL type-safe. Les comparaisons de colonnes sont typées. Impossible de comparer une colonne string à un int. Le DSL génère le SQL. Empêche l'injection SQL et les erreurs de type. Le même pattern s'applique à tout langage spécifique au domaine.
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
// DSL for SQL queries
}
users {
(Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column typesCollections
Opérations sur List
Les collections Kotlin ont des APIs fonctionnelles riches. map, filter, reduce sont standard. groupBy partitionne par clé. chunked divise en listes de taille fixe. windowed crée des fenêtres glissantes. Toutes retournent de nouvelles collections. Utilisez asSequence() pour l'évaluation paresseuse.
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 } // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2) // [[1,2],[3,4],[5]]Sequence
Sequence est une collection paresseuse (comme Java Stream). Les opérations sont évaluées à la demande. Pas de collections intermédiaires. Efficace pour les grandes collections avec opérations chaînées. Utilisez asSequence() pour convertir. Forcez avec toList(), toSet(), etc.
val result = (1..1000000).asSequence()
.map { it * 2 }
.filter { it > 100 }
.take(10)
.toList()
// Lazy: only computes what is needed
// No intermediate collectionsDestructuration
La destructuration sépare les objets en variables. Fonctionne avec Pair, Triple, data classes et entrées de Map. Les fonctions componentN() le permettent. withIndex() associe index et valeur. Utile pour les valeurs de retour multiples et l'itération. Les data classes génèrent automatiquement componentN.
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)Mutable vs Immutable
Kotlin a des collections mutables (MutableList) et immuables (List). Préférez immuable pour la sécurité. toList() crée une copie immuable. toMutableList() crée une copie mutable. Les interfaces immuables n'exposent pas de méthodes de mutation, empêchant la modification accidentelle.
val mutable = mutableListOf(1, 2, 3)
mutable.add(4) // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4) // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()Associate & Partition
toMap/associate convertissent en maps. partition divise en deux listes par prédicat. flatten fusionne les listes imbriquées. flatMap mappe et aplatit. Ces opérations remplacent les boucles verbeuses par des expressions déclaratives. Toutes retournent de nouvelles collections.
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap() // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten() // [1,2,3]Pièges courants
== vs ===
Kotlin == appelle equals (égalité de valeur), contrairement à Java. === vérifie l'égalité de référence. Utilisez == pour la comparaison de valeurs. === est rarement nécessaire. Pour Integer, les valeurs -128 à 127 sont mises en cache, donc === peut être vrai ou faux. Utilisez toujours == pour les valeurs.
val a = Integer(127)
val b = Integer(127)
a == b // true (value equality, calls equals)
a === b // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d // true
c === d // false (not cached)Companion Object
Kotlin n'a pas de membres statiques. companion object contient les méthodes et constantes "statiques". const val est une vraie constante de compilation. Le companion object est une instance singleton. @JvmStatic rend les méthodes appelables comme statiques depuis Java. Utilisez des fonctions de premier niveau pour de vrais statiques.
class MyClass {
companion object {
const val CONSTANT = 42
fun create() = MyClass()
}
}
MyClass.CONSTANT // 42
MyClass.create() // Static-like access
// companion object is a real object, not staticCopie de data class
Les data classes génèrent automatiquement copy() qui crée une copie modifiée. L'original est inchangé (immuable). Seuls les champs spécifiés changent. Utile pour les mises à jour. Combiné à la destructuration, les data classes sont puissantes pour modéliser les données. Évitez var dans les data classes pour l'immuabilité.
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // New instance
// alice is unchanged
// copy uses named arguments for changed fieldsExhaustivité des classes sealed
Les classes sealed restreignent les sous-types au même fichier/package. when sur les classes sealed est exhaustif : le compilateur avertit si une branche manque. Pas de else nécessaire si tous les cas sont couverts. Ajouter un nouveau sous-type provoque des avertissements partout. Idéal pour les machines à états et les résultats.
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
is Success -> r.value
// Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
is Success -> r.value
is Failure -> 0
} // No else neededExtension vs Membre
Les fonctions membre ont toujours priorité sur les fonctions d'extension avec la même signature. Les extensions sont résolues à la compilation (dispatch statique), les membres à l'exécution (dispatch dynamique). Les extensions ne peuvent pas surcharger les membres. Utilisez les extensions pour les fonctions utilitaires, pas pour le polymorphisme.
class Foo {
fun bar() = "member" // Member function
}
fun Foo.bar() = "extension" // Extension function
Foo().bar() // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)Kotlin Multiplatform
Code commun
Kotlin Multiplatform (KMP) partage du code entre plateformes. expect/actual déclare des implémentations spécifiques à chaque plateforme. commonMain contient le code partagé. Les source sets spécifiques à chaque plateforme implémentent actual. Partagez la logique métier, gardez l'UI native. Gradle configure les cibles.
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
fun greet() = "Hello from ${getPlatformName()}"
}Module partagé
Les projets multiplateformes utilisent le bloc kotlin dans Gradle. Définissez les cibles (android, ios). commonMain a les dépendances partagées. Les source sets de plateforme peuvent avoir des deps spécifiques. iOS utilise Kotlin/Native pour la compilation directe. Partagez la logique, pas l'UI.
// build.gradle.kts (shared module)
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
}
}
}Réseau (Ktor)
Ktor est un client HTTP multiplateforme. L'API est commune, le moteur est spécifique à la plateforme. Android utilise le moteur OkHttp/Android. iOS utilise Darwin. Les coroutines fonctionnent entre plateformes. Partagez le réseau, la base de données (SQLDelight) et la logique métier.
// Shared HTTP client
suspend fun fetchUser(): User {
return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same APISQLDelight
SQLDelight génère du Kotlin type-safe depuis SQL. Les fichiers .sq contiennent le SQL avec des requêtes nommées. Génère des objets de requête type-safe. Fonctionne entre plateformes (Android, iOS, JVM). Le SQL est la source de vérité. Les migrations de schéma sont suivies. Alternative à Room pour le multiplateforme.
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()Compose Multiplatform
Compose Multiplatform étend Jetpack Compose à iOS, Desktop et Web. Partagez le code UI entre plateformes. Même API @Composable. Points d'entrée spécifiques à la plateforme. Encore expérimental pour iOS. Réduit la duplication d'UI. Plugin Gradle : compose-multiplatform.
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { Greeting("World") }
}
}
// Desktop
fun main() = application {
Window { Greeting("Desktop") }
}Tests Kotlin
JUnit 5
JUnit 5 est le framework de test standard. Kotlin permet des noms de test avec backticks pour la lisibilité. assertEquals, assertThrows sont des assertions courantes. @BeforeEach, @AfterEach pour le setup/teardown. @ParameterizedTest pour les tests data-driven. Utilisez kotlin.test pour le multiplateforme.
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
@Test
fun `test addition`() {
val calc = Calculator()
assertEquals(5, calc.add(2, 3))
}
@Test
fun `throws on negative`() {
assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
}
}MockK
MockK est une bibliothèque de simulation native Kotlin. Supporte les classes final et les fonctions d'extension. every stubbe, verify vérifie les appels. @MockK crée des mocks, @InjectMockKs les injecte. coEvery/coVerify pour les coroutines. Meilleur support Kotlin que Mockito.
class UserServiceTest {
@MockK
lateinit var repo: UserRepository
@InjectMockKs
lateinit var service: UserService
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { repo.find(1) } returns User("Alice")
}
@Test
fun `test find`() {
assertEquals("Alice", service.find(1).name)
verify { repo.find(1) }
}
}Kotest
Kotest est un framework de test Kotlin-first. Plusieurs styles : StringSpec, BehaviorSpec, FunSpec. shouldBe est une assertion fluente. Tests de propriétés avec Arb. Supporte les tests data-driven. S'intègre avec Spring et Ktor. Plus idiomatique Kotlin que JUnit.
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
"addition should work" {
Calculator().add(2, 3) shouldBe 5
}
"should throw on negative" {
shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
}
})Tests de coroutines
runTest de kotlinx-coroutines-test fournit le temps virtuel. Les délais sont sautés instantanément. advanceUntilIdle exécute les coroutines en attente. Bien plus rapide que le temps réel. Utilisez TestDispatcher pour un contrôle fin. La bibliothèque Turbine teste les émissions de Flow.
@Test
fun `test async`() = runTest {
val result = fetchData()
assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantlyTurbine (Tests de Flow)
Turbine teste les émissions de Flow. awaitItem obtient la prochaine émission. awaitComplete vérifie la complétion. awaitError vérifie une erreur. Le bloc test se suspend jusqu'à ce que le flux se termine. Bien plus propre que la collecte manuelle. Essentiel pour les tests de Flow.
@Test
fun `test flow`() = runTest {
flowOf(1, 2, 3).test {
awaitItem() shouldBe 1
awaitItem() shouldBe 2
awaitItem() shouldBe 3
awaitComplete()
}
}
// Test errors
flow<Int> { throw Exception() }.test {
awaitError() shouldBe Exception()
}Coroutines avancées
Portée de coroutine
CoroutineScope définit une durée de vie pour les coroutines. viewModelScope s'annule automatiquement à la destruction du VM. SupervisorJob empêche les échecs enfants d'annuler les frères. Les scopes personnalisés nécessitent une annulation explicite. Les scopes propagent l'annulation aux enfants. N'utilisez jamais GlobalScope en production (non annulable).
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = api.getData()
_data.value = data
}
}
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel() // Cancel all childrenDispatchers
Les Dispatchers routent les coroutines vers les pools de threads. Main : thread UI (Android). IO : E/S bloquantes (64+ threads). Default : travail CPU (nombre de CPU threads). Unconfined : thread appelant (avancé). withContext change de dispatcher temporairement. Choisir le bon dispatcher améliore les performances et empêche les gelures UI.
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
val data = readDisk()
}Opérateurs de Flow
Opérateurs de Flow : map transforme, filter sélectionne, flatMapMerge/flatMapConcat chaîne les flux. Les cold flows démarrent frais à chaque collecteur. StateFlow détient une valeur (comme LiveData). SharedFlow diffuse à plusieurs collecteurs. stateIn convertit cold en hot. buffer/conflate contrôlent la backpressure.
flowOf(1, 2, 3, 4, 5)
.map { it * 2 }
.filter { it > 4 }
.collect { println(it) } // 6, 8, 10
// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1Gestion des exceptions
CoroutineExceptionHandler attrape les exceptions non capturées dans launch. Les exceptions async se propagent vers await. SupervisorJob isole les échecs enfants. CancellationException est spécial : relancée, non attrapée par catch. N'avalez jamais CancellationException. Utilisez try/finally ou use() pour le nettoyage. L'annulation se propage à travers les appels suspend.
val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
throw RuntimeException("oops")
}
// try/catch in coroutine
try {
riskyCall()
} catch (e: Exception) {
// handle
}Channels
Les Channels permettent la communication coroutine-à-coroutine. send se suspend quand plein, receive se suspend quand vide. capacity : buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() signale la complétion. produce crée une coroutine producteur. Channel est hot : les valeurs sont consommées une fois. Préférez Flow pour la plupart des usages.
val channel = Channel<Int>(capacity = 10)
launch {
for (i in 1..5) channel.send(i)
channel.close()
}
launch {
for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
for (i in 1..10) send(i)
}Snippets Kotlin associés
Copy-paste ready code for common tasks.
Null Safety
Use nullable types, safe calls, Elvis, and smart casts for null-safe code.
Data Classes
Model immutable data with auto-generated equals, copy, and destructuring.
Coroutines
Use launch, async, await, and structured concurrency with supervisorScope.
Extension Functions
Add methods to existing types with extensions and infix operators.
Sealed Classes
Model closed hierarchies and UI state with sealed classes and when.
When Expression
Branch on values, ranges, and types with when as statement or expression.
Collections
Filter, map, group, partition, and chunk with functional operators.
Delegation
Delegate interfaces, lazy properties, observables, and custom delegates.
Was this helpful?