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>> }
Nous avons une interface, par exemple, nous écrivons un client GitHub et voulons effectuer quelques opérations pour cela:
- Utilisateur de connexion.
- 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> }
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) -> {
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) {
- 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) {
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 :
- Replacing context . , ;
- 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 â :)