← Retour au cours
▶ Aperçu gratuit · Leçon offerte

Android Studio et Kotlin moderne : null safety, coroutines, Flow, sealed classes

⏱ 840 min · 🎬 Lecon · 🏆 20 XP
🎬
Vidéo en production
Notre équipe pédagogique tourne actuellement cette leçon avec un·e formateur·rice expert·e. Le contenu textuel ci-dessous est complet et utilisable dès maintenant.

Leçon 1 — Android Studio et Kotlin moderne

Hedgehog IDE, null safety, coroutines, Flow, sealed classes : la fondation du développeur Android 2026.

🎯 Objectifs pédagogiques

  • Installer et configurer Android Studio Hedgehog avec SDK Android 14/15, AVD et émulateurs.
  • Maîtriser le null safety de Kotlin (?, !!, ?., ?:, let) et les data classes.
  • Utiliser les sealed classes pour modéliser des états finis (Result<T>, UI State).
  • Lancer du code asynchrone avec les coroutines (suspend, launch, async, withContext).
  • Émettre et consommer des flux réactifs avec Flow, StateFlow et SharedFlow.

1. Android Studio Hedgehog : le poste de travail moderne

Android Studio Hedgehog (version 2023.1, basée sur IntelliJ IDEA 2023.1) est l'IDE officiel recommandé par Google pour développer sur Android 14 et 15. Il intègre le compilateur Kotlin K2, le nouveau Layout Inspector, le Profiler unifié (CPU, Memory, Energy, Network) et un émulateur Resizable supportant les pliables, les tablettes et Android Auto en simulation simultanée.

L'écosystème repose sur trois composants : le SDK Android (API levels, build-tools, platform-tools), le Gradle build system avec ses plugins Android Gradle Plugin (AGP 8.2+) et Kotlin Gradle Plugin, et l'AVD Manager qui crée des émulateurs configurables. Pour Android 15 (API 35), il faut le SDK 35, build-tools 35.0.0 et AGP 8.5 minimum.

Les Profilers sont l'outil critique pour optimiser une app : le CPU Profiler trace les appels de fonctions, le Memory Profiler détecte les fuites (LeakCanary intégré), l'Energy Profiler mesure l'impact batterie, et le Layout Inspector permet d'inspecter la hiérarchie Compose en temps réel avec recomposition counts.

"Kotlin is now Google's preferred language for Android app development. Over 70% of the top 1,000 apps on the Play Store use Kotlin." — Android Developers — Kotlin and Android

2. Null safety : la philosophie Kotlin

Kotlin élimine à la compilation la billion-dollar mistake de Tony Hoare : le NullPointerException. Toute variable est non-nullable par défaut. Pour autoriser null, il faut explicitement marquer le type avec ? : val name: String (impossible null) versus val name: String? (peut être null).

Cinq opérateurs gèrent les types nullables : ?. (safe call : user?.address?.city retourne null si chaîne brisée), ?: (Elvis : name ?: "Anonyme"), !! (force unwrap, à éviter — déclenche NPE), ?.let { } (exécute le bloc si non-null), et as? (safe cast).

OpérateurComportementExemple
?. safe callRetourne null si receveur null, sinon appelleuser?.email?.length
?: ElvisValeur par défaut si null à gauchename ?: "—"
!! forceLance NPE si null (anti-pattern)email!!.trim()
?.let { }Exécute le lambda si non-nulltoken?.let { save(it) }
as? safe castRetourne null si cast impossible(view as? TextView)?.text

3. Data classes, sealed classes et scope functions

Les data classes génèrent automatiquement equals(), hashCode(), toString(), copy() et destructuration. Idéales pour modéliser des DTO, des UI States, des Entity Room. Exemple : data class User(val id: Long, val email: String, val isPremium: Boolean = false).

Les sealed classes (et sealed interface depuis Kotlin 1.5) modélisent des hiérarchies fermées vérifiables à la compilation. Combinées au when exhaustif, elles éliminent les branches manquantes. Le pattern Result<T> est l'usage canonique.

Les scope functions (let, run, with, apply, also) restructurent le code : apply retourne le receveur (configuration d'objet), let retourne le résultat du lambda (transformation), also retourne le receveur (side effect), run retourne le résultat (block de calcul avec receveur), with idem mais sans receveur null.

✏️ Cas pratique : sealed class Result<T>

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// Usage exhaustif via when
fun render(result: Result<User>) = when (result) {
    is Result.Success -> showUser(result.data)
    is Result.Error   -> showError(result.exception.message)
    Result.Loading    -> showSpinner()
    // Pas de else : le compilateur vérifie l'exhaustivité
}

4. Coroutines : l'asynchrone structuré

Les coroutines remplacent les callbacks, AsyncTask (déprécié) et RxJava pour 80% des cas. Une fonction suspend peut s'interrompre et reprendre sans bloquer le thread. Elles sont lancées dans un CoroutineScope (viewModelScope, lifecycleScope, GlobalScope à éviter) et exécutées sur un Dispatcher (Main UI, IO réseau/disque, Default calcul CPU).

launch retourne un Job (fire and forget). async retourne un Deferred<T> dont on attend la valeur via .await(). withContext(Dispatchers.IO) { ... } change de dispatcher temporairement, classique pour un appel réseau depuis le Main thread.

"Coroutines is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously." — kotlinlang.org — Coroutines guide

5. Flow, StateFlow, SharedFlow

Flow est l'équivalent réactif de RxJava (Observable) intégré à Kotlin. Un Flow est cold par défaut : il ne produit rien tant qu'on ne le collecte pas via .collect { }. Idéal pour des streams de données issus de Room (DAO retourne Flow<List<Entity>>) ou d'API SSE.

StateFlow est hot, conserve la dernière valeur, idéal pour les UI States dans un ViewModel : private val _state = MutableStateFlow(UiState.Idle); val state: StateFlow<UiState> = _state.asStateFlow(). Le Composable observe via .collectAsStateWithLifecycle().

SharedFlow est hot, ne conserve pas de valeur initiale, idéal pour des événements one-shot (snackbar, navigation) : MutableSharedFlow<NavEvent>().

✏️ Cas pratique : coroutine + Flow

class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow<Result<User>>(Result.Loading)
    val state: StateFlow<Result<User>> = _state.asStateFlow()

    fun loadUser(id: Long) = viewModelScope.launch {
        _state.value = Result.Loading
        try {
            val user = withContext(Dispatchers.IO) { repository.fetchUser(id) }
            _state.value = Result.Success(user)
        } catch (e: Throwable) {
            _state.value = Result.Error(e)
        }
    }
}
💡 Hook productivité : active "Power Save Mode" dans Android Studio (File > Power Save Mode) pendant les longues sessions pour économiser batterie et CPU. Et apprends les raccourcis Ctrl+Shift+A (Find Action), Ctrl+B (Go to declaration), Shift+Shift (Search Everywhere).
⚠️ Piège : ne jamais utiliser GlobalScope.launch dans une app Android. Les coroutines lancées dans GlobalScope survivent à la destruction du composant et provoquent des fuites mémoire. Toujours utiliser viewModelScope ou lifecycleScope.

Points-clés à retenir

  • Android Studio Hedgehog + SDK 35 (Android 15) + AGP 8.5 = stack 2026.
  • Null safety Kotlin : 5 opérateurs (?., ?:, !!, let, as?) — éviter !!.
  • Data classes pour DTO/State, sealed classes pour hiérarchies fermées exhaustives.
  • Coroutines : suspend + scopes (viewModelScope) + dispatchers (Main/IO/Default).
  • Flow cold, StateFlow pour UI State, SharedFlow pour événements one-shot.

Pour aller plus loin

Approfondissement technique

La maîtrise avancée de Kotlin pour Android dépasse largement la simple connaissance des coroutines et des sealed classes. En 2026, les équipes seniors exploitent un ensemble d'outils linguistiques qui transforment la manière dont les flux de données circulent dans une application : combinaisons réactives, structures algébriques, contracts d'inférence, et partage de code multiplateforme. Cette section approfondit les mécanismes qui distinguent un projet Android industriel d'un prototype, en mettant l'accent sur la composition de Flow, la modélisation par sealed interfaces, l'optimisation mémoire via value classes, ainsi que les promesses de Kotlin Multiplatform Mobile pour mutualiser la logique métier entre Android et iOS.

Le premier pilier reste la composition des flux. Les opérateurs combine, zip, flatMapLatest, flatMapMerge et flatMapConcat répondent chacun à un cas précis. Là où combine émet une nouvelle valeur dès que l'un des flux sources change, zip attend une émission de chaque côté pour produire une paire — utile pour synchroniser deux requêtes parallèles. flatMapLatest est l'allié des recherches utilisateur : il annule la requête précédente dès qu'une nouvelle frappe est détectée, évitant ainsi les conditions de course et les résultats obsolètes affichés à l'écran. À cela s'ajoutent debounce et distinctUntilChanged qui filtrent les émissions trop rapprochées ou redondantes, indispensables pour limiter la pression réseau dans une SearchBar réactive.

Le second pilier est la modélisation. Les sealed interfaces, généralisation des sealed classes depuis Kotlin 1.5, permettent d'exprimer des hiérarchies fermées tout en autorisant l'implémentation multiple — un état d'écran peut ainsi à la fois être un UiState et un Loadable. Couplés au pattern matching exhaustif du when, ils garantissent qu'aucun cas n'est oublié à la compilation. Les value classes (@JvmInline value class) offrent quant à elles une sécurité de type sans coût d'allocation : un UserId reste un Long à l'exécution mais devient incompatible avec un OrderId à la compilation, éliminant des bugs subtils d'inversion de paramètres.

Channels, SharedFlow et bus d'événements

Les Channels constituent la primitive de communication hot entre coroutines. Contrairement aux Flow qui sont cold (ils ne produisent qu'à l'abonnement), un Channel pousse activement des valeurs dès qu'elles sont disponibles. Le pattern de référence pour exposer des événements one-shot (snackbar, navigation, toast) consiste à utiliser un Channel<Event>(Channel.BUFFERED) côté ViewModel et un .receiveAsFlow() côté UI. Le SharedFlow, lui, autorise plusieurs collecteurs et un buffer configurable via replay et onBufferOverflow : on l'utilise pour diffuser un signal global comme « l'utilisateur s'est déconnecté ». StateFlow reste le choix par défaut pour représenter un état UI qui a toujours une valeur courante. La règle pratique est simple : StateFlow pour l'état, SharedFlow pour les notifications, Channel pour les événements one-shot consommés une seule fois.

Le piège classique consiste à exposer ces événements via un MutableLiveData ou un MutableStateFlow : un changement de configuration (rotation) ou un retour depuis l'arrière-plan rejoue alors l'événement, et l'utilisateur voit deux fois la même snackbar. La discipline est de toujours retourner une vue immuable (SharedFlow, Flow) depuis le ViewModel et de garder la version mutable strictement privée.

Contracts, inline functions et Kotlin Multiplatform

Les contracts (kotlin.contracts.contract) sont une fonctionnalité encore expérimentale mais déjà largement utilisée dans la stdlib. Ils permettent au compilateur d'inférer le smart cast au-delà des limites d'une fonction : déclarer contract { returns() implies (value != null) } dans un requireNotNull donne au compilateur la garantie qu'après l'appel la valeur n'est plus nulle, supprimant les !! inutiles. Combinés aux fonctions inline qui éliminent la création d'objets lambda et autorisent le return non-local, les contracts permettent d'écrire des DSL expressifs sans surcoût runtime.

Kotlin Multiplatform Mobile (KMM), désormais stable, autorise le partage de la couche domaine et data entre Android et iOS. Un module shared compile en JAR pour Android et en framework Objective-C pour iOS. Les coroutines y sont disponibles, Ktor remplace Retrofit, SQLDelight remplace Room. La promesse n'est pas le partage de l'UI (qui reste native via Compose côté Android et SwiftUI côté iOS) mais celui de la logique métier — souvent 60 à 80 % du code d'une application bien architecturée. Les équipes mid-size adoptent KMM pour mutualiser les ViewModels via la bibliothèque Decompose ou Voyager. La courbe d'apprentissage est réelle mais le retour sur investissement devient positif au-delà de la deuxième feature partagée.

Cas pratiques détaillés

Cas 1 — SearchBar avec debounce et flatMapLatest

L'objectif est de construire une SearchBar qui interroge une API après 300 ms d'inactivité, annule automatiquement les requêtes obsolètes et n'affiche que le dernier résultat pertinent. Le scénario classique se résout en quelques lignes grâce à la composition d'opérateurs Flow.

class SearchViewModel(
    private val repo: ProductRepository
) : ViewModel() {

    private val query = MutableStateFlow("")

    val results: StateFlow<SearchUiState> = query
        .debounce(300)
        .filter { it.length >= 2 }
        .distinctUntilChanged()
        .flatMapLatest { q ->
            flow {
                emit(SearchUiState.Loading)
                emit(
                    runCatching { repo.search(q) }
                        .fold(
                            { SearchUiState.Success(it) },
                            { SearchUiState.Error(it.message.orEmpty()) }
                        )
                )
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = SearchUiState.Idle
        )

    fun onQueryChanged(q: String) { query.value = q }
}

Cas 2 — Sealed interface pour un état UI complet

Modéliser un écran qui peut être en chargement, vide, erreur ou succès — tout en gardant la donnée précédente affichée pendant un rafraîchissement — est un exercice quotidien. Une sealed interface combinée à un type produit (data class) donne une représentation à la fois exhaustive et confortable à consommer côté Compose.

sealed interface OrdersUiState {
    data object Loading : OrdersUiState
    data class Empty(val message: String) : OrdersUiState
    data class Error(val cause: Throwable) : OrdersUiState
    data class Content(
        val orders: List<Order>,
        val isRefreshing: Boolean = false
    ) : OrdersUiState
}

@Composable
fun OrdersScreen(state: OrdersUiState) {
    when (state) {
        OrdersUiState.Loading -> FullScreenSpinner()
        is OrdersUiState.Empty -> EmptyView(state.message)
        is OrdersUiState.Error -> ErrorView(state.cause)
        is OrdersUiState.Content -> OrdersList(
            orders = state.orders,
            isRefreshing = state.isRefreshing
        )
    }
}

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
callbackFlowExcellenteMoyenneAdapter une API callback en Flow
flow { emit() }ExcellenteTrès bonneProducteur séquentiel maîtrisé
StateFlowTrès bonneExcellenteÉtat UI courant unique
SharedFlow(replay=0)Très bonneBonneÉvénements non rejouables
Channel BUFFEREDBonneMoyenneÉvénements one-shot
LiveDataCorrecteBonneLegacy / interop XML
RxJava 3BonneFaibleExistant non migré
Coroutines + JobExcellenteTrès bonneConcurrence structurée

Pièges fréquents en production

  • Oublier viewModelScope — Lancer une coroutine via GlobalScope ou CoroutineScope(Dispatchers.IO) dans un ViewModel échappe au cycle de vie et provoque des fuites mémoire et des écritures sur des vues détruites. Toujours utiliser viewModelScope côté ViewModel et lifecycleScope côté Activity/Fragment, ou rememberCoroutineScope() en Compose.
  • Confondre Flow cold et StateFlow hot — Un Flow rejoue son producteur à chaque collecte. Si l'opération est coûteuse (requête réseau, lecture disque), elle se déclenche autant de fois qu'il y a de collecteurs. Convertir via shareIn ou stateIn avec SharingStarted.WhileSubscribed(5000) pour partager une instance et économiser les ressources.
  • Bloquer Dispatchers.Main — Appeler un runBlocking ou une fonction synchrone lourde dans une coroutine sur Dispatchers.Main gèle l'UI. Wrapper systématiquement les I/O dans withContext(Dispatchers.IO) et les calculs CPU dans Dispatchers.Default.
  • Catch trop large — Un try/catch (Throwable) sur l'ensemble d'une coroutine attrape aussi CancellationException, ce qui désactive la concurrence structurée. Préférer try/catch (Exception) et toujours rethrow les CancellationException, ou utiliser runCatching qui gère ce cas correctement depuis Kotlin 1.6.
  • Nullable mutable non synchronisé — Smart-cast échoue sur les propriétés var mutables : if (user.name != null) print(user.name.length) ne compile pas car name peut changer entre les deux accès. Copier dans une val locale ou utiliser ?.let { }.
  • Sealed class sans exhaustive when — Sans le mot-clé when utilisé comme expression (assigné ou retourné), Kotlin n'exige pas l'exhaustivité. Activer le warning NON_EXHAUSTIVE_WHEN_STATEMENT en erreur dans la config gradle.
  • SharedFlow sans buffer — Un MutableSharedFlow() par défaut a extraBufferCapacity=0 et bloque l'émetteur si aucun collecteur n'est prêt. Toujours définir extraBufferCapacity ou onBufferOverflow = BufferOverflow.DROP_OLDEST selon la sémantique voulue.

Outils et écosystème

  • Kotlin Symbol Processing (KSP) — Successeur de KAPT, 2 à 3 fois plus rapide pour la génération de code (Hilt, Room, Moshi). Migration recommandée pour tout projet 2026. Voir kotlinlang.org/docs/ksp-overview.
  • Detekt — Analyseur statique pour Kotlin avec règles configurables (complexité cyclomatique, longueur de fonction, magic numbers). Intégrable au pipeline CI via detektCheck dans Gradle.
  • ktlint — Formateur officiel suivant le Kotlin Coding Conventions. Hook pre-commit recommandé pour homogénéiser le style sans débat d'équipe.
  • kotlinx.serialization — Sérialiseur JSON natif Kotlin, plus rapide que Gson, avec support des sealed classes et value classes. Compatible Retrofit via retrofit2-kotlinx-serialization-converter.
  • Arrow-kt — Bibliothèque de programmation fonctionnelle apportant Either, Option, Validated et le contexte raise de Kotlin 1.9. Utile pour modéliser les erreurs métier sans exceptions.
  • Kotlin Multiplatform Wizard — Générateur officiel JetBrains pour bootstrap un projet KMM avec Android, iOS et code partagé. Disponible sur kmp.jetbrains.com.

Citations sources officielles

"Coroutines are lightweight threads, designed for asynchronous programming. They allow you to write asynchronous code in a sequential way, making it easier to read and maintain." — developer.android.com/kotlin/coroutines
"StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property." — kotlinlang.org StateFlow

Glossaire (10 termes)

TermeDéfinition
CoroutineFonction suspendable s'exécutant sur un dispatcher, alternative légère aux threads
Suspending functionFonction marquée suspend pouvant être mise en pause sans bloquer le thread
StateFlowFlow chaud avec une valeur courante toujours disponible, idéal pour l'état UI
SharedFlowFlow chaud multi-collecteurs configurable (replay, buffer), pour notifications
ChannelPrimitive de communication bidirectionnelle entre coroutines
Sealed interfaceHiérarchie fermée extensible permettant l'héritage multiple
Value classWrapper inline sans coût d'allocation runtime, type-safe à la compilation
Inline functionFonction dont le corps est inliné à l'appel, supprime la création de lambdas
ContractHint au compilateur sur le comportement d'une fonction pour smart casts étendus
KMMKotlin Multiplatform Mobile, partage code domaine entre Android et iOS

Ressources d'approfondissement

Continuez le parcours 🚀

La leçon suivante est également gratuite. Découvrez-la sans inscription.

Leçon 2 — Continuer →
🍪 Nous utilisons des cookies essentiels et, avec ton accord, des cookies analytiques. En savoir plus

⚙️ Préférences cookies

Choisis quels cookies tu acceptes — modifiable à tout moment.

🔐 Essentiels (obligatoires)Authentification, session, sécurité. Toujours actifs.
📊 Analytics anonymesMesure d'audience anonymisée — aucune donnée personnelle.
📣 MarketingPublicités ITAG pertinentes sur d'autres sites.
💬 Contactez-nous sur WhatsApp