Envoi d'événements de ViewModel vers Activity / Fragment dans MVVM

Aujourd'hui, nous allons parler de la façon d'échanger des événements entre les activités / fragments et ViewModel dans MVVM. Pour obtenir des données du ViewModel, il est recommandé dans Activity / Fragment de s'abonner aux données LiveData situées dans le ViewModel. Mais que faire pour envoyer des événements uniques (et pas seulement), comme afficher une notification ou, par exemple, ouvrir un autre fragment?



Alors bonjour à tous!

Je m'appelle Alexei, je suis développeur Android chez Home Credit Bank.

Dans cet article, je vais partager notre façon d'envoyer et de recevoir des événements de ViewModels vers des vues (activités / fragments).

Dans notre application «Biens à tempérament de Home Credit Bank», nous utilisons des fragments, nous en parlerons donc, mais tout est également pertinent pour l'activité.

Que voulons-nous?


Nous avons un fragment, qui comprend plusieurs ViewModel, les données sont liées par DataBinding . Tous les événements utilisateur reçoivent ViewModel. Si l'événement est de navigation: vous devez ouvrir un autre fragment / activité, afficher AlertDialog , Snackbar , une demande système pour les autorisations, etc., alors un tel événement doit être exécuté dans le fragment.

Et quel est en fait le problème?


Les cycles de vie des vues et des modèles de vue ne sont pas liés. Il est impossible de se lier aux écouteurs, car ViewModel ne devrait rien savoir des fragments, et ne devrait pas non plus contenir de lien vers des fragments, sinon, comme vous le savez, la mémoire commencera à « se réjouir ».

L'approche standard pour l'interaction entre Fragments et ViewModels consiste à s'abonner à LiveData , situé dans ViewModel. LiveData impossible de transmettre des événements directement via LiveData , car cette approche ne tient pas compte du fait que l'événement est déjà terminé ou non.

Quelles solutions existent:


1. Utilisez SingleLiveEvent
Avantages: l' événement est exécuté une fois.
Inconvénients: un événement - un événement SingleLiveEvent . Avec un grand nombre d'événements, N objets d'événement apparaissent dans le ViewModel, chacun devant être abonné dans le fragment.

2. Un bon exemple .
Avantages: un événement est également exécuté une fois, vous pouvez transférer des données de viewModel vers un fragment.
Inconvénients: les données de l'événement sont obligatoires, mais si vous devez exécuter un événement sans données (val content: T) , vous devrez créer une autre classe. Il ne résout pas le problème de l'exécution d'un type d'événement une fois (l'événement lui-même est exécuté une fois, mais ce type d'événement sera exécuté autant de fois que nous l'exécutons à partir du ViewModel). Par exemple, les N-requêtes sont asynchrones, mais il n'y a pas de réseau. Chaque demande renverra une erreur réseau et récupérera un fragment de N événements concernant une erreur réseau, N alertes s'ouvriront dans le fragment. L'utilisateur n'approuvera pas une telle décision :). Nous devons lui montrer une fois un message avec cette erreur. En d'autres termes, ce type d'événement doit être exécuté une fois.

Solution


Nous prenons l'idée de SingleLiveEvent pour enregistrer les informations de gestion des événements comme base.

Définissez les types d'événements possibles


 enum class Type { EXECUTE_WITHOUT_LIMITS, //    –   ,             EXECUTE_ONCE, //      WAIT_OBSERVER_IF_NEEDED,//    ,     -       WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE //    ,     -           } 

Créer une classe de base d'événements - NavigationEvent


isHandled indique si l'événement a été reçu (nous pensons qu'il a été exécuté si Observer a été reçu dans le fragment).

 open class NavigationEvent(var isHandled: Boolean = false, var type: Events.Type) 

Créer une classe d' Emitter - Emitter


La classe d'émetteur d'événements hérite de LiveData < NavigationEvent >. Cette classe sera utilisée dans le ViewModel pour distribuer des événements.

 class Emitter : MutableLiveData<NavigationEvent>() { private val waitingEvents: ArrayList<NavigationEvent> = ArrayList() private var isActive = false override fun onInactive() { isActive = false } override fun onActive() { isActive = true val postingEvents = ArrayList<NavigationEvent>() waitingEvents .forEach { if (hasObservers()) { this.value = it postingEvents.add(it) } }.also { waitingEvents.removeAll(postingEvents) } } private fun newEvent(event: NavigationEvent, type: Type) { event.type = type this.value = when (type) { Type.EXECUTE_WITHOUT_LIMITS, Type.EXECUTE_ONCE -> if (hasObservers()) event else null Type.WAIT_OBSERVER_IF_NEEDED, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (hasObservers() && isActive) event else { waitingEvents.add(event) null } } } } /** Clear All Waiting Events */ fun clearWaitingEvents() = waitingEvents.clear() } 

isActive - nous devons comprendre si au moins un Observer est abonné à Emitter . Et dans le cas où l'abonné est apparu et que les événements qui l'attendent se sont accumulés, nous envoyons ces événements. Une clarification importante: l'envoi d'événements n'est pas nécessaire via this.postValue(event) , mais via le setter this.value = event . Sinon, l'abonné ne recevra que le dernier événement de la liste.

La méthode d'envoi d'un nouvel événement, newEvent(event, type) , accepte deux paramètres - en fait, l'événement lui-même et le type de cet événement.

Afin de ne pas mémoriser tous les types d'événements (noms longs), nous allons créer des méthodes publiques distinctes qui n'accepteront que l'événement lui-même:

 class Emitter : MutableLiveData<NavigationEvent>() { … /** Default: Emit Event for Execution */ fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) /** Emit Event for Execution Once */ fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) /** Wait Observer Available and Emit Event for Execution */ fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) /** Wait Observer Available and Emit Event for Execution Once */ fun waitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE) } 

Formellement, vous pouvez déjà vous abonner à Emitter dans le ViewModel et recevoir des événements sans égard à leur gestion (que l'événement ait déjà été traité ou non).

Créer une classe Observateur d'événements - EventObserver


 class EventObserver(private val handlerBlock: (NavigationEvent) -> Unit) : Observer<NavigationEvent> { private val executedEvents: HashSet<String> = hashSetOf() /** Clear All Executed Events */ fun clearExecutedEvents() = executedEvents.clear() override fun onChanged(it: NavigationEvent?) { when (it?.type) { Type.EXECUTE_WITHOUT_LIMITS, Type.WAIT_OBSERVER_IF_NEEDED -> { if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } } Type.EXECUTE_ONCE, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } } } } } 

Cet observateur accepte une fonction d'ordre supérieur comme entrée - le traitement des événements sera écrit dans un fragment (exemple ci-dessous).

La méthode clearExecutedEvents() pour nettoyer les événements exécutés (ceux qui auraient dû être exécutés une fois). Obligatoire lors de la mise à jour de l'écran, par exemple, dans swipeToRefresh() .

Eh bien, en fait, la principale méthode onChange() , qui se produit lorsque de nouvelles données d'émetteur sont reçues, auxquelles cet observateur souscrit.

Dans le cas où l'événement a un type d'exécution d'un nombre illimité de fois, nous vérifions si l'événement a été exécuté et le traitons. Nous exécutons l'événement et indiquons qu'il est reçu et traité.

 if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } 

Si l'événement est du type qui doit être exécuté une fois, vérifiez si la classe de cet événement se trouve dans la table de hachage. Sinon, exécutez l'événement et ajoutez la classe de cet événement à la table de hachage.

 if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } 

Mais comment transférer des données à l'intérieur d'événements?


Pour ce faire, l'interface MyFragmentNavigation est MyFragmentNavigation , qui consistera en classes héritées de NavigationEvent() . Différentes classes sont créées ci-dessous avec et sans paramètres passés.

 interface MyFragmentNavigation { class ShowCategoryList : NavigationEvent() class OpenProduct(val productId: String, val productName: String) : NavigationEvent() class PlayVideo(val url: String) : NavigationEvent() class ShowNetworkError : NavigationEvent() } 

Comment ça marche dans la pratique


Envoi d'événements à partir de ViewModel:

 class MyViewModel : ViewModel() { val emitter = Events.Enitter() fun doOnShowCategoryListButtonClicked() = emitter.emitAndExecute(MyNavigation.ShowCategoryList()) fun doOnPlayClicked() = emitter.waitAndExecuteOnce(MyNavigation.PlayVideo(url = "https://site.com/abc")) fun doOnProductClicked() = emitter.emitAndExecute(MyNavigation.OpenProduct( productId = "123", productTitle = " Samsung") ) fun doOnNetworkError() = emitter.emitAndExecuteOnce(MyNavigation.ShowNetworkError()) fun doOnSwipeRefresh(){ emitter.clearWaitingEvents() ..//loadData() } } 

Réception d'événements dans un fragment:

 class MyFragment : Fragment() { private val navigationEventsObserver = Events.EventObserver { event -> when (event) { is MyFragmentNavigation.ShowCategoryList -> ShowCategoryList() is MyFragmentNavigation.PlayVideo -> videoPlayerView.loadUrl(event.url) is MyFragmentNavigation.OpenProduct -> openProduct(id = event.productId, name = event.otherInfo) is MyFragmentNavigation.ShowNetworkError -> showNetworkErrorAlert() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Observer   ViewModels     myViewModel.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelSecond.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelThird.emitter.observe(viewLifecycleOwner, navigationEventsObserver) } private fun ShowCategoryList(){ ... } private fun openProduct(id: String, name: String){ ... } private fun showNetworkErrorAlert(){ ... } } 

En substance, nous avons obtenu un analogue de Rx- BehaviorSubjec ta et EventBus -a, uniquement basé sur LiveData , dans lequel Emitter peut collecter des événements avant l'apparition d'un artiste-Observer, et dans lequel Observer peut surveiller les types d'événements et, si nécessaire, les appeler une seule fois.

Bienvenue dans les commentaires avec des suggestions.

Lien vers la source .
Plan de versement de Home Credit Bank .

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


All Articles