Dans cet article, je parlerai du fonctionnement des coroutines et de leur création. Considérez l'application dans une exécution séquentielle et parallèle. Parlons de la gestion des erreurs, du débogage et des moyens de tester la coroutine. À la fin, je résumerai et parlerai des impressions qui sont restées après l'application de cette approche.
L'article a été préparé sur la base des documents de mon rapport sur
MBLT DEV 2018 , à la fin de l'article - un lien vers la vidéo.
Style cohérent
Fig. 2.1Quel était le but des développeurs Corutin? Ils voulaient que la programmation asynchrone soit aussi simple que possible. Il n'y a rien de plus simple que d'exécuter le code «ligne par ligne» en utilisant les constructions syntaxiques du langage: try-catch-finally, boucles, instructions conditionnelles, etc.
Considérons deux fonctions. Chacun est exécuté sur son propre thread (Fig. 2.1). La première est exécutée sur le thread
B et retourne un résultat
dataB , puis nous devons passer ce résultat à la deuxième fonction, qui prend
dataB comme argument et est déjà en cours d'exécution sur le thread
A. Avec coroutine, nous pouvons écrire notre code comme le montre la fig. 2.1. Considérez comment y parvenir.
Fonctions
longOpOnB, longOpOnA - les soi-disant fonctions de
suspension , avant lesquelles le thread est libéré, et après l'achèvement de leur travail, il redevient occupé.
Pour que ces deux fonctions soient effectivement exécutées dans un thread différent de celui appelé, tout en conservant un style d'écriture «cohérent», nous devons les plonger dans le contexte de la coroutine.
Cela se fait en créant des coroutines à l'aide du soi-disant Coroutine Builder. Dans la figure, c'est le
lancement , mais il y en a d'autres, par exemple,
async ,
runBlocking . J'en parlerai plus tard.
Le dernier argument est un bloc de code exécuté dans le contexte de la coroutine: appel aux fonctions suspend, ce qui signifie que tout le comportement ci-dessus n'est possible que dans le contexte de la coroutine ou dans une autre fonction suspend.
Il existe d'autres paramètres dans la méthode Coroutine Builder, par exemple, le type de lancement, le thread dans lequel le bloc sera exécuté et d'autres.
Gestion du cycle de vie
Coroutine Builder nous donne la valeur de retour comme valeur de retour - une sous-classe de la classe
Job (Fig.2.2). Avec lui, nous pouvons gérer le cycle de vie de la corutine.
Commencez avec la méthode
start () , annulez avec la méthode
cancel () , attendez la fin du travail à l'aide de la méthode
join ( ), abonnez-vous à l'événement d'achèvement du travail et plus encore.
Fig. 2.2Changement de débit
Vous pouvez modifier le flux d'exécution de la coroutine en modifiant l'élément de contexte de la coroutine qui est responsable de la planification. (Fig. 2.3)
Par exemple, la corutine 1 s'exécutera dans un thread d'
interface utilisateur , tandis que la corutine 2 dans un thread extrait du pool
Dispatchers.IO .
Fig.2.3La bibliothèque coroutine fournit également une fonction de suspension
avecContext (CoroutineContext) , avec laquelle vous pouvez basculer entre les threads dans le contexte d'une coroutine. Ainsi, le saut entre les threads peut ĂŞtre assez simple:
Fig. 2.4.Nous commençons notre coroutine sur le thread d'interface utilisateur 1 → afficher l'indicateur de charge → passer au thread de travail 2, libérant le principal → y effectuer une longue opération qui ne peut pas être effectuée dans le thread d'interface utilisateur → renvoyer le résultat au thread d'interface utilisateur 3 → et déjà y travailler avec elle, rendant les données reçues et masquant l'indicateur de chargement.
Il semble assez confortable jusqu'à présent, passez à autre chose.
Fonction de suspension
Considérez le travail de corutin sur l'exemple du cas le plus courant - travailler avec des requêtes réseau en utilisant la bibliothèque Retrofit 2.
La première chose que nous devons faire est de convertir l'appel de
rappel en une fonction de
suspension pour profiter de la fonctionnalité coroutine:
Fig. 2,5Pour contrôler l'état de la coroutine, la bibliothèque fournit des fonctions de la forme
suspendXXXXCoroutine , qui fournissent un argument qui implémente l'interface
Continuation , en utilisant les méthodes
resumeWithException et
resume dont nous pouvons reprendre la coroutine en cas d'erreur et de succès, respectivement.
Ensuite, nous allons déterminer ce qui se passe lorsque la méthode resumeWithException est appelée, et tout d'abord, assurez-vous que nous devons en quelque sorte annuler l'appel de demande réseau.
Fonction de suspension. Annulation d'appel
Pour annuler l'appel et d'autres actions liées à la libération des ressources inutilisées, lors de l'implémentation de la fonction de suspension, vous pouvez utiliser la méthode
suspendCancellableCoroutine qui
sort de la boîte (Fig. 2.6). Ici, l'argument de bloc implémente déjà l'interface
CancellableContinuation , dont l'une des méthodes supplémentaires est
invokeOnCancellation , qui vous permet de vous inscrire pour une erreur ou un événement d'annulation de coroutine réussi. Par conséquent, ici, il est également nécessaire d'annuler l'appel de méthode.
Fig. 2.6Afficher les modifications dans l'interface utilisateur
Maintenant que la fonction de suspension a été préparée pour les requêtes réseau, vous pouvez utiliser son appel dans le thread d'interface utilisateur de la coroutine comme séquentiel, tandis que pendant l'exécution de la requête, le flux sera libre et le flux de mise à niveau sera utilisé pour exécuter la demande.
Ainsi, nous implémentons le comportement asynchrone par rapport au flux UI, mais nous l'écrivons dans un style cohérent (Fig. 2.6).
Si après avoir reçu la réponse, vous devez effectuer le travail acharné, par exemple, écrire les données reçues dans la base de données, alors cette fonction, comme cela a déjà été montré, peut être facilement effectuée en utilisant
withContext sur le pool de flux de retour et continuer l'exécution sur l'interface utilisateur sans une seule ligne de code.
Fig. 2.7Malheureusement, ce n'est pas tout ce dont nous avons besoin pour le développement d'applications. Envisagez la gestion des erreurs.
Gestion des erreurs: try-catch-finally. Annuler Coroutine: CancellationException
Une exception qui n'a pas été détectée à l'intérieur de la coroutine est considérée comme non gérée et peut entraîner un blocage de l'application. En plus des situations normales, une exception est levée en reprenant la coroutine à l'aide de la méthode
resumeWithException sur la ligne correspondante de l'appel à la fonction de suspension. Dans ce cas, l'exception passée en tant qu'argument est levée inchangée. (Fig. 2.8)
Fig. 2.8Pour la gestion des exceptions, la construction standard du langage try catch finally est disponible. Maintenant, le code qui peut afficher l'erreur dans l'interface utilisateur prend la forme suivante:
Fig. 2.9Dans le cas de l'annulation de la coroutine, ce qui peut être réalisé en appelant la méthode d'annulation du Job #, une
CancellationException est levée. Cette exception est gérée par défaut et n'entraîne pas de plantages ni d'autres conséquences négatives.
Cependant, lorsque vous utilisez la construction
try / catch , elle sera interceptée dans le
bloc catch , et vous devez en tenir compte dans les cas si vous souhaitez gérer uniquement des situations vraiment «erronées». Par exemple, la gestion des erreurs dans l'interface utilisateur lorsqu'il est possible «d'annuler» les demandes ou l'enregistrement des erreurs est fournie. Dans le premier cas, l'erreur sera affichée à l'utilisateur, bien qu'elle n'existe pas, et dans le second, une exception inutile sera enregistrée et encombrera les rapports.
Pour ignorer la situation d'annulation des coroutines, vous devez modifier légèrement le code:
Fig. 2.10Journalisation des erreurs
Considérez la trace de la pile d'exceptions d'exception.
Si vous lancez une exception directement dans le bloc de code de la coroutine (Fig. 2.11), la trace de la pile semble nette, avec seulement quelques appels de la coroutine, elle indique correctement la ligne et les informations sur l'exception. Dans ce cas, vous pouvez facilement comprendre à partir de la trace de pile où exactement, dans quelle classe et dans quelle fonction l'exception a été levée.
Fig. 2.11Cependant, les exceptions qui sont passées à la méthode
resumeWithException de
suspendre- fonctions, en règle générale, ne contiennent pas d'informations sur la coroutine dans laquelle elles se sont produites. Par exemple (Fig. 2.12), si vous reprenez la coroutine de la fonction de suspension précédemment implémentée avec la même exception que dans l'exemple précédent, la trace de la pile ne donnera pas d'informations sur où rechercher spécifiquement l'erreur.
Fig. 2.12Pour comprendre quelle coroutine a repris avec une exception, vous pouvez utiliser l'
élément de contexte
CoroutineName . (Fig. 2.13)
L'élément
CoroutineName est utilisé pour le débogage, en lui passant le nom de la coroutine, vous pouvez l'extraire dans des fonctions de suspension et, par exemple, compléter le message d'exception. Autrement dit, au moins, il sera clair où chercher une erreur.
Cette approche ne fonctionnera que si la fonction de suspension en est exclue:
Fig. 2.13Journalisation des erreurs. ExceptionHandler
Pour modifier la journalisation des exceptions pour une coroutine particulière, vous pouvez définir votre propre ExceptionHandler, qui est l'un des éléments du contexte de la coroutine. (Fig. 2.14)
Le gestionnaire doit implémenter l'interface
CoroutineExceptionHandler . En utilisant l'opérateur + remplacé pour le contexte coroutine, vous pouvez remplacer le gestionnaire d'exceptions standard par le vôtre. L'exception non
gérée tombera dans la méthode
handleException , où vous pourrez en faire ce dont vous avez besoin. Par exemple, ignorez complètement. Cela se produira si vous laissez le gestionnaire vide ou ajoutez vos propres informations:
Fig. 2.14Voyons Ă quoi pourrait ressembler la journalisation de notre exception:
- Vous devez vous souvenir de la CancellationException , que nous voulons ignorer.
- Ajoutez vos propres journaux.
- N'oubliez pas le comportement par défaut, qui comprend la journalisation et la fermeture de l'application, sinon l'exception «disparaîtra» tout simplement et il ne sera pas clair ce qui s'est passé.
Maintenant, dans le cas de lever une exception, une liste de trace de pile sera envoyée au logcat avec les informations ajoutées:
Fig. 2.15Exécution parallèle. async
Considérez le fonctionnement parallèle des fonctions de suspension.
Async est le mieux adapté pour organiser des résultats parallèles à partir de plusieurs fonctions. Async, comme le
lancement - Coroutine Builder. Sa commodité est qu'en utilisant la méthode
expect () , il retourne des données en cas de succès ou lève une exception qui s'est produite lors de l'exécution de la coroutine. La méthode wait attendra la fin de la coroutine, si elle n'est pas déjà terminée, sinon elle retournera immédiatement le résultat du travail. Notez que l'attente est une fonction de suspension et ne peut donc pas être exécutée en dehors du contexte d'une coroutine ou d'une autre fonction de suspension.
En utilisant async, obtenir des données de deux fonctions en parallèle ressemblera à ceci:
Fig. 2.16Imaginez que nous sommes confrontés à la tâche d'obtenir des données à partir de deux fonctions en parallèle. Ensuite, vous devez les combiner et les afficher. En cas d'erreur, il est nécessaire de dessiner l'interface utilisateur, annulant toutes les demandes en cours. Un tel cas se retrouve souvent dans la pratique.
Dans ce cas, l'erreur doit être traitée comme suit:
- Apportez la gestion des erreurs à l'intérieur de chaque async-corutine.
- En cas d'erreur, annulez toutes les coroutines. Heureusement, pour cela, il est possible de spécifier un travail parent, lors de l'annulation de laquelle tous ses enfants sont annulés.
- Nous proposons une implémentation supplémentaire pour comprendre si toutes les données ont été chargées avec succès. Par exemple, nous supposons que si wait renvoie null, une erreur s'est produite lors de la réception des données.
Avec tout cela à l'esprit, la mise en œuvre de la coroutine parentale devient un peu plus compliquée. La mise en œuvre de async-corutin est également compliquée:
Fig. 2.17Cette approche n'est pas la seule possible. Par exemple, vous pouvez implémenter l'exécution parallèle avec la gestion des erreurs à l'aide d'
ExceptionHandler ou de
SupervisorJob .
Coroutines imbriquées
Regardons le travail de la coroutine imbriquée.
Par défaut, la coroutine imbriquée est créée à l'aide d'une étendue externe et hérite de son contexte. En conséquence, la coroutine imbriquée devient une fille et le parent externe.
Si nous annulons la coroutine externe, les coroutines imbriquées ainsi créées, qui ont été utilisées dans l'exemple précédent, seront également annulées. Il sera également utile lorsque vous quittez l'écran lorsque vous devez annuler les demandes en cours. De plus, la corutine parent attendra toujours la fin de la fille.
Vous pouvez créer une coroutine indépendante de l'extérieur à l'aide d'une étendue globale. Dans ce cas, lorsque la coroutine externe est annulée, celle imbriquée continuera de fonctionner comme si de rien n'était:
Fig. 2.18
Vous pouvez créer un enfant de la coroutine globale imbriquée en remplaçant l'élément contextuel par la clé
Job par le travail parent, ou vous pouvez utiliser pleinement le contexte de la coroutine parent. Mais dans ce cas, il convient de se rappeler que tous les éléments de la coroutine parent sont pris en charge: le pool de threads, le gestionnaire d'exceptions, etc.:
Fig. 2.19Maintenant, il est clair que si vous utilisez Coroutine de l'extérieur, vous devez leur fournir la possibilité d'installer soit une instance du travail, soit le contexte du parent. Et les développeurs de bibliothèques doivent envisager la possibilité de l'installer en tant qu'enfant, ce qui cause des inconvénients.
Points d'arrĂŞt
Les coroutines affectent l'affichage des valeurs des objets en mode débogage. Si vous placez un point d'arrêt à l'intérieur de la prochaine coroutine sur la fonction
logData , alors quand elle se déclenche, nous voyons que tout va bien ici et les valeurs sont affichées correctement:
Fig. 2,20Maintenant, récupérez
dataA en utilisant la coroutine imbriquée, en laissant un point d'arrêt sur
logData :
Fig. 2.21La tentative de développement du bloc this pour essayer de trouver les valeurs souhaitées échoue. Ainsi, le débogage en présence de fonctions de suspension devient difficile.
Tests unitaires
Les tests unitaires sont assez simples. Vous pouvez utiliser le runBlocking de Coroutine Builder
pour cela .
runBlocking bloque un thread jusqu'à ce que toutes ses coroutines imbriquées se terminent, ce qui est exactement ce dont vous avez besoin pour les tests.
Par exemple, si l'on sait que quelque part à l'intérieur de la méthode coroutine est utilisée pour l'implémenter, alors pour tester la méthode, il suffit de l'
encapsuler dans
runBlocking .
runBlocking peut être utilisé pour tester une fonction de suspension:
Fig. 2.22Des exemples
Enfin, je voudrais montrer quelques exemples d'utilisation de la corutine.
Imaginez que nous devons exécuter trois requêtes A, B et C en parallèle, montrer leur achèvement et refléter le moment d'achèvement des requêtes A et B.
Pour ce faire, vous pouvez simplement envelopper les coroutines de requĂŞte A et B dans une seule et la travailler comme avec un seul ensemble:
Fig. 2.23L'exemple suivant montre comment utiliser la boucle for régulière pour exécuter des requêtes périodiques avec un intervalle de 5 secondes:
Fig. 2,24Conclusions
Parmi les inconvénients, je note que les coroutines sont un outil relativement jeune, donc si vous voulez les utiliser sur la prod, vous devez le faire avec prudence. Il y a des difficultés de débogage, un petit passe-partout dans la mise en œuvre de choses évidentes.
En général, les coroutines sont assez faciles à utiliser, en particulier pour implémenter des tâches asynchrones non compliquées. En particulier, du fait que des constructions de langage standard peuvent être utilisées. Les coroutines se prêtent facilement aux tests unitaires et tout cela sort de la boîte de la même entreprise qui développe le langage.
Signaler la vidéo
Il s'est avéré que beaucoup de lettres. Pour ceux qui aiment en savoir plus - vidéo de mon reportage sur
MBLT DEV 2018 :
Documents utiles sur le sujet: