Hedgehog IDE, null safety, coroutines, Flow, sealed classes : la fondation du développeur Android 2026.
?, !!, ?., ?:, let) et les data classes.suspend, launch, async, withContext).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
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érateur | Comportement | Exemple |
|---|---|---|
?. safe call | Retourne null si receveur null, sinon appelle | user?.email?.length |
?: Elvis | Valeur par défaut si null à gauche | name ?: "—" |
!! force | Lance NPE si null (anti-pattern) | email!!.trim() |
?.let { } | Exécute le lambda si non-null | token?.let { save(it) } |
as? safe cast | Retourne null si cast impossible | (view as? TextView)?.text |
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.
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é
}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
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>().
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)
}
}
}Ctrl+Shift+A (Find Action), Ctrl+B (Go to declaration), Shift+Shift (Search Everywhere).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.?., ?:, !!, let, as?) — éviter !!.suspend + scopes (viewModelScope) + dispatchers (Main/IO/Default).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.
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.
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.
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 }
}
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
)
}
}
| Approche | Performance | Lisibilité | Cas d'usage |
|---|---|---|---|
| callbackFlow | Excellente | Moyenne | Adapter une API callback en Flow |
| flow { emit() } | Excellente | Très bonne | Producteur séquentiel maîtrisé |
| StateFlow | Très bonne | Excellente | État UI courant unique |
| SharedFlow(replay=0) | Très bonne | Bonne | Événements non rejouables |
| Channel BUFFERED | Bonne | Moyenne | Événements one-shot |
| LiveData | Correcte | Bonne | Legacy / interop XML |
| RxJava 3 | Bonne | Faible | Existant non migré |
| Coroutines + Job | Excellente | Très bonne | Concurrence structurée |
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.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.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.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.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 { }.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.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.detektCheck dans Gradle.retrofit2-kotlinx-serialization-converter.raise de Kotlin 1.9. Utile pour modéliser les erreurs métier sans exceptions."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
| Terme | Définition |
|---|---|
| Coroutine | Fonction suspendable s'exécutant sur un dispatcher, alternative légère aux threads |
| Suspending function | Fonction marquée suspend pouvant être mise en pause sans bloquer le thread |
| StateFlow | Flow chaud avec une valeur courante toujours disponible, idéal pour l'état UI |
| SharedFlow | Flow chaud multi-collecteurs configurable (replay, buffer), pour notifications |
| Channel | Primitive de communication bidirectionnelle entre coroutines |
| Sealed interface | Hiérarchie fermée extensible permettant l'héritage multiple |
| Value class | Wrapper inline sans coût d'allocation runtime, type-safe à la compilation |
| Inline function | Fonction dont le corps est inliné à l'appel, supprime la création de lambdas |
| Contract | Hint au compilateur sur le comportement d'une fonction pour smart casts étendus |
| KMM | Kotlin Multiplatform Mobile, partage code domaine entre Android et iOS |
La leçon suivante est également gratuite. Découvrez-la sans inscription.
Leçon 2 — Continuer →Choisis quels cookies tu acceptes — modifiable à tout moment.