Modèles de corutine et antipatterns dans Kotlin

Modèles de corutine et antipatterns dans Kotlin


J'ai décidé d'écrire sur certaines choses qui, à mon avis, sont et ne devraient pas être évitées lors de l'utilisation de la coroutine Kotlin.


Enveloppez les appels asynchrones dans coroutineScope ou utilisez SupervisorJob pour gérer les exceptions


Si une exception peut se produire dans le bloc async , ne comptez pas sur le try/catch .


 val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } // (1) fun loadData() = scope.launch { try { doWork().await() // (2) } catch (e: Exception) { ... } } 

Dans l'exemple ci-dessus, la fonction doWork démarre une nouvelle coroutine (1), qui peut lever une exception non gérée. Si vous essayez d'envelopper doWork avec un try/catch (2), l'application se bloquera toujours.


En effet, l'échec d'un composant enfant du travail entraîne l'échec immédiat de son parent.


Une façon d'éviter l'erreur consiste à utiliser SupervisorJob (1).


L'échec ou l'annulation de l'exécution du composant enfant n'entraînera pas l'échec du parent et n'affectera pas les autres composants.

 val job = SupervisorJob() // (1) val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } fun loadData() = scope.launch { try { doWork().await() } catch (e: Exception) { ... } } 

Remarque : cela ne fonctionnera que si vous démarrez explicitement votre appel coroutine asynchrone avec SupervisorJob . Ainsi, le code ci-dessous plantera toujours votre application, car async s'exécute dans le cadre de la coroutine parent (1).


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async { // (1) // may throw Exception }.await() } catch (e: Exception) { ... } } 

Une autre façon d'éviter un plantage, qui est plus préférable, consiste à encapsuler async dans coroutineScope (1). Désormais, lorsqu'une exception se produit dans async , elle annule toutes les autres coroutines créées dans cette zone, sans toucher la zone externe. (2)


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = coroutineScope { // (1) async { ... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } } 

De plus, vous pouvez gérer les exceptions à l'intérieur du bloc async .


Utiliser le gestionnaire principal pour les coroutines racine


Si vous devez effectuer un travail d'arrière-plan et mettre à jour l'interface utilisateur à l'intérieur de votre coroutine racine, démarrez-la à l'aide du répartiteur principal.


 val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 

Dans l'exemple ci-dessus, nous démarrons la coroutine racine en utilisant le répartiteur CoroutineScope dans CoroutineScope (1). Avec cette approche, chaque fois que nous aurons besoin de mettre à jour l'interface utilisateur, nous devrons changer de contexte (2).


Dans la plupart des cas, il est préférable de créer CoroutineScope immédiatement avec le répartiteur principal, ce qui entraînera une simplification du code et un changement de contexte moins explicite.


 val scope = CoroutineScope(Dispatchers.Main) fun login() = scope.launch { view.showLoading() withContext(Dispatcher.IO) { networkClient.login(...) } view.hideLoading() } 

Évitez d'utiliser asynchrone / attente inutile


Si vous utilisez la fonction async et appelez immédiatement await , vous devez arrêter de le faire.


 launch { val data = async(Dispatchers.Default) { /* code */ }.await() } 

Si vous souhaitez changer le contexte de la coroutine et suspendre immédiatement la coroutine parent, alors withContext est le moyen le plus préférable pour cela.


 launch { val data = withContext(Dispatchers.Default) { /* code */ } } 

En termes de performances, ce n'est pas un gros problème (même en considérant que l' async crée une nouvelle coroutine pour le travail), mais async sémantiquement implique que vous voulez exécuter plusieurs coroutines en arrière-plan et ensuite seulement les attendre.


Évitez l'annulation du travail


Si vous devez annuler la coroutine, n'annulez pas le travail.


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() // (1) } 

Le problème avec le code ci-dessus est que lorsque nous annulons le travail, nous le mettons dans un état terminé . Les coroutines lancées dans le cadre d'un travail terminé ne seront pas exécutées (1).


Si vous souhaitez annuler toutes les coroutines dans une zone spécifique, vous pouvez utiliser la fonction cancelChildren . En outre, il est recommandé de prévoir la possibilité d'annuler des tâches individuelles (2).


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() } 

Évitez d'écrire la fonction de pause à l'aide du répartiteur implicite


N'écrivez pas la fonction de suspend , dont l'exécution dépendra du gestionnaire de coroutine particulier.


 suspend fun login(): Result { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

Dans l'exemple ci-dessus, la fonction de connexion est une fonction de suspension et elle échouera si vous la démarrez à partir d'une coroutine que le répartiteur principal n'utilisera pas.


 launch(Dispatcher.Main) { // (1)    val loginResult = login() ... } launch(Dispatcher.Default) { // (2)   val loginResult = login() ... } 

CalledFromWrongThreadException: seul le thread source qui a créé la hiérarchie des composants View peut y accéder.

Créez votre fonction de suspension afin qu'elle puisse être exécutée à partir de n'importe quel gestionnaire de coroutine.


 suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

Nous pouvons maintenant appeler notre fonction de connexion depuis n'importe quel répartiteur.


 launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... } 

Évitez d'utiliser la portée globale


Si vous utilisez GlobalScope partout dans votre application Android, vous devez arrêter de le faire.


 GlobalScope.launch { // code } 

La portée globale est utilisée pour lancer des coroutines de niveau supérieur qui s'exécutent tout au long de la vie de l'application et ne sont pas annulées à l'avance.

Le code d'application doit généralement utiliser le CoroutineScope défini par l' application , il n'est donc pas recommandé d'utiliser async ou de lancer dans GlobalScope .

Dans Android, la coroutine peut être facilement limitée au cycle de vie d'une activité, d'un fragment, d'une vue ou d'un ViewModel.


 class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch { // code } } 

Source: https://habr.com/ru/post/fr432942/


All Articles