Chapitre 3
03 - Kotlin
03 - Kotlin
03 - Kotlin : Cours complet
Table des matières
- Le langage moderne
- Null safety
- Data classes et sealed classes
- Extensions et scope functions
- Coroutines : bases
- Structured concurrency et cancellation
- Flow, StateFlow, SharedFlow, channels
- Kotlin DSL (builds)
- Ktor client
- kotlinx.serialization
- Injection de dépendances : Hilt & Koin
- Kotlin 2.0 (K2) et Multiplatform
- Résumé et checklist
1. Le langage moderne
Kotlin est un langage statiquement typé, créé par JetBrains (2011, 1.0 en 2016), devenu en 2018 le premier langage Android. Il compile vers JVM bytecode, JavaScript et natif (Kotlin/Native), et alimente Kotlin Multiplatform.
Principes du langage :
- Expression plutôt que statement ;
- Immutabilité par défaut (val) ;
- Interop Java fluide ;
- Nullability encodée dans le type ;
- Fonctionnel + orienté objet, ni l'un ni l'autre exclusivement.
fun main() {
val users = listOf("alice", "bob", "carol")
val greeting = users
.filter { it.startsWith("a") }
.joinToString { "Bonjour $it" }
println(greeting) // Bonjour alice
}
2. Null safety
2.1 Types nullable vs non-nullable
val nonNull: String = "toujours là" // jamais null
val nullable: String? = null // peut être null
2.2 Opérateurs
| Opérateur | Rôle |
|---|---|
?. | Appel sûr (null → null) |
!! | Assertion non-null (crash si null) |
?: | Elvis (valeur par défaut) |
?.let {} | Exécute si non-null |
val length = nullable?.length ?: 0 // 0 si null
nullable?.let { println(it.uppercase()) } // exécuté seulement si non-null
Règle d'or : bannir !! hors des cas justifiés (tests, contractes documentés). Préférer let, ?: et les vérifications explicites.
2.3 Smart casts
Le compilateur affine le type après une vérification :
fun describe(x: Any?) {
if (x is String) {
println(x.length) // smart cast : x est String ici
}
val s = x as? String ?: return // cast sûr avec Elvis
}
2.4 Kotlin null-safe vs Java
En Java, tout peut être null sans warning. En Kotlin, la nullité fait partie du contrat de type : l'IDE et le compilateur vous forcent à traiter le cas null.
3. Data classes et sealed classes
3.1 Data classes
Une data class génère automatiquement equals, hashCode, toString, copy et componentN (destructuration).
data class User(val id: Long, val name: String, val email: String? = null)
val u1 = User(1, "Alice")
val u2 = u1.copy(email = "alice@example.com")
val (id, name) = u2 // destructuration
3.2 Sealed classes / sealed interface
Représente un ensemble fermé de sous-types. Indispensable pour modéliser un état.
sealed interface UiState {
data object Loading : UiState
data class Success(val products: List<Product>) : UiState
data class Error(val message: String) : UiState
}
fun render(state: UiState) = when (state) {
is UiState.Loading -> showSpinner()
is UiState.Success -> showList(state.products)
is UiState.Error -> showError(state.message)
}
Avantages : exhaustivité du when (le compilateur vérifie tous les cas), représentation claire des états.
3.3 data object
Depuis Kotlin 1.9, data object pour les objets avec toString propre. Convention : les états sans paramètres sont des data object.
4. Extensions et scope functions
4.1 Extension functions
Ajoute une fonction à un type sans héritage.
fun String.toSlug(): String =
lowercase(Locale.ROOT)
.replace(Regex("[^a-z0-9\\s-]"), "")
.trim()
.replace(Regex("\\s+"), "-")
println("Bonjour le monde".toSlug()) // bonjour-le-monde
4.2 Extension properties
val List<Int>.sumOrDefault: Int get() = if (isEmpty()) 0 else sum()
4.3 Scope functions
| Fonction | Receiver | Retour | Usage |
|---|---|---|---|
let | it | résultat | transform + null-safe |
run | this | résultat | config + calcul |
with | this | résultat | opérer sur un objet |
apply | this | l'objet | configurer (retourne this) |
also | it | l'objet | effets secondaires |
val item = Item().apply {
name = "Chaussures"
price = 45.0
}
repository.save(item).also { log("saved") }
5. Coroutines : bases
5.1 Pourquoi ?
Les callbacks empilés (« callback hell ») sont illisibles ; les threads sont coûteux. Les coroutines permettent un code séquentiel suspendable sans bloquer le thread.
5.2 Suspend functions
Une fonction suspend peut se mettre en pause sans bloquer le thread.
suspend fun fetchUser(id: Long): User = withContext(Dispatchers.IO) {
api.get("/users/$id")
}
5.3 launch vs async
launch: lance un job, pas de résultat (fire-and-forget).async: lance un Deferred, récupère le résultat avecawait.
val job = scope.launch { download() } // Unit
val deferred = scope.async { compute() } // Deferred<Int>
val result = deferred.await()
5.4 Dispatchers
| Dispatcher | Usage |
|---|---|
Dispatchers.Main | UI (Android : main thread) |
Dispatchers.IO | Réseau, fichiers, DB |
Dispatchers.Default | Calcul CPU |
Dispatchers.Unconfined | Rare, à éviter |
Ne jamais changer de contexte si ce n'est pas nécessaire : withContext(Dispatchers.IO) { ... } déplace uniquement le bloc.
6. Structured concurrency et cancellation
6.1 Le principe
Toutes les coroutines vivent dans un scope. La mort du scope annule ses enfants : c'est la structured concurrency.
class MainViewModel : ViewModel() {
fun load() {
viewModelScope.launch { // scope lié au ViewModel
try {
val data = repository.fetch()
_state.value = Success(data)
} catch (e: CancellationException) {
throw e // relancer l'annulation
} catch (e: Exception) {
_state.value = Error(e.message ?: "")
}
}
}
}
6.2 Cancellation cooperative
Une coroutine ne s'annule qu'à un point de suspension. Les fonctions suspend (delay, withContext, etc.) sont cancellable. Un calcul CPU pur doit vérifier ensureActive().
while (i < limit) {
ensureActive() // lance CancellationException si annulé
// calcul lourd
}
6.3 Bonnes pratiques
- Toujours
launchdans un scope (jamais deGlobalScope). - Catch
CancellationExceptionpour relancer, jamais pour l'avaler. runCatchingavale aussi les CancellationException : attention.
7. Flow, StateFlow, SharedFlow, channels
7.1 Flow
Flow = stream froid, asynchrone, qui émet séquentiellement.
fun fibonacci(): Flow<Int> = flow {
var a = 0; var b = 1
while (true) {
emit(a)
val tmp = a + b; a = b; b = tmp
}
}
fibonacci()
.take(10)
.collect { println(it) } // 0 1 1 2 3 5 8 13 21 34
Operators : map, filter, flatMapLatest, combine, buffer, debounce, distinctUntilChanged, catch.
7.2 StateFlow
- Hot, conserve le dernier état, expose
value. - Parfait pour l'état du ViewModel.
private val _search = MutableStateFlow("")
val search: StateFlow<String> = _search.asStateFlow()
val results = search
.debounce(300)
.mapLatest { query -> repository.search(query) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
7.3 SharedFlow
- Hot, n'implémente pas
value, permet le replay configurable. - Utile pour les événements (un événement consommé une fois).
private val _events = MutableSharedFlow<UiEvent>(replay = 0, extraBufferCapacity = 16)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
7.4 Channels
- Canal de communication producteur → consommateur.
Channel(capacity),sendsuspend si plein,receive.
val channel = Channel<Int>(Channel.BUFFERED)
scope.launch { repeat(5) { channel.send(it) } }
scope.launch { for (x in channel) println(x) }
En pratique Android : Flow + StateFlow couvrent ~90 % des besoins ; les channels servent aux cas de files/backpressure explicites.
7.5 Collecter dans Compose
val state by viewModel.state.collectAsStateWithLifecycle()
collectAsStateWithLifecycle (androidx.lifecycle) gère la reprise/arrêt selon le lifecycle.
8. Kotlin DSL (builds)
Le Kotlin DSL rend les fichiers Gradle typés et auto-complétés.
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "com.example.app"
compileSdk = 36
defaultConfig {
minSdk = 24
targetSdk = 36
}
buildFeatures {
buildConfig = true
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation("io.ktor:ktor-client-okhttp:3.1.0")
}
Bonnes pratiques : catalog + .kts (déjà couvert au chapitre 02) ; écrire des tâches personnalisées en Kotlin pur, pas en Groovy.
9. Ktor client
Client HTTP asynchrone multi-plateforme, écrit en Kotlin.
val client = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
install(HttpTimeout) {
requestTimeoutMillis = 10_000
}
defaultRequest { url("https://api.example.com") }
}
suspend fun fetchProducts(): List<Product> =
client.get("/products").body()
Engines : OkHttp (Android), Darwin (iOS), CIO, JS. Il supporte les plugins : logging, auth, cookies, WebSockets, SSE.
10. kotlinx.serialization
Sérialisation typée sans réflexion (génère du code au compile-time).
@Serializable
data class Product(
val id: Long,
val name: String,
@SerialName("price_cents") val priceCents: Long,
val tags: List<String> = emptyList()
)
val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
val text = """{"id":1,"name":"Chaussures","price_cents":4500,"tags":["sport"]}"""
val product = json.decodeFromString<Product>(text)
val encoded = json.encodeToString(product)
- Plugin Gradle requis :
org.jetbrains.kotlin.plugin.serialization. - Génère des sérialiseurs à la compilation : rapide, pas de réflexion.
11. Injection de dépendances : Hilt & Koin
11.1 Pourquoi la DI ?
- Tester (mock) plus facilement ;
- Découpler la construction des objets de leur usage ;
- Configurer un « graph de dépendances » central.
11.2 Hilt (compile-time, Dagger basé)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideHttpClient(): HttpClient = HttpClient(OkHttp) { ... }
}
@HiltViewModel
class ProductsViewModel @Inject constructor(
private val repository: ProductsRepository
) : ViewModel()
@AndroidEntryPoint
class MainActivity : ComponentActivity()
Avantages : vérifié à la compilation, performant. Inconvénient : KSP + plugin, apprentissage de Dagger sous-jacent.
11.3 Koin (runtime, DSL simple)
val appModule = module {
single<HttpClient> { HttpClient(OkHttp) }
singleOf(::ProductsRepository)
viewModelOf(::ProductsViewModel)
}
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin { androidLogger(); androidContext(this@MyApplication); modules(appModule) }
}
}
Avantages : simple, rapide à démarrer, logique en runtime. Inconvénient : erreurs détectées à l'exécution, pas à la compilation.
11.4 Hilt vs Koin
| Critère | Hilt | Koin |
|---|---|---|
| Vérification | Compile-time (KSP/Dagger) | Runtime |
| Performance | Excellente | Légèrement plus lent |
| Courbe | Plus raide | Douce |
| Multiplatform | Non (Android) | Oui |
12. Kotlin 2.0 (K2) et Multiplatform
12.1 Le compilateur K2
- Kotlin 2.0 (2024) introduit K2 : nouveau frontend du compilateur.
- Gains : compilation jusqu'à 2× plus rapide sur certains projets, meilleure analyse, base pour les futures features.
- Compatible avec les plugins principaux (Compose, serialization, kapt→KSP à migrer).
12.2 KMP (Kotlin Multiplatform)
Partager la logique (domaine, réseau, storage) entre Android, iOS, desktop, web.
/shared/src/
commonMain/kotlin/ # code partagé
androidMain/ # implémentation Android (JVM)
iosMain/ # implémentation iOS (native)
// commonMain
expect class Platform() { val name: String }
// androidMain
actual class Platform actual constructor() {
actual val name: String = "Android ${Build.VERSION.SDK_INT}"
}
// iosMain
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName
}
expect/actual: contrat partagé avec implémentations par plateforme.- Compose Multiplatform permet de partager l'UI aussi (Android, iOS, desktop).
- Écosystème : Ktor, kotlinx.serialization, kotlinx.coroutines fonctionnent en multiplateforme.
13. Résumé et checklist
13.1 Points clés
- Kotlin : nullability typée, immutabilité, expression-oriented.
sealed+when= modélisation d'états exhaustive.- Extensions et scope functions : code lisible et fonctionnel.
- Coroutines : suspend, launch/async, dispatchers.
- Structured concurrency : tout vit dans un scope, tout s'annule proprement.
- Flow froid pour les streams ; StateFlow pour l'état ; SharedFlow pour les événements.
- Ktor + kotlinx.serialization : réseau typé et rapide.
- Hilt (compile-time) vs Koin (runtime) : choisir selon les priorités.
- K2 : compilation plus rapide ; KMP : partager la logique.
13.2 Checklist
- J'écris sans
!!inutile - Je modélise un état UI avec sealed interface
- J'utilise data classes et copy
- Je lance des coroutines dans un scope
- Je gère la cancellation proprement
- Je crée un StateFlow exposé en lecture seule
- Je consomme une API Ktor avec kotlinx.serialization
- Je choisis Hilt ou Koin avec arguments
- Je connais les apports de K2 et de KMP
Prochain chapitre
→ 04-Android-UI : Jetpack Compose, XML Views, navigation, lifecycle-aware components, Hilt.