Envío de eventos desde ViewModel a Activity / Fragment en MVVM

Hoy hablaremos sobre cómo intercambiar eventos entre Actividades / Fragmentos y ViewModel en MVVM. Para obtener datos de ViewModel, se recomienda que en Activity / Fragment se suscriba a los datos de LiveData que se encuentran en ViewModel. Pero, ¿qué hacer para enviar eventos únicos (y no solo), como mostrar una notificación o, por ejemplo, abrir otro fragmento?



¡Hola a todos!

Mi nombre es Alexei, soy desarrollador de Android en Home Credit Bank.

En este artículo, compartiré nuestra forma de enviar y recibir eventos desde ViewModels a Vistas (Actividades / Fragmentos).

En nuestra aplicación "Productos a plazos de Home Credit Bank" utilizamos fragmentos, por lo tanto, hablaremos de ellos, pero todo también es relevante para la Actividad.

Que queremos


Tenemos un Fragmento, que incluye varios ViewModel, los datos están vinculados por DataBinding . Todos los eventos de usuario reciben ViewModel. Si el evento es de navegación: debe abrir otro fragmento / actividad, mostrar AlertDialog , Snackbar , una solicitud de permisos del sistema, etc., entonces dicho evento debe ejecutarse en el fragmento.

¿Y cuál es, de hecho, el problema?


Las vistas y los ciclos de vida de ViewModels no están relacionados. Es imposible vincularse con los oyentes, ya que ViewModel no debe saber nada sobre los fragmentos, y tampoco debe contener un enlace a los fragmentos, de lo contrario, como saben, la memoria comenzará a " alegrarse ".

El enfoque estándar para la interacción entre Fragments y ViewModels es suscribirse a LiveData , ubicado en ViewModel. LiveData imposible transmitir eventos directamente a través de LiveData , debido a que este enfoque no tiene en cuenta si el evento ya se ha completado o no.

Qué soluciones existen:


1. Use SingleLiveEvent
Pros: el evento se ejecuta una vez.
Contras: un evento - un evento SingleLiveEvent . Con una gran cantidad de eventos, N objetos de evento aparecen en ViewModel, cada uno de los cuales tendrá que suscribirse en el fragmento.

2. Un buen ejemplo .
Pros: un evento también se ejecuta una vez, puede transferir datos desde viewModel para fragmentar.
Contras: se requieren datos en el evento, pero si necesita ejecutar un evento sin datos (val content: T) , deberá crear otra clase. No resuelve el problema de ejecutar un tipo de evento una vez (el evento en sí se ejecuta una vez, pero este tipo de evento se ejecutará tantas veces como lo ejecutemos desde ViewModel). Por ejemplo, las solicitudes N van de forma asincrónica, pero no hay red. Cada solicitud volverá con un error de red, y extraerá un fragmento de N eventos sobre un error de red, se abrirán N alertas en el fragmento. El usuario no aprobará tal decisión :). Deberíamos mostrarle una vez un mensaje con este error. En otras palabras, este tipo de evento debe ejecutarse una vez.

Solución


Tomamos la idea de SingleLiveEvent para guardar la información de manejo de eventos como base.

Definir posibles tipos de eventos.


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

Crear una clase base de evento - NavigationEvent


isHandled indica si el evento se recibió (creemos que se ejecutó si se recibió Observer en el fragmento).

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

Crear una clase de Emitter - Emitter


La clase de emisor de eventos hereda de LiveData < NavigationEvent >. Esta clase se usará en ViewModel para enviar 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 : debemos entender si al menos un Observer está suscrito al Emitter . Y en el caso de que el suscriptor haya aparecido y los eventos que lo esperan se hayan acumulado, enviamos estos eventos. Una aclaración importante: el envío de eventos es necesario no a través de this.postValue(event) , sino a través del setter this.value = event . De lo contrario, el suscriptor recibirá solo el último evento de la lista.

El método para enviar un nuevo evento, newEvent(event, type) , acepta dos parámetros: de hecho, el evento en sí y el tipo de este evento.

Para no recordar todos los tipos de eventos (nombres largos), crearemos métodos públicos separados que solo aceptarán el evento en sí:

 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, ya puede suscribirse a Emitter en ViewModel y recibir eventos sin importar su manejo (ya sea que el evento ya haya sido procesado o no).

Crear una clase 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) } } } } } } 

Este observador acepta una función de orden superior como entrada: el procesamiento de eventos se escribirá en un fragmento (ejemplo a continuación).

El método clearExecutedEvents() para limpiar eventos ejecutados (aquellos que deberían haberse ejecutado una vez). Se requiere al actualizar la pantalla, por ejemplo, en swipeToRefresh() .

Bueno, en realidad, el método onChange() principal, que ocurre cuando se reciben nuevos datos del emisor, al que se suscribe este observador.

En caso de que el evento tenga un tipo de ejecución de un número ilimitado de veces, verificamos si el evento se ha ejecutado y lo procesamos. Ejecutamos el evento e indicamos que es recibido y procesado.

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

Si el evento es del tipo que debería ejecutarse una vez, compruebe si la clase de este evento está en la tabla hash. De lo contrario, ejecute el evento y agregue la clase de este evento a la tabla hash.

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

Pero, ¿cómo transferir datos dentro de eventos?


Para hacer esto, se MyFragmentNavigation interfaz MyFragmentNavigation , que consistirá en clases heredadas de NavigationEvent() . A continuación se crean varias clases con y sin parámetros pasados.

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

Cómo funciona en la práctica


Envío de eventos desde 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() } } 

Recepción de eventos en un 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(){ ... } } 

En esencia, tenemos un análogo de Rx- BehaviorSubjec ta y EventBus -a, solo basado en LiveData , en el que Emitter puede recopilar eventos antes de que aparezca el ejecutor Observer, y en el que Observer puede monitorear los tipos de eventos y, si es necesario, llamarlos solo una vez.

Bienvenido en los comentarios con sugerencias.

Enlace a la fuente .
Plan de pago del Home Credit Bank .

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


All Articles