Bonjour, Habr! Je vous présente une traduction d'un
article de Paulo Sato sur l'utilisation de Kotlin Coroutines au lieu de RxJava dans leurs projets Android.
RxJava en tant que bazooka, la plupart des applications n'utilisent même pas la moitié de sa puissance de feu. L'article expliquera comment le remplacer par des coroutines Kotlin (coroutines).
Je travaille avec RxJava depuis plusieurs années. C'est certainement l'une des meilleures bibliothèques pour tout projet Android, qui est toujours sous le choc aujourd'hui, surtout si vous programmez en Java. Si vous utilisez Kotlin, nous pouvons dire que la ville a un nouveau shérif.
La plupart utilisent RxJava uniquement pour contrôler les threads et empêcher l'enfer de rappel (si vous ne savez pas ce que c'est, considérez-vous chanceux et
c'est pourquoi ). Le fait est que nous devons garder à l'esprit que la véritable puissance de RxJava est la programmation réactive et la contre-pression. Si vous l'utilisez pour contrôler les demandes asynchrones, vous utilisez le bazooka pour tuer l'araignée. Elle fera son travail, mais c'est exagéré.
Un inconvénient notable de RxJava est le nombre de méthodes. Il est énorme et a tendance à se répandre dans tout le code. Dans Kotlin, vous pouvez utiliser des coroutines pour implémenter la plupart des comportements que vous avez créés précédemment à l'aide de RxJava.
Mais ... que sont les coroutines?
Corutin est un moyen de gérer des tâches compétitives dans un thread. Le thread fonctionnera jusqu'à ce qu'il soit arrêté et le contexte changera pour chaque coroutine sans créer de nouveau thread.
Les coroutines de Kotlin sont encore expérimentales, mais elles étaient incluses dans Kotlin 1.3, j'ai donc écrit une nouvelle classe UseCase (pour une architecture propre) en les utilisant ci-dessous. Dans cet exemple, un appel coroutine est encapsulé dans un seul fichier. Ainsi, d'autres couches ne dépendront pas des coroutines en cours d'exécution, offrant une architecture plus déconnectée.
package com.psato.devcamp.interactor.usecase import android.util.Log import kotlinx.coroutines.experimental.* import kotlinx.coroutines.experimental.android.UI import kotlin.coroutines.experimental.CoroutineContext abstract class UseCase<T> { protected var parentJob: Job = Job() //var backgroundContext: CoroutineContext = IO var backgroundContext: CoroutineContext = CommonPool var foregroundContext: CoroutineContext = UI protected abstract suspend fun executeOnBackground(): T fun execute(onComplete: (T) -> Unit, onError: (Throwable) -> Unit) { parentJob.cancel() parentJob = Job() launch(foregroundContext, parent = parentJob) { try { val result = withContext(backgroundContext) { executeOnBackground() } onComplete.invoke(result) } catch (e: CancellationException) { Log.d("UseCase", "canceled by user") } catch (e: Exception) { onError(e) } } } protected suspend fun <X> background(context: CoroutineContext = backgroundContext, block: suspend () -> X): Deferred<X> { return async(context, parent = parentJob) { block.invoke() } } fun unsubscribe() { parentJob.cancel() } }
Tout d'abord, j'ai créé une tâche parent. C'est la clé pour annuler toutes les coroutines qui ont été créées dans la classe UseCase. Lorsque nous appelons exécution, il est important que les anciennes tâches soient annulées pour être sûr de ne pas avoir manqué une seule coroutine (cela se produira également si nous nous désabonnons de cette UseCase).
Aussi, j'invoque le démarrage (UI). Cela signifie que je veux créer une coroutine qui sera exécutée dans le thread d'interface utilisateur. Après cela, j'appelle la méthode d'arrière-plan qui crée async dans CommonPool (cette approche aura en fait de mauvaises performances). À son tour, async renverra Deffered, puis j'appellerai sa méthode d'attente. Il attend la fin de la coroutine d'arrière-plan, ce qui apportera un résultat ou une erreur.
Cela peut être utilisé pour implémenter la plupart de tout ce que nous avons fait avec RxJava. Voici quelques exemples.
La carte
J'ai téléchargé les résultats de searchShow et les ai modifiés pour retourner le nom du premier spectacle.
Code RxJava:
public class SearchShows extends UseCase { private ShowRepository showRepository; private ResourceRepository resourceRepository; private String query; @Inject public SearchShows(ShowRepository showRepository, ResourceRepository resourceRepository) { this.showRepository = showRepository; this.resourceRepository = resourceRepository; } public void setQuery(String query) { this.query = query; } @Override protected Single<String> buildUseCaseObservable() { return showRepository.searchShow(query).map(showInfos -> { if (showInfos != null && !showInfos.isEmpty() && showInfos.get(0).getShow() != null) { return showInfos.get(0).getShow().getTitle(); } else { return resourceRepository.getNotFoundShow(); } }); } }
Code Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) : UseCase<String>() { var query: String? = null override suspend fun executeOnBackground(): String { query?.let { val showsInfo = showRepository.searchShow(it) val showName: String? = showsInfo?.getOrNull(0)?.show?.title return showName ?: resourceRepository.notFoundShow } return "" } }
ZIP
Zip prendra deux émissions d'Observer et les assemblera dans une nouvelle émission. Notez qu'avec RxJava, vous devez spécifier de passer un appel en parallèle en utilisant subscribeOn dans chaque Single. Nous voulons obtenir les deux en même temps et les retourner ensemble.
Code RxJava:
public class ShowDetail extends UseCase { private ShowRepository showRepository; private String id; @Inject public SearchShows(ShowRepository showRepository) { this.showRepository = showRepository; } public void setId(String id) { this.id = id; } @Override protected Single<Show> buildUseCaseObservable() { Single<ShowDetail> singleDetail = showRepository.showDetail(id).subscribeOn(Schedulers.io()); Single<ShowBanner> singleBanner = showRepository.showBanner(id).subscribeOn(Schedulers.io()); return Single.zip(singleDetail, singleBanner, (detail, banner) -> new Show(detail,banner)); }
Code Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) : UseCase<Show>() { var id: String? = null override suspend fun executeOnBackground(): Show { id?.let { val showDetail = background{ showRepository.showDetail(it) } val showBanner = background{ showRepository.showBanner(it) } return Show(showDetail.await(), showBanner.await()) } return Show() } }
Flatmap
Dans ce cas, je recherche des émissions qui ont une chaîne de requête et pour chaque résultat (limité à 200 résultats), j'obtiens également la note de l'émission. Au final, je retourne une liste de spectacles avec les notes correspondantes.
Code RxJava:
public class SearchShows extends UseCase { private ShowRepository showRepository; private String query; @Inject public SearchShows(ShowRepository showRepository) { this.showRepository = showRepository; } public void setQuery(String query) { this.query = query; } @Override protected Single<List<ShowResponse>> buildUseCaseObservable() { return showRepository.searchShow(query).flatMapPublisher( (Function<List<ShowInfo>, Flowable<ShowInfo>>) Flowable::fromIterable) .flatMapSingle((Function<ShowInfo, SingleSource<ShowResponse>>) showInfo -> showRepository.showRating(showInfo.getShow().getIds().getTrakt()) .map(rating -> new ShowResponse(showInfo.getShow().getTitle(), rating .getRating())).subscribeOn(Schedulers.io()), false, 4).toList(); } }
Code Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository) : UseCase<List<ShowResponse>>() { var query: String? = null override suspend fun executeOnBackground(): List<ShowResponse> { query?.let { query -> return showRepository.searchShow(query).map { background { val rating: Rating = showRepository.showRating(it.show!!.ids!!.trakt!!) ShowResponse(it.show.title!!, rating.rating) } }.map { it.await() } } return arrayListOf() } }
Laisse-moi t'expliquer. En utilisant RxJava, mon référentiel renvoie une seule émission de List, j'ai donc besoin de plusieurs émissions, une pour chaque ShowInfo. Pour ce faire, j'ai appelé flatMapPublisher. Pour chaque problème, je dois mettre en surbrillance ShowResponse et, à la fin, les rassembler tous dans une liste.
Nous nous retrouvons avec cette construction: List foreach → (ShowInfo → ShowRating → ShowResponse) → List.
Avec les coroutines, j'ai fait une carte pour chaque élément List pour le convertir en List <Deffered>.
Comme vous pouvez le voir, la plupart de ce que nous avons fait avec RxJava est plus facile à implémenter avec des appels synchrones. Les coroutines peuvent même gérer flatMap, qui, je crois, est l'une des fonctions les plus complexes de RxJava.
Il est bien connu que les coroutines peuvent être légères (
voici un exemple), mais les résultats m'ont laissé perplexe. Dans cet exemple, RxJava a démarré en environ 3,1 secondes, tandis que les coroutines ont mis environ 5,8 secondes pour s'exécuter sur CommonPool.
Ces résultats ont soulevé la question devant moi qu'il pourrait y avoir quelque chose d'inapproprié en eux. Plus tard, j'ai trouvé ça. J'ai utilisé Retrofit Call, qui a bloqué le flux.
Il existe deux façons de corriger cette erreur, le choix dépend de la version d'Android Studio que vous utilisez. Dans Android Studio 3.1, nous devons nous assurer que nous ne bloquons pas le thread d'arrière-plan. Pour cela, j'ai utilisé cette bibliothèque:
implémentation 'ru.gildor.coroutines: kotlin-coroutines-retrofit: 0.12.0'
Ce code crée une extension de la fonction Retrofit Call pour suspendre le flux:
public suspend fun <T : Any> Call<T>.await(): T { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T?>) { if (response.isSuccessful) { val body = response.body() if (body == null) { continuation.resumeWithException( NullPointerException("Response body is null: $response") ) } else { continuation.resume(body) } } else { continuation.resumeWithException(HttpException(response)) } } override fun onFailure(call: Call<T>, t: Throwable) {
Dans Android Studio 3.2, vous pouvez mettre à jour la bibliothèque de corutine vers la version 0.25.0. Cette version a CoroutineContext IO (vous pouvez voir le commentaire correspondant dans ma classe UseCase).
L'exécution sur CommonPool sans appel bloquant a pris 2,3 secondes et 2,4 secondes avec les E / S et les appels bloquants.

J'espère que cet article vous inspirera à utiliser la corutine, une alternative plus légère et peut-être plus rapide à RxJava et qu'il sera un peu plus facile de comprendre que vous écrivez du code synchronisé qui s'exécute de manière asynchrone.