La combinación del enfoque multiplataforma y nativo en el desarrollo de aplicaciones móviles.

Para lanzar aplicaciones para una sola plataforma móvil no es relevante y debe ocuparse de desarrollar dos versiones a la vez, para iOS y Android. Y aquí puede elegir dos formas: trabajar en los lenguajes de programación "nativos" para cada sistema operativo o usar marcos multiplataforma.

Al desarrollar uno de los proyectos en DD Planet, confié en la última opción. Y en este artículo hablaré sobre la experiencia de desarrollar una aplicación multiplataforma, los problemas que encontramos y las soluciones encontradas.

Tres enfoques para desarrollar aplicaciones móviles multiplataforma


Para comenzar, considere qué enfoques se utilizan cuando necesita obtener dos aplicaciones a la vez: para iOS y Android.

El primero es el más costoso, tanto en tiempo como en recursos: desarrollar una aplicación separada para cada plataforma. La complejidad de este enfoque radica en el hecho de que cada uno de los sistemas operativos requiere su propio enfoque: esto se expresa tanto en el lenguaje en el que se está desarrollando (para Android - Java o Kotlin, para iOS - Objective-C o Swift), como en los métodos para describir la parte de la interfaz de usuario aplicaciones (axml y xib o archivos de guión gráfico, respectivamente).

Este hecho solo nos lleva al hecho de que para este enfoque es necesario formar dos equipos de desarrollo. Además, tendrá que duplicar la lógica para cada una de las plataformas: interacción con la API y la lógica de negocios.

imagen

Pero, ¿qué pasa si aumenta el número de API utilizadas?

imagen

Esto plantea la pregunta: ¿cómo reducir la cantidad requerida de recursos humanos? Deshágase de la necesidad de duplicar el código para cada plataforma. Hay un número suficiente de marcos y tecnologías que resuelven este problema.

El uso de un marco multiplataforma (Xamarin.Forms, por ejemplo) hace posible escribir código en un lenguaje de programación y describir la lógica de datos y la lógica de IU una vez, en un solo lugar. Por lo tanto, la necesidad de usar dos equipos de desarrollo desaparece. Y como resultado de compilar el proyecto, obtenemos dos aplicaciones nativas en la salida. Y este es el segundo enfoque.

imagen

Creo que muchos saben lo que es Xamarin, o al menos lo han escuchado, pero ¿cómo funciona? Xamarin se basa en la implementación de código abierto de la plataforma .NET - Mono. Mono incluye su propio compilador de C #, tiempo de ejecución, así como varias bibliotecas, incluida la implementación de WinForms y ASP.Net.

El objetivo del proyecto es permitir que los programas escritos en C # se ejecuten en sistemas operativos que no sean Windows: sistemas Unix, Mac OS y otros. El marco Xamarin en sí mismo, en esencia, es una biblioteca de clase que brinda a los desarrolladores acceso al SDK de la plataforma y a los compiladores para estos. Xamarin.Forms, a su vez, le permite no solo escribir para ambas plataformas en el mismo idioma, sino también diseñar pantallas usando el marcado XAML, familiar para aquellos que ya tenían experiencia con las aplicaciones WPF. Como resultado del ensamblaje del proyecto, obtenemos un aspecto casi idéntico en todas las plataformas, ya que en la etapa de compilación todos los controles XF se convierten a nativos para cada plataforma.

imagen

El desarrollador se ve obligado a escribir código para cada plataforma solo si se necesita acceso a cualquier función de la plataforma (por ejemplo, un escáner de huellas digitales o nivel de batería) o si es necesario ajustar el comportamiento del control. En algunos casos, al desarrollar una aplicación, puede ser necesario escribir código dependiente de la plataforma, pero incluso en este caso, nadie prohíbe llevar las funciones de la plataforma a la interfaz e interactuar con ella desde un proyecto común.

Un lenguaje de programación, código pequeño, etc. Todo suena hermoso pero, Xamarin.Forms no es una bala de plata, y toda su belleza se rompe en piedras de realidad. Tan pronto como surge una situación cuando los controles XF incorporados ya no cumplen con los requisitos para ellos, la estructura de las pantallas y los controles se vuelve cada vez más complicada. Para garantizar un trabajo cómodo con pantallas de un proyecto común, debe escribir más y más renders personalizados.

Esto pasará al tercer enfoque, que usamos al desarrollar aplicaciones.

Ya descubrimos que usar Xamarin Forms puede complicar el trabajo, en lugar de simplificarlo. Por lo tanto, para implementar pantallas arquitectónicamente complejas, diseñar elementos y controles que sean fundamentalmente diferentes de los nativos, se encontró un compromiso y la posibilidad de combinar el primer y el segundo enfoque.

Tenemos los mismos tres proyectos: un proyecto PCL común, pero sin Xamarin Forms, y dos proyectos Xamarin para Android y Xamarin para iOS. Todavía existe la oportunidad de escribir todo en un idioma, lógica común entre dos proyectos, pero no hay limitaciones de un solo marcado XAML. El componente UI está controlado por cada plataforma y utiliza herramientas nativas, en Android - AXML nativo, en iOS - archivos XIB. Cada plataforma tiene la capacidad de cumplir con sus pautas, ya que la conexión entre Core y los proyectos de plataforma se organiza solo a nivel de datos.

Para organizar tal relación, puede usar el patrón de diseño MVVM y su implementación bastante popular para Xamarin - MVVMCross. Su uso le permite mantener un ViewModel común para cada pantalla, que describe la "lógica de negocios" completa del trabajo y confía su representación a la plataforma. También permite que dos desarrolladores trabajen con la misma pantalla (uno con lógica, el otro con UI) y no interfieran entre sí. Además de la implementación del patrón, obtenemos una cantidad suficiente de herramientas para el trabajo: la implementación de DI e IoC. Para elevar la interacción con la plataforma al nivel de código común, un desarrollador solo necesita declarar una interfaz e implementarla en la plataforma. Para cosas típicas, MvvmCross ya proporciona un conjunto de sus propios complementos. En el equipo, utilizamos el complemento de mensajería para intercambiar mensajes entre la plataforma y el código común y el complemento para trabajar con archivos (seleccionar imágenes de la galería, etc.).

Resolvemos los problemas de diseño complejo y navegación multinivel.


Como se mencionó anteriormente, cuando se usan representaciones complejas en la pantalla, el marco puede complicar la vida más que facilitarla. Pero, ¿qué se llama un elemento complejo? Como me dedico principalmente al desarrollo de iOS, se considerará un ejemplo de esta plataforma. Por ejemplo, una cosa tan trivial como un campo de entrada puede tener varios estados y suficiente lógica para cambiar y visualizar.

imagen

En el curso de trabajar con la entrada del usuario, tal control de entrada se desarrolló aquí. Puede elevar su nombre sobre el campo de entrada, trabajar con máscaras, establecer prefijos, postfijos, notificar cuando se presiona CapsLock, validar información en dos modos: prohibición de entrada y salida de información de error. La lógica dentro del control toma aproximadamente ~ 1000 líneas. Y, parecería: ¿qué puede ser complicado en el diseño del campo de entrada?

Un ejemplo simple de un control complejo que vimos. ¿Qué hay de las pantallas?

imagen

Para empezar, aclararé que, en la mayoría de los casos, una pantalla de aplicación es una clase: UIViewController, que describe su comportamiento. Durante el desarrollo, se requería la creación de navegación multinivel. El concepto de la aplicación desarrollada se reduce a administrar sus bienes inmuebles e interactuar con vecinos y organizaciones municipales. Por lo tanto, se construyeron tres niveles de navegación: propiedad, nivel de presentación (hogar, ciudad, región) y tipo de contenido. Todo el cambio se lleva a cabo en una pantalla.

Esto se hizo para que el usuario, esté donde esté, entienda qué tipo de contenido ve. Para organizar dicha navegación, la pantalla principal de la aplicación no consta de un solo controlador. Visualmente, se puede dividir en 3 partes, pero ¿alguien puede tratar de adivinar cuántos controladores se usan aquí?

imagen

Quince controladores principales. Y esto es solo por contenido.

Aquí tal monstruo vive en la pantalla principal y se siente bastante bien. Quince controladores para una pantalla son, por supuesto, muchos. Esto afecta la velocidad de toda la aplicación y necesita optimizarla de alguna manera.

Rechazamos la inicialización síncrona: todos los modelos de vista se inicializan en segundo plano y solo cuando es necesario. Para reducir el tiempo de renderizado, también abandonamos los archivos xib para estas pantallas: el posicionamiento absoluto y las matemáticas son siempre más rápidos que calcular las dependencias entre elementos.

Para realizar un seguimiento de tantos controladores, debe comprender:

  • En qué condición se encuentra cada uno de ellos;
  • ¿Dónde está el usuario?
  • Lo que espera ver cuando se mude a otro controlador.

Para hacer esto, escribí un procesador de navegación separado que almacena información sobre la ubicación del usuario, el tipo de contenido que está viendo, el historial de navegación, etc. Él controla el orden y la necesidad de la inicialización.

Dado que cada pestaña es un control deslizante del controlador (para crear una transición de deslizamiento sobre ellas), debe comprender: cada una de ellas puede estar en su propio estado (por ejemplo, "Noticias" está abierta en una y "Votación" en la otra). Esto es seguido por el mismo procesador de navegación. Incluso cambiando el nivel de presentación desde el hogar a la región, seguiremos con el mismo tipo de contenido.

Controlamos el flujo de datos en tiempo real.


Al trabajar con tantos datos en la aplicación, debe organizar la entrega de información relevante en todas las secciones en tiempo real. Para resolver este problema, se pueden distinguir 3 métodos:

  1. Acceda a la API por temporizadores o disparadores y vuelva a solicitar contenido relevante en las pantallas;
  2. Tener una conexión permanente con el servidor y recibir cambios en tiempo real;
  3. Recibe empuje con cambios de contenido.

Cada enfoque tiene sus pros y sus contras, por lo que es mejor usar los tres, eligiendo solo las fortalezas de cada uno. Dividimos condicionalmente el contenido dentro de la aplicación en varios tipos: hot, regular y service. Esto se hace para determinar el tiempo aceptable entre el evento y la notificación del usuario. Por ejemplo, queremos ver un mensaje de chat inmediatamente después de que nos lo envíen; este es un contenido interesante. Otra opción: encuesta de vecinos. No hay diferencia cuando lo vemos, ahora o en un minuto, porque este es un contenido ordinario. Las notificaciones pequeñas dentro de la aplicación (mensajes no leídos, comandos, etc.) son contenido de servicio que necesita entrega urgente, pero que no toma muchos datos.

Resulta que:

  • Contenido activo: conexión permanente con la API;
  • Contenido normal: solicitudes http a la API;
  • Contenido del sistema: notificaciones push.

Lo más interesante es mantener una conexión constante. Escribir su propio cliente para trabajar con sockets web es un paso en el agujero del conejo, por lo que debe buscar otras soluciones. Como resultado, nos detuvimos en la biblioteca SignalR. Veamos de qué se trata.

ASP.Net SignalR es una biblioteca de Microsoft que simplifica la interacción cliente-servidor en tiempo real, proporcionando comunicación bidireccional entre el cliente y el servidor. El servidor incluye una API completa para administrar la conexión, eventos de desconexión de conexión, un mecanismo para combinar clientes conectados en grupos y autorización.

SignalR puede usar websockets, LongPolling y solicitudes http como transporte. Puede especificar el tipo de transporte por la fuerza o confiar en la biblioteca: si se puede utilizar websocket, funcionará a través de websocket, si esto no es posible, se desactivará hasta que encuentre un transporte aceptable. Este hecho resultó ser muy práctico, dado que está planeado usarlo en dispositivos móviles.

Total, qué beneficio obtenemos:

  • Capacidad para intercambiar mensajes de cualquier tipo entre el cliente y el servidor;
  • El mecanismo para cambiar automáticamente entre sockets web, agrupación larga y solicitudes Http;
  • Información sobre el estado actual de la conexión;
  • Una oportunidad para unir clientes en grupos;
  • Métodos prácticos para manipular la lógica de enviar mensajes en un grupo;
  • La capacidad de escalar el servidor.

Esto, por supuesto, no satisface todas las necesidades, pero notablemente hace la vida más fácil.

Dentro del proyecto, se usa un contenedor en la biblioteca SignalR, lo que simplifica aún más el trabajo con él, a saber:

  • Supervisa el estado de la conexión, se vuelve a conectar de acuerdo con las condiciones especificadas y en caso de interrupción;
  • Capaz de reemplazar o volver a abrir rápidamente la conexión, matando asincrónicamente la antigua y dándola al recolector de basura para que se rompa; como resultó, el método de conexión funciona diez veces más rápido que el método de cierre (Dispose or Stop), y esta es la única forma de cerrarla;
  • Organiza una cola para enviar mensajes de modo que la reconexión o reapertura de la conexión no interrumpa el envío;
  • Transfiere el control a los delegados apropiados en caso de errores imprevistos.

Cada uno de estos contenedores (los llamamos clientes) funciona en conjunto con el sistema de almacenamiento en caché y, en caso de desconexión, puede solicitar solo los datos que podrían haberse perdido durante este tiempo. "Cada uno" porque varios compuestos activos se mantienen simultáneamente. Dentro de la aplicación hay un mensajero completo, y se utiliza un cliente separado para atenderlo.

El segundo cliente es responsable de recibir las notificaciones. Como ya dije, el tipo habitual de contenido se obtiene a través de solicitudes http, en el futuro su actualización recae en este cliente, que informa todos los cambios importantes en él (por ejemplo, la votación ha pasado de un estado a otro, la publicación de nuevas noticias).

Visualice los datos en la aplicación.


imagen

Obtener datos es una cosa, mostrar es otra. La actualización de datos en tiempo real tiene sus propias dificultades. Como mínimo, debe decidir cómo presentar estas actualizaciones al usuario. En la aplicación utilizamos tres tipos de notificaciones:

  1. Notificación de contenido no leído;
  2. Actualización automática de datos en la pantalla;
  3. Oferta de contenido.

La forma más familiar y común de mostrar que en algún lugar hay contenido nuevo es resaltar el ícono de la sección. Por lo tanto, casi todos los iconos tienen la capacidad de mostrar el notificador de contenido no leído como un punto rojo. Cosas más interesantes son con actualizaciones automáticas.

La actualización automática de datos solo es posible cuando el nuevo contenido no reorganiza la pantalla y no cambia el tamaño de los controles. Por ejemplo, en la pantalla de la encuesta: la información sobre los votos solo cambiará el valor de la barra de progreso y los porcentajes. Dichos cambios no implicarán ningún cambio de tamaño; pueden aplicarse instantáneamente sin problemas.

Las dificultades surgen cuando necesita agregar contenido nuevo a las listas. De hecho, todas las listas de la aplicación son ScrollView y tienen varias características: tamaño de ventana, tamaño de contenido y posición de desplazamiento. Todos tienen un comienzo estático (parte superior de la pantalla con coordenadas 0; 0) y pueden expandirse hacia abajo. Agregar nuevo contenido a la lista, al final, no presenta ningún problema, la lista durará. Pero el nuevo contenido debería aparecer en la parte superior, y esta es la imagen:

imagen

Al estar en 3 elementos, estaremos en 2: el desplazamiento rebotará hacia arriba. Y dado que el contenido nuevo puede llegar constantemente, el usuario no podrá desplazarse normalmente. Podría decir: ¿por qué no calcular el tamaño del nuevo contenido y desplazar el desplazamiento hacia abajo a este valor? Sí, se puede hacer. Pero luego debe controlar manualmente la posición de desplazamiento, y si en ese momento el usuario se desplaza en cualquier dirección, su acción se interrumpirá. Es por eso que tales pantallas no pueden actualizarse en tiempo real sin el consentimiento del usuario.

La mejor solución en esta situación sería informar al usuario que mientras se desplazaba por el feed, alguien publicaba contenido nuevo. En nuestro diseño, se ve como un círculo rojo en la esquina de la pantalla. Al hacer clic en él, el usuario da su consentimiento condicional para que lo regresemos a la parte superior de la pantalla y muestre contenido nuevo.

Con este enfoque, por supuesto, evitamos los problemas de "hacer malabarismos" con el contenido, pero aún tenían que resolverse. Es decir, en la pantalla de chat, ya que durante la comunicación y la interacción con la pantalla, el contenido nuevo debe mostrarse en diferentes lugares.

La diferencia entre el chat y las listas normales es que el contenido nuevo está en la parte inferior de la pantalla. Como se trata de una "cola", puede agregar contenido allí sin mucha dificultad. El usuario pasa el 90% del tiempo aquí, lo que significa que debe mantener constantemente la posición de desplazamiento y desplazarla hacia abajo al recibir y enviar mensajes. En una conversación en vivo, tales acciones deben realizarse con bastante frecuencia.

El segundo punto: cargar el historial mientras se desplaza hacia arriba. Justo al cargar la historia, nos encontramos en una situación en la que es necesario colocar los mensajes por encima del nivel de revisión (lo que implicará un sesgo) para que el desplazamiento sea suave y continuo. Y como ya sabemos, para no molestar al usuario, es imposible controlar manualmente la posición de desplazamiento.

Y encontramos una solución: la entregamos. El cambio de pantalla resolvió dos problemas a la vez:

  1. La cola de la lista está en la parte superior, por lo que podemos agregar una historia sin interrupciones sin interferir con el desplazamiento del usuario;
  2. El último mensaje siempre está en la parte superior de la lista y no necesitamos desplazar la pantalla antes que él.

imagen

Esta solución también ayudó a acelerar el renderizado, eliminando operaciones innecesarias con el control de desplazamiento.

Hablando de rendimiento. En las primeras versiones de la pantalla, se detectaban reducciones notables al desplazarse por los mensajes.Dado que el contenido en el "dinero" es heterogéneo (texto, archivos, fotos), debe recalcular constantemente el tamaño de la celda, agregar y eliminar elementos en el dinero. Por lo tanto, se requería la optimización de la burbuja. Hicimos lo mismo que con la pantalla principal, renderizando parcialmente la masa con posicionamiento absoluto.

Al trabajar con listas en iOS, antes de dibujar una celda, debe saber su altura. Por lo tanto, antes de agregar un nuevo mensaje a la lista, debe preparar toda la información necesaria para mostrar en una secuencia separada, calcular la altura de las celdas, procesar los datos del usuario y solo después de descubrir y almacenar en caché todo lo que se necesita, agregar la celda a la lista.

Como resultado, obtenemos un desplazamiento suave y una transmisión de IU no sobrecargada.

Para resumir:


  • El desarrollo multiplataforma ahorra tiempo y dinero;
  • , , ;
  • , ;
  • ;
  • SignalR – - ;
  • ;
  • , , ;
  • , SignalR-, , , , .

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


All Articles