Bonjour, Habr!
Nous vous rappelons que nous avons déjà une
précommande pour le livre tant attendu sur la langue Kotlin de la célèbre série Big Nerd Ranch Guides. Aujourd'hui, nous avons décidé de porter à votre attention la traduction d'un article décrivant les coroutines Kotlin et le bon fonctionnement des flux dans Android. Le sujet est discuté très activement, par conséquent, pour être complet, nous vous recommandons également de consulter
cet article de Habr et
cet article détaillé du blog Axmor Software.
Le cadre concurrentiel moderne de Java / Android inflige un enfer aux rappels et conduit à des états de blocage, car Android n'a pas un moyen assez simple de garantir la sécurité des threads.
Les coroutines Kotlin sont une boîte à outils très efficace et complète qui rend la gestion de la concurrence beaucoup plus facile et plus productive.
Pause et blocage: quelle est la différenceLes coroutines ne remplacent pas les threads, mais fournissent plutôt un cadre pour les gérer. La philosophie de corutin est de définir un contexte qui vous permet d'
attendre la fin des opérations en arrière-plan sans bloquer le thread principal.
L'objectif de Corutin dans ce cas est de se passer de rappels et de simplifier la concurrence.
Exemple le plus simplePour commencer, prenons l'exemple le plus simple: exécutez coroutine dans le contexte de
Main
(thread principal). Dans celui-ci, nous allons extraire l'image du flux d'
IO
et renvoyer cette image pour traitement vers
Main
.
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }
Le code est simple en tant que fonction à un seul thread. De plus, alors que
getImage
est exécuté dans le pool alloué de threads d'
IO
, le thread principal est libre et peut assumer n'importe quelle autre tâche! La fonction withContext interrompt la coroutine actuelle pendant l'exécution de son action (
getImage()
). Dès que
getImage()
revient et que le
looper
du thread principal devient disponible, la coroutine reprend le travail dans le thread principal et appelle
imageView.setImageBitmap(image)
.
Le deuxième exemple: nous avons maintenant besoin de 2 tâches d'arrière-plan pour qu'elles puissent être utilisées. Nous utiliserons le duo async / attente pour que ces deux tâches soient effectuées en parallèle, et utiliserons leur résultat dans le thread principal dès que les deux tâches seront prêtes:
val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()
async
est similaire au
launch
, mais retourne
deferred
(une entité Kotlin équivalente à
Future
), donc son résultat peut être obtenu en utilisant
await()
. Lorsqu'il est appelé sans paramètres, il fonctionne dans le contexte par défaut de la portée actuelle.
Encore une fois, le thread principal reste libre pendant que nous attendons nos 2 valeurs.
Comme vous pouvez le voir, la fonction de
launch
renvoie
Job
, qui peut être utilisée pour attendre la fin de l'opération - cela se fait à l'aide de la fonction
join()
. Il fonctionne comme dans n'importe quelle autre langue, avec la mise en garde qu'il
suspend simplement
la coroutine et ne bloque pas le flux .
EnvoiLa répartition est un concept clé lorsque vous travaillez avec des coroutines. Cette action vous permet de "sauter" d'un fil à l'autre.
Considérez à quoi ressemble l'équivalent pour la répartition dans
Main
en Java, c'est-à-dire
runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);
Main
implémentation du contexte
Main
pour Android est un
Handler
basé sur un
Handler
. Il s'agit donc bien d'une implémentation très adaptée:
launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }
launch(Dispatchers.Main)
envoie
Runnable
à
Handler
, donc son code ne s'exécute pas immédiatement.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED)
exécutera immédiatement son expression lambda dans le thread actuel.
Dispatchers.Main
s'assure que lorsque la coroutine reprend son travail, elle sera dirigée vers le fil principal ; en outre, Handler est utilisé ici comme une implémentation Android native pour l'envoi à la boucle d'événements de l'application.
L'implémentation exacte ressemble à ceci:
val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main")
Voici un bon article pour vous aider à comprendre les subtilités de l'envoi dans Android:
Comprendre Android Core: Looper, Handler et HandlerThread .
Contexte CoroutineLe contexte de la coroutine (également appelé gestionnaire de la coroutine) détermine dans quel thread son code sera exécuté, que faire si une exception est levée et fait référence au contexte parent pour propager l'annulation.
val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... }
job.cancel()
annulera toutes les coroutines dont le parent est
job
. Un exceptionHandler recevra toutes les exceptions levées dans ces coroutines.
PortéeL'interface
coroutineScope
simplifie la gestion des erreurs:
Si l'une de ses coroutines filles échoue, la portée entière et toutes les coroutines enfants seront également annulées.
Dans l'exemple
async
, s'il n'était pas possible d'extraire la valeur, alors qu'une autre tâche continuait de fonctionner, nous avons un état endommagé et nous devons y remédier.
Lorsque vous travaillez avec
coroutineScope
, la fonction
useValues
sera appelée uniquement si l'extraction des deux valeurs est réussie. De plus, si
deferred2
échoue,
deferred1
sera annulé.
coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) }
Vous pouvez également «mettre dans la portée» une classe entière pour définir un
CoroutineContext
par défaut et l'utiliser.
Un exemple de classe qui implémente l'interface
CoroutineScope
:
open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } }
Exécuter Corutin dans
CoroutineScope
:
Le gestionnaire de
launch
ou
async
par défaut devient maintenant le gestionnaire d'étendue actuel.
launch { val foo = withContext(Dispatchers.IO) { … }
Lancement autonome de coroutine (en dehors de tout CoroutineScope):
GlobalScope.launch(Dispatchers.Main) {
Vous pouvez même définir la portée d'une application en définissant le répartiteur
Main
par défaut:
object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate }
Remarques- Les coroutines limitent l'interopérabilité avec Java
- Limitez la mutabilité pour éviter les verrous
- Les coroutines sont conçues pour attendre, pas pour organiser les threads
- Évitez les E / S dans
Dispatchers.Default
(et Main
...) - c'est à cela que Dispatchers.IO est destiné - Les flux consomment des ressources, donc des contextes à un seul thread sont utilisés
Dispatchers.Default
basé sur ForkJoinPool
, introduit dans Android 5+- Les coroutines peuvent être utilisées via des canaux
Se débarrasser des verrous et des rappels à l'aide des canauxDéfinition de canal à partir de la documentation JetBrains:
Channel Channel
conceptuellement très similaire à BlockingQueue
. La principale différence est qu'il ne bloque pas l'opération de vente, il prévoit une suspension d' send
(ou une offer
non bloquante), et au lieu de bloquer l'opération de prise, il prévoit une suspension de receive
.
ActeursConsidérez un outil simple pour travailler avec les canaux:
Actor
.
Actor
, encore une fois, est très similaire à
Handler
: nous définissons le contexte de la coroutine (c'est-à-dire le thread dans lequel nous allons effectuer des actions) et travaillons avec lui dans un ordre séquentiel.
La différence, bien sûr, est que les corutines sont utilisées ici;
Vous pouvez spécifier la puissance et le code exécuté - pause .
En principe, l'
actor
redirigera toute commande vers le canal coroutine. Il
garantit l'exécution d'une commande et limite les opérations dans son contexte . Cette approche aide parfaitement à se débarrasser des appels de
synchronize
et à garder tous les threads libres!
protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }
Dans cet exemple, nous utilisons les classes Kotlin scellées, en choisissant l'action à effectuer.
sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update()
De plus, toutes ces actions seront mises en file d'attente, elles ne seront jamais exécutées en parallèle. Il s'agit d'un moyen pratique pour atteindre les
limites de variabilité .
Cycle de vie Android + CoroutinesLes acteurs peuvent également être très utiles pour contrôler l'interface utilisateur Android, simplifier l'annulation des tâches et éviter de surcharger le thread principal.
job.cancel()
cela et appelons
job.cancel()
lorsque l'activité est détruite.
class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()
La classe
SupervisorJob
est similaire au
Job
normal, à la seule exception près que l'annulation ne s'étend que vers l'aval.
Par conséquent, nous n'annulons pas toutes les coroutines d'une
Activity
lorsque l'une d'elles échoue.
Les choses vont un peu mieux avec
une fonction d'extension qui vous permet d'accéder à ce
CoroutineContext
depuis n'importe quelle
View
de
CoroutineScope
.
val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext
Maintenant, nous pouvons combiner tout cela, la fonction
setOnClick
crée un acteur combiné pour contrôler ses actions
onClick
. En cas de clics multiples, les actions intermédiaires seront ignorées, éliminant ainsi les erreurs ANR (l'application ne répond pas), et ces actions seront effectuées dans le cadre de l'
Activity
. Par conséquent, lorsque l'activité sera détruite, tout cela sera annulé.
fun View.setOnClick(action: suspend () -> Unit) {
Dans cet exemple, nous avons défini le
Channel
sur
Conflated
afin qu'il ignore certains événements s'il y en a trop. Vous pouvez le remplacer par
Channel.UNLIMITED
si vous préférez mettre les événements en file d'attente sans en perdre aucun, mais que vous souhaitez tout de même protéger l'application des erreurs ANR.
Vous pouvez également combiner les coroutines et les frameworks Lifecycle pour automatiser l'annulation des tâches liées à l'interface utilisateur:
val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }
Simplifiez la situation avec les rappels (partie 1)Voici comment transformer l'utilisation des API basées sur le rappel avec
Channel
.
L'API fonctionne comme ceci:
requestBrowsing(url, listener)
analyse le dossier situé dans url
.- L'
listener
reçoit onMediaAdded(media: Media)
pour tout fichier multimédia trouvé dans ce dossier. listener.onBrowseEnd()
est appelé lors de l'analyse du dossier
Voici l'ancienne fonction d'
refresh
du fournisseur de contenu pour le navigateur VLC:
private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } }
Comment l'améliorer?Créez une chaîne qui s'exécutera en
refresh
. Désormais, les rappels du navigateur dirigent uniquement les médias vers cette chaîne, puis la ferment.
Maintenant, la fonction de
refresh
est devenue plus claire. Elle crée une chaîne, appelle le navigateur VLC, puis forme une liste de fichiers multimédias et la traite.
Au lieu des
consumeEach
select
ou
consumeEach
vous pouvez utiliser
for
pour attendre le média, et cette boucle se rompra dès que le
browserChannel
fermera.
private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)
Simplifier la situation avec les rappels (partie 2): RetrofitLa deuxième approche: nous n'utilisons pas du tout les coroutines kotlinx, mais nous utilisons un framework core coroutine.
Voyez comment les coroutines fonctionnent réellement!
La fonction
retrofitSuspendCall
encapsule une demande d'
Retrofit Call
pour en faire une fonction de
suspend
.
En utilisant
suspendCoroutine
nous appelons la méthode
Call.enqueue
et
Call.enqueue
la coroutine. Le rappel fourni de cette manière appellera
continuation.resume(response)
pour reprendre la coroutine avec une réponse du serveur dès qu'elle est reçue.
Ensuite, nous avons juste besoin de combiner nos fonctions Retrofit dans
retrofitSuspendCall
pour retourner les résultats de requête en les utilisant.
suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }
Ainsi, l'appel bloquant le réseau se fait dans le thread Retrofit dédié, la coroutine est là, en attente d'une réponse du serveur, et il n'y a nulle part où l'utiliser dans l'application!
Cette implémentation est inspirée de la
bibliothèque gildor / kotlin-coroutines-retrofit .
Il existe également un
adaptateur JakeWharton / retrofit2-kotlin-coroutines avec une autre implémentation donnant un résultat similaire.
ÉpilogueChannel
peut être utilisé de nombreuses autres manières; Consultez
BroadcastChannel pour des implémentations plus puissantes que vous pourriez trouver utiles.
Vous pouvez également créer des chaînes à l'aide de la fonction
Produire .
Enfin, en utilisant des canaux, il est pratique d'organiser la communication entre les composants de l'interface utilisateur: l'adaptateur peut transmettre des événements de clic à son fragment / activité via
Channel
ou, par exemple, via
Actor
.