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,
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 } } } } 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>() { … fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) 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() 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() ..
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)
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 .