Sala de reuniones L̶i̶t̶t̶l̶e̶ Helper v 2

Este artículo describe en detalle las etapas de desarrollo de la aplicación móvil Meeting Room Helper: desde el inicio de la idea hasta el lanzamiento. La aplicación está escrita en Kotlin y construida sobre una arquitectura MVVM simplificada, sin usar enlace de datos. La parte de la interfaz de usuario se actualiza utilizando objetos LiveData. Las razones para rechazar el enlace de datos se detallan y explican. La arquitectura utiliza una serie de soluciones interesantes que permiten dividir lógicamente el programa en archivos pequeños, lo que en última instancia simplifica el soporte de código.



Descripción del proyecto


Hace 3 años en nuestra empresa surgió la idea de desarrollar un pequeño proyecto para la reserva instantánea de salas de reuniones. La mayoría de los gerentes de recursos humanos y Arcadia prefieren usar el calendario de Outlook para tales fines, pero ¿qué pasa con el resto?

Daré 2 ejemplos de la vida del desarrollador.

  1. Cualquier equipo periódicamente tiene un deseo espontáneo de realizar un rally rápido durante 5-10 minutos. Este deseo puede superar a los desarrolladores en cualquier rincón de la oficina, y para no distraer a los colegas que los rodean, ellos (los desarrolladores y no solo) comienzan a buscar una conversación gratuita. Al migrar de una sala a otra (en nuestra oficina, las salas de reuniones están dispuestas en una fila), los colegas "comprueban cuidadosamente" cuál de las salas está actualmente libre. Como resultado, distraen a sus colegas dentro. Esos tipos siempre lo han sido y siempre lo serán, incluso si la ejecución debe ser filmada en el estatuto corporativo por la interrupción del mitin. Quien entendió, él entenderá.
  2. Y aquí hay otro caso. Acabas de salir del comedor y te diriges a ti mismo, pero aquí tu colega (o gerente) de otro departamento te intercepta. Quiere decirte algo urgente, y para estos fines necesitas una sala de reuniones. De acuerdo con las regulaciones, primero debe reservar una habitación (desde su teléfono o computadora) y solo luego ocuparla. Es bueno si tiene un teléfono móvil con Outlook móvil. Y si no? ¿Volver a la computadora y luego nuevamente para volver a la sala de reuniones? ¿Para obligar a cada empleado a poner Outlook Express en el teléfono y asegurarse de que todos lleven los teléfonos con ellos? Estos no son nuestros métodos.

Es por eso que hace 2.5 años cada una de las salas de reuniones estaba equipada con su propia tableta:



Para este proyecto, mi colega desarrolló la primera versión de la aplicación: Meeting Room Little Helper ( aquí puede leer sobre esto ). MRLH permitió reservar, cancelar y renovar una reserva, mostró el estado de las conversaciones restantes. Reconocer la identidad de un empleado (utilizando el servicio en la nube de Microsoft Face API y nuestros analizadores internos) se ha convertido en un "truco" innovador. La aplicación resultó ser sólida y sirvió fielmente a la compañía durante 2.5 años.

Pero el tiempo pasó ... aparecieron nuevas ideas. Quería algo nuevo y decidimos volver a escribir la aplicación.

Términos de referencia


Como suele suceder, pero desafortunadamente no siempre, el desarrollo comenzó con la preparación de especificaciones técnicas. En primer lugar, llamamos a los tipos que usan tabletas con más frecuencia para hacer reservas. Dio la casualidad de que, sobre todo, eran adictos a los recursos humanos y a los gerentes que anteriormente habían usado Outlook exclusivamente. De ellos recibimos los siguientes comentarios (de los requisitos, queda claro de inmediato qué solicitó RR.HH. y qué solicitaron los gerentes):

  • debe agregar la posibilidad de reservar cualquier sala de reuniones desde cualquier tableta (anteriormente, cada tableta le permitía reservar solo su sala);
  • sería genial mirar el calendario de manifestaciones para una reunión de todo el día (idealmente, para cualquier día);
  • todo el ciclo de desarrollo debe llevarse a cabo en poco tiempo (durante 6-7 semanas).

Todo está claro con los deseos del cliente, pero ¿qué pasa con los requisitos técnicos y el futuro? Agregue algunos requisitos para el proyecto del gremio de desarrolladores:

  • El sistema debería funcionar tanto con tabletas existentes como con tabletas nuevas;
  • escalabilidad del sistema: desde 50 conversaciones y más (esto debería ser suficiente con un margen para la mayoría de los clientes si el sistema comienza a replicarse);
  • mantener la funcionalidad anterior (la primera versión de la aplicación utilizó la API de Java para comunicarse con los servicios de Outlook, y planeamos reemplazarla con una API especializada de Microsoft Graph, por lo que era importante no perder la funcionalidad);
  • minimización del consumo de energía (las tabletas funcionan con una batería externa, porque el centro de negocios no permitió perforar sus paredes para tender nuestros cables);
  • nuevo diseño UX / UI, que refleja ergonómicamente todas las innovaciones.

Total 8 puntos. Los requisitos son bastante justos. Además, estipulamos las reglas generales de desarrollo:

  • use solo tecnologías avanzadas (esto permitirá que el equipo se desarrolle como especialistas y no se estanque en un solo lugar, al tiempo que simplificará el soporte del proyecto en el futuro previsible);
  • siga las mejores prácticas, pero no las dé por sentado ciegamente, ya que la regla principal de cualquier profesional (y un desarrollador que se esfuerza por esto) es evaluar todo críticamente;
  • Escribir código limpio y ordenado (tal vez esto es lo más difícil cuando intentas combinar innovación y tiempo de desarrollo limitado).

Un comienzo ha sido hecho. ¡Como siempre, es entusiasta! Veamos que pasa después.

Diseño


Diseño de aplicación desarrollado por el diseñador UX:




Esta es la pantalla principal. Se mostrará la mayor parte del tiempo. Toda la información necesaria se encuentra ergonómicamente aquí:

  • el nombre de la habitación y su número;
  • estado actual;
  • tiempo hasta la próxima reunión (o hasta su finalización);
  • los estados de las habitaciones restantes en la parte inferior de la pantalla.

Tenga en cuenta: el dial solo muestra 12 horas, como el sistema está configurado para las necesidades de la empresa (las tabletas Arcadia funcionan de 8 a.m. a 8 p.m., se encienden y apagan automáticamente)




Para reservar una habitación, simplemente llame a la ventana de reserva e indique la duración del rally. Los pasos para reservar las habitaciones restantes son similares, solo comienzan haciendo clic en el icono de la habitación.



Si desea programar una reunión para un momento específico, vaya a la siguiente pestaña, en la lista de reuniones que se celebrarán hoy en la sala de reuniones, y haga clic en tiempo libre. Además, todo es como en el primer caso.

El árbol de transición completo debería verse así:




Tratemos de implementarlo de manera competente.

Pila de tecnología


Las técnicas de desarrollo se desarrollan bastante rápido y cambian. Durante otros 2 años, Java fue el lenguaje oficial de desarrollo de Android. Todos escribieron en Java y usaron enlace de datos. Ahora, me parece, nos estamos moviendo hacia la programación reactiva y Kotlin. Java es un gran lenguaje, pero tiene algunas imperfecciones en comparación con lo que Kotlin y AndroidX tienen para ofrecer. Kotlin y AndroidX pueden reducir el uso de enlace de datos al mínimo, si no lo excluyen por completo. A continuación intentaré explicar mi punto de vista.

Kotlin


Creo que muchos desarrolladores de Android ya se han cambiado a Kotlin, y por lo tanto estarán de acuerdo conmigo en que escribir un nuevo proyecto de Android en 2019 en cualquier idioma que no sea Kotlin es como luchar contra el mar. Por supuesto que puedes discutir, pero ¿qué pasa con Flutter y Dart? ¿Qué pasa con C ++, C # e incluso Cordova? A lo que responderé: la elección es siempre suya.

En 480 aC el rey persa Jerjes ordenó a sus soldados cruzar el mar como castigo por destruir parte de su ejército durante una tormenta, y cinco siglos después, el emperador romano Calígula declaró la guerra a Poseidón. Una cuestión de gustos. Para 9 de cada 10, Kotlin es bueno, pero para 10 puede ser malo. Todo depende de ti, de tus deseos y aspiraciones.

Kotlin es mi elección. El lenguaje es simple y hermoso. Escribir en él es fácil y agradable, y lo más importante, no es necesario escribir demasiado: clase de datos, objeto, setter y getter opcional, expresiones lambda simples y funciones de extensión. Esto es solo una pequeña parte de lo que este lenguaje tiene para ofrecer. Si aún no te has cambiado a Kotlin, ¡no dudes en irte! En la sección con práctica, demostraré algunas de las ventajas del lenguaje (no es una oferta publicitaria).

Modelo-Vista-VistaModelo


MVVM es actualmente la arquitectura de aplicación recomendada de Google. Durante el desarrollo, nos adheriremos a este patrón en particular, sin embargo, no lo cumpliremos por completo, ya que MVVM recomienda usar el enlace de datos, pero lo rechazamos.

Pros de MVVM

  • Diferenciación de lógica de negocios y UI. En la implementación correcta de MVVM, no debería haber un solo androide de importación en ViewModel, a excepción de los objetos LiveData de los paquetes AndroidX o Jetpack. El uso adecuado deja automáticamente todo el trabajo de la interfaz de usuario dentro de fragmentos y actividades. ¿No es genial?
  • El nivel de encapsulación se bombea. Será más fácil trabajar en equipo: ahora pueden trabajar todos juntos en una pantalla y no interferir entre sí. Mientras un desarrollador trabaja con la pantalla, otro puede construir un ViewModel, y un tercero puede escribir consultas en el Repositorio.
  • MVVM tiene un efecto positivo en la escritura de pruebas unitarias. Este artículo se sigue del anterior. Si todas las clases y métodos están encapsulados para que no funcionen con la interfaz de usuario, se pueden probar fácilmente.
  • Una solución natural con rotación de pantalla. No importa cuán extraño pueda sonar, pero esta característica se adquiere automáticamente, con la transición a MVVM (porque los datos se almacenan en ViewModel). Si verifica las aplicaciones bastante populares (VK, Telegram, Sberbank-Online y Aviasales), resulta que exactamente la mitad de ellas no pueden rotar la pantalla. Lo que me causa cierta sorpresa y malentendido como usuario de estas aplicaciones.

¿Por qué es peligroso MVVM?

  • Fuga de memoria Este peligroso error ocurre si infringe las leyes de uso de LiveData y observador. Examinaremos este error en detalle en la sección de práctica.
  • Amplia vista del modelo. Si intenta ajustar toda la lógica empresarial en ViewModel, obtendrá un código ilegible. La forma de salir de esta situación puede ser dividir el ViewModel en una jerarquía o usar Presentadores. Eso es exactamente lo que hice.

Reglas para trabajar con MVVM

Comencemos con la mayoría de los errores y pasemos a los menos errores:

  • el cuerpo de la solicitud no debe estar en ViewModel (solo en el Repositorio);
  • Los objetos LiveData se definen en ViewModel, no se arrojan dentro del Repositorio, porque las solicitudes en el Repositorio se procesan usando Rx-Java (o corutinas);
  • todas las funciones de procesamiento deben trasladarse a clases y archivos de terceros ("Presentadores"), para no saturar ViewModel y no distraer de la esencia.

Livedata


LiveData es una clase de titular de datos observable. A diferencia de un observable regular, LiveData es consciente del ciclo de vida, lo que significa que respeta el ciclo de vida de otros componentes de la aplicación, como actividades, fragmentos o servicios. Esta conciencia garantiza que LiveData solo actualiza los observadores de componentes de la aplicación que se encuentran en un estado de ciclo de vida activo.
Fuente: developer.android.com/topic/libraries/architecture/livedata

Se puede sacar una conclusión simple de la definición: LiveData es una herramienta de programación reactiva confiable. Lo usaremos para actualizar la parte de la interfaz de usuario sin enlace de datos. Por qué

La estructura de los archivos XML no permite una distribución concisa de los datos obtenidos de <data> ... </data>. Si todo está claro con archivos pequeños, ¿qué pasa con los archivos grandes? ¿Qué hacer con pantallas complejas, múltiples incluidas y pasando múltiples campos? ¿Usar modelos en todas partes? Obtener enlaces de campo rígidos? Y si el campo debe formatearse, ¿llama a los métodos de los paquetes Java? Esto hace que el código sea irremediable y completamente espagueti. Para nada lo que MVVM prometió.

Rechazar el enlace de datos hará que los cambios en la parte de la IU sean transparentes. Todas las actualizaciones ocurrirán directamente dentro del observador. Porque Como el código de Kolin es conciso y claro, no tendremos problemas con el observador hinchado. Escribir y mantener el código será más fácil. Los archivos XML se usarán solo para el diseño, sin ninguna propiedad dentro.

El enlace de datos es una herramienta poderosa. Es excelente para resolver algunos problemas, y armoniza bien con Java, pero con Kotlin ... Con Kotlin, en la mayoría de los casos, el enlace de datos es simplemente rudimentario. El enlace de datos solo complica el código y no ofrece ventajas competitivas.

En Java, tenía una opción: usar el enlace de datos o escribir mucho código feo. En Kotlin, puede acceder a los elementos de vista directamente, omitiendo findViewById (), así como su propiedad. Por ejemplo:

// Instead of TextView textView = findViewById<TextView>(R.id.textView) textView.text = "Hello, world!" textView.visibility = View.VISIBLE 

Surge una pregunta lógica: ¿por qué molestarse con los modelos de jardinería en archivos XML, invocar métodos Java en archivos XML, sobrecargar la lógica de la parte XML si se puede evitar todo esto?

Corutinas en lugar de Thread () y Rx-Java


Las corutinas son increíblemente livianas y fáciles de usar. Son ideales para la mayoría de las tareas asincrónicas simples: procesar resultados de consultas, actualizar la interfaz de usuario, etc.

Las rutinas pueden reemplazar efectivamente Thread () y Rx-Java en casos donde no se requiere un alto rendimiento, porque pagan ligereza con rapidez. Rx-Java, sin duda, es más funcional, sin embargo, para tareas simples no se requieren todos sus activos.

Microsoft y el resto


Para trabajar con los servicios de Outlook, se utilizará la API de Microsoft Graph. Con los permisos apropiados, a través de él puede obtener toda la información necesaria sobre empleados, salas y eventos (reuniones). Para el reconocimiento facial, se utilizará el servicio en la nube API de Microsoft Face.

Mirando un poco más adelante, diré que para resolver el problema de escalabilidad, se usó el almacenamiento en la nube Firebase. Esto se discutirá a continuación.

Arquitectura


Problemas de escalabilidad


Es bastante difícil hacer que el sistema sea escalable total o parcialmente. Esto es especialmente difícil de hacer si la primera versión de la aplicación no era escalable, y la segunda debería ser. La aplicación v1 envió solicitudes a todas las habitaciones a la vez. Cada una de las tabletas enviaba regularmente solicitudes al servidor para actualizar todos los datos. Al mismo tiempo, los dispositivos no se sincronizaron entre sí, porque el proyecto simplemente no tiene su propio servidor.

Por supuesto, si seguimos el mismo camino y enviamos N solicitudes de cada una de las N tabletas, en algún momento volcaremos la API de Microsoft Graph o congelaremos nuestro sistema.

Sería lógico utilizar una solución cliente-servidor en la que el servidor sondee el gráfico, acumule datos y, previa solicitud, brinde información a las tabletas, pero aquí nos encontramos con la realidad. El equipo del proyecto consta de 2 personas (desarrollador y diseñador de Android). Deben cumplir el plazo de 7 semanas y no se proporciona el backend, porque escalar es un requisito del desarrollador. ¿Pero esto no significa que la idea deba ser abandonada?

Probablemente la única solución correcta en esta situación será el uso del almacenamiento en la nube. Firebase reemplazará el servidor y actuará como un búfer. Luego resulta lo siguiente: cada tableta sondea solo su dirección de la API de Microsoft Graph y, si es necesario, sincroniza los datos en el almacenamiento en la nube, desde donde pueden ser leídos por otros dispositivos.

La ventaja de esta implementación será una respuesta rápida, porque Firebase funciona en modo de tiempo real. Reduciremos la cantidad de solicitudes enviadas al servidor N veces, lo que significa que el dispositivo funcionará con batería un poco más. Desde un punto de vista financiero, el proyecto no subió de precio, porque Para este proyecto, la versión gratuita de Firebase es suficiente con múltiples reservas: 1 GB de almacenamiento, 10 mil autorizaciones por mes y 100 conexiones a la vez. Las desventajas podrían incluir la dependencia de un marco de terceros, pero Firebase inspira confianza en nosotros, porque Es un producto estable mantenido y desarrollado por Google.

La idea general del nuevo sistema era la siguiente: N tabletas y una plataforma en la nube para la sincronización de datos en tiempo real. Comencemos a diseñar la aplicación en sí.

LiveData en el repositorio


Parece que recientemente establecí las reglas de buena forma e inmediatamente violé una de ellas. A diferencia del uso recomendado de LiveData dentro de ViewModel, en este proyecto los objetos LiveData se inicializan en el repositorio, y todos los repositorios se declaran como singleton. Por qué

Una solución similar está asociada con el modo de aplicación. Las tabletas están abiertas de 8 a.m. a 8 p.m. Todo este tiempo, solo se ha lanzado el Meeting Room Helper. Como resultado, muchos objetos pueden y deben ser de larga duración (es por eso que todos los repositorios están diseñados como singleton).

En el curso del trabajo, el contenido de la interfaz de usuario se cambia regularmente, lo que a su vez implica la creación y recreación de objetos ViewModel. Resulta que si usa LiveData dentro de ViewModel, entonces para cada fragmento creado se creará su propio ViewModel con un conjunto de objetos LiveData especificados. Si 2 fragmentos similares se muestran simultáneamente en la pantalla, con ViewModel diferente y un Base-ViewModel común, entonces durante la inicialización habrá una duplicación de los objetos LiveData del Base-ViewModel. En el futuro, estos duplicados ocuparán espacio en la memoria hasta que sean destruidos por el "recolector de basura". Porque Si ya tenemos un repositorio en forma de singleton y queremos minimizar el costo de volver a crear pantallas, sería conveniente transferir objetos LiveData a un repositorio de singleton, facilitando así los objetos ViewModel y acelerando la aplicación.

Por supuesto, esto no significa que deba transferir todos los LiveData del ViewModel al repositorio, pero debe abordar este problema con más cuidado y hacer su elección conscientemente. La desventaja de este enfoque es el aumento en el número de objetos de larga vida, porque Todos los repositorios se definen como singleton y cada uno de ellos almacena objetos LiveData. Pero en un caso específico, Meeting Room Helper no es un punto negativo, porque la aplicación se ejecuta sin parar todo el día, sin cambiar el contexto a otras aplicaciones.

Arquitectura resultante




  • Todas las solicitudes se ejecutan en repositorios. Todos los repositorios (en Meeting Room Helper hay 11 de ellos) están diseñados como singleton. Se dividen por tipo de objetos devueltos y se ocultan detrás de las fachadas.
  • La lógica empresarial reside en ViewModel. Gracias al uso de "Presentadores", el tamaño total de todos los ViewModel (hay 6 en el proyecto) resultó ser inferior a 120 líneas.
  • La actividad y el fragmento solo están involucrados en el cambio de la parte de la interfaz de usuario, utilizando el observador y LiveData devuelto por ViewModel.
  • Las funciones para procesar y generar datos se almacenan en "presentador". Utiliza activamente las funciones de permiso de Kotlin para el procesamiento de datos.

La lógica de fondo se ha movido a Intent-Service:

  • Servicio de actualización de eventos. Servicio responsable de sincronizar los datos de la sala actual en Firebase y Graph API.
  • Servicio de reconocimiento de usuario. Se ejecuta solo en la tableta maestra. Responsable de agregar nuevo personal al sistema. Comprueba una lista de personas ya capacitadas con una lista de Active Directory. Si aparecen nuevas personas, el servicio las agrega a la API de Face y reentrena la red neuronal. Al finalizar la operación, se apaga. Comienza cuando comienza la aplicación.
  • El servicio de notificación en línea notifica a otras tabletas que esta tableta está funcionando, es decir La batería externa no está agotada. Funciona a través de Firebase.

El resultado fue una arquitectura bastante flexible y correcta desde el punto de vista de la distribución de responsabilidades, que cumple con todos los requisitos del desarrollo moderno. Si en el futuro abandonamos la API de Microsoft Graph, Firebase o cualquier otro módulo, pueden reemplazarse fácilmente por otros nuevos sin interferir con el resto de la aplicación. La presencia de un extenso sistema de "presentadores" hizo posible llevar todas las funciones de procesamiento de datos más allá del núcleo. Como resultado, la arquitectura se ha vuelto cristalina, lo cual es una gran ventaja. El problema de un ViewModel descuidado ha desaparecido por completo.

A continuación daré un ejemplo del paquete de uso común en una aplicación desarrollada.

Practica Ver actualizaciones


Dependiendo del estado de la sala de reuniones, el dial muestra una de las siguientes condiciones:




Además, los arcos temporales de las manifestaciones se ubican a lo largo del contorno de la esfera, y el centro realiza una cuenta regresiva hasta el final de la reunión o hasta el comienzo de la próxima concentración. Todo esto lo hace la biblioteca de lienzo que desarrollamos. Si la cuadrícula de reuniones ha cambiado, debemos actualizar los datos en la biblioteca.

Dado que LiveData se anuncia en los repositorios, es más lógico comenzar con ellos.

Repositorios


FirebaseRoomRepository : una clase responsable de enviar y procesar solicitudes en Firebase relacionadas con el modelo de sala.

 // 1. object FirebaseRoomRepository { private val database = FirebaseFactory.database val rooms: MutableList<Room> = ArrayList() // 2. var currentRoom: MutableLiveData<Room?> = MutableLiveData() val onlineStatus: MediatorLiveData<HashMap<String, Boolean>> = MediatorLiveData() var otherRooms: MutableLiveData<List<Room>> = MutableLiveData() var ownRoom: MutableLiveData<Room> = MutableLiveData() // 3. private val roomsListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { updateRooms(dataSnapshot) } override fun onCancelled(error: DatabaseError) {} } init { // 4. database.getReference(ROOMS_CURRENT_STATES) .addValueEventListener(roomsListener) } // 5. private fun updateRooms(dataSnapshot: DataSnapshot) { rooms.updateRooms(dataSnapshot) otherRooms.updateOtherRooms(rooms) ownRoom.updateOwnRoom(rooms) currentRoom.updateCurrentRoom(rooms, ownRoom) } } 

Para demostrarlo, el código de inicialización de firebase del oyente se simplificó ligeramente (se eliminó la función de reconexión). Echemos un vistazo a lo que sucede aquí:

  1. el repositorio está diseñado como un singleton (en Kotlin, es suficiente para reemplazar la palabra clave de clase con objeto);
  2. inicialización de objetos LiveData;
  3. ValueEventListener se declara como una variable para evitar volver a crear una clase anónima en caso de reconexión (recuerde, simplifiqué la inicialización eliminando la reconexión en caso de desconexión);
  4. inicialización de ValueEventListener (si los datos en Firebase cambian, el escucha ejecutará y actualizará de inmediato los datos en los objetos LiveData);
  5. Actualizaciones a objetos LiveData.

Las funciones mismas se mueven a un archivo separado FirebaseRoomRepositoryPresenter y se decoran como funciones de extensión.

 fun MutableLiveData<List<Room>>.updateOtherRooms(rooms: MutableList<Room>) { this.postValue(rooms.filter { !it.isOwnRoom() }) } 

Ejemplo de función de extensión de FirebaseRoomRepositoryPresenter

También para una comprensión general de la imagen, daré una lista del objeto Room.

 // 1. data class Room(var number: String = "", var nickName: String = "", var email: String? = null, var imgSmall: String? = null, var imgOffline: String? = null, var imgFree: String? = null, var imgWait: String? = null, var imgBusy: String? = null, var events: List<Event.Short> = emptyList()) // 2. 

  1. Clase de datos. Este modificador genera y anula automáticamente los métodos toString (), HashCode () e equal (). Ya no hay necesidad de redefinirlos usted mismo.
  2. La lista de eventos del objeto Sala. Es esta lista la que se requiere para actualizar los datos en la biblioteca de marcado.

Todas las clases de repositorios están ocultas detrás de la clase de fachada.

 object Repository { // 1. private val firebaseRoomRepository = FirebaseRoomRepository // ......... /** * Rooms queries */ fun getOtherRooms() = firebaseRoomRepository.otherRooms fun getOwnRoom() = firebaseRoomRepository.ownRoom fun getAllRooms() = firebaseRoomRepository.rooms // 2. fun getCurrentRoom() = firebaseRoomRepository.currentRoom //   // ....... } 

  1. Arriba puede ver una lista de todas las clases de repositorio usadas y fachadas de segundo nivel. Esto simplifica la comprensión general del código y muestra una lista de todas las clases de repositorio conectadas.
  2. Una lista de métodos que devuelven referencias a objetos LiveData desde FirebaseRoomRepository. Los setters y getters de Kotlin son opcionales, por lo que no necesita escribirlos innecesariamente.

Dicha organización le permite ajustar cómodamente de 20 a 30 solicitudes en un repositorio raíz. Si su aplicación tiene más solicitudes, deberá dividir la fachada raíz en 2 o más.

ViewModel


BaseViewModel es el ViewModel base del que se heredan todos los ViewModels. Incluye un solo objeto currentRoom, usado universalmente.

 // 1. open class BaseViewModel : ViewModel() { // 2. fun getCurrentRoom() = Repository.getCurrentRoom() } 

  1. El marcador abierto significa que puede heredar de la clase. Por defecto en Kotlin, todas las clases y métodos son finales, es decir. las clases no se pueden heredar y los métodos no se pueden redefinir. Esto es para proteger contra cambios versionados incompatibles accidentales. Daré un ejemplo.

    Estás desarrollando una nueva versión de la biblioteca. En algún momento, por una razón u otra, decide cambiar el nombre de la clase o cambiar la firma de algún método. Al cambiarlo, accidentalmente creaste incompatibilidad de versión. Vaya ... Si probablemente supieras que alguien podría anular el método y la clase fuera heredada, probablemente habrías sido más preciso y difícilmente te habrías disparado en el pie. Para hacer esto, en Kotlin, por defecto, todo se declara como final, y para la cancelación hay un modificador "abierto".
  2. El método getCurrentRoom () devuelve un enlace al objeto LiveData de la sala actual desde el Repositorio, que, a su vez, se toma del FirebaseRoomRepository. Cuando se llama a este método, el objeto Room regresará con toda la información sobre la sala, incluida una lista de eventos.

Para convertir datos de un formato a otro, utilizaremos la transformación. Para hacer esto, cree un MainFragmentViewModel y heredelo de BaseViewModel .

MainFragmentViewModel es una clase derivada de BaseViewModel. Este ViewModel se usa solo en MainFragment.

 // 1. class MainFragmentViewModel: BaseViewModel () { // 2. var currentRoomEvents = Transformations.switchMap(getCurrentRoom()) { val events: MutableLiveData<List<Event.Short>> = MutableLiveData() // some business logic events.postValue(it?.eventsList) events } // 3. val currentRoomEvents2 = MediatorLiveData<List<Event.Short>>().apply { addSource(getCurrentRoom()) { room -> // some business logic postValue(room?.eventsList) } } } 

  1. Tenga en cuenta la falta del modificador abierto. Esto significa que nadie hereda de la clase.
  2. currentRoomEvents: un objeto obtenido mediante la transformación. Tan pronto como cambia el objeto de la sala actual, se realiza la transformación y se actualiza el objeto currentRoomEvents.
  3. MediatorLiveData. El resultado es idéntico a la transformación (se muestra como referencia).

La primera opción se usa para convertir datos de un tipo a otro, que es lo que necesitábamos, y la segunda opción es necesaria para ejecutar cierta lógica empresarial. Sin embargo, la conversión de datos no ocurre. Recuerde que la importación de Android en ViewModel no es válida. Por lo tanto, inicio solicitudes adicionales desde aquí o reinicio los servicios según sea necesario.

Aviso importante! Para que la transformación o el mediador funcione, alguien debe estar suscrito a ellos desde un fragmento o actividad. De lo contrario, el código no se ejecutará porque nadie esperará un resultado (estos son objetos de observación).

Fragmento principal


El paso final en la conversión de datos a resultado. MainFragment incluye una biblioteca de marcación y un visor de páginas en la parte inferior de la pantalla.

 class MainFragment : BaseFragment() { // 1. private lateinit var viewModel: MainFragmentViewModel // 2. private val currentRoomObserver = Observer<List<Event.Short>> { clockView.updateArcs(it) } override fun onAttach(context: Context?) { super.onAttach(context) // 3. viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_main, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // 4. viewModel.currentRoomEvents.observe(viewLifecycleOwner, currentRoomObserver) } } 

  1. Inicialización de MainFragmentViewModel. El modificador lateinit indica que prometemos inicializar este objeto más tarde, antes de usarlo. Kotlin intenta proteger al programador de la escritura incorrecta del código, por lo que debemos decir de inmediato que el objeto puede ser nulo o poner lateinit. En este caso, ViewModel debe ser inicializado por el objeto.
  2. Observador-oyente para actualizar el dial.
  3. Inicializando el ViewModel. Tenga en cuenta que esto sucede inmediatamente después de que el fragmento se haya unido a la actividad.
  4. Después de crear la actividad, nos suscribimos a los cambios en el objeto currentRoomEvents. Tenga en cuenta que no me suscribo al ciclo de vida del fragmento (esto), sino al objeto viewLifecycleOwner. El hecho es que en la biblioteca de soporte 28.0.0 y AndroidX 1.0.0 se detectó un error cuando el observador estaba "desuscrito". Para resolver este problema, se lanzó un parche en forma de viewLifecycleOwner, y Google recomienda suscribirse a él. Esto soluciona el problema del observador zombie cuando el fragmento murió y el observador continúa trabajando. Si todavía está usando esto, asegúrese de reemplazarlo con viewLifecycleOwner.

Por lo tanto, quiero demostrar la simplicidad y belleza de MVVM y LiveData sin usar el enlace de datos. Tenga en cuenta que en este proyecto infringe la regla generalmente aceptada al colocar LiveData en el Repositorio debido a los detalles del proyecto. Sin embargo, si los moviéramos al ViewModel, la imagen general permanecería sin cambios.

Como guinda de un pastel, he preparado un breve video con una demostración (los nombres están manchados de acuerdo con los requisitos de seguridad, pido disculpas):




Resumen


Como resultado de la aplicación en el primer mes, se revelaron algunos errores en la visualización de reuniones cruzadas (Outlook le permite crear varios eventos al mismo tiempo, mientras que nuestro sistema no lo hace). Ahora el sistema ha estado funcionando durante 3 meses. No se observan errores o fallas.

PD: Gracias jericho_code por el comentario. En Kotlin, puede y debe inicializar List <> en el modelo usando emptyList (), luego no se crea un objeto adicional.
 var events: List<Event.Short> = emptyList() //      EmptyList var events: List<Event.Short> = ArrayList() //    

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


All Articles