Dans les applications mobiles des réseaux sociaux, l'utilisateur aime, écrit un commentaire, puis parcourt le flux, démarre la vidéo et remet le même. Tout cela est rapide et presque simultané. Si la mise en œuvre de la logique métier de l'application est complètement bloquée, l'utilisateur ne pourra pas accéder à la bande tant que les éléments similaires pour l'enregistrement avec des scellés n'auront pas été téléchargés. Mais l'utilisateur n'attendra pas, par conséquent, dans la plupart des applications mobiles, les tâches asynchrones fonctionnent, qui sont démarrées et terminées indépendamment les unes des autres. L'utilisateur effectue plusieurs tâches en même temps et ne se bloque pas. Une tâche asynchrone démarre et s'exécute tandis que l'utilisateur démarre la suivante.

En déchiffrant le rapport de
Stepan Goncharov sur
AppsConf, nous aborderons
l' asynchronie: nous nous plongerons dans l'architecture des applications mobiles, discuterons de la raison pour laquelle nous devrions séparer une couche pour effectuer des tâches asynchrones, nous analyserons les exigences et les solutions existantes, nous passerons en revue les avantages et les inconvénients, et envisagerons l'une des implémentations de cette approche. Nous apprenons également comment gérer les tâches asynchrones, pourquoi chaque tâche a son propre ID, quelles sont les stratégies d'exécution et comment elles aident à simplifier et accélérer le développement de l'application entière.
À propos de l'orateur: Stepan Goncharov (
stepango ) travaille chez Grab - c'est comme Uber, mais en Asie du Sud-Est. Il est impliqué dans le développement Android depuis plus de 9 ans. Intéressé par Kotlin depuis 2014 et depuis 2016 - l'utilise dans la prod. Organisé par Kotlin User Group à Singapour. C'est l'une des raisons pour lesquelles tous les exemples de code seront sur Kotlin, et non pas parce que c'est à la mode.
Nous examinerons une approche pour concevoir les composants de votre application. Il s'agit d'un guide d'action pour ceux qui souhaitent ajouter de nouveaux composants à l'application, les concevoir facilement, puis les développer. Les développeurs iOS peuvent utiliser l'approche iOS. L'approche s'applique également à d'autres plateformes. Je m'intéresse à Kotlin depuis 2014, donc tous les exemples seront dans cette langue. Mais ne vous inquiétez pas - vous pouvez écrire la même chose en Swift, Objective-C et dans d'autres langages.
Commençons par les problèmes et les inconvénients des
extensions réactives . Les problèmes sont typiques pour d'autres primitives asynchrones, donc nous disons RX - gardez à l'esprit l'avenir et la promesse, et tout fonctionnera de la même manière.
Problèmes de réception
Seuil d'entrée élevé . Le RX est assez complexe et volumineux - il compte 270 opérateurs, et il n'est pas facile d'apprendre à toute l'équipe comment les utiliser correctement. Nous ne discuterons pas de ce problème - il dépasse la portée du rapport.
Dans RX, vous devez
gérer manuellement vos abonnements, ainsi que surveiller le cycle de vie de l'application . Si vous êtes déjà abonné à Single ou Observable, vous
ne pouvez pas le comparer avec un autre SIngle , car vous recevrez toujours un nouvel objet et il y aura toujours des abonnements différents pour l'exécution.
Dans RX, il n'y a aucun moyen de comparer les abonnements et les flux .
Nous essaierons de résoudre certains de ces problèmes. Nous allons résoudre chaque problème une fois, puis réutiliser le résultat.
Problème numéro 1: effectuer une tâche plus d'une fois
Un problème courant dans le développement est le travail inutile et la répétition des mêmes tâches plus d'une fois. Imaginez que nous ayons un formulaire pour entrer des données et un bouton d'enregistrement. Lorsque vous appuyez sur, une demande est envoyée, mais si vous cliquez plusieurs fois pendant l'enregistrement du formulaire, plusieurs demandes identiques seront envoyées. Nous avons donné le bouton pour tester l'AQ, ils ont appuyé 40 fois en une seconde - nous avons reçu 40 demandes, parce que, par exemple, l'animation n'avait pas le temps de fonctionner.
Comment résoudre le problème? Chaque développeur a sa propre approche préférée pour résoudre: l'un collera un anti-
debounce
, l'autre bloquera le bouton au cas où par
clickable = false
. Il n'y a pas d'approche générale, donc ces bogues apparaîtront ou disparaîtront de notre application. Nous ne résolvons le problème que lorsque l'AQ nous dit: «Oh, j'ai cliqué ici, et ça s'est cassé»!
Une solution évolutive?
Pour éviter de telles situations, nous encapsulerons RX ou un autre framework asynchrone -
nous ajouterons des ID à toutes les opérations asynchrones . L'idée est simple - nous avons besoin d'un moyen de les comparer, car généralement cette méthode n'est pas dans les cadres. Nous pouvons terminer la tâche, mais nous ne savons pas si elle est déjà terminée ou non.
Appelons notre wrapper «Act» - d'autres noms sont déjà pris. Pour ce faire, créez une petite
typealias
et une
interface
simple dans laquelle il n'y a qu'un seul champ:
typealias Id = String interface Act { val id: Id }
C'est pratique et réduit légèrement la quantité de code. Plus tard, si String ne l'aime pas, nous le remplacerons par autre chose. Dans ce petit morceau de code, nous observons un fait amusant.
Les interfaces peuvent contenir des propriétés.
Pour les programmeurs qui viennent de Java, c'est inattendu. Habituellement, ils ajoutent des méthodes
getId()
à l'intérieur de l'interface, mais ce n'est pas la bonne solution, du point de vue de Kotlin.
Comment allons-nous concevoir?
Une petite digression. Lors de la conception, j'adhère à deux principes. La première consiste à
décomposer les exigences des composants et leur mise en œuvre en petits morceaux . Cela permet un contrôle granulaire de l'écriture de code. Lorsque vous créez un gros composant et essayez de tout faire en même temps, c'est mauvais. Habituellement, ce composant ne fonctionne pas et vous commencez à insérer des béquilles, je vous invite donc à écrire par petites étapes contrôlées et à en profiter. Le deuxième principe consiste
à vérifier l'opérabilité après chaque étape et à
répéter la procédure .
Pourquoi ne suffit-il pas d'une pièce d'identité?
Revenons au problème. Nous avons fait le premier pas - nous avons ajouté un identifiant, et tout était simple - l'interface et le champ. Cela ne nous a rien donné, car l'interface ne contient aucune implémentation et ne fonctionne pas seule, mais vous permet de comparer les opérations.
Ensuite, nous ajouterons des composants qui nous permettront d'utiliser l'interface et de comprendre que nous voulons exécuter une sorte de demande une deuxième fois lorsque cela n'est pas nécessaire. La première chose que nous ferons est d'
introduire de nouvelles abstractions .
Présentation de nouvelles abstractions: MapDisposable
Il est important de choisir le bon nom et l'abstraction familiers aux développeurs qui travaillent dans votre base de code. Puisque j'ai des exemples sur RX, nous utiliserons le concept RX et des noms similaires à ceux utilisés par les développeurs de la bibliothèque. Nous pouvons donc facilement expliquer à nos collègues ce qu'ils ont fait, pourquoi et comment cela devrait fonctionner. Pour sélectionner un nom, consultez la
documentation CompositeDiposable .
Créons une petite interface MapDisposable qui
contient des informations sur les tâches en cours et les
appels dispose () lors de la suppression . Je ne donnerai pas l'implémentation, vous pouvez voir toutes les sources
sur mon GitHub .
Nous appelons MapDisposable de cette façon car le composant fonctionnera comme une Map, mais il aura des propriétés CompositeDiposable.
Présentation de nouvelles abstractions: ActExecutor
Le composant abstrait suivant est
ActExecutor. Il démarre ou ne démarre pas de nouvelles tâches, dépend de MapDisposable et délègue la gestion des erreurs. Comment choisir un nom -
voir la documentation .
Prenez l'analogie la plus proche du JDK. Il a un exécuteur dans lequel vous pouvez passer le fil et faire quelque chose. Il me semble que c'est un composant sympa et bien conçu, prenons-le comme base.
Nous créons ActExecutor et une interface simple pour cela, en adhérant au principe de petites étapes simples. Le nom lui-même dit que c'est un composant auquel nous transmettons quelque chose et qu'il commence à faire quelque chose. ActExecutor a une méthode dans laquelle nous passons
Act
et, au cas où, gérons les erreurs, car sans elles, il n'y a aucun moyen.
interface ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit = ::logError) } interface MapDisposable { fun contains(id: Id): Boolean fun add(id: Id, disposable: () -> T) fun remove(id: Id) }
MapDisposable est également limité: prenez l'interface Carte et copiez les contenus,
add
et
remove
méthodes. La méthode
add
diffère de Map: le deuxième argument est le lambda pour la beauté et la commodité. La commodité est que nous pouvons synchroniser le lambda pour éviter des
conditions de course inattendues. Mais nous n'en parlerons pas, nous continuerons sur l'architecture.
Implémentation de l'interface
Nous avons déclaré toutes les interfaces et essaierons d'implémenter quelque chose de simple. Prenez
CompletableAct et
SingleAct .
class CompletableAct ( override val id: Id, override val completable: Completable ) : Act class SingleAct<T : Any>( override val id: Id, override val single: Single<T> ) : Act
CompletableAct est un wrapper sur Completable. Dans notre cas, il contient simplement un ID - c'est ce dont nous avons besoin. SingleAct est presque le même. Nous pouvons également implémenter Maybe et Flowable, mais nous attarder sur les deux premières implémentations.
Pour Single, nous avons spécifié le type générique
<T : Any>
. En tant que développeur Kotlin, je préfère utiliser une telle approche.
Essayez d'utiliser des génériques non nuls.
Maintenant que nous avons un ensemble d'interfaces, nous implémentons une logique pour empêcher l'exécution des mêmes requêtes.
class ActExecutorImpl ( val map: MapDisposable ): ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit ) = when { map.contains(act.id) -> { log("${act.id} - in progress") } else startExecution(act, e) log("${act.id} - Started") } }
Nous prenons une carte et vérifions s'il y a une demande. Sinon, nous commençons à exécuter la demande et l'ajoutons à la carte juste au moment de l'exécution. Après l'exécution avec n'importe quel résultat: erreur ou succès, supprimez la demande de la carte.
Pour très attentif - il n'y a pas de synchronisation, mais la synchronisation est dans le code source sur GitHub.
fun startExecution(act: Act, e: (Throwable) -> Unit) { val removeFromMap = { mapDisposable.remove(act.id) } mapDisposable.add(act.id) { when (act) { is CompletableAct -> act.completable .doFinally(removeFromMap) .subscribe({}, e) is SingleAct<*> -> act.single .doFinally(removeFromMap) .subscribe({}, e) else -> throw IllegalArgumentException() } }
Utilisez lambdas comme dernier argument pour améliorer la lisibilité du code. C'est magnifique et vos collègues vous remercieront.
Nous utiliserons d'autres puces Kotlin et ajouterons des
fonctions d'extension pour Completable et Single. Avec eux, nous n'avons pas besoin de chercher une méthode d'usine pour créer un CompletableAct et SingleAct - nous les créerons via des fonctions d'extension.
fun Completable.toAct(id: Id): Act = CompletableAct(id, this) fun <T: Any> Single<T>.toAct(id: Id): Act = SingleAct(id, this)
Les fonctions d'extension peuvent être ajoutées à n'importe quelle classe.
Résultat
Nous avons implémenté plusieurs composants et une logique très simple. Maintenant, la règle principale que nous devons suivre est de
ne pas forcer un abonnement à la main . Lorsque nous voulons exécuter quelque chose - nous le donnons via Executor. Ainsi qu'avec du fil - personne ne les démarre eux-mêmes.
fun act() = Completable.timer(2, SECONDS).toAct("Hello") executor.apply { execute(act()) execute(act()) execute(act()) } Hello - Act Started Hello - Act Duplicate Hello - Act Duplicate Hello - Act Finished
Nous avons convenu une fois au sein de l'équipe, et maintenant il y a toujours une garantie que les ressources de notre application ne seront pas dépensées pour l'exécution de demandes identiques et inutiles.
Le premier problème a été résolu. Développons maintenant la solution pour lui donner de la flexibilité.
Problème numéro 2: quelle tâche annuler?
Ainsi que dans les cas où il est nécessaire d'
annuler une demande ultérieure , il se peut que nous devions annuler la précédente. Par exemple, nous avons modifié les informations sur notre utilisateur pour la première fois et les avons envoyées au serveur. Pour une raison quelconque, l'envoi a pris beaucoup de temps et n'a pas abouti. Nous avons à nouveau modifié le profil utilisateur et envoyé la même demande une deuxième fois. Dans ce cas, cela n'a aucun sens de générer un ID spécial pour la demande - les informations de la deuxième tentative sont plus pertinentes et la
demande précédente est annulée .
La solution actuelle ne fonctionnera pas, car elle annulera toujours l'exécution de la demande avec les informations pertinentes. Nous devons en quelque sorte étendre la solution pour contourner le problème et ajouter de la flexibilité. Pour ce faire, comprendre ce que nous voulons tous? Mais nous voulons comprendre quelle tâche annuler, comment ne pas copier-coller et comment l'appeler.
Ajouter des composants
Nous appelons des stratégies de comportement de requête et créons deux interfaces pour elles:
StrategyHolder et
Strategy . Nous créons également 2 objets qui sont responsables de la stratégie à appliquer.
interface StrategyHolder { val strategy: Strategy } sealed class Strategy object KillMe : Strategy() object SaveMe : Strategy()
Je n'utilise pas d'
énumération - j'aime plus la
classe scellée . Ils sont plus légers, consomment moins de mémoire et sont plus faciles et plus pratiques à étendre.
La classe scellée est plus facile à étendre et à écrire plus courte.
Mise à jour des composants existants
À ce stade, tout est simple. Nous avions une interface simple, maintenant ce sera l'héritier de StrategyHolder. Comme ce sont des interfaces, il n'y a pas de problème d'héritage. Dans l'implémentation de CompletableAct, nous allons insérer un autre
override
et y ajouter la valeur par défaut pour nous assurer que les modifications resteront compatibles avec le code existant.
interface Act : StrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe ) : Act
Stratégies
J'ai choisi la stratégie
SaveMe , qui me semble évidente. Cette stratégie n'annule que les demandes suivantes - la première demande sera toujours active jusqu'à ce qu'elle soit terminée.
Nous avons travaillé un peu sur notre implémentation. Nous avions une méthode d'exécution et nous y avons maintenant ajouté une vérification de stratégie.
- Si la stratégie SaveMe est la même que ce que nous faisions auparavant, alors rien n'a changé.
- Si la stratégie est KillMe , supprimez la demande précédente et lancez-en une nouvelle.
override fun execute(act: Act, e: (Throwable) -> Unit) = when { map.contains(act.id) -> when (act.strategy) { KillMe -> { map.remove(act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) }
Résultat
Nous avons pu gérer facilement les stratégies en écrivant un minimum de code. En même temps, nos collègues sont heureux et nous pouvons faire quelque chose comme ça.
executor.apply { execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello«, KillMe)) } Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Finished
Nous créons une tâche asynchrone, passons la stratégie et chaque fois que nous démarrons une nouvelle tâche, toutes les précédentes, et non les suivantes, seront annulées.
Problème numéro 3: les stratégies ne suffisent pas
Passons à un problème intéressant que j'ai rencontré sur quelques projets. Nous élargirons notre solution pour traiter des cas plus compliqués. Un de ces cas, particulièrement pertinent pour les réseaux sociaux, est
«j'aime / je n'aime pas» . Il y a un message et nous voulons l'aimer, mais en tant que développeurs, nous ne voulons pas bloquer l'intégralité de l'interface utilisateur et afficher la boîte de dialogue en plein écran avec chargement jusqu'à ce que la demande soit terminée. Oui, et l'utilisateur sera mécontent. Nous voulons tromper l'utilisateur: il appuie sur le bouton et, comme si cela s'était déjà produit - une belle animation a commencé. Mais en fait, il n'y avait pas de pareil - nous attendons que la tromperie devienne vraie. Pour prévenir la fraude, nous devons gérer de manière transparente l'aversion pour l'utilisateur.
Ce serait bien de gérer cela correctement afin que l'utilisateur obtienne le résultat souhaité. Mais il nous est difficile, en tant que développeurs, de traiter à chaque fois
des demandes différentes et mutuellement exclusives .
Il y a trop de questions. Comment comprendre que les requêtes sont liées? Comment stocker ces connexions? Comment gérer des scripts complexes et non copier-coller? Comment nommer de nouveaux composants? Les tâches sont complexes et ce que nous avons déjà mis en œuvre ne convient pas à la solution.
Groupes et stratégies pour les groupes
Créez une interface simple appelée
GroupStrategyHolder . C'est un peu plus compliqué - deux champs au lieu d'un.
interface GroupStrategyHolder { val groupStrategy: GroupStrategy val groupKey: String } sealed class GroupStrategy object Default : GroupStrategy() object KillGroup : GroupStrategy()
En plus de la stratégie pour une demande spécifique, nous introduisons une nouvelle entité - un groupe de demandes. Ce groupe aura également des stratégies. Nous ne considérerons que l'option la plus simple avec deux stratégies:
Default - la stratégie par défaut lorsque nous ne faisons rien avec les requêtes, et
KillGroup - tue toutes les requêtes existantes du groupe et en lance une nouvelle.
interface Act : StrategyHolder, GroupStrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe, override val groupStrategy: GroupStrategy = Default override val groupKey: String = "" ) : Act
Nous répétons les étapes dont j'ai parlé plus tôt: nous prenons l'interface, développons et ajoutons deux champs supplémentaires à CompletableAct et SingleAct.
Mettre à jour l'implémentation
Nous revenons à la méthode Execute. La troisième tâche est plus compliquée, mais la solution est assez simple: nous vérifions la stratégie de groupe pour une demande spécifique et, s'il s'agit de KillGroup, nous tuons tout le groupe et exécutons la logique habituelle.
MapDisposable -> GroupDisposable ... override fun execute(act: Act, e: (Throwable) -> Unit) { if (act.groupStrategy == KillGroup) groupDisposable.removeGroup(act.groupKey) return when { groupDisposable.contains(act.groupKey, act.id) -> when (act.strategy) { KillMe -> { stop(act.groupKey, act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) } }
Le problème est complexe, mais nous avons déjà une infrastructure assez adéquate - nous pouvons l'étendre et résoudre le problème. Si vous regardez notre résultat, que devons-nous faire maintenant?
Résultat
fun act(id: String)= Completable.timer(2, SECONDS).toAct( id = id, groupStrategy = KillGroup, groupKey = "Like-Dislike-PostId-1234" ) executor.apply { execute(act(“Like”)) execute(act(“Dislike”)) execute(act(“Like”)) } Like - Act Started Like - Act Canceled Dislike - Act Started Dislike - Act Canceled Like - Act Started Like - Act Finished
Si nous avons besoin de requêtes aussi complexes, nous ajoutons deux champs: groupStrategy et group ID. L'ID de groupe est un paramètre spécifique, car pour prendre en charge de nombreuses demandes similaires de type «J'aime / Je n'aime pas», vous devez créer un groupe pour chaque paire de demandes qui appartiennent au même objet. Dans ce cas, vous pouvez nommer le groupe Like-Dislike-PostId et y ajouter l'ID de publication. Chaque fois que nous aimons les messages voisins, nous serons sûrs que tout fonctionne correctement pour le message précédent et pour le suivant.
Dans notre exemple synthétique, nous essayons d'exécuter une séquence semblable à ne pas aimer. Lorsque nous effectuons la première action, puis la seconde - la précédente est annulée et la suivante comme annule la précédente aversion. Voilà ce que je voulais.
Dans le dernier exemple, nous avons utilisé des paramètres nommés pour créer des actes. Cela aide à refroidir la lisibilité du code, surtout quand il y a beaucoup de paramètres.
Pour une lecture plus facile, utilisez des paramètres nommés.
L'architecture
Voyons comment cette décision peut affecter notre architecture. Sur les projets, je vois souvent que le modèle de vue ou le présentateur assument de nombreuses responsabilités, telles que des hacks, pour gérer la situation avec ou non. Habituellement, toute cette logique dans le modèle d'affichage: beaucoup de code en double avec verrouillage de bouton, gestionnaires LifeCycle, abonnements.

Tout ce que notre exécuteur fait maintenant était une fois dans Presenter ou View Model. Si l'architecture est mature, les développeurs pourraient amener cette logique à une sorte d'interacteurs ou de cas d'utilisation, mais la logique a été dupliquée à plusieurs endroits.
Après avoir adopté Executor, le modèle de vue devient plus simple et toute la logique leur est cachée. Si vous avez apporté cela à Presenter et à l'interacteur, vous savez que l'interaction et le présentateur deviennent plus faciles. En général, j'étais satisfait.

Quoi d'autre à ajouter?
Un autre avantage de la solution actuelle est qu'elle est extensible. Que souhaiterions-nous ajouter en tant que développeurs qui travaillent sur une application mobile et luttent chaque jour avec des bogues et de nombreuses demandes simultanées?
Les possibilités
La
mise en œuvre du cycle de vie est restée dans les coulisses, mais en tant que développeurs mobiles, nous y pensons toujours et nous inquiétons pour que rien ne s'écoule. Je souhaite
enregistrer et restaurer les demandes de redémarrage des applications.
Chaînes d'appels. En raison de l'encapsulation des chaînes RX, il devient possible de les sérialiser, car par défaut RX ne sérialise pas.
Peu de gens savent combien de demandes simultanées sont en cours d'exécution à un moment donné dans leurs applications. Je ne dirais pas que c'est un gros problème pour les petites et moyennes applications. Mais pour une grande application qui fait beaucoup de travail en arrière-plan, il est agréable de comprendre les causes des plantages et des plaintes des utilisateurs. Sans infrastructure supplémentaire, les développeurs n'ont tout simplement pas d'informations pour comprendre la raison: peut-être que la raison est dans l'interface utilisateur, ou peut-être dans un grand nombre de demandes constantes en arrière-plan. Nous pouvons étendre notre solution et ajouter une sorte de
métriques .
Examinons les possibilités plus en détail.
Traitement du cycle de vie
class ActExecutorImpl( lifecycle: Lifecycle ) : ActExecutor { inir { lifecycle.doOnDestroy { cancelAll() } } ...
Ceci est un exemple d'implémentation du cycle de vie. Dans le cas le plus simple - avec
Destroy
fragments ou annulé avec
Activity
, nous
transmettons le gestionnaire de cycle de vie à notre exécuteur , et lorsque
l'événement onDestroy se produit, nous supprimons toutes les demandes . Il s'agit d'une solution simple qui élimine le besoin de copier-coller un code similaire dans View Models. LifeData fait à peu près la même chose.
Sauvegarde / restauration
Puisque nous avons des wrappers, nous pouvons créer
des classes distinctes pour les actes , à l'intérieur desquelles il y aura une logique pour créer des tâches asynchrones. De plus, nous pouvons
enregistrer ce nom dans la base de données et le
restaurer à partir de la base de données au démarrage de l'application en utilisant la méthode d'usine ou quelque chose de similaire.
Dans le même temps, nous aurons la possibilité de travailler hors ligne et nous redémarrerons les demandes qui se sont terminées avec des erreurs lorsque Internet apparaît. En l'absence d'Internet ou avec des erreurs de demande, nous les enregistrons dans la base de données, puis les restaurons et les exécutons à nouveau. Si vous pouvez le faire avec un RX régulier sans wrappers supplémentaires, veuillez écrire dans les commentaires, ce serait intéressant.
Chaînes d'appel
Nous pouvons également
lier nos lois . Une autre option d'extension consiste
à exécuter des chaînes de requête . Par exemple, vous avez une entité qui doit être créée sur le serveur, et une autre entité, qui dépend de la première, doit être créée exactement au moment où nous sommes sûrs que la première demande a réussi. Cela peut également être fait. Bien sûr, ce n'est pas si trivial, mais avoir une classe qui contrôle le lancement de toutes les tâches asynchrones est possible. L'utilisation de RX nu est plus difficile à faire.
Mesures
Il est intéressant de voir
combien de requêtes parallèles sont effectuées en moyenne en arrière-plan . Ayant des mesures, vous pouvez comprendre la cause des plaintes des utilisateurs concernant la léthargie. , , .
, , ,
, - - 10% . , .
Conclusion
— , . «» . , , , , .
, , . — - , , — . — . , . .
.
. Kotlin, . ,
.
AppsConf 2018, AppsConf 2019 . 38 : , Android, UX, , - , , Kotlin.
, youtube- 22–23 .