Maîtriser les Coroutines Kotlin : Guide complet 2026
Apprenez à maîtriser les coroutines Kotlin pour le développement Android : suspend functions, scopes, dispatchers et patterns avancés.

Les coroutines Kotlin ont révolutionné la programmation asynchrone sur Android. Fini le callback hell et les AsyncTask obsolètes : avec les coroutines, vous écrivez du code asynchrone qui ressemble à du code synchrone, tout en restant performant et maintenable.
Les coroutines sont légères (des milliers peuvent tourner sur un seul thread), annulables nativement, et s'intègrent parfaitement avec Jetpack et l'écosystème Android moderne.
Comprendre les fondamentaux
Avant de plonger dans le code, comprenons ce qui rend les coroutines si puissantes.
Qu'est-ce qu'une coroutine ?
Une coroutine est une instance de calcul suspendable. Contrairement aux threads, les coroutines ne bloquent pas : elles suspendent leur exécution et libèrent le thread pour d'autres tâches.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // Suspend sans bloquer
println("World!")
}
println("Hello,")
}
// Output: Hello, World!Suspend functions : le cœur des coroutines
Le mot-clé suspend indique qu'une fonction peut suspendre l'exécution de la coroutine sans bloquer le thread.
suspend fun fetchUserData(userId: String): User {
return withContext(Dispatchers.IO) {
// Appel réseau - s'exécute sur un thread IO
apiService.getUser(userId)
}
}
suspend fun fetchUserWithPosts(userId: String): UserWithPosts {
// Exécution séquentielle
val user = fetchUserData(userId)
val posts = fetchUserPosts(userId)
return UserWithPosts(user, posts)
}Règle d'or : Une suspend function ne peut être appelée que depuis une autre suspend function ou depuis une coroutine.
Les Coroutine Scopes
Le scope définit le cycle de vie de vos coroutines. C'est crucial pour éviter les memory leaks.
viewModelScope : le scope pour les ViewModels
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepository.getUser(userId)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message)
}
}
}
}
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val user: User) : UserUiState()
data class Error(val message: String?) : UserUiState()
}lifecycleScope : pour les Activities et Fragments
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UserUiState.Loading -> showLoading()
is UserUiState.Success -> showUser(state.user)
is UserUiState.Error -> showError(state.message)
}
}
}
}
}
}N'utilisez jamais GlobalScope dans une application Android. Les coroutines lancées avec GlobalScope ne sont pas liées au lifecycle et peuvent causer des memory leaks.
Les Dispatchers : contrôler l'exécution
Les dispatchers déterminent sur quel thread s'exécute votre coroutine.
Les 4 dispatchers principaux
// Main : thread principal (UI)
viewModelScope.launch(Dispatchers.Main) {
textView.text = "Mise à jour UI"
}
// IO : opérations I/O (réseau, base de données)
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchFromNetwork()
}
// Default : calculs CPU intensifs
viewModelScope.launch(Dispatchers.Default) {
val result = heavyComputation(data)
}
// Unconfined : hérite du contexte appelant (usage rare)withContext : changer de dispatcher
class ImageProcessor {
suspend fun processImage(bitmap: Bitmap): Bitmap {
return withContext(Dispatchers.Default) {
// Traitement CPU intensif sur Default
applyFilters(bitmap)
}
}
suspend fun saveToGallery(bitmap: Bitmap) {
withContext(Dispatchers.IO) {
// Écriture disque sur IO
saveToFile(bitmap)
}
}
}
// Usage dans le ViewModel
viewModelScope.launch {
val processed = imageProcessor.processImage(originalBitmap)
imageProcessor.saveToGallery(processed)
// Retour automatique sur Main pour update UI
_uiState.value = UiState.Success(processed)
}Exécution parallèle avec async/await
Pour exécuter des tâches en parallèle et combiner leurs résultats, utilisez async.
suspend fun loadDashboard(): Dashboard {
return coroutineScope {
// Lancement parallèle
val userDeferred = async { userRepository.getUser() }
val statsDeferred = async { statsRepository.getStats() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
// Attente des résultats
Dashboard(
user = userDeferred.await(),
stats = statsDeferred.await(),
notifications = notificationsDeferred.await()
)
}
}Avec async, les 3 appels s'exécutent en parallèle. Si chaque appel prend 1 seconde, le total sera ~1 seconde au lieu de 3 secondes en séquentiel.
Gestion des erreurs
try/catch classique
viewModelScope.launch {
try {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
} catch (e: HttpException) {
_uiState.value = UiState.Error("Erreur serveur: ${e.code()}")
} catch (e: IOException) {
_uiState.value = UiState.Error("Erreur réseau")
} catch (e: Exception) {
_uiState.value = UiState.Error("Erreur inattendue")
}
}CoroutineExceptionHandler
class UserViewModel : ViewModel() {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
_uiState.value = UiState.Error(throwable.message)
Timber.e(throwable, "Erreur dans la coroutine")
}
fun loadUser(userId: String) {
viewModelScope.launch(exceptionHandler) {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
}
}
}Result wrapper pattern
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}
suspend fun <T> safeApiCall(apiCall: suspend () -> T): Result<T> {
return try {
Result.Success(apiCall())
} catch (e: Exception) {
Result.Error(e)
}
}
// Usage
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): Result<User> = safeApiCall {
api.getUser(id)
}
}
// Dans le ViewModel
viewModelScope.launch {
when (val result = userRepository.getUser(userId)) {
is Result.Success -> _uiState.value = UiState.Success(result.data)
is Result.Error -> _uiState.value = UiState.Error(result.exception.message)
}
}Prêt à réussir tes entretiens Android ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Cancellation : annuler proprement
Les coroutines supportent l'annulation coopérative. C'est essentiel pour éviter les fuites de ressources.
Annulation automatique avec les scopes
class SearchViewModel : ViewModel() {
private var searchJob: Job? = null
fun search(query: String) {
// Annule la recherche précédente
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300) // Debounce
val results = searchRepository.search(query)
_searchResults.value = results
}
}
}Vérifier l'annulation
suspend fun processLargeList(items: List<Item>) {
items.forEach { item ->
// Vérifie si la coroutine est annulée
ensureActive()
processItem(item)
}
}
suspend fun downloadFiles(urls: List<String>) = coroutineScope {
urls.map { url ->
async {
try {
downloadFile(url)
} catch (e: CancellationException) {
cleanupPartialDownload(url)
throw e // Re-throw pour propager l'annulation
}
}
}.awaitAll()
}Ne jamais swallow CancellationException : si vous attrapez Exception, re-throw CancellationException pour que l'annulation se propage correctement.
Flow : programmation réactive
Flow est l'équivalent coroutines de RxJava Observable, mais plus simple et intégré.
Créer et collecter un Flow
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = userApi.getUsers()
emit(users)
delay(5000) // Polling toutes les 5 secondes
}
}
// Flow depuis Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// Collecte dans le ViewModel
viewModelScope.launch {
userDao.getAllUsers()
.catch { e -> _uiState.value = UiState.Error(e.message) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}StateFlow vs SharedFlow
class EventViewModel : ViewModel() {
// StateFlow : garde la dernière valeur, idéal pour l'état UI
private val _uiState = MutableStateFlow(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// SharedFlow : pour les events one-shot (navigation, snackbar)
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onButtonClick() {
viewModelScope.launch {
_events.emit(UiEvent.NavigateToDetail)
}
}
}
sealed class UiEvent {
object NavigateToDetail : UiEvent()
data class ShowSnackbar(val message: String) : UiEvent()
}Opérateurs Flow essentiels
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.debounce(300)
.flatMapLatest { users ->
fetchUserDetails(users)
}
.catch { e ->
emit(emptyList())
}
.onEach { users ->
analytics.logUserCount(users.size)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)Patterns avancés
Retry avec backoff exponentiel
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
Timber.w("Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
// Usage
val user = retryWithBackoff {
userApi.getUser(userId)
}Timeout
suspend fun fetchWithTimeout() {
try {
val result = withTimeout(5000L) {
api.fetchData()
}
processResult(result)
} catch (e: TimeoutCancellationException) {
showError("La requête a pris trop de temps")
}
}
// Ou avec une valeur par défaut
val result = withTimeoutOrNull(5000L) {
api.fetchData()
} ?: defaultValueConclusion
Les coroutines Kotlin sont devenues indispensables pour le développement Android moderne. Elles offrent une approche élégante et performante de la programmation asynchrone, parfaitement intégrée à l'écosystème Jetpack.
Checklist
- ✅ Utiliser
viewModelScopeetlifecycleScopepour éviter les leaks - ✅ Choisir le bon Dispatcher (Main, IO, Default)
- ✅ Gérer les erreurs avec try/catch ou Result wrapper
- ✅ Implémenter l'annulation coopérative
- ✅ Utiliser Flow pour les streams de données réactifs
- ✅ Préférer StateFlow pour l'état UI, SharedFlow pour les events
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Maîtriser les coroutines vous donnera un avantage significatif dans vos projets Android et lors de vos entretiens techniques. Pratiquez régulièrement et n'hésitez pas à explorer les patterns avancés !
Tags
Partager
Articles similaires

Kotlin 2.3 pour Android : Destructuration par Nom, KMP et Questions d'Entretien 2026
Questions d'entretien Kotlin 2.3 pour développeurs Android en 2026. Destructuration par nom, KMP, paramètres de contexte, Flow et coroutines avec exemples de code.

Jetpack Compose : Animations avancées pas à pas
Guide complet des animations avancées en Jetpack Compose : transitions, AnimatedVisibility, Animatable, gestures et performance pour des interfaces fluides.

Top 20 questions d'entretien Jetpack Compose en 2026
Les 20 questions les plus posées en entretien sur Jetpack Compose : recomposition, state, navigation, performance et architecture.