Comment utiliser les coroutines dans les aliments et dormir paisiblement la nuit

Les coroutines sont un outil puissant pour l'exĂ©cution de code asynchrone. Ils travaillent en parallĂšle, communiquent entre eux et consomment peu de ressources. Il semblerait que sans crainte, les coroutines puissent ĂȘtre introduites en production. Mais il y a des peurs et elles interfĂšrent.

Le rapport de Vladimir Ivanov sur AppsConf concerne le fait que le diable n'est pas si terrible et que vous pouvez utiliser des coroutines dĂšs maintenant:



À propos du confĂ©rencier : Vladimir Ivanov ( dzigoro ) est un dĂ©veloppeur Android de premier plan Ă  EPAM avec 7 ans d'expĂ©rience, aime l'architecture de solutions, React Native et le dĂ©veloppement iOS, et a Ă©galement la certification Google Cloud Architect .

Tout ce que vous lisez est un produit de la production d'expérience et d'études diverses, alors prenez-le tel quel, sans aucune garantie.

Coroutines, Kotlin et RxJava


Pour information: l'état actuel de la corutine est dans la version, à gauche Beta. Kotlin 1.3 est sorti, les coroutines sont déclarées stables et il y a la paix dans le monde.



J'ai rĂ©cemment menĂ© une enquĂȘte sur Twitter auprĂšs des utilisateurs de coroutine:

  • 13% des coroutines dans les aliments. Tout va bien;
  • 25% les essaient dans le projet pour animaux de compagnie;
  • 24% - Qu'est-ce que Kotlin?
  • La majeure partie de 38% de RxJava est partout.

Les statistiques ne sont pas heureuses. Je pense que RxJava est un outil trop complexe pour les tùches dans lesquelles il est couramment utilisé par les développeurs. Les coroutines conviennent mieux pour contrÎler le fonctionnement asynchrone.

Dans mes rapports précédents, j'ai parlé de la refonte de RxJava vers les coroutines de Kotlin, donc je ne m'attarderai pas là-dessus en détail, mais je ne ferai que rappeler les principaux points.

Pourquoi utilisons-nous des coroutines?


Parce que si nous utilisons RxJava, les exemples d'implémentation habituels ressemblent à ceci:

interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> } //RxJava 2 implementation 

Nous avons une interface, par exemple, nous écrivons un client GitHub et voulons effectuer quelques opérations pour cela:

  1. Utilisateur de connexion.
  2. Obtenez une liste des référentiels GitHub.

Dans les deux cas, les fonctions renverront des objets métier simples: GitHubUser ou une liste de GitHubRepository.

Le code d'implémentation de cette interface est le suivant:

 private fun attemptLoginRx () { showProgress(true) compositeDisposable.add(apiClient.login(auth) .flatMap { user -> apiClient.getRepositories(user.repos_url, auth) } .map { list -> list.map { it.full_name } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally { showProgress(false) } .subscribe( { list -> showRepositories(this, list) }, { error -> Log.e("TAG", "Failed to show repos", error) } )) } 

- Nous prenons compositeDisposable pour qu'il n'y ait pas de fuite mémoire.
- Ajoutez un appel à la premiÚre méthode.
- Nous utilisons des opérateurs pratiques pour obtenir l'utilisateur, par exemple flatMap .
- Nous obtenons une liste de ses référentiels.
- Nous Ă©crivons une Boilerplate pour qu'elle fonctionne sur les bons threads.
- Lorsque tout est prĂȘt, nous affichons la liste des rĂ©fĂ©rentiels pour l'utilisateur connectĂ©.

Difficultés du code RxJava:

  • La complexitĂ© À mon avis, le code est trop compliquĂ© pour la simple tĂąche de deux appels rĂ©seau et d'afficher quelque chose sur l' interface utilisateur .
  • Traces de pile non liĂ©es. Les traces de pile ne sont presque pas liĂ©es au code que vous Ă©crivez.
  • DĂ©passement des ressources . RxJava gĂ©nĂšre beaucoup d'objets sous le capot et les performances peuvent diminuer.

Quel sera le mĂȘme code avec les coroutines jusqu'Ă  la version 0.26?

À 0,26, l'API a changĂ© et nous parlons de production. Personne n'a encore rĂ©ussi Ă  appliquer 0,26 dans la prod, mais nous y travaillons.

Avec les coroutines, notre interface va changer de façon assez importante . Les fonctions cesseront de renvoyer les simples et autres objets d'assistance. Ils renverront immédiatement des objets métier: GitHubUser et une liste de GitHubRepository. Les fonctions GitHubUser et GitHubRepository auront des modificateurs de suspension . C'est bien, car suspendre ne nous oblige presque à rien:

 interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> } //Base interface 

Si vous regardez le code qui utilise déjà l'implémentation de cette interface, il changera considérablement par rapport à RxJava:

 private fun attemptLogin () { launch(UI) { val auth = BasicAuthorization(login, pass) try { showProgress(true) val userlnfo = async { apiClient.login(auth) }.await() val repoUrl = userlnfo.repos_url val list = async { apiClient.getRepositories(repoUrl, auth) }.await() showRepositories( this, list.map { it -> it.full_name } ) } catch (e: RuntimeException) { showToast("Oops!") } finally { showProgress(false) } } } 

- L'action principale a lieu oĂč nous appelons async coroutine builder , attendons une rĂ©ponse et obtenons userlnfo .
- Nous utilisons les données de cet objet.
- Effectuez un autre appel asynchrone et appelez en attente .

Tout semble comme si aucun travail asynchrone ne se passait, et nous Ă©crivons simplement les commandes dans la colonne et elles sont exĂ©cutĂ©es. En fin de compte, nous faisons ce qui doit ĂȘtre fait sur l'interface utilisateur.

Pourquoi les coroutines sont-elles meilleures?

  • Ce code est plus facile Ă  lire. Il est Ă©crit comme s'il Ă©tait cohĂ©rent.
  • TrĂšs probablement, les performances de ce code sont meilleures que sur RxJava.
  • Il est trĂšs simple d'Ă©crire des tests, mais nous y reviendrons un peu plus tard.

2 marches sur le cÎté


Écartons-nous un peu, il y a encore quelques points à discuter.

Étape 1. withContext vs launch / async


En plus de coroutine builder async, il existe coroutine builder withContext .

Lancez ou async créez un nouveau contexte Coroutine , ce qui n'est pas toujours nécessaire. Si vous avez un contexte Coroutine que vous souhaitez utiliser dans l'application, vous n'avez pas besoin de le recréer. Vous pouvez simplement réutiliser un existant. Pour ce faire, vous aurez besoin d'un générateur de coroutine avec Context. Il réutilise simplement le contexte Coroutine existant. Ce sera 2-3 fois plus rapide, mais maintenant c'est une question sans principes. Si les chiffres exacts sont intéressants, alors voici la question sur stackoverflow avec des repÚres et des détails.

RĂšgle gĂ©nĂ©rale: utilisez withContext sans aucun doute lĂ  oĂč il tient sĂ©mantiquement. Mais si vous avez besoin d'un chargement parallĂšle, par exemple plusieurs images ou Ă©lĂ©ments de donnĂ©es, alors async / wait est votre choix.

Étape 2. Refactoring


Et si vous refactorisez une chaßne RxJava vraiment complexe? Je suis tombé sur cela en production:

 observable1.getSubject().zipWith(observable2.getSubject(), (t1, t2) -> { // side effects return true; }).doOnError { // handle errors } .zipWith(observable3.getSubject(), (t3, t4) -> { // side effects return true; }).doOnComplete { // gather data } .subscribe() 

J'avais une chaßne compliquée avec un sujet public , avec des fermetures à glissiÚre et des effets secondaires dans chaque fermeture éclair qui envoyaient autre chose au bus de l'événement. La tùche au moins était de se débarrasser du bus d'événement. Je me suis assis pendant une journée, mais je n'ai pas pu refactoriser le code pour résoudre le problÚme. La bonne décision s'est avérée de tout jeter et de réécrire le code sur coroutine en 4 heures .

Le code ci-dessous est trĂšs similaire Ă  ce que j'ai obtenu:

 try { val firstChunkJob = async { call1 } val secondChunkJob = async { call2 } val thirdChunkJob = async { call3 } return Result( firstChunkJob.await(), secondChunkJob.await(), thirdChunkJob.await()) } catch (e: Exception) { // handle errors } 

- Nous faisons async pour une tĂąche, pour les deuxiĂšme et troisiĂšme.
- Nous attendons le résultat et mettons tout cela dans un objet.
- C'est fait!

Si vous avez des chaĂźnes complexes et qu'il y a des coroutines, alors refactorisez simplement. C'est vraiment rapide.

Qu'est-ce qui empĂȘche les dĂ©veloppeurs d'utiliser des coroutines dans prod?


À mon avis, en tant que dĂ©veloppeurs, nous ne sommes actuellement empĂȘchĂ©s d'utiliser des coroutines que par crainte de quelque chose de nouveau:

  • Nous ne savons pas quoi faire du cycle de vie , de l' activitĂ© et du cycle de vie des fragments. Comment travailler avec des coroutines dans ces cas?
  • Il n'y a aucune expĂ©rience dans la rĂ©solution de tĂąches complexes quotidiennes en production Ă  l'aide de corutine.
  • Pas assez d'outils. Un tas de bibliothĂšques et de fonctions ont Ă©tĂ© Ă©crites pour RxJava. Par exemple RxFCM . RxJava lui-mĂȘme a beaucoup d'opĂ©rateurs, ce qui est bien, mais qu'en est-il de la coroutine?
  • Nous ne comprenons pas vraiment comment tester les coroutines.

Si nous nous débarrassons de ces quatre peurs, nous pouvons dormir paisiblement la nuit et utiliser des coroutines en production.

Voyons point par point.

1. Gestion du cycle de vie


  • Les coroutines peuvent fuir comme jetables ou AsyncTask . Ce problĂšme doit ĂȘtre rĂ©solu manuellement.
  • Pour Ă©viter une exception de pointeur nul alĂ©atoire , les coroutines doivent ĂȘtre arrĂȘtĂ©es.

ArrĂȘter


Connaissez-vous Thread.stop () ? Si vous l'avez utilisĂ©, alors pas pour longtemps. Dans JDK 1.1, la mĂ©thode a Ă©tĂ© immĂ©diatement dĂ©clarĂ©e obsolĂšte, car il est impossible de prendre et d'arrĂȘter un certain morceau de code et il n'y a aucune garantie qu'il se terminera correctement. TrĂšs probablement, vous n'obtiendrez que la corruption de mĂ©moire .

Par conséquent, Thread.stop () ne fonctionne pas . Vous avez besoin que l'annulation soit coopérative, c'est-à-dire le code de l'autre cÎté pour savoir que vous l'annulez.

Comment appliquons-nous les arrĂȘts avec RxJava:

 private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() } 


Dans RxJava, nous utilisons CompositeDisposable .

- Ajoutez la variable compositeDisposable Ă  l'activitĂ© dans le fragment ou dans le prĂ©sentateur, oĂč nous utilisons RxJava.
- Dans onDestro y ajoutez Dispose et toutes les exceptions disparaissent d'elles-mĂȘmes.

Environ le mĂȘme principe avec les coroutines:

 private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() 
 } } override fun onDestroy() { job?.cancel() } 

Prenons un exemple de tĂąche simple .

En rÚgle générale, les constructeurs de coroutine renvoient un travail et, dans certains cas, sont reportés .

- Nous pouvons mémoriser ce travail.
- Donnez la commande "lancer" le générateur de coroutine . Le processus démarre, quelque chose se passe, le résultat de l'exécution est mémorisé.
- Si nous ne transmettons rien d'autre, alors «lancer» démarre la fonction et nous renvoie un lien vers le travail.
- Le travail est mémorisé, et dans onDestroy, nous disons «annuler» et tout fonctionne bien.

Quel est le problĂšme de l'approche? Chaque travail a besoin d'un champ. Vous devez conserver une liste des travaux pour les annuler tous ensemble. L'approche conduit Ă  la duplication de code, ne le faites pas.

La bonne nouvelle, c'est que nous avons des alternatives : CompositeJob et Lifecycle job .

CompositeJob est un analogue de compositeDisposable. Cela ressemble Ă  ceci :

 private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() } 

- Pour un fragment, nous commençons un travail.
- Nous mettons tout le travail dans CompositeJob et donnons la commande: "job.cancel () pour tout le monde!" .

L'approche est facilement implémentée en 4 lignes, sans compter la déclaration de classe:

 Class CompositeJob { private val map = hashMapOf<String, Job>() fun add(job: Job, key: String = job.hashCode().toString()) = map.put(key, job)?.cancel() fun cancel(key: String) = map[key]?.cancel() fun cancel() = map.forEach { _ ,u -> u.cancel() } } 


Vous aurez besoin de:

- carte avec une clé de chaßne,
- ajouter une méthode, dans laquelle vous ajouterez un travail,
- paramÚtre clé facultatif.

Si vous souhaitez utiliser la mĂȘme clĂ© pour le mĂȘme travail, veuillez. Sinon, hashCode rĂ©soudra notre problĂšme. Ajoutez le travail Ă  la carte que nous avons passĂ©e et annulez le prĂ©cĂ©dent avec la mĂȘme clĂ©. Si nous remplissons trop la tĂąche, le rĂ©sultat prĂ©cĂ©dent ne nous intĂ©resse pas. Nous l'annulons et le conduisons Ă  nouveau.

L'annulation est simple: nous obtenons le travail par clé et l'annulons. La deuxiÚme annulation pour la carte entiÚre annule tout. Tout le code est écrit en une demi-heure sur quatre lignes et cela fonctionne. Si vous ne voulez pas écrire, prenez l'exemple ci-dessus.

Travail adapté au cycle de vie


Avez-vous utilisé Android Lifecycle , propriétaire ou observateur de Lifecycle ?


Notre activitĂ© et nos fragments ont certains Ă©tats. Faits saillants: crĂ©Ă©, dĂ©marrĂ© et repris . Il existe diffĂ©rentes transitions entre les États. LifecycleObserver vous permet de vous abonner Ă  ces transitions et de faire quelque chose lorsqu'une des transitions se produit.

Cela semble assez simple:

 public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { 
 } } 

Vous raccrochez l'annotation avec un paramÚtre sur la méthode, et elle est appelée avec la transition correspondante. Utilisez simplement cette approche pour coroutine:

 class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { Log.d("AndroidJob", "Cancelling a coroutine") cancel() } } 

- Vous pouvez Ă©crire la classe de base AndroidJob .
- Nous transférerons le cycle de vie à la classe.
- L'interface LifecycleObserver implémentera le travail.

Tout ce dont nous avons besoin:

- Dans le constructeur, ajoutez Ă  Lifecycle en tant qu'observateur.
- Abonnez-vous à ON_DESTROY ou à toute autre chose qui nous intéresse.
- Faire annuler dans ON_DESTROY.
- Obtenez un parentJob dans votre fragment.
- Appelez le constructeur Joy jobs ou le cycle de vie de votre fragment d'activité. Aucune différence.
- Passez ce parentJob en tant que parent .

Le code fini ressemble Ă  ceci:

 private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) { // code } } 

Lorsque vous annulez parent, toutes les coroutines enfant sont annulées et vous n'avez plus besoin d'écrire quoi que ce soit dans le fragment. Tout se passe automatiquement, plus ON_DESTROY. L'essentiel , n'oubliez pas de passer parent = parentJob .

Si vous utilisez, vous pouvez écrire une rÚgle de charpie simple qui vous mettra en évidence: "Oh, vous avez oublié votre parent!"

Avec   Gestion de cycle de vie triĂ©e. Nous avons quelques outils qui vous permettent de faire tout cela facilement et confortablement.

Qu'en est-il des scénarios complexes et des tùches non triviales en production?

2. Cas d'utilisation complexes


Les scénarios complexes et les tùches non triviales sont:

- Opérateurs - opérateurs complexes dans RxJava: flatMap, debounce, etc.
- Gestion des erreurs - gestion des erreurs complexes. Pas seulement try..catch , mais imbriqué par exemple.
- La mise en cache est une tùche non triviale. En production, nous avons rencontré un cache et voulions obtenir un outil pour résoudre facilement le problÚme de mise en cache avec les coroutines.

Répéter


Quand nous avons pensé aux opérateurs pour coroutine, la premiÚre option était repeatWhen () .

Si quelque chose s'est mal passĂ© et que Corutin n'a pas pu atteindre le serveur Ă  l'intĂ©rieur, nous voulons rĂ©essayer plusieurs fois avec une sorte de repli exponentiel. La raison est peut-ĂȘtre une mauvaise connexion et nous obtiendrons le rĂ©sultat souhaitĂ© en rĂ©pĂ©tant l'opĂ©ration plusieurs fois.

Avec les coroutines, cette tùche est facilement implémentée:

 suspend fun <T> retryDeferredWithDelay( deferred: () -> Deferred<T>, tries: Int = 3, timeDelay: Long = 1000L ): T { for (i in 1..tries) { try { return deferred().await() } catch (e: Exception) { if (i < tries) delay(timeDelay) else throw e } } throw UnsupportedOperationException() } 


Implémentation de l'opérateur:

- Il prend différé .
- Vous devrez appeler async pour obtenir cet objet.
- Au lieu de différé, vous pouvez passer à la fois un bloc de suspension et généralement n'importe quelle fonction de suspension.
- La boucle for - vous attendez le résultat de votre coroutine. Si quelque chose se produit et que le compteur de répétition n'est pas épuisé, réessayez via Delay . Sinon, alors non.

La fonction peut ĂȘtre facilement personnalisĂ©e: mettez un retard exponentiel ou passez une fonction lambda qui calculera le retard en fonction des circonstances.

Utilisez-le, cela fonctionne!

Zips


On les rencontre aussi souvent. LĂ  encore, tout est simple:

 suspend fun <T1, T2, R> zip( source1: Deferred<T1>, source2: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zipper.apply(sourcel.await(), source2.await()) } suspend fun <T1, T2, R> Deferred<T1>.zipWith( other: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zip(this, other, zipper) } 

- Utilisez la fermeture éclair et appelez en attente sur votre différé.
- Au lieu de différé, vous pouvez utiliser la fonction de suspension et le générateur de coroutine avec withContext. Vous transmettrez le contexte dont vous avez besoin.

Cela fonctionne à nouveau et j'espÚre avoir supprimé cette peur.

Cache



Avez-vous une implémentation de cache en production avec RxJava? Nous utilisons RxCache.


Dans le diagramme de gauche: View et ViewModel . À droite, les sources de donnĂ©es: les appels rĂ©seau et la base de donnĂ©es.

Si nous voulons que quelque chose soit mis en cache, le cache sera une autre source de données.

Types de cache:

  • Source rĂ©seau pour les appels rĂ©seau.
  • Cache en mĂ©moire .
  • Cache persistant avec expiration Ă  stocker sur le disque afin que le cache survive au redĂ©marrage de l'application.

Écrivons un cache simple et primitif pour le troisiùme cas. Le constructeur de Coroutine withContext vient à la rescousse.

 launch(UI) { var data = withContext(dispatcher) { persistence.getData() } if (data == null) { data = withContext(dispatcher) { memory.getData() } if (data == null) { data = withContext(dispatcher) { network.getData() } memory.cache(url, data) persistence.cache(url, data) } } } 

- Vous exécutez chaque opération avec withContext et voyez si des données arrivent.
- Si les données de persistance ne viennent pas, vous essayez de les obtenir à partir de memory.cache .
- S'il n'y a pas non plus de memory.cache, contactez la source réseau et récupérez vos données. N'oubliez pas, bien sûr, de mettre toutes les caches.

Il s'agit d'une implĂ©mentation plutĂŽt primitive et il y a beaucoup de questions, mais la mĂ©thode fonctionne si vous avez besoin d'un cache au mĂȘme endroit. Pour les tĂąches de production, ce cache n'est pas suffisant. Il faut quelque chose de plus compliquĂ©.

Rx a RxCache


Pour ceux qui utilisent encore RxJava, vous pouvez utiliser RxCache. Nous l'utilisons toujours aussi. RxCache est une bibliothÚque spéciale. Vous permet de mettre en cache des données et de gérer son cycle de vie.

Par exemple, vous voulez dire que ces données expireront aprÚs 15 minutes: "S'il vous plaßt, aprÚs cette période de temps, n'envoyez pas de données depuis le cache, mais envoyez-moi de nouvelles données."

La bibliothÚque est merveilleuse en ce qu'elle soutient déclarativement l'équipe. La déclaration est trÚs similaire à ce que vous faites avec Retrofit :

 public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> } 

- Vous dites que vous avez un CacheProvider .
- Démarrez une méthode et dites que la durée de vie de LifeCache est de 15 minutes. La clé par laquelle il sera disponible est Fonctionnalités .
- Renvoie Observable <Reply , oĂč Reply est un objet de bibliothĂšque auxiliaire pour travailler avec le cache.

L'utilisation est assez simple:

 val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) ) 

- Depuis le cache Rx, accédez à RestApi .
- Tournez-vous vers CacheProvider .
- Nourrissez-le d'un observable.
- La bibliothĂšque elle-mĂȘme saura quoi faire: allez dans le cache ou non, si le temps est Ă©coulĂ©, tournez-vous vers Observable et effectuez une autre opĂ©ration.

L'utilisation de la bibliothĂšque est trĂšs pratique et j'aimerais en obtenir une similaire pour coroutine.

Coroutine Cache en développement


Dans EPAM, nous écrivons la bibliothÚque Coroutine Cache , qui exécutera toutes les fonctions de RxCache. Nous avons écrit la premiÚre version et l'avons exécutée au sein de l'entreprise. DÚs la sortie de la premiÚre version, je me ferai un plaisir de la publier sur mon Twitter. Cela ressemblera à ceci:

 val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() } 

Nous aurons une fonction suspendre getFeatures . Nous passerons la fonction sous forme de bloc Ă  une fonction spĂ©ciale d'ordre supĂ©rieur avec Cache , qui dĂ©terminera ce qui doit ĂȘtre fait.

Peut-ĂȘtre ferons-nous la mĂȘme interface pour prendre en charge les fonctions dĂ©claratives.

Gestion des erreurs




La gestion simple des erreurs est souvent trouvée par les développeurs et est généralement résolue assez simplement. Si vous n'avez pas de choses compliquées, alors dans catch vous attrapez l' exception et regardez ce qui s'est passé là-bas, écrivez dans le journal ou affichez une erreur à l'utilisateur. Sur l'interface utilisateur, vous pouvez facilement le faire.

Dans les cas simples, tout est normalement simple - la gestion des erreurs avec les coroutines se fait via try-catch-finally .

En production, en plus des cas simples, il y a:

- Try -catch imbriqué,
- De nombreux types d' exceptions ,
- Erreurs dans le réseau ou dans la logique métier,
- Erreurs utilisateur. Il a de nouveau fait quelque chose de mal et Ă©tait Ă  blĂąmer pour tout.

Nous devons nous y préparer.

Il existe 2 solutions: CoroutineExceptionHandler et l'approche avec les classes Result .

Gestionnaire d'exceptions Coroutine


Il s'agit d'une classe spéciale pour gérer les cas d'erreurs complexes. ExceptionHandler vous permet de prendre votre exception comme argument comme une erreur et de la gérer.

Comment traitons-nous habituellement les erreurs complexes?

L'utilisateur a appuyĂ© sur quelque chose, le bouton n'a pas fonctionnĂ©. Il doit dire ce qui n'a pas fonctionnĂ© et le diriger vers une action spĂ©cifique: vĂ©rifier Internet, le Wi-Fi, rĂ©essayer plus tard ou supprimer l'application et ne plus jamais l'utiliser. Dire cela Ă  l'utilisateur peut ĂȘtre assez simple:

 val handler = CoroutineExceptionHandler(handler = { , error -> hideProgressDialog() val defaultErrorMsg = "Something went wrong" val errorMsg = when (error) { is ConnectionException -> userFriendlyErrorMessage(error, defaultErrorMsg) is HttpResponseException -> userFriendlyErrorMessage(Endpoint.EndpointType.ENDPOINT_SYNCPLICITY, error) is EncodingException -> "Failed to decode data, please try again" else -> defaultErrorMsg } Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() }) 

- Obtenons le message par défaut: "Quelque chose s'est mal passé!" et analyser l'exception.
- S'il s'agit d'une exception ConnectionException, nous prenons un message localisé des ressources: «Mec, allume le Wi-Fi et tes problÚmes disparaßtront. Je le garantis. "
- Si le serveur a dit quelque chose de mal , alors vous devez dire au client: «Déconnectez-vous et reconnectez-vous», ou «Ne faites pas cela à Moscou, faites-le dans un autre pays», ou «Désolé, camarade. Tout ce que je peux faire, c'est simplement dire que quelque chose s'est mal passé. »
- S'il s'agit d'une erreur complÚtement différente , par exemple, par manque de mémoire , nous disons: "Quelque chose s'est mal passé, je suis désolé."
- Tous les messages sont affichés.

Ce que vous Ă©crivez dans CoroutineExceptionHandler sera exĂ©cutĂ© sur le mĂȘme Dispatcher oĂč vous exĂ©cutez la coroutine. Par consĂ©quent, si vous donnez la commande "Launch" UI, alors tout se passe sur l'interface utilisateur. Vous n'avez pas besoin de rĂ©partition sĂ©parĂ©e , ce qui est trĂšs pratique.

L'utilisation est simple:

 launch(uiDispatcher + handler) { ... } 

Il y a un opérateur plus . Dans le contexte Coroutine, ajoutez un gestionnaire et tout fonctionne, ce qui est trÚs pratique. Nous l'avons utilisé pendant un certain temps.

Classes de résultats


Plus tard, nous avons rĂ©alisĂ© que le CoroutineExceptionHandler pourrait ĂȘtre manquant. Le rĂ©sultat, qui est formĂ© par le travail de coroutine, peut consister en plusieurs donnĂ©es, provenant de diffĂ©rentes parties ou traiter plusieurs situations.

L'approche Classes de résultats permet de faire face à ce problÚme:

 sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() } 

- Dans votre logique métier, vous démarrez une classe de résultats .
- Marquer comme scellé .
- Vous héritez de la classe deux autres classes de données: Success et Error .
— Success , .
— Error exception.

- :

 override suspend fun doTask(): Result = withContext(CommonPool) { if ( !isSessionValidForTask() ) { return@withContext Result.Error(Exception()) } 
 try { Result.Success(restApi.call()) } catch (e: Exception) { Result.Error(e) } } 

Coroutine context — Coroutine builder withContex .

, :

— , error. .
— RestApi -.
— , Result.Success .
— , Result.Error .

- , ExceptionHandler .

Result classes , . Result classes, ExceptionHandler try-catch.

3.


, . unit- , , . unit-.

, . , unit-, 2 :

  1. Replacing context . , ;
  2. Mocking coroutines . .

Replacing context


presenter:

 val login() { launch(UI) { 
 } } 

, login , UI-. , , . , , unit-.

:

 val login (val coroutineContext = UI) { launch(coroutineContext) { ... } } 

— login coroutineContext. , . Kotlin , UI .
— Coroutine builder Coroutine Contex, .

unit- :

 fun testLogin() { val presenter = LoginPresenter () presenter.login(Unconfined) } 


— LoginPresenter login - , , Unconfined.
— Unconfined , , . .

Mocking coroutines


— . Mockk unit-. unit- Kotlin, . suspend- coEvery -.

login githubUser :

 coEvery { apiClient.login(any()) } returns githubUser 

Mockito-kotlin , — . , , :

 given { runBlocking { apiClient.login(any()) } }.willReturn (githubUser) 

runBlocking . given- , .

Presenter :

 fun testLogin() { val githubUser = GithubUser('login') val presenter = LoginPresenter(mockApi) presenter.login (Unconfined) assertEquals(githubUser, presenter.user()) } 

— -, , GitHubUser .
— LoginPresenter API, . .
— presenter.login Unconfined , Presenter , .

! .

Pour résumer



  • Rx- . . , RxJava RxJava. - — , .
  • . , . Unit- — , , , . — welcome!
  • . , , , , . .


Liens utiles



Actualités

30 Mail.ru . , .

AppsConf , .

, , , .

youtube- AppsConf 2018 — :)

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


All Articles