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.

Guide complet des Coroutines Kotlin pour Android

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.

Pourquoi les coroutines ?

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.

BasicCoroutine.ktkotlin
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.

SuspendFunction.ktkotlin
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

UserViewModel.ktkotlin
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

UserFragment.ktkotlin
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)
                    }
                }
            }
        }
    }
}
Attention aux leaks

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

Dispatchers.ktkotlin
// 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

ImageProcessor.ktkotlin
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.

ParallelExecution.ktkotlin
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()
        )
    }
}
Performance

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

ErrorHandling.ktkotlin
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

ExceptionHandler.ktkotlin
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

ResultPattern.ktkotlin
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

SearchViewModel.ktkotlin
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

CancellationCheck.ktkotlin
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

FlowBasics.ktkotlin
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

StateFlowVsSharedFlow.ktkotlin
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

FlowOperators.ktkotlin
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

RetryPattern.ktkotlin
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

TimeoutPattern.ktkotlin
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()
} ?: defaultValue

Conclusion

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 viewModelScope et lifecycleScope pour é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

#kotlin
#coroutines
#android
#async
#concurrency

Partager

Articles similaires