Enviando eventos do ViewModel para Atividade / Fragmento no MVVM

Hoje falaremos sobre como trocar eventos entre Atividades / Fragmentos e ViewModel no MVVM. Para obter dados do ViewModel, é recomendável que em Activity / Fragment assine os dados do LiveData encontrados no ViewModel. Mas o que fazer para enviar eventos únicos (e não apenas), como mostrar uma notificação ou, por exemplo, abrir outro fragmento?



Então olá pessoal!

Meu nome é Alexei, sou desenvolvedor Android no Home Credit Bank.

Neste artigo, compartilharei nossa maneira de enviar e receber eventos do ViewModels para Views (Activities / Fragments).

Em nosso aplicativo “Bens de Parcelamento do Crédito Imobiliário” , usamos fragmentos, portanto falaremos sobre eles, mas tudo também é relevante para a Atividade.

O que nós queremos?


Temos um fragmento, que inclui vários ViewModel, os dados são vinculados por DataBinding . Todos os eventos do usuário recebem o ViewModel. Se o evento for de navegação: você precisa abrir outro fragmento / atividade, mostrar AlertDialog , Snackbar , uma solicitação de sistema para Permissões, etc., e esse evento deve ser executado no fragmento.

E qual é, de fato, o problema?


Os ciclos de vida de Views e ViewModels não são relacionados. É impossível estabelecer ligação com os ouvintes, pois o ViewModel não deve saber nada sobre fragmentos e também não deve conter um link para fragmentos; caso contrário, como você sabe, a memória começará a se " alegrar ".

A abordagem padrão para a interação entre os Fragments e o ViewModels é assinar o LiveData , localizado no ViewModel. LiveData impossível transmitir eventos diretamente através do LiveData , devido ao fato de que essa abordagem não leva em consideração se o evento já foi concluído ou não.

Que soluções existem:


1. Use SingleLiveEvent
Prós: o evento é executado uma vez.
Contras: um evento - um SingleLiveEvent . Com um grande número de eventos, N objetos de evento aparecem no ViewModel, cada um dos quais precisará ser inscrito no fragmento.

2. Um bom exemplo .
Prós: um evento também é executado uma vez, você pode transferir dados do viewModel para o fragmento.
Contras: os dados no evento são obrigatórios, mas se você precisar executar um evento sem dados (val content: T) , precisará criar outra classe. Ele não resolve o problema de executar um tipo de evento uma vez (o próprio evento é executado uma vez, mas esse tipo de evento será executado quantas vezes for executado no ViewModel). Por exemplo, as solicitações N são assíncronas, mas não há rede. Cada solicitação retornará com um erro de rede e atrairá um fragmento de N eventos sobre um erro de rede. N alertas serão abertos no fragmento. O usuário não aprovará essa decisão :). Deveríamos mostrar a ele uma vez uma mensagem com esse erro. Em outras palavras, esse tipo de evento deve ser executado uma vez.

Solução


Adotamos a idéia do SingleLiveEvent para salvar as informações de manipulação de eventos como base.

Definir possíveis tipos de eventos


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

Criar uma classe base de eventos - NavigationEvent


isHandled indica se o evento foi recebido (acreditamos que foi executado se o Observer foi recebido no fragmento).

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

Criar uma classe de Emitter - Emitter


A classe do emissor de eventos herda de LiveData < NavigationEvent >. Essa classe será usada no ViewModel para despachar eventos.

 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 - precisamos entender se pelo menos um Observer está inscrito no Emitter . E no caso em que o assinante tenha aparecido e os eventos à sua espera tenham se acumulado, enviamos esses eventos. Um esclarecimento importante: o envio de eventos é necessário, não através do this.postValue(event) , mas através do setter this.value = event . Caso contrário, o assinante receberá apenas o último evento da lista.

O método para enviar um novo evento, newEvent(event, type) , aceita dois parâmetros - na verdade, o próprio evento e o tipo desse evento.

Para não lembrar de todos os tipos de eventos (nomes longos), criaremos métodos públicos separados que aceitarão apenas o próprio evento:

 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) } 

Formalmente, você já pode se inscrever no Emitter no ViewModel e receber eventos sem levar em consideração a manipulação deles (independentemente de o evento já ter sido processado ou não).

Criar uma classe de observador de eventos - 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) } } } } } } 

Esse Observer aceita uma função de ordem superior como entrada - o processamento do evento será gravado em um fragmento (exemplo abaixo).

O método clearExecutedEvents() para limpar eventos executados (aqueles que deveriam ter sido executados uma vez). Necessário ao atualizar a tela, por exemplo, em swipeToRefresh() .

Bem, na verdade, o principal método onChange() , que ocorre quando novos dados do emissor são recebidos, aos quais esse observador assina.

Caso o evento tenha um tipo de execução ilimitado de vezes, verificaremos se o evento foi executado e o processaremos. Executamos o evento e indicamos que ele é recebido e processado.

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

Se o evento for do tipo que deve ser executado uma vez, verifique se a classe desse evento está na tabela de hash. Caso contrário, execute o evento e adicione a classe deste evento à tabela de hash.

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

Mas como transferir dados dentro de eventos?


Para fazer isso, a interface MyFragmentNavigation é MyFragmentNavigation , que consiste em classes herdadas de NavigationEvent() . Várias classes são criadas abaixo com e sem parâmetros passados.

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

Como funciona na prática


Enviando eventos do 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() } } 

Recebendo eventos em um fragmento:

 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(){ ... } } 

Em essência, obtivemos um análogo do Rx- BehaviorSubjec ta e EventBus -a, baseado apenas no LiveData , no qual o Emitter pode coletar eventos antes da aparência de um artista-Observer e no qual o Observer pode monitorar os tipos de eventos e, se necessário, chamá-los apenas uma vez.

Bem-vindo nos comentários com sugestões.

Link para a fonte .
Parcelamento do Home Credit Bank .

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


All Articles