1. Livres & vidéos
  2. Kotlin
  3. Maîtrisez l’asynchrone avec les coroutines
Extrait - Kotlin Du code au Play Store : le guide complet pour développeurs Android
Extraits du livre
Kotlin Du code au Play Store : le guide complet pour développeurs Android Revenir à la page d'achat du livre

Maîtrisez l’asynchrone avec les coroutines

Comprendre l’asynchronisme en Android

L’asynchronisme est un mécanisme central de toute application Android moderne. Il permet d’exécuter plusieurs opérations en parallèle sans bloquer l’interface utilisateur. Concrètement, c’est ce qui distingue une application réactive d’une application qui se fige dès qu’elle attend une réponse réseau ou un accès disque.

Les outils asynchrones sur Android ont beaucoup évolué : des threads Java aux AsyncTask, puis à RxJava, et aujourd’hui aux Kotlin coroutines. Cette progression vise un même objectif, écrire du code asynchrone simple à lire et fiable.

1. Les fondements de l’asynchronisme mobile

Sur mobile, la responsivité de l’interface est une exigence non négociable. Sur Android, le thread principal (UI thread) est seul responsable de la mise à jour de l’interface et de la gestion des interactions utilisateur. Toute opération bloquante sur ce thread dégrade immédiatement l’expérience : l’application ne répond plus aux touches, les animations sautent, et au-delà de quelques secondes le système affiche une boîte de dialogue Application Not Responding (ANR) qui peut tuer l’application.

Cette architecture mono-thread pour l’interface utilisateur découle de contraintes techniques bien réelles liées à la gestion de la mémoire et à la cohérence des états visuels. Les toolkits d’interface graphique moderne ne sont généralement pas conçus pour être manipulés depuis plusieurs threads (thread-safe) sur Android, car la synchronisation nécessaire pour supporter les accès concurrents dégraderait considérablement les performances de rendu. Android adopte donc une approche où l’interface utilisateur appartient exclusivement au thread principal, tandis que toutes les autres opérations doivent s’exécuter sur des threads dédiés.

Les opérations typiquement asynchrones dans une application Android couvrent un spectre large d’activités : appels réseau vers des API distantes, accès aux bases de données locales, lecture et écriture...

Coroutines Kotlin : les fondamentaux

Les coroutines Kotlin simplifient la programmation asynchrone sur Android. Elles permettent d’écrire du code asynchrone qui se lit comme du code séquentiel : pas de chaîne de fonctions de rappel (callbacks), pas de gestion manuelle de threads. Sous le capot, le compilateur Kotlin transforme ces fonctions en une machine à états qui gère les suspensions et reprises d’exécution.

Les coroutines reposent sur le principe de concurrence structurée (principe qui lie le cycle de vie des coroutines à leur portée parente). Cela permet de se concentrer sur la logique métier sans gérer manuellement les threads, et de produire un code plus lisible et moins exposé aux bugs classiques de concurrence.

1. Concept de suspension et continuation

Le mécanisme de suspension est l’un des concepts clés des coroutines, permettant l’interruption temporaire de l’exécution d’une fonction sans bloquer le thread sous-jacent. Cette suspension libère le thread pour exécuter d’autres tâches, optimisant ainsi l’utilisation des ressources système. Lorsque l’opération asynchrone se termine, la coroutine peut reprendre son exécution exactement là où elle s’était arrêtée, préservant l’état local et la pile d’appel.

Cette apparente simplicité repose sur une transformation de code effectuée par le compilateur Kotlin. Chaque fonction suspend est transformée en machine à états qui encode les différents points de suspension possibles. Cette transformation préserve la sémantique du code séquentiel tout en permettant l’exécution asynchrone.

La fonction suspend constitue l’unité de base de la programmation avec oroutines. Ces fonctions peuvent être interrompues et reprises, permettant l’intégration naturelle d’opérations asynchrones dans un flux de contrôle séquentiel.

suspend fun fetchUserData(userId: Int): User { 
 
val userData = apiService.getUser(userId) 
val userPreferences = preferencesService.getPreferences(userId) 
 
return User(userData, userPreferences) 
} 

Cette fonction suspend peut appeler d’autres fonctions...

Dispatchers et gestion des threads

Les Dispatchers indiquent sur quel pool de threads une coroutine doit s’exécuter. Plutôt que de gérer manuellement les pools de threads, on déclare le dispatcher voulu et la coroutine est planifiée en conséquence. Choisir le bon dispatcher est important pour les performances et la responsivité de l’application.

L’architecture des Dispatchers repose sur une spécialisation fonctionnelle où chaque type d’opération bénéficie d’un environnement d’exécution optimisé. Cette segmentation permet l’optimisation fine des ressources système tout en simplifiant nettement les décisions développeur. Le choix du Dispatcher approprié influence directement les performances, la responsivité et la consommation énergétique de l’application.

1. Dispatchers.Main : le gardien de l’interface utilisateur

Le Dispatcher Main constitue le lien entre l’exécution asynchrone des coroutines et les exigences synchrones de l’interface utilisateur Android. Ce dispatcher garantit que toutes les opérations de mise à jour d’interface s’exécutent sur le thread principal, respectant ainsi les contraintes d’Android. L’utiliser correctement évite les violations de thread qui peuvent corrompre l’état visuel ou faire planter l’application.

L’intégration automatique avec le thread principal simplifie la gestion des fonctions de rappel asynchrones. Les fonctions suspend qui se terminent dans un contexte Main peuvent directement modifier l’interface utilisateur sans bascule explicite vers le thread approprié.

class UserViewModel : ViewModel() { 
    private val _userState = MutableStateFlow<UiState>(UiState.Loading) 
    val userState = _userState.asStateFlow() 
    fun loadUser(id: Int) { 
        viewModelScope.launch { 
            _userState.value = UiState.Loading 
 
            val user = withContext(Dispatchers.IO) { 
                userRepository.getUser(id) ...

Flow : streams de données réactives

Les Kotlin Flow simplifient la gestion des flux de données asynchrones sur Android. Là où les approches classiques reposent sur des fonctions de rappel ou des API d’observation souvent verbeuses, Flow propose une API composable qui s’intègre avec les coroutines. Les Flow servent de support aux architectures réactives, où les données circulent entre les couches de l’application au fur et à mesure qu’elles deviennent disponibles.

Flow s’inspire de la programmation fonctionnelle : on déclare une suite de transformations (map, filter, etc.) appliquées au flux de données, plutôt que de gérer manuellement la production et la consommation. Cela simplifie la composition de plusieurs sources de données et facilite l’alimentation des interfaces utilisateur réactives. pour gérer les interactions entre sources de données multiples et interfaces utilisateur réactives.

1. Concepts fondamentaux et cold streams

Les Flow sont des flux froids (cold flow, qui ne produisent des valeurs que quand on s’y abonne). Tant qu’aucun collecteur n’est branché, aucune donnée n’est émise. Cela les distingue des flux chauds (hot flow), qui produisent des valeurs en continu, indépendamment des abonnés. La nature cold (froide) des Flow garantit que chaque collecteur reçoit l’intégralité de la séquence de données depuis le début, évitant les problèmes de synchronisation temporelle courants avec les hot streams.

La création d’un Flow s’effectue via différents builders qui encapsulent les sources de données et définissent les règles de production. Le builder flow constitue l’approche la plus flexible, permettant l’émission de valeurs arbitraires via la fonction emit dans un contexte suspend.

fun numberFlow(): Flow<Int> = flow { 
    for (i in 1..5) { 
        delay(100) emit (i) 
    } 
} 

Cette définition crée un Flow qui émet les nombres de 1 à 5 avec un délai de 100 ms entre chaque émission. La nature suspend du block...

Patterns avancés et bonnes pratiques

Les patterns avancés présentés ici résument les bonnes pratiques de la communauté Android pour écrire du code asynchrone fiable. Les appliquer dès le départ évite des bugs récurrents et facilite la maintenance.

L’écosystème coroutines s’est standardisé au fil du temps. Les solutions ad hoc ont laissé place à des approches documentées qui facilitent la collaboration entre développeurs et rendent le comportement des applications plus prévisible.

1. Composition et orchestration complexe

La composition de coroutines multiples nécessite une coordination soignée qui prend en compte les dépendances entre tâches, la gestion des délais d’expiration et la propagation d’erreurs. Cette orchestration dépasse les simples appels async/await pour devenir une architecture de coordination qui garantit la cohérence des résultats même en cas de pannes partielles.

Le pattern de composition parallèle permet l’exécution simultanée d’opérations indépendantes avec agrégation ultérieure des résultats. Cette approche optimise les temps d’exécution tout en maintenant la simplicité du code séquentiel.

suspend fun loadCompleteUserProfile(userId: Int): UserProfile { 
    return coroutineScope { 
        val userDeferred = async { userService.getUser(userId) } 
        val postsDeferred = async { postService.getUserPosts(userId) } 
        val friendsDeferred = async { friendService.getUserFriends(userId) } 
        val settingsDeferred = async { settingsService.getUserSettings(userId) } 
        UserProfile( 
            user = userDeferred.await(), 
            posts = postsDeferred.await(), 
            friends = friendsDeferred.await(), 
            settings...