Introducción
A la hora de hacer
aplicaciones, el 90% de los dispositivos Android usan solo Java, y la
mayoría de iOS usa Objective-C/Swift frente al uso de código C++ en
ambas. Este sistema puede servir para muchos casos, pero implica en
tener que codificar en 2 plataformas distintas. Por otro lado, si se
separa el diseño en un SDK en C++ que contenga la lógica, y se usa
un modelo vista – controlador, delegando el aspecto y la
interacción a la parte móvil, conseguimos programar la lógica solo
una vez, en C++, y tener separadas las interfaces en en cada
plataforma. Se puede dar una vuelta de rosca más, y usar una web o
framework que genere la misma interfaz para Android e iOS, aunque por
el momento no hay una herramienta lo suficientemente potente o
consolidada que ofrezca unos resultados de calidad.
Este enfoque no
llama nada la atención si alguna vez has utilizado QT para hacer la
interfaz de tu librería, en lugar de usar directamente ventanas de
OpenGL o interfaces a tu S.O de escritorio. Incluso, puede usarse QT
para hacer aplicaciones Android e iOS, pero aun no veo maduro el
tema. Y por supuesto este diseño sirve para estos casos
tradicionales en escritorio, hay muchas formas de afrontar esta
problemática, pero voy a centrarme más en el caso concreto de los
sistemas móviles.
Dropbox por ejemplo, utiliza este sistema porque creen que tiene muchas ventajas, puedes leer más sobre como hacen sus diseños en esta entrada.
Diseño
El diseño separa 2
grandes partes:
- Por un lado, a nivel de aplicación, la interfaz o capa de presentación y el front-end, encargado de la comunicación al usuario con la librería encargada de la lógica.
- Por otro lado, a nivel de motor, el back-end que procesa la entrada desde front-end para recibir y enviar todos los datos necesarios para el correcto funcionamiento a la capa de aplicación.
Como se puede ver en
la ilustración, la App Android y la App de iOS pueden diferir en
lógica y elementos. Al ser 2 S.O distintos cada uno tiene sus
peculiaridades, aunque ya he dicho antes que hay frameworks que
pueden facilitar este desarrollo y podría ser delegada esta tarea en ellos.
Los front-end serán
las capas encargadas de “traducir” el lenguaje de aplicación al
nativo. En el caso de Android consistirá en una interfaz JNI que
cambie funciones y valores de Java a C++ y viceversa, haciendo un uso
correcto del back-end. En el caso de iOS la interfaz será entre
Objective-C/Swift y C++. Aunque tengan diferente sintaxis o diferentes patrones de diseño, es deseable que sean calcados, para tener un conocimiento homogeneo del funcionamiento para todas las plataformas.
Añadir que en la
parte de front-end, además de hacer llamadas a nativo, es deseable
poder recibirlas. De esta forma se pueden hacer callbacks o llamadas
asíncronas que notifiquen eventos en la capa de presentación. La
forma más sencilla de hacer esto es proveer al inicio de la
aplicación de un puntero a función, o callback que sea llamado de
forma asíncrona y la aplicación pueda procesar esas peticiones
usando threads o hilos coordinados.
Habitualmente uso la
siguiente terminología:
- Front-end de Android, InterfaceFromCJNI para llamadas de C++ a Java, e InterfaceToCJNI para las llamadas de Java a C++
- Front-end de iOS, InterfaceFromONI para llamadas de C++ a Objective-C e InterfaceToCONI para el otro sentido. Usando un singleton y delegates tan comunes en iOS, se pueden unir en una sola clase.
- Back-end, es conjunto, en C++ y lo suelo colocar como un Facade con llamadas estáticas por su versatilidad y flexibilidad si se rediseña el SDK.
- Core o lógica serán todas las llamadas encargadas de la gestión lógica de la aplicación deseada. Algunas directamente llamadas desde la fachada, pero no tienen porque estar todas. De esta forma puedes hacer distintas fachadas en función de las configuraciones al exterior.
Diferencias
Pros:
- Ahorras el programar la lógica en distintos lenguajes 2 veces.
- Más fácil reciclar el código para usar también en escritorio y otros S.O
- Separación lógica de diferentes comportamientos.
- Con algo de código extra puedes adaptar un mismo core para aprovecharlo en distintos proyectos.
- Permite la gestión externa de errores mediante logs y variables.
Contras:
- Back-end vs core puede ser redundante.
- Toda la lógica tiene que tener un puente hasta el core, muchas veces código repetitivo.
- No es común el perfil de programador
Logs a bajo y alto nivel
Aunque hoy en día
cualquier programa se puede debugear con muchas herramientas, gdb,
eclipse, Visual Studio, Xcode... No hay que subestimar el uso de logs
estratégicamente colocados en nuestro código. Por varias razones:
- Dan información util.
- Ayudan a aislar errores de forma rápida.
- Son rápidos, no necesitas parar la ejecución.
- En Release también funcionan.
Por ejemplo, podemos
conseguir que los logs de warnings comenten que esperaban algún dato
o estado y que se ha seguido ejecutando a pesar de ello. Se puede
informar errores producidos en la ejecución, y se puede terminar o
no con ella. O simplemente se puede informar de que se pasan por
diversos Checkpoints o zonas, para saber que partes se han ejecutado
y que otras partes no.
Los logs toman una
especial importancia en plataformas móviles, porque son más
dificiles de debuguear que los ejecutables normales, cuesta más
tiempo hacerlo, y a veces es un verdadero quebradero de cabeza.
Por supuesto esta
información no sustituye el uso de debuggers, pero es complementaria
y muy útil si son colocados de forma estratégica. Además, si usas
alguna macro, se pueden mostrar o dejar de mostrar según interese
con el cambio de alguna definición. Una de mis headers favoritos
para logs es este.
Peculiaridades de Android
Android es una de
las plataformas móviles más extendida actualmente. Por desgracia,
el ecosistema Android cambia constantemente y las herramientas de
desarrollo se han ido adaptando a lo largo del tiempo. Además, hay
una gran fragmentación de dispositivos, no es algo intrínsicamente
malo, pero seo hace que haya que testear muchos dispositivos
diferentes y el diseño de interfaz tiene ciertas peculiaridades. Por
cierto, aunque lioso al principio, una vez comprendido es fácil de
usar y potente, en el caso de iOS no es así, porque al principio
contaban solo con 1 tipo de dispositivo, y el número ha ido
creciendo a lo largo del tiempo, y los parches para adaptarse a todos
ellos con el diseño de una interfaz son un desastre.
Cosas que debes
saber antes de desarrollar en Android
- Normalmente necesitas saber Java, usar C++ es complicado y engorroso al principio.
- Para manejar una librería C++ necesitas usar JNI, es decir, traducir llamadas y variables de java a C++
- En Java tienes que cargar la librería dinámica (*.so) de C++, Ejemplo
- Para realizar llamadas de C++ a Java puedes usar un puntero a función y recoger mensajes, como podeis ver aquí.
- El verdadero reto es crear las llamadas desde C++ usando JNI
- Una buena estrategia puede ser usar todo librerías estáticas, y crear una librería dinámica forzando al compilador que meta todo el código para evitar problemas con factorias. Para GNU en Cmake deberías TARGET_LINK_LIBRARIES(cmakeproject -Wl,--whole-archive yourcpplibrary -Wl,--no-whole-archive)
Peculiaridades de iOS
iOS es otra de las
plataformas más extendidas. Es más dificil publicar aplicaciones en
la store que en Android, debido a sus multiples restricciones en los
procesos de validación. Normalmente los dispositivos son más caros,
y sus usuarios están más dispuestos a pagar. Hace unos años era
más fácil hacer interfaces, porque no había variedad de tamaños,
pero ultimamente han salido diversos modelos, y se han sacado de la
manga los constrains y auto-layouts para lidiar con diversos
dispositivos. Habiendo usado los de Android e iOS, me quedo sin duda
con Android. El sistema de APPLE no está nada pulido, da muchos
comportamientos imprevistos, y la teoría no se corresponde para nada
con la práctica. En Android no es tan complicado.
Cosas que debes
saber antes de desarrollar en iOS.
- Objective-C es una variante de C/C++, su integración es sencilla y no necesitas un JNI.
- El standard de ficheros de *.m, pero si lo renombras a *.mm aceptará código C.
- Solo se puede linkar librerías estáticas, por temas de seguridad (según ellos)
- El ejecutable final debe tener por tanto todas las librerias, y su configuración es totalmente manual.
- Es recomensable usar variables user-defined en el proyecto, combinadas con $(SRCROOT) y paths relativos puede tener proyectos multiusuario sin tener que cambiar la configuración de forma manual entre ordenadores.