Suspension sur blocage

Cet article vise à montrer comment utiliser Kotlin Coroutines et supprimer Reaxtive eXtensions (Rx) .


Bénéfices


Pour commencer, considérons quatre avantages de Coroutines par rapport à Rx:


Suspension sur blocage


Pour exécuter du code non bloquant à l'aide de Rx, vous devez écrire quelque chose comme ceci:


Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text = "$it seconds have passed" } 

Ce qui crée effectivement un nouveau fil. Les threads sont des objets lourds en termes de mémoire et de performances.


Les deux sont essentiels dans le monde du développement mobile.


Vous pouvez obtenir le même comportement à l'aide de l'extrait de code suivant:


 launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } } 

Essentiellement, les Coroutines sont des threads légers mais nous ne créons aucun vrai thread.
Ici, nous utilisons la fonction delay () non bloquante, qui est une fonction de suspension spéciale qui ne bloque pas un thread mais suspend la Coroutine.


Manipulation naturelle de la contre-pression par rapport au manuel


La contre-pression se produit lorsque les observables produisent des objets plus rapidement que leurs observateurs ne les consomment.
Lorsque vous utilisez Rx, vous devez spécifier explicitement comment vous allez gérer la contre-pression.
Il existe 2 approches de base:


  • Utiliser des opérateurs de limitation, de tampons ou de fenêtres
  • Le modèle de traction réactive

Alors que les Coroutines peuvent suspendre, elles fournissent une réponse naturelle à la gestion de la contre-pression.
Ainsi, aucune action supplémentaire n'est requise.


Synchroniser le style de code sur async


La nature de base d'une application mobile est de réagir aux actions des utilisateurs. C'est pourquoi Reactive eXtensions serait un bon choix.


Cependant, vous devez écrire un code dans un style fonctionnel. Si vous écriviez de façon impérative, cela pourrait être un peu difficile.


Tandis que les Coroutines vous permettent d'écrire du code asynchrone comme s'il s'agissait de fonctions de synchronisation habituelles. Par exemple,


 suspend fun showTextFromRemote() { val text = remote.getText() textView.text = text } 

Même si je travaille avec le style fonctionnel depuis longtemps, il est toujours plus facile de lire et de déboguer un code impératif.


Native sur la bibliothèque tierce


Les coroutines sont une fonctionnalité native intégrée de Kotlin.


Vous n'avez pas besoin d'ajouter de dépendances supplémentaires. Actuellement, toutes les bibliothèques principales pourraient traiter des coroutines.


Par exemple,


Retrofit


 interface Api { @Get("users") suspend fun loadUsers() : List<User> } 

Pièce


 interface Dao { @Update suspend fun update(user: UserEntity) } 

Ainsi, vous pouvez créer une application qui est complètement suspendue - en commençant la couche d'interface utilisateur, en passant par le domaine et en terminant par la couche de données.


L'appli


Passons aux affaires. Nous allons créer une application maître-détail classique.
La première page contiendrait une liste infinie de livraisons.
Au clic sur l'élément, nous ouvrirons une page de détails.
De plus, nous prendrons en charge le mode hors ligne - toutes les données seront mises en cache.
De plus, j'utiliserai l'architecture MVVM où le rôle ViewModel est joué par Fragment au lieu de ViewModel d'AAC. Il y a plusieurs raisons:
Les fragments sont généralement très chauves - il suffit de lier viewModel à XML.


Des fonctionnalités telles que la définition de la couleur de la barre d'état ne pouvaient pas être effectuées dans AAC ViewModel - vous devez déclencher la méthode du fragment. L'utilisation de fragment comme ViewModel nous permettrait de stocker toutes les fonctionnalités associées (gérer un écran donné) dans une classe.


Commençons par créer BaseViewModel:


 abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO){ protected abstract val layoutId: Int protected abstract val bindings: B protected lateinit var viewBinding: V override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { viewBinding = DataBindingUtil.inflate(inflater, layoutId, container, false) return viewBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.lifecycleOwner = viewLifecycleOwner viewBinding.setVariable(BR.bindings, bindings) } override fun onDestroy() { cancel() super.onDestroy() } } 

Nous marquons notre ViewModel comme CoroutineScope afin de pouvoir démarrer des coroutines à l'intérieur des modèles de vue et toute coroutine lancée serait limitée au cycle de vie d'un fragment.


Nous devons explicitement spécifier la fin de la durée de vie de la portée appelant la méthode cancel() pour annuler toutes les demandes en cours afin d'éviter les fuites de mémoire.


Nous définissons retainInstance = true afin que dans les changements de configuration, le fragment ne soit pas recréé afin que nous puissions terminer toutes les demandes de longue durée.


De plus, nous devons définir lifecycleOwner sur binding pour activer la liaison de données bidirectionnelle .


Gestion des exceptions


Selon la documentation de Coroutines:


 Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). The former treat exceptions as unhandled, similar to Java's Thread.uncaughtExceptionHandler 

Puisque nous utilisons le générateur de lancement dans la plupart des cas, nous devons spécifier CoroutineExceptionHandler
CoroutineExceptionHandler est CoroutineContext.Element qui peut être utilisé pour créer un contexte de coroutine à l'aide de l'opérateur plus.
Je déclarerai le gestionnaire statique comme suit:


 val exceptionHandler = CoroutineExceptionHandler { _, throwable -> Timber.e(throwable) } 

Et changez BaseViewModel:


 abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler) 

À partir de là, toute exception survenue dans la coroutine lancée à l'intérieur de la portée de ViewModel serait livrée au gestionnaire donné.
Ensuite, je dois déclarer mon API et DAO:


 interface DeliveriesApi { @GET("deliveries") suspend fun getDeliveries(@Query("offset") offset: Int, @Query("limit") limit: Int): List<DeliveryResponse> } @Dao interface DeliveryDao { @Query("SELECT * FROM ${DeliveryEntity.TABLE_NAME}") fun getAll(): DataSource.Factory<Int, DeliveryEntity> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(delivery: DeliveryEntity) } 

Comme vous pouvez le voir, j'ai marqué les méthodes comme suspendues afin que nous puissions simplement déclarer les objets de réponse attendus. De plus, l'annulation de la coroutine parent annulera également l'appel réseau.
La même chose pour DAO.
La seule différence est que nous voulons offrir la possibilité d'observer la base de données.
Le moyen le plus simple consiste à utiliser la prise en charge intégrée des données en direct. Mais si nous marquions getAll () comme suspendu, cela provoquerait une erreur de compilation
erreur:


 Not sure how to convert a Cursor to this method's return type ... 

Ici, nous n'avons pas besoin de suspendre car:


  • Les requêtes DB sont effectuées en arrière-plan par défaut
  • LiveData résultant est conscient du cycle de vie afin que nous n'ayons pas besoin de l'annuler manuellement

Nous devons en quelque sorte combiner des sources de données distantes et locales.
Il convient de s'en souvenir - il ne devrait y avoir qu'un seul point de vérité.
Selon la conception hors ligne , ce serait du stockage local. Ainsi, nous observerions l'état de la base de données. Lorsqu'il n'y a rien à récupérer, nous demandons des données à distance et les insérons dans la base de données.
Nous présenterons la classe Listing


 data class Listing<T>( val pagedList: LiveData<PagedList<T>>, val dataState: LiveData<DataState>, val refreshState: LiveData<DataState>, val refresh: () -> Unit, val retry: () -> Unit ) 

Allons val par val:


  • pagedList - les données principales qui sont construites comme PagedList pour permettre le défilement infini et enveloppées avec LiveData pour permettre l'observation des données
  • dataState - l'un des trois états dans lesquels nos données peuvent être: succès, en cours d'exécution, erreur. Également enveloppé dans LiveData pour observer les changements
  • refreshState - lorsque nous déclenchons l'actualisation des données par balayage pour actualiser, nous avons besoin d'un outil permettant de faire la distinction entre les commentaires de demande de rafraîchissement et les commentaires de demande de page suivante. Pour le premier, nous voulons afficher une erreur à la fin de la liste mais pour une erreur de rafraîchissement, nous voulons afficher un message toast et masquer un chargeur.
  • refresh () - rappel à déclencher lors d'un balayage pour rafraîchir
  • retry () - rappel à déclencher en cas d'erreur de chargement de pagedList
    Ensuite, modèle de vue de liste:


     class DeliveryListViewModel : BaseViewModel<DeliveryListBindings, DeliveryListBinding>(), DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings { override val layoutId: Int = R.layout.delivery_list override val bindings: DeliveryListBindings = this private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } private val listing = deliveryGateway.getDeliveries() override val dataState = listing.dataState override val isRefreshing = Transformations.switchMap(listing.refreshState) { MutableLiveData(it == DataState.Loading) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupList() setupRefresh() } private fun setupList() { val adapter = DeliveriesAdapter(this, this) viewBinding.deliveries.adapter = adapter viewBinding.deliveries.setHasFixedSize(true) listing.pagedList.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) listing.dataState.observe(viewLifecycleOwner, Observer { adapter.updateDataState(it) }) } private fun setupRefresh() { listing.refreshState.observe(viewLifecycleOwner, Observer { if (it is DataState.Error) { Toast.makeText(context, it.message, LENGTH_SHORT).show() } }) } override fun refresh() { listing.refresh() } override fun onDeliveryClicked(delivery: Delivery) { view?.findNavController()?.navigate(DeliveryListViewModelDirections.toDetails(delivery)) } override fun onRetryClicked() { listing.retry() } } 

    Commençons par la déclaration de classe.



Tout d'abord DeliveryListBindings et DeliveryListBinding. Le premier est notre interface déclarée pour coller le modèle de vue avec la vue XML. La seconde est la classe autogénérée basée sur XML. Nous avons besoin du second pour définir notre interface de liaisons et notre cycle de vie en XML.


De plus, il est recommandé de référencer les vues à l'aide de cette liaison autogénérée plutôt que d'utiliser la synthèse de kotlin.


Cela peut être le cas lorsque le référentiel via la vue synthétique n'existe pas dans la vue actuelle. Avec la liaison de données, vous échouerez rapidement même à l'étape de la compilation.


Ensuite, trois interfaces: DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings.


  1. DeliveryListBindings - liaisons pour l'écran lui-même. Par exemple, il contient la méthode refresh () qui est appelée lors d'un balayage vertical.
  2. DeliveryListItemBindings - liaisons pour un élément de la liste. Par exemple, onClicked ()
  3. DeliveryListErrorBindings - liaisons pour la vue d'erreur qui est également l'élément de liste affiché sur l'état d'erreur. Par exemple, il contient la méthode retry ()

Ainsi, nous traitons tout dans le modèle à vue unique, car il s'agit d'un seul écran, mais également du principe de séparation d'interface


Tournons une attention particulière sur cette ligne:


 private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } 

DeliveryGateway doit effectuer des requêtes à partir du thread principal. Il doit donc déclarer les méthodes suspendues ou CoroutineScope pour lancer de nouvelles coroutines sur cette étendue. Nous choisirions la deuxième approche car nous avons besoin de nos LiveData depuis le début, puis nous attendrions simplement les mises à jour. C'est très similaire à l'abonnement à une instance liveData lorsque nous transmettons lifecycleOwner (qui fait souvent référence à «ceci»). Voici de la même manière que nous passons «ceci» comme CoroutineScope


L'interface CoroutineScope se compose d'un seul champ - CoroutineContext. En substance, une portée et un contexte sont les mêmes choses. La différence entre un contexte et une portée réside dans leur objectif.


Pour en savoir plus, je recommanderais un article de Roman Elizarov. Ainsi, fournir une portée à DeliveryGateway entraînera également l'utilisation du même contexte. Spécifiquement thread, job et gestionnaire d'exceptions.
Jetons maintenant un œil à DeliveryGateway lui-même:


 class DeliveryBoundGateway( private val db: DataBase, private val api: DeliveriesApi, private val deliveryDao: DeliveryDao, private val coroutineScope: CoroutineScope ) : DeliveryGateway { private val boundaryCallback = DeliveriesBoundaryCallback( api = api, coroutineScope = coroutineScope, handleResponse = { insertIntoDatabase(it) } ) @MainThread override fun getDeliveries(): Listing<Delivery> { val refreshTrigger = MutableLiveData<Unit>() val refreshState = Transformations.switchMap(refreshTrigger) { refresh() } val pagingConfig = Config( initialLoadSizeHint = PAGE_SIZE, pageSize = PAGE_SIZE, prefetchDistance = PAGE_SIZE ) val deliveries = deliveryDao.getAll() .toLiveData( config = pagingConfig, boundaryCallback = boundaryCallback ) return Listing( pagedList = deliveries, dataState = boundaryCallback.dataState, retry = { boundaryCallback.helper.retryAllFailed() }, refresh = { refreshTrigger.value = null }, refreshState = refreshState ) } /** * When refresh is called, we simply run a fresh network request and when it arrives, clear * the database table and insert all new items in a transaction. * <p> * Since the PagedList already uses a database bound data source, it will automatically be * updated after the database transaction is finished. */ @MainThread private fun refresh(): LiveData<DataState> { boundaryCallback.refresh() val dataState = MutableLiveData<DataState>() dataState.value = DataState.Loading coroutineScope.launch { try { val deliveries = api.getDeliveries(0, PAGE_SIZE) db.withTransaction { deliveryDao.clear() insertIntoDatabase(deliveries) } dataState.postValue(DataState.Loaded) } catch (throwable: Throwable) { Timber.w(throwable) dataState.postValue(DataState.Error(throwable.message)) } } return dataState } private suspend fun insertIntoDatabase(deliveries: List<DeliveryResponse>) { deliveries.forEach { delivery -> val entity = deliveryConverter.fromNetwork(delivery) deliveryDao.insert(entity) } } companion object { const val PAGE_SIZE = 20 } } 

Ici, nous construisons la structure LiveData depuis le début, puis en utilisant les coroutines, chargez les données et postez-les dans LiveData. De plus, nous utilisons l'implémentation de PagedList.BoundaryCallback () pour coller la base de données locale et l'API distante. Lorsque nous atteignons la fin de la liste paginée, borderCallback est déclenché et charge le prochain bloc de données.

Comme vous pouvez le voir, nous utilisons coroutineScope pour lancer de nouvelles coroutines.


Étant donné que cette portée est égale au cycle de vie du fragment - toutes les demandes en attente seraient annulées lors du rappel onDestroy() du fragment.


La page de détail de livraison est assez simple - nous passons simplement un objet de livraison en tant que colis à partir de l'écran principal à l'aide du plug-in de sauvegarde du composant de navigation. Sur l'écran des détails, liez simplement un objet à un XML.


 class DeliveryViewModel : BaseViewModel<DeliveryBindings, DeliveryBinding>(), DeliveryBindings { override val layoutId: Int = R.layout.delivery override val bindings: DeliveryBindings = this private val args: DeliveryViewModelArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.delivery = args.delivery viewBinding.image.clipToOutline = true } } 

Contactez moi


Voici le lien vers le code source de github.


Vous êtes invités à laisser des commentaires et des questions ouvertes.

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


All Articles