MFormations
Modern Mobile Engineering

Chapitre 3

03 - Kotlin

03 - Kotlin

03 - Kotlin : Cours complet

Table des matières

  1. Le langage moderne
  2. Null safety
  3. Data classes et sealed classes
  4. Extensions et scope functions
  5. Coroutines : bases
  6. Structured concurrency et cancellation
  7. Flow, StateFlow, SharedFlow, channels
  8. Kotlin DSL (builds)
  9. Ktor client
  10. kotlinx.serialization
  11. Injection de dépendances : Hilt & Koin
  12. Kotlin 2.0 (K2) et Multiplatform
  13. 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érateurRô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

FonctionReceiverRetourUsage
letitrésultattransform + null-safe
runthisrésultatconfig + calcul
withthisrésultatopérer sur un objet
applythisl'objetconfigurer (retourne this)
alsoitl'objeteffets 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 avec await.
val job = scope.launch { download() }          // Unit
val deferred = scope.async { compute() }        // Deferred<Int>
val result = deferred.await()

5.4 Dispatchers

DispatcherUsage
Dispatchers.MainUI (Android : main thread)
Dispatchers.IORéseau, fichiers, DB
Dispatchers.DefaultCalcul CPU
Dispatchers.UnconfinedRare, à é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 launch dans un scope (jamais de GlobalScope).
  • Catch CancellationException pour relancer, jamais pour l'avaler.
  • runCatching avale 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), send suspend 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èreHiltKoin
VérificationCompile-time (KSP/Dagger)Runtime
PerformanceExcellenteLégèrement plus lent
CourbePlus raideDouce
MultiplatformNon (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.