Dam M08 2009 Qa03
Dam M08 2009 Qa03
Dam M08 2009 Qa03
y dispositivos móviles
Programación multimedia y
dispositivos móviles
Programación multimedia
y dispositivos móviles
Introducción ................................................................................................................... 6
1. Análisis de tecnologías para aplicaciones en dispositivos móviles ........................ 6
1.1. Limitaciones que plantea la ejecución de aplicaciones en dispositivos móviles:
desconexión, seguridad, memoria, consumo de batería, almacenamiento.............. 6
1.2. Tecnologías disponibles ................................................................................... 8
1.3. Entornos de trabajo integrado .......................................................................... 10
1.3.1. Registros de la aplicación, Logcat ............................................................... 11
1.3.2. Poniendo en práctica: instalación de entorno de desarrollo ..................... 12
1.4. Módulos para el desarrollo de aplicaciones móviles ........................................ 13
1.4.1. Android Studio: tipos de vista .................................................................... 14
1.4.2. Archivos y carpetas de los módulos ........................................................... 15
1.4.3. Poniendo en práctica: creando un proyecto .............................................. 16
1.5. Emuladores ........................................................................................................ 18
1.6. Integración en el entorno de desarrollo ........................................................... 19
1.6.1. Poniendo en práctica: crear un dispositivo virtual con el emulador de
Android Studio ...................................................................................................... 19
1.7. Configuraciones. Tipos y características. Dispositivos soportados ................... 23
1.8. Perfiles. Características. Arquitectura y requerimientos. Dispositivos
soportados ................................................................................................................ 24
1.9. Modelo de estados de una aplicación para dispositivos móviles. Activo, pausa
y destruido ................................................................................................................ 29
1.9.1. Poniendo en práctica: ejecuta la aplicación desde Android Studio ........... 31
1.9.2. Poniendo en práctica: comprendiendo el ciclo de vida de una aplicación 32
1.9.3. Ciclo de vida de una aplicación: descubrimiento, instalación, ejecución,
actualización y borrado......................................................................................... 33
1.10. Modificación de aplicaciones existentes ......................................................... 35
1.10.1. Poniendo en práctica: descarga el código y realiza una modificación ..... 35
1.11. Utilización de entornos de ejecución del administrador de aplicaciones ...... 37
1.11.1. Poniendo en práctica: ejecuta la aplicación en un dispositivo real ......... 37
2. Programación de aplicaciones para dispositivos móviles .................................... 39
2.1. Herramientas y fases de construcción........................................................... 39
2.1.1. Herramientas .......................................................................................... 39
Programación multimedia
y dispositivos móviles
Introducción
A lo largo de los temas de este libro veremos el desarrollo de aplicaciones móviles en
entorno Android con lenguaje Kotlin. El enfoque de este es completamente práctico
desde el primer momento, aportando los conceptos teóricos que aplicamos en el
desarrollo de una aplicación real.
Como desarrollador, te enviarán diferentes casos de uso donde se indican los requisitos
del cliente y qué se espera que haga la aplicación.
Comencemos este proyecto por los conceptos básicos y preparando el entorno para
trabajar.
Estas limitaciones hoy en día no vienen dadas tanto por la capacidad de procesamiento
o memoria como por el entorno en el que se utilizan estos dispositivos. Entre ellas,
definimos las más relevantes:
Desconexión
Seguridad
En lo que se refiere seguridad física, la limitación viene dada por la propia virtud del
dispositivo: al ser ligeros y fáciles de llevar, son a la vez susceptibles de ser más
fácilmente sustraídos.
Consumo de batería
Por su propia naturaleza de ser lo más ligeros posible, la batería suele ser uno de los
elementos más comprometidos, y el diseño de las aplicaciones debe tener en
consideración qué recursos utiliza en cada momento, liberando aquellos que no sean
necesarios para ahorrar los tan preciados miliamperios de la batería.
Memoria y almacenamiento
A día de hoy, los dispositivos móviles cada vez incorporan más memoria y capacidad de
almacenamiento, aunque no por ello debemos desaprovecharla, ya que es un recurso
Programación multimedia
y dispositivos móviles
limitado que no podemos ampliar sin adquirir un nuevo dispositivo. Por ello, las
aplicaciones móviles deben optimizar el uso de los recursos, ya que también debe
compartirlos con otras aplicaciones.
¿Qué interés hay en utilizar otras tecnologías diferentes a las de cada plataforma? El
motivo es ahorrar recursos. Imaginemos una empresa dedicada a programar juegos para
Android e iOS. Tiene dos opciones: la primera es contratar un equipo de desarrolladores
Android nativos y otro de desarrolladores iOS nativos que paralelamente desarrollen el
mismo juego pero en plataformas diferentes, multiplicando por dos el coste en horas;
la segunda es utilizar una tecnología híbrida que se programe una vez y funcione
igualmente en iOS y Android, con lo que solo se necesita un equipo de desarrollo híbrido.
La última opción parece prometedora, pero conlleva algunos inconvenientes: la mayoría
de las tecnologías híbridas están basadas en web, es decir, se crea una especie de
aplicación web que se mostrará en el dispositivo como si fuese nativa, utilizando algunas
librerías para el acceso al hardware, como la cámara, etcétera. Esto hace que sean
aplicaciones menos potentes, que no puedan acceder a ciertos tipos de hardware y que
sean más lentas. Quizá por ello, estas tecnologías son populares durante un tiempo y
luego dejan de usarse, lo que supone que los programadores tengan que cambiar
constantemente de tecnología, impidiendo que ganen experiencia.
Otras tecnologías como Unity para juegos o Xamarin para aplicaciones son más
potentes. Sin embargo, no son públicas e implican un gasto de licencia. Otro aspecto
para tener en cuenta es que las plataformas móviles evolucionan rápidamente y no
tienen ningún interés en ser compatibles con los frameworks híbridos ya existentes. Si
eres un programador nativo, podrás adaptarte rápidamente a las nuevas interfaces o
Programación multimedia
y dispositivos móviles
Como hemos mencionado antes, las aplicaciones nativas son aquellas que se
implementan en el lenguaje específico que facilita la plataforma/fabricante para el
desarrollo. Por ejemplo, para Android tenemos Java y Kotlin sobre Android Studio. En la
plataforma de Apple para iPhone nos encontramos con Objective-C en las aplicaciones
antiguas y Swift en las nuevas, sobre el entorno Xcode.
Las aplicaciones híbridas son aquellas en las que un mismo código pueda compilarse
para dos o más plataformas diferentes, ahorrando recursos de desarrollo. Podemos
mencionar algunas de ellas:
Plataformas de destino
Plataforma Lenguaje Compañía
• Instant run, que permite ver cambios de código sin reiniciar la aplicación.
• Desarrollo unificado para todos los dispositivos Android (móvil, TV, smartwatch,
Android Auto).
Todas las herramientas que ofrece Android Studio se agrupan en una interfaz gráfica,
las más importantes las podemos identificar en la siguiente imagen:
Programación multimedia
y dispositivos móviles
2 1
https://developer.android.com/studio
Es importante contar con bastante memoria RAM y espacio en disco para poder trabajar
de una forma fluida, teniendo en cuenta que, al utilizar dispositivos virtuales, consumirá
también bastante memoria y disco.
Como ejemplo supongamos que desarrollamos una aplicación que es compatible con
móviles y con smartwatch: la base de funcionalidades de la aplicación será la misma para
ambas aplicaciones, pero la interfaz de usuario cambiaría. Podemos crear tres módulos:
uno contendrá las funciones compartidas de ambos dispositivos y crearemos dos
módulos más para cada dispositivo, de modo que compartimos código en ambas
aplicaciones, facilitando el desarrollo, test y ampliaciones futuras.
En Android Studio podemos crear diferentes tipos de módulos siguiendo la ruta File >
New > New Module, de los cuales algunos son:
Programación multimedia
y dispositivos móviles
Para visualizar los módulos y los ficheros que los componen, Android Studio nos ofrece
varios tipos de vistas que podemos cambiar con el menú desplegable de la parte
superior de la ventana de proyecto.
Vista de Android: muestra una vista simplificada de la jerarquía de los ficheros del
proyecto, con los elementos más relevantes en el desarrollo de
una aplicación Android.
Como hemos comentado anteriormente, los módulos contienen los archivos de código
y los recursos que componen la aplicación. En función del tipo de módulo, podemos
encontrar diferentes carpetas que organizan el contenido. Los más relevantes son:
- values: en esta carpeta tenemos varios ficheros que almacenan los estilos y
códigos de colores usados en la aplicación, así como los textos que se utilizan
en la aplicación para su posterior traducción.
• Gradle Scripts: esta sección agrupa los ficheros necesarios para la compilación
de la aplicación, en la que podemos encontrar al menos dos: uno para la
compilación del proyecto y otro para cada módulo que contenga el proyecto.
Estos ficheros los veremos con mayor detalle en los siguientes capítulos, donde
hablaremos de la fase de compilación.
Programación multimedia
y dispositivos móviles
En el siguiente paso nos muestra una serie de plantillas predefinidas, según el tipo de
aplicación que vamos a desarrollar. Para este proyecto seleccionaremos Empty Activity,
esto creará un proyecto con una única pantalla en blanco.
Programación multimedia
y dispositivos móviles
Por último, indicaremos el API Level mínimo con que será compatible la aplicación.
Veremos este concepto en los siguientes temas con más detalle y la importancia de su
selección.
Tras un tiempo de preparación y ejecución de varias tareas que realiza Android Studio,
tendremos el proyecto preparado para comenzar a desarrollar.
Llegados a este punto, os invitamos a navegar por la estructura del proyecto que ha
generado Android Studio, localizar los ficheros más relevantes y dónde se guarda el
código de la aplicación y el diseño de las pantallas.
Programación multimedia
y dispositivos móviles
Una vez familiarizados con esta estructura, para mantener el código organizado debéis
crear un módulo de tipo biblioteca y responder a las siguientes preguntas:
¿Qué diferencias hay entre la estructura del módulo aplicación y del módulo que hemos
creado?
¿En qué carpeta y módulo debemos colocar el código que queremos compartir con otros
módulos?
1.5. Emuladores
En el ciclo de desarrollo de cualquier tipo de aplicación necesitaremos probar el
funcionamiento y corregir los posibles errores que surjan. Cuando desarrollamos
aplicaciones de escritorio o web, estas pruebas las podemos realizar en nuestro propio
ordenador.
A continuación, crearemos un dispositivo virtual que usaremos a lo largo del libro para
probar los ejemplos y el desarrollo de la aplicación IlernaTodo.
La primera vez no tendremos ningún dispositivo virtual y el asistente nos propone crear
uno nuevo.
Programación multimedia
y dispositivos móviles
En el siguiente paso nos aparece una lista de posibles dispositivos que podemos simular:
televisores, móviles, smartwatch o tabletas. Para este caso seleccionamos un Píxel 2, es
un dispositivo de pantalla media o habitual a día de hoy y un buen punto de partida para
probar las aplicaciones.
El siguiente paso nos muestra la configuración del dispositivo. Clicando sobre el botón
Show Advance Settings vemos en detalle la cantidad de memoria RAM destinada al
dispositivo virtual.
Al finalizar, el asistente nos muestra una lista de todos los dispositivos virtuales que
tenemos configurados, y podemos encender o apagar los que consideremos para probar
la aplicación en los diferentes dispositivos. En este punto ya podemos ver que disponer
Programación multimedia
y dispositivos móviles
Para encender el dispositivo virtual que queremos, tendremos que clicar sobre el botón
con el icono de play. Tras unos minutos, tendremos un dispositivo virtual arrancado que
funciona prácticamente igual que un dispositivo real.
Programación multimedia
y dispositivos móviles
- Tamaño de pantalla.
• Smartwatch: los relojes inteligentes han visto cómo sus funcionalidades han
aumentado gracias a la miniaturización y el desarrollo de procesadores más
potentes y de bajo consumo, lo que ha permitido crear sistemas con características
como Bluetooth, Wifi, acelerómetros, sensores de ritmo de cardiaco, GPS o
brújulas. Este tipo de dispositivos se ha abierto camino rápidamente en el mundo
del deporte y la salud, gracias a los sensores que incorporan es posible crear
aplicaciones que monitorizan nuestra actividad física, como la calidad del sueño.
En el caso de que desarrollemos una aplicación para monitorizar la actividad y/o salud
de las personas, buscaremos ciertas características:
• Dispositivo ligero.
Para este tipo de aplicaciones, los dispositivos idóneos son los de tipo smartwatch, que
disponen de las características indicadas y tienen ventajas sobre los otros dispositivos
para cumplir con este tipo de aplicaciones.
Las aplicaciones destinadas para automoción son muy específicas y limitadas por lo que
implica el uso de sistemas que puedan distraer al conductor, de modo que podemos
definir un perfil de aplicaciones destinadas a su uso en automóvil con una interfaz
reducida e integrada con el reconocimiento de voz.
Al igual que con las smart TV, si desarrollamos una aplicación en el ámbito de rutas, de
información del tráfico o de contenidos de música, los dispositivos Android Auto pueden
marcar la diferencia gracias a su adaptación e integración con el vehículo.
Android Studio pone a nuestra disposición muchas utilidades que nos facilitan el
desarrollo y la depuración de nuestras aplicaciones. Es necesario analizar no solo el
código, sino también las interfaces gráficas. Imagina un proyecto con layouts complejos
en el que las vistas se solapan unas sobre otras, o cuya posición es relativa a otra. En una
aplicación así, es difícil saber por qué una vista ha quedado fuera de la pantalla, por qué
está mal situada o por qué la velocidad de presentación es baja y crea cuellos de botella
en la interfaz.
Por fortuna, a cambio tenemos el Layout Inspector, o inspector de diseños. Esta utilidad
nos analizar la interfaz gráfica por completo. Podemos comparar nuestro layout con el
prototipo de diseño o mockup. Así seremos capaces de comprobar que la alineación de
las vistas sea la que deseamos, o la que nos piden los diseñadores el departamento de
UI & UX. El Layout Inspector es de extremada importancia en aplicaciones cuya interfaz
de usuario se crea durante la ejecución, pues se tiene menor control sobre el resultado
que cuando diseñamos la interfaz estáticamente con código XML. El Layout Inspector
nos permite comprobar qué aspecto tendrá nuestra interfaz gráfica en cualquier
momento del desarrollo. Además, disponemos de otra herramienta llamada Layout
Validation con la que podemos observar cómo nuestro layout se adapta a terminales
móviles con diferentes tamaños de pantalla.
Programación multimedia
y dispositivos móviles
https://github.com/android/sunflower
Si la aplicación rueda sobre un terminal más antiguo, cuando cambies la vista de la app
podrás actualizar la vista de pantalla del Inspector mediante el icono de recarga, que es
una flecha circular:
Puedes seleccionar cualquier componente gráfico haciendo clic sobre él, tanto en el
árbol de vistas como en el visor de pantalla. En ese momento se mostrarán los atributos
de la vista en la tabla de propiedades. Si quisieras seleccionar una vista que esté bajo
otro componente, podrías hacer clic sobre ella en el árbol de vistas, o rotando el layout
en el visor de pantalla y haciendo luego clic sobre la vista. La rotación permite una visión
3D de la superposición de las vistas una encima de otra, lo que puede resultar muy útil
para depurar y diseñar la interfaz gráfica:
Programación multimedia
y dispositivos móviles
Si el layout es grande y complejo, quizá sea más sencillo centrarse solo en una parte del
diseño cada vez. Para ello, puedes aislar un conjunto de vistas de la jerarquía, para que
sean los únicos que veas en el árbol de vistas y en el visor de pantalla. Para aislar las
vistas, el dispositivo tiene que estar aún conectado, para que pueda tomarse otra
captura y análisis de pantalla para el visor. Para aislar una vista, pulsa con el botón
derecho del ratón sobre la vista en el árbol de vista o en el visor y escoge Show Only
Subtree (mostrar solo el subárbol). Para volver a ver todos los componentes del layout,
haz clic con el botón derecho sobre la vista y escoge Show All (mostrar todo).
Quizá te confundan los márgenes que dibuja el visor de pantalla para delimitar las vistas.
Si quieres ocultar esas líneas, haz clic en el icono de View Options con forma de ojo y haz
clic sobre Show Borders, el check desaparecerá. La otra opción, Show View Label, te
permite mostrar u ocultar las etiquetas de cada componente. Las etiquetas te facilitan
comprender qué vista se corresponde con qué elemento en el árbol, pero si hay
demasiadas y te molestan, puedes ocultarlas con esta opción.
https://developer.android.com/studio/debug/layout-inspector
Del mismo modo, cuando el usuario vuelve a solicitar la aplicación, el sistema se encarga
de crear el proceso para mostrar la aplicación en pantalla.
Vemos que el sistema no cerrará nunca la aplicación que tengan el foco, es decir, aquella
aplicación que se está mostrando en la pantalla. Si la aplicación pierde el foco debido a
que el usuario cambia de aplicación o está leyendo un mensaje o atendiendo una
llamada, pasa a segundo plano y puede ser cerrada por el sistema si este necesita los
recursos.
Es vital entender el funcionamiento del ciclo de vida de las aplicaciones para desarrollar
aplicaciones robustas, ya que tenemos que manejar el ciclo de vida para determinar en
qué momento recuperamos los datos de la aplicación.
Es vital comprender el ciclo de vida de una aplicación, para crear aplicaciones robustas,
durante el desarrollo tendremos debemos ejecutar el código en el momento indicado
para que la aplicación funcione correctamente.
Programación multimedia
y dispositivos móviles
• onStart(): en este estado la actividad se prepara para ser visible mientras entra en
primer plano y recibe el foco.
Abre el proyecto que hemos creado previamente, debes tener creado un dispositivo
virtual y situarte en el icono play (flecha verde),
Para comprender mejor el ciclo de vida, vamos a ponerlo en práctica en el proyecto que
hemos creado previamente. Vamos al archivo MainActivity.kt; si no puedes localizarlo,
repasa el punto 1.3, en el que habla sobre la estructura del proyecto.
Este fichero APK se puede instalar en un dispositivo Android y nos puede servir para
probar o realizar una demo, pero no es la forma más común de distribuir las aplicaciones
a los usuarios. Para realizar la distribución, Google pone a nuestra disposición Google
Play Store, donde podemos subir las aplicaciones, previo registro y pago del alta como
desarrollador. Una vez subida, nuestra aplicación estará disponible al público para poder
instalarla desde la Google Play Store.
Si el usuario ya no quiere utilizar más una aplicación, puede ser eliminada del sistema
de forma permanente junto con todos sus archivos y recursos desde las opciones de
configuración del dispositivo, en esta sección disponemos de una lista de aplicaciones
instaladas.
Como podemos ver en la captura de pantalla, estas pueden variar según la versión del
sistema operativo. Tenemos el detalle de los recursos, notificaciones, permisos, espacio
utilizado de la aplicación y una opción de desinstalar.
Programación multimedia
y dispositivos móviles
https://gitlab.com/ilerna/common/kotlin.git
Nuestra primera tarea es descargar el código en Android Studio, podemos hacerlo desde
la opción Check out project from Version Control.
Programación multimedia
y dispositivos móviles
En la siguiente ventana indica la URL del repositorio de Git y pulsa sobre el botón Test
para comprobar que puedes acceder correctamente.
Tras clonar el repositorio y que Android Studio realice las tareas necesarias para
importar el proyecto, muévete a la rama 01_Creando_Proyecto. Lo podrás hacer desde
el menú VCS > Git > Branches, donde aparecerá una ventana con las ramas disponibles
en el repositorio.
Historia de usuario
Tarea
Test
Pero la prueba de fuego de una aplicación es cuando llega a los dispositivos reales,
donde podemos encontrar situaciones, características del dispositivo o capas de
software del fabricante que pueden influir en la aplicación.
Con Android Studio podemos ejecutar las aplicaciones que tenemos en desarrollo en
dispositivos reales, de una manera fácil e integrada en el flujo de desarrollo.
Paso 1. Conecta el móvil al ordenador. Si usas un sistema Windows, puede ser necesario
instalar el driver si no lo reconoce automáticamente.
https://developer.android.com/studio/run/oem-usb?hl=es-419
Recapitulando
Llegado al final de este tema, hemos cubierto los siguientes puntos:
Con lo hemos visto en este capítulo de introducción, estamos preparados para continuar
el desarrollo de la aplicación IlernaTodo y convertirnos en auténticos desarrolladores
móviles.
Programación multimedia
y dispositivos móviles
2.1.1. Herramientas
posible que al principio solo necesitemos una pequeña parte. Ya vimos algunas de sus
funcionalidades en el tema anterior, y, mientras lo usamos, aprenderemos otras tantas.
Pero aparte del entorno de desarrollo, o integrado en él, podemos hacer uso de otras
herramientas muy útiles y, dependiendo del proyecto, imprescindibles. Hagamos un
listado rápido:
sobre todo cuando los servicios no están bien documentados o quizá aún están
en fase de desarrollo. Si queremos tener una idea de los que nos devolverá el
servicio cuando lo usemos en nuestra app, necesitamos estas aplicaciones.
Postman es una de las más conocidas, pero existen otras, como SoapUI, Widzler
o SoapSonar.
• Axure: para diseñar nuestra interfaz gráfica antes de empezar con el desarrollo,
podemos usar papel y lápiz, pero si queremos un resultado más profesional y
guardar los diseños junto con el resto del código, podemos utilizar una aplicación
de diseño de pantallas, como Axure, MockPlus, Adobe XD, Mock Flow,
JustInMind, Cacoo, Frame Box o muchas otras. Estas aplicaciones nos permiten
colocar los widgets de cada ventana, moverlos, cambiarlos de tamaño, etcétera.
Algunas de ellas también nos permiten diseñar un flujo de ventanas, que será el
camino que tome el usuario de una pantalla a otra mientras utiliza nuestra app.
necesarios. Es estupendo que el programador tenga una vena artística, pero eso no es
suficiente. Hoy en día, el ingeniero de software debe conocer las técnicas de
programación, las mejores prácticas, los patrones de diseño, las pruebas de calidad, el
análisis de los casos de uso, etcétera.
Si únicamente necesitamos una pequeña aplicación que tenga alguna utilidad para
nosotros, o simplemente queremos programar para aprender, quizá entonces no
necesitamos más que un poco de artesanía, pero si necesitamos llevar a cabo un gran
proyecto profesional con éxito, debemos conocer y aplicar las técnicas de la ingeniería
del software, es decir, debemos seguir un camino bien marcado y medido durante el
desarrollo; de otro modo, terminaremos malgastando tiempo y recursos en una
aplicación llena de fallos que nadie desea descargar en su móvil. Para entender toda la
ingeniería del software necesitaríamos una biblioteca entera, así que no vamos a entrar
en ello, pero es imprescindible al menos conocer algunos puntos básicos, como las fases
del desarrollo software, también conocido como el ciclo de vida del software:
Planificación
El cliente, el jefe de producto o nosotros mismos cuando se nos ocurre una idea para
una app tenemos solo un concepto abstracto de lo que queremos. Sin embargo, antes
de cortar, necesitamos medir. Debemos especificar cada una de las tareas que nuestro
programa debe realizar, qué hará y cómo. Antes de arrancar Android Studio, debemos
tener preparado un análisis funcional, es decir, debemos listar las funcionalidades que
ofrecerá nuestra app. También es de gran utilidad diseñar con antelación la interfaz
gráfica, o al menos una aproximación: las pantallas, los campos y cómo navegará el
usuario por la app. Al definir todos los parámetros que influirán en el desarrollo,
podremos tener una idea real del tiempo y los recursos técnicos y financieros que serán
necesarios. Si existe un cliente, debemos repasar con él que la app hará lo que dice en
el informe de requisitos y funcionalidades, de modo que podamos acotar el trabajo
necesario y repartirlo entre equipos de desarrollo.
Algunos tipos de pruebas pueden realizarse al mismo tiempo que se codifican las
funcionalidades. En Android tenemos las pruebas unitarias y las pruebas instrumentales,
que nos permiten asegurar que progresamos sin fallos. Las pruebas unitarias o unit tests
permiten probar el funcionamiento de módulos de código puro, es decir, que no tienen
llamadas al sistema operativo o que, de tenerlas, pueden emularse. Las pruebas
instrumentales permiten probar el código del módulo directamente en un terminal para
asegurarnos de que funcionan incluso las llamadas al sistema. Cuando varios sistemas
de la misma aplicación están terminados, pueden hacerse pruebas de integración para
asegurar que la coordinación entre ellos funciona como debería. Normalmente, en
proyectos profesionales, tendremos también un equipo de QA o quality assurance, en
el que los técnicos son especialistas en probar y encontrar los posibles fallos del código
antes de que la aplicación llegue al usuario. Existen también otros tipos de pruebas que
pueden realizarse cuando la aplicación está completa, por ejemplo, test de seguridad
para impedir que la aplicación deje al descubierto información personal del usuario, o
datos de conexión con los que pueda emularse al usuario en el servidor. Por defecto, en
un proyecto de Android Studio, al compilar en release el código pasará por ProGuard,
que es un ofuscador y optimizador de código. Este filtro reducirá al mínimo el tamaño
del código y los recursos, y cambiará el nombre de todas las funciones, variables y clases.
De este modo, al hacker que desempaquete el APK y estudie sus archivos le resultará
casi imposible de entender. Existen otras herramientas para comprobar la seguridad de
nuestra aplicación, como Zed Attack Proxy (ZAP), Quick Android Review Kit (QARK),
Drozer o Mobile Security Framework (MobSF).
Despliegue y mantenimiento
https://developer.android.com/distribute/console
El mantenimiento de una app consiste en detectar los fallos que aparecen cuando los
usuarios ya están utilizando la aplicación. Para detectar esos fallos, podemos escuchar
diferentes fuentes. Una sería el contacto directo con los clientes, que nos comentan los
errores que han encontrado. Pero hay otros mecanismos mucho más eficientes, como
introducir un código en la app que nos avise cada vez que algo falla. Por ejemplo, con
unas pocas líneas de código, nuestra app podría llamar a los servicios de Firebase
Crashlytics cada vez que detecte una excepción. El servidor de Firebase, por su parte,
nos enviará una alerta al email informándonos del error. Entonces podríamos entrar en
la web de Crashlytics, ver los detalles del error, la línea y el archivo en el que ocurrieron,
y programar un parche de código para nuestra próxima versión.
https://firebase.google.com/docs/crashlytics/get-started-android
Programación multimedia
y dispositivos móviles
En el desarrollo de apps Android nativas podemos utilizar tanto Java como Kotlin, pero
hoy en día preferimos este último, al ser un lenguaje moderno, potente y con mayores
perspectivas. Java lleva años siendo uno de los lenguajes más versátiles, debido a la
potencia de su bytecode y su máquina virtual, que más tarde muchos otros lenguajes
han imitado. La máquina virtual de Java o JVM es una especie de procesador virtual que
interpreta los bytecodes y los ejecuta como el código máquina de cada procesador físico
en el que está instalada. Gracias al JVM, Java ha sido un lenguaje capaz de conquistar
los dispositivos más inverosímiles con gran éxito, como, por ejemplo, lavadoras o
frigoríficos inteligentes. En el caso de los dispositivos Android, las versiones previas a
Android 5, Lollipop, disponían del Dalvik, una máquina virtual que traducía el bytecode
de Java al del procesador del móvil. El formato del archivo APK y los archivos .dex que
incluye en su interior vienen de esa época, y aún se utilizan. Pero a partir de Android 5
el sistema operativo venía integrado con otra máquina más potente para la ejecución
de aplicaciones Android llamada Android Runtime (ART), que dejó obsoleto al Dalvik.
Cuando creamos nuestro proyecto, Android Studio genera una estructura de directorios
que debemos entender. Pero más allá de lo que nos ayude el IDE, es nuestra obligación
mientras desarrollamos ir creando y manteniendo una estructura de directorios lógica y
fácil de navegar. Por ejemplo, si nuestra app hace uso de una base de datos y de servicios
web, podemos crear un directorio llamado "repositorio", y dentro dos subdirectorios
llamados "local" (para la base de datos) y "remoto" (para las llamadas a los servicios
web). Cada equipo o desarrollador puede tener unas preferencias distintas a la hora de
crear la estructura de directorios, pero antes de empezar a programar, debemos llegar
a un acuerdo con el resto para que las clases, interfaces y objetos estén ordenados y
sean de fácil acceso. En todos los procesos de desarrollo debemos ser ordenados si
queremos que nuestro código sea legible y libre de errores.
Patrones de diseño
Veamos un ejemplo con un patrón de diseño sencillo, por ejemplo, el llamado Singleton,
se trata de un patrón de diseño creacional. Imaginemos que tenemos una clase FileUtil
con métodos y variables que utilizamos para copiar y pegar archivos, abrir y leer datos
de ellos, etcétera. Es necesaria solo una instancia de la clase para todas las operaciones
de la app. Pero si al crear nuestra clase FileUtil no nos aseguramos de controlar su
instanciación, puede que terminemos creando varios objetos FileUtil en diversas partes
del código. Si utilizamos el patrón Singleton, haremos que FileUtil solo pueda crearse
una vez. En Java, la clase sería algo así:
public class FileUtil {
private static FileUtil INSTANCE = null;
// Construcor privado para impedir instanciación directa
private FileUtil(){}
// Función sincronizada para evitar problemas con código asíncrono
private synchronized static void createInstance() {
if(INSTANCE == null) {
INSTANCE = new FileUtil();
}
}
// El usuario de la clase obtendrá una instancia con esta función
public static FileUtil getInstance() {
if(INSTANCE == null) createInstance();
return INSTANCE;
}
// Las funciones que realmente solucionan nuestras tareas
public void funcionesUtiles() {
//...
}
}
Por fortuna, en Kotlin todo es más sencillo, y no tendríamos más que utilizar un objeto
en lugar de una clase, de esta forma:
object FileUtil {
// Las funciones que realmente solucionan nuestras tareas
fun funcionesUtiles() {
//...
}
}
Son muchos los patrones de diseño, y hay varias formas de agruparlos. Unos se utilizan
constantemente, y otros rara vez. En cualquier caso, es aconsejable hacer un repaso de
todos ellos, porque pueden ahorrarnos muchos quebraderos de cabeza y evitar que
perdamos el tiempo reinventando la rueda. Por fortuna, incluso el SDK de Android nos
Programación multimedia
y dispositivos móviles
ayudará con esto, pues algunas de sus clases implementan ya patrones conocidos,
como, por ejemplo, el Modelo Vista VistaModelo, Adapter, Builder.
Clean Code
Robert Martin, también conocido como Uncle Bob, es un gurú de la ingeniería del
software, autor de varios libros clave en el desarrollo software. Clean Code y Clean
Architecture son dos de sus libros. En ellos se dan una serie de pautas para la
construcción de un código robusto y libre de errores de diseño y programación. El
desarrollo Clean trata de mantener unas estructuras y unas normas en nuestro código,
con el objetivo de dividir las funcionalidades en tareas cada vez más pequeñas. Así, cada
parte del código desarrollará una y solo esa pequeña tarea, y la comunicación entre las
partes está controlada para que cada una conozca y utilice solo lo imprescindible de
otra. De este modo, los errores de un módulo no se trasladarán al resto y serán
fácilmente identificables. El siguiente diagrama de Uncle Bob es ya un clásico en el
desarrollo de aplicaciones, veámoslo:
Cuando analizamos los requisitos de nuestra aplicación, encontramos los casos de uso,
es decir, las funcionalidades, las tareas que el usuario pide a la app. Esos casos de uso
necesitan estructuras básicas de datos llamadas entidades. Por ejemplo, en una app de
geolocalización, un caso de uso es “dame mi posición actual”, y la entidad sería una clase
Localización que tuviese como campos la latitud, la longitud, la altura, la precisión,
etcétera. Para presentar los datos de la identidad obtenidos por el caso de uso “dame
mi posición”, necesitaríamos un código que formatease los datos, un presenter que
convertirá los datos en coma flotante de latitud y longitud en una cadena de caracteres
formateada, por ejemplo, así: “Mi posición: $latitud / $longitud”. Pero ahora
necesitamos las clases de la interfaz gráfica para presentar ese texto en un campo
Programación multimedia
y dispositivos móviles
TextView, por ejemplo. Las flechas que se ven hacia dentro del círculo significan la
dirección de uso, es decir, las capas exteriores conocen y usan a las capas interiores,
pero no al revés. Las capas interiores son abstractas y, por lo tanto, no dependen de las
exteriores. Por ejemplo, si decidimos que nuestra app utilizará una librería de base de
datos en lugar de otra, el cambio no debería afectar a la estructura de nuestros datos,
ni a los casos de uso, ni siquiera al código que controla las peticiones a la base de datos.
SOLID
Son las siglas de cinco principios Clean que pueden ayudarnos a mejorar la calidad de
nuestro código. En la práctica, quizá no conviene esforzarse en llevarlos todos a cabo,
pero algunos son imprescindibles en cualquier proyecto, por simple que sea. Vamos a
enumerarlos rápidamente.
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
S: single responsibility. Cada clase, cada método y cada variable deben tener un único
objetivo, no pueden valer para dos cosas distintas, porque de hacerlo estarías
complicando el código innecesariamente.
O: open-closed. Las entidades deben estar abiertas para ser extendidas con más
funcionalidad, pero no debe permitirse su modificación una vez diseñadas, de modo que
la funcionalidad primordial siga siendo igual.
L: Liskov substitution. Precisamente por el principio anterior, este principio asegura que
una clase puede ser sustituida en el código por cualquiera de sus subclases sin modificar
el funcionamiento, porque hemos asegurado que llevará a cabo las mismas operaciones
básicas, aparte de otras añadidas.
2.3.1. Compilación
Antes de que nuestro código en Kotlin pueda ser ejecutado en el procesador virtual del
emulador, o en el real de un terminal físico, debe pasar por varios filtros, conocidos en
conjunto como compilador. Cuando seleccionamos la opción Run o el icono de flecha
verde del menú de Android Studio, el compilador analizará el código y comprobará que
no tenga errores, realizará algunas mejoras en él para aumentar su eficiencia y lo
transformará en un lenguaje comprensible para el procesador del móvil. Si es la primera
vez que ejecutamos nuestra app, Android Studio creará algunas nuevas carpetas dentro
del directorio de proyecto. Por ejemplo, bajo la carpeta "app", creará "build", "outputs",
"apk" y, dentro, "debug". En esa última carpeta, el entorno de desarrollo guardará el
archivo app-debug.apk, que es un APK especialmente diseñado para ser depurado en el
emulador o terminal que tengamos conectado. Es importante recordar que ese APK no
vale para ser distribuido, es solo un APK de pruebas.
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
}
No es necesario que lo entendamos todo. Aquí nos interesa comprender las partes
defaultConfig y buildTypes. En la primera van las configuraciones generales que tendrá
por defecto nuestra app. ApplicacionId es el nombre completo de nuestra app, con
nuestro dominio: "com.tudominio" y el nombre de la app: "tuaplicacion". Debe ser único
en el Play Store, o Google no te dejará subir la app. En defaultConfig se define también
la versión de nuestra app, el SDK mínimo con el que es compatible y la versión del SDK
para la que está desarrollada. En buildTypes está la opción release, para definir cómo
será compilada la app en modo producción. En este caso, está desactivada la opción
minify. Esta opción permite indicar si queremos reducir, optimizar y ofuscar nuestro
código dentro del APK para que sea menos pesado, más rápido y más difícil de
decompilar por un hacker. Así que, cuando terminemos de depurar, deberíamos activar
esta opción usando true en lugar de false para la variable minifyEnabled.
Veamos ahora cómo crear un par de variantes de producto, una para la versión demo y
otra para la versión completa:
flavorDimensions "version"
productFlavors {
demo {
dimension "version"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
}
completa {
dimension "version"
applicationIdSuffix ".completa"
versionNameSuffix "-completa"
}
}
Creamos la dimensión version, que tendrá dos opciones, demo y completa. Cada una
añadirá un sufijo al nombre de la aplicación, así como a la versión. Una vez sincronizado
el proyecto con los nuevos cambios, podríamos seleccionar la variante de compilación
que deseamos ejecutar mediante la pestaña Build Variants que podemos encontrar en
la barra izquierda de Android Studio. Veamos una imagen de las opciones que
tendríamos:
Programación multimedia
y dispositivos móviles
Manejar todas las opciones que permite Gradle es muy difícil, pero a medida que lo
utilicemos iremos aprendiendo, en cualquier caso, siempre es buen momento para
echarle una ojeada a la documentación oficial.
https://developer.android.com/studio/build
2.3.2. Preverificación
Pero aún podemos ir más allá: Android Studio nos brinda una herramienta de revisión
de código llamada Lint, que puede ayudarnos a identificar y corregir problemas
estructurales en nuestra app. Al pasar el escáner de Lint, la herramienta nos hará un
listado de problemas y recomendaciones, cada uno con un mensaje descriptivo y con un
nivel de gravedad, para mejorar el código de nuestra aplicación. Podemos configurar
Lint para que solo nos avise de errores de cierto calibre, si no nos interesan algunas de
las mejoras menos importantes que pueda sugerirnos. La herramienta Lint comprueba
nuestro proyecto Android para buscar posibles errores y posibles optimizaciones de
Programación multimedia
y dispositivos móviles
2.3.3. Empaquetado
Una vez que tenemos nuestro APK o Bundle, podremos publicarlo en el Play Store de
Google o en otros repositorios. Aunque el estándar es el Play Store, y a Google le gustaría
que fuese el único, existen otros como F-Droid, Aptoide, Getjar, Uptodown o Amazon,
pero tengamos en cuenta que, para poder instalar apps de estas fuentes, tendremos
que activar la opción del móvil: Ajustes > Seguridad > Orígenes desconocidos.
Programación multimedia
y dispositivos móviles
Estas plataformas son gratuitas y no piden nada por subir las aplicaciones. Google
tampoco pide mucho más, pero debemos registrarnos como desarrolladores antes de
que podamos subir nuestra primera app. En el proceso de registro se nos pedirá un único
pago de 25 dólares. Merece la pena, como programador, para poder acceder a la consola
de desarrollador del Play Store y a sus herramientas. Para los desarrolladores de iOS sale
bastante más caro, está en 99 dólares anuales, y también son mucho más estrictos con
el estilo de las apps que permiten subir.
Sería difícil encontrar a alguien que aún no sepa descargar, instalar y ejecutar una app
en su móvil desde cualquier store. Sin embargo, como desarrolladores debemos
aprender también otras formas de instalación y ejecución. Ya conocemos la opción del
menú de Android Studio Run > Run app o el icono de flecha verde, que lanza en el
dispositivo seleccionado la aplicación elegida, pero imaginemos que nos pasan un
archivo APK. Para instalarlo lo más rápido sería llamar al ADB desde la línea de
comandos. El Android Debug Bridge, o ADB, es una aplicación incluida en el SDK para la
depuración de aplicaciones, aunque puede ayudarnos en muchos otros casos. Podemos
acceder a la línea de comandos de nuestro ordenador y teclear adb --version, con lo que
obtendremos la versión instalada. Si el comando falla, deberemos revisar la instalación
y la variable path del sistema. Al otro lado del puente, tendremos nuestro terminal de
pruebas conectado al ordenador mediante un cable USB. Para que el móvil se
comunique con el ADB, debemos habilitar la depuración USB en Ajustes > Opciones de
desarrollo del terminal. Si las opciones de desarrollo no aparecen en el menú de ajustes
de nuestro teléfono es porque aún no las hemos activado. Para activarlas, debemos
acceder a Ajustes > Información del teléfono > Número de compilación y pulsar sobre él
siete veces seguidas: se nos mostrará un mensaje que informa de que se han activado.
En algunos móviles estas rutas pueden ser ligeramente diferentes, como, por ejemplo:
Ajustes > Sistema > Avanzado > Opciones para desarrolladores. Para asegurarnos de que
el ADB ha conectado ordenador y móvil, lancemos el comando adb devices:
Programación multimedia
y dispositivos móviles
Como podemos observar, tenemos conectados tres terminales diferentes, a los que
podemos acceder mediante comandos. Si solo tenemos un móvil conectado, es muy
sencillo, lanzamos el comando y listo. Pero si tenemos varios conectados al mismo
tiempo, debemos especificar para qué terminal va dirigida la llamada. Imaginemos que
queremos instalar nuestro APK:
Una vez instalada, podríamos lanzar la app de la siguiente manera con el comando adb
shell am start -n nombre_app/nombre_activity:
Si no hubiésemos creado los build variant, el comando podría haber sido más simple:
adb shell am start -n com.ilernaonline.miapp/.MainActivity.
https://developer.android.com/studio/command-line/adb
Programación multimedia
y dispositivos móviles
2.3.5. Depuración
En muchas ocasiones, veremos que la aplicación que estamos desarrollando tiene algún
comportamiento extraño que no podemos explicar. La mejor manera de conocer la
causa del problema y arreglar el error es depurar la app. Tenemos diferentes
alternativas. La más evidente es utilizar la opción depuración que tiene Android Studio.
Podemos ejecutar la opción del menú: Run > Debug app o hacer clic en el icono debug,
que es un bichito verde. Al seleccionar la opción debug, la aplicación será instalada en
el dispositivo seleccionado en modo depuración:
3. Cualquier variable del código podrá ser observada en modo depuración. Si se trata
de objetos, podrás navegar por todas sus propiedades.
4. Vemos cómo aparecen las variables de la función actual. En este ejemplo, como
puedes ver, la función actual es la que aparece en el panel de la izquierda:
onViewCreated.
Programación multimedia
y dispositivos móviles
Otro método de depuración muy utilizado también sería ejecutar la app sin modo de
depuración, pero insertando mensajes en puntos clave del código. De esta manera,
podríamos comprobar la lista de mensajes para entender por dónde ha pasado la
ejecución, qué valores tenía cada variable en ese punto, etcétera. Imaginemos que
queremos depurar la misma función: onViewCreated. Añadiríamos una llamada a alguna
de las función de la clase Log, por ejemplo:
Log.e("FirstFragment", "En onViewCreated, la variable vale: $variable.")
El Logcat es una gran herramienta que nos permitirá ver y filtrar los mensajes de
depuración de todas las aplicaciones arrancadas en el terminal o emulador. Debemos
tener en cuenta que las aplicaciones instaladas en release, muy probablemente, no
podrán depurarse de este modo, pues se desactiva la opción de depurado. Sí podrán
verse, no obstante, los mensajes de las excepciones que generen un crash durante la
ejecución incluso en release.
Otro caso que pudiera darse sería la necesidad de depurar una app desde un terminal
que no esté conectado a nuestro ordenador mediante un cable USB. Si el dispositivo
móvil está conectado a la misma red que nuestro ordenador, por ejemplo,
compartiendo la misma red wifi, podremos depurar la app con comandos del ADB.
Programación multimedia
y dispositivos móviles
Primero tendremos que apuntar la dirección IP del terminal en la red wifi, por ejemplo,
accediendo a Ajustes > Información del teléfono > Estado > Dirección IP. En nuestro caso,
el móvil está en la 192.168.0.18. Ahora debemos conectar el móvil a nuestro ordenador
con el cable USB y lanzar el comando adb tcpip 5555, que define el puerto del ADB como
el 5555. Ahora podemos desconectar el cable USB y lanzar el comando adb connect
192.168.0.18. Si todo va bien, recibiremos la confirmación. Ahora podremos ver el
Logcat del terminal como si estuviese conectado por USB.
2.4.1. Actividad
La actividad es la más común de las clases del SDK de Android para crear una pantalla
en nuestra app. Dentro de cada actividad, colocaremos los diferentes componentes de
la interfaz gráfica, como campos de introducción de texto, botones, etcétera. Toda
actividad debe declararse en el manifiesto de nuestra app (AndroidManifest.xml), pues,
de otro modo, obtendremos un error de compilación. Normalmente, además,
crearemos un recurso layout que defina los elementos que componen la pantalla, así
como su posición dentro de ella. De lo contrario, tendríamos que crear los componentes
en tiempo de ejecución, lo que resulta más difícil y tendrá sentido solo en algunos casos
particulares.
• LinearLayout: uno de los más sencillos y, a la vez, más usados. Este contenedor
agrupa las vistas en su interior de forma lineal, en dirección horizontal o vertical,
dependiendo del parámetro orientation. Veamos un ejemplo:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:orientation="vertical" >
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="EditText 1" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="EditText 2" />
<EditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top"
android:hint="EditText 3" />
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Button 1" />
</LinearLayout>
• TableLayout: un contenedor tipo tabla que utiliza filas y columnas para alinear
los elementos en la vista. Las columnas y filas pueden tener un tamaño particular
o ampliarse, y las celdas pueden extenderse más allá de sus límites.
https://developer.android.com/reference/androidx/constraintlayout/widget/Constr
https://developer.android.com/guide/topics/ui/declaring-layout
Dentro de cada contenedor layout podremos colocar nuestras vistas, que son los
componentes visuales que el usuario puede ver y con los que puede interactuar. Ya
hemos visto algunos, pero hagamos una lista de los más usados:
Veamos un ejemplo práctico para entender mejor estos conceptos. Crearemos un nuevo
proyecto con la opción File > New > New project > Empty Activity. Android Studio
generará el esqueleto de una app lista para ejecutar, con una sola actividad llamada
MainActivity. Si abrimos el archivo MainActivity, veremos algo como:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Los layouts se definen con código XML, pero no debemos alarmarnos antes de tiempo,
pues no tendremos que recordar todos y cada uno de los parámetros. Para facilitarnos
el trabajo, el editor de layouts dispone de capacidades gráficas con las que podremos
añadir, eliminar y distribuir los componentes de nuestra interfaz gráfica sin necesidad
de escribir código XML. Sí es cierto que, en ocasiones, nos será más sencillo modificar
directamente el código, por eso es conveniente ir comprendiendo cada etiqueta XML.
Por ejemplo, la segunda línea indica que el layout será un ConstraintLayout, que es uno
de los más utilizados actualmente. Dentro del layout encontramos un TextView, que no
es más que un texto que el usuario verá pero no podrá editar.
Hagamos una prueba: hagamos clic tras TextView, pulsemos Intro y añadamos la línea:
android:id="@+id/tvSaludo". Hemos establecido un indicador para el componente de
modo que podamos acceder a él desde nuestro código. Ahora observemos que en la
esquina superior derecha del editor existen tres opciones: Code, Split y Design. La opción
Code muestra el código XML del layout; Split muestra tanto el código como el diseño
visual; y Design muestra únicamente el diseño gráfico de la ventana.
<TextView
android:id="@+id/tvSaludo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnCambiarSaludo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvSaludo" />
</androidx.constraintlayout.widget.ConstraintLayout>
Programación multimedia
y dispositivos móviles
El editor visual ha generado el código para nuestro botón por sí solo. Volvamos al código
de la actividad, y añadamos el código:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnCambiarSaludo.setOnClickListener {
tvSaludo.text = "Bienvenidos a Android, programadores!!"
}
}
}
Quizá lo más importante de las clases del SDK de Android, y no solo hablamos de las
actividades, es el ciclo de vida. Ya vimos un esquema sobre el ciclo de vida de las
actividades en el tema anterior, pero merece la pena recalcarlo, pues muchos errores
de programación Android son debidos a un descuido en el ciclo de vida tanto de
actividades como de fragmentos, servicios y otros componentes del sistema operativo.
Estos componentes tienen lo que se denomina un contexto, que es una especie de
identificador del recurso. De hecho, una clase Activity hereda de la clase Context a través
de una larga cadena de subclases. Vamos a comprobar cómo un descuido con los
contextos puede ser un gran problema. Añadamos un código que se ejecutará siempre
que la actividad vuelva a ser visible, sobrescribiendo la función onResume:
override fun onResume() {
super.onResume()
tvSaludo.text = "Ahora el contexto es: $this"
Log.e("MainActivity", "Ahora el contexto es: $this")
}
Este código no hace más que cambiar el texto del TextView y escribir un mensaje de
error en el Logcat. Lancemos la aplicación al terminal. Ahora rotemos el terminal.
Observaremos que el identificador ha cambiado. Esto se debe a que, cada vez que
rotamos el móvil, el giroscopio o acelerómetro integrado en el terminal avisarán al
sistema operativo de que la orientación de la pantalla ha cambiado. El sistema
preguntará a la app cómo desea adaptarse al cambio de formato de portrait a landscape,
o viceversa. Por defecto, la actividad será destruida y recreada con la nueva
configuración de pantalla. Lo mismo ocurriría con otros cambios del sistema, podemos
adaptarnos a ellos o dejar que el sistema operativo destruya y vuelva a construir la
actividad.
Lo más probable, sobre todo si no utilizamos fragments, es que nuestra app esté
compuesta de más de una activity. Para navegar de una a otra, podemos utilizar un
objeto intent. Veámoslo con un ejemplo: vamos a crear una nueva actividad en nuestra
app. En el menú de Android Studio, seleccionamos File > New > Activity > Empty Activity
y aceptamos las opciones por defecto de la nueva actividad. El wizard añadirá no solo
una clase MainActivity2, sino también su layout y la línea en el manifiesto. Para navegar
de nuestra actividad a la nueva, modificaremos el código de este modo:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnCambiarSaludo.setOnClickListener {
val intent = Intent(this, MainActivity2::class.java)
startActivity(intent)
}
}
}
Creamos un objeto intent: el primer parámetro es el contexto del paquete, pero valdrá
con el contexto de nuestra actividad. El segundo parámetro es la clase de la activity a la
que deseamos navegar. Una vez creado el intent, llamaremos a la función de la
startActivity con él y la nueva actividad se mostrará sobre la actual. Decimos "sobre"
porque el sistema Android guardará una pila de vistas una sobre otra según vayamos
abriéndolas, lo que se conoce como back stack. Cuando cerremos una ventana,
presionando Atrás, por ejemplo, se mostrará la anterior, la que está debajo en la pila.
Cuando llamamos a otra activity, tanto dentro como fuera de nuestra app, podemos leer
la información que devuelve. Antes de AndroidX, el código de la primera actividad sería
algo como:
btnLanzarSegundaActividad.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivityForResult(intent, REQUEST_SEGUNDA_ACTIVIDAD)
}
Programación multimedia
y dispositivos móviles
• resultCode: normalmente será una de los valores RESULT_OK (para indicar que
la actividad terminó adecuadamente y pueden utilizarse los valores devueltos) y
RESULT_CANCELED (si el usuario canceló el proceso y debemos tomar las
medidas adecuadas ante el rechazo del proceso).
• data: un objeto intent con los datos requeridos por la activity que lanzamos.
Podremos obtener los datos mediante diferentes métodos en función al tipo de
datos, como getStringExtra, getIntExtra, getBooleanExtra, getByteExtra,
etcétera.
Programación multimedia
y dispositivos móviles
Android maneja las pilas de actividades asociadas a lo que se conoce como tareas. Una
tarea es un conjunto de actividades con las que interactúa el usuario mientras realiza
una función. Normalmente, una aplicación tendrá una tarea con una pila de actividades,
aunque no tiene por qué ser siempre así.
El orden de las actividades en la pila no puede modificarse una vez introducidas. Sin
embargo, sí puede configurarse de antemano, mediante el uso de parámetros en la
etiqueta <activity> del manifest o mediante parámetros en la función startActivity. Los
parámetros de la función tendrán prioridad sobre los del manifest en caso de que existan
ambos y sean incompatibles.
https://developer.android.com/reference/android/app/Activity
https://developer.android.com/guide/components/activities/tasks-and-back-stack
Android Studio creará el proyecto, que tiene una clase activity llamada MainActivity con
un layout llamado activity_main. Vamos a editar el layout para que tenga todos los
campos que necesitamos. Primero creamos seis campos TextView, unos servirán de
etiquetas y otros de valores. Los dispondremos así:
Para ordenar los componentes, podemos hacer que empiecen donde acaban otros o
que empiecen donde empiezan otros. Es cuestión de jugar con los enlaces de la cajita
que aparece en Constraint Widget.
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="6969" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@+id/tvId"
app:layout_constraintTop_toBottomOf="@+id/tvId"
tools:text="John Smith" />
<TextView
android:id="@+id/tvPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@+id/tvName"
app:layout_constraintTop_toBottomOf="@+id/tvName"
tools:text="555 123 123" />
<TextView
android:id="@+id/lblId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp"
android:text="ID:"
app:layout_constraintEnd_toStartOf="@+id/tvId"
app:layout_constraintTop_toTopOf="@+id/tvId" />
<TextView
android:id="@+id/lblName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nombre:"
app:layout_constraintEnd_toEndOf="@+id/lblId"
app:layout_constraintTop_toTopOf="@+id/tvName" />
<TextView
android:id="@+id/lblPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Teléfono:"
app:layout_constraintEnd_toEndOf="@+id/lblName"
app:layout_constraintTop_toTopOf="@+id/tvPhone" />
<Button
android:id="@+id/btnContacto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Contacto..."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvPhone" />
<Button
android:id="@+id/btnLlamar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Llamar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
Programación multimedia
y dispositivos móviles
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnContacto" />
</androidx.constraintlayout.widget.ConstraintLayout>
Editemos ahora el código de MainActivity. Vamos a darles una utilidad a los dos botones
que tenemos:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnContacto.setOnClickListener {
obtenerContacto()
}
btnLlamar.setOnClickListener {
llamar()
}
}
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher" ...
Programación multimedia
y dispositivos móviles
companion object {
private const val TAG = "MainActivity"
private const val PERMISO = Manifest.permission.READ_CONTACTS
private const val REQUEST_PERMISOS = 1
private const val REQUEST_CONTACTO = 1
private const val REQUEST_SEGUNDA_ACTIVIDAD = 2
}
Parece mucho trabajo, pero en realidad el sistema nos ayuda bastante a mantener la
seguridad del usuario. Si tratásemos de saltarnos este procedimiento, la llamada que
hicimos a startActivityForResult lanzaría una excepción que haría terminar la app.
Bien, ya hemos pedido permiso y hemos lanzado la activity. El usuario verá la actividad
principal de la app de contactos, con una lista de la que podrá seleccionar uno de ellos.
Cuando seleccione un contacto, la app de contactos se cerrará y volveremos a la nuestra.
De hecho, cuando lo haga, se llamará a la función onActivityResult de nuestra actividad:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK && data != null) {
when(requestCode) {
REQUEST_CONTACTO -> {
procesarContacto(data)
}
}
}
Programación multimedia
y dispositivos móviles
Este código es algo complejo, pero no hay que asustarse, es la forma que tiene Android
de cedernos los datos que tiene en el sistema, y es muy similar en todos los casos. Si un
día dudamos, no tenemos más que consultarlo en la documentación o en Google.
Básicamente, el intent que nos devolvió la actividad de contactos tiene una URL hacia
una base de datos del sistema. Mediante esa URL, creamos un cursor para ir
recuperando los datos uno por uno. Una vez recuperados los datos, cerramos el cursor
y mostramos los datos.
https://developer.android.com/studio/write/layout-editor
2.4.2. Fragment
Quizá lo primero es ver cómo luce, así que lancemos la app en un emulador o terminal
y juguemos con ella. Estudiando el código, veremos cómo Android Studio ha creado tres
clases: MainActivity, FirstFragment y SecondFragment. El código de MainActivity no
tiene nada de particular, nada que indique cómo se mostrarán los fragments. Pero
pulsemos Ctrl y hagamos clic sobre activity_main para ver el layout de la actividad.
Vemos el CoordinatorLayout y la barra de herramientas Toolbar. Más abajo veremos un
include hacia otro layout, que será donde se defina el contenido de la ventana. Mientras
presionamos Ctrl, hacemos clic sobre @layout/content_main. Veremos el código XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
ver que el gráfico de navegación se define con el atributo app:navGraph y que, en este
caso, su valor es @navigation/nav_graph. Pulsemos Ctrl y clic sobre nav_graph para ver
el gráfico de navegación:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.ilernaonline.miapp.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.ilernaonline.miapp.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
Aparte de activities y fragments, podemos utilizar los diálogos para ventanas más
sencillas. Por norma general, un diálogo no ocupará toda la pantalla, sino que mostrará
un mensaje al usuario con un par de botones. Podría mostrar, además, algunos controles
que faciliten al usuario la introducción de datos o la elección de alguna preferencia antes
de continuar otro proceso. Podríamos crear un diálogo a partir de una activity:
diseñaríamos el layout como hicimos antes, pero utilizando el tema Theme.Holo.Dialog
en el manifest: <activity android:theme="@android:style/Theme.Holo.Dialog">.
Entonces, la actividad se mostrará en forma de diálogo en lugar de mostrarse en pantalla
completa. Sin embargo, si el diseño del layout no es muy complejo, podemos utilizar las
subclases de Dialog que nos proporciona el SDK, veamos algunas:
Imaginemos ahora que requerimos al usuario que elija una opción entre varias:
val colores = arrayOf("Rojo", "Verde", "Azul")
AlertDialog.Builder(context)
.setTitle("Elige un color")
.setItems(colores) { dialog, which ->
Log.d("Dialog", "Color elegido: ${colores[which]}")
}
.create()
.show()
Establecemos la hora que se mostrará por defecto, las 12 horas y 5 minutos; false
indica que no deseamos que se muestre en formato de 24 horas. Cuando el
usuario seleccione la hora que quiere, recibiremos los datos en hourOfDay y
minute. Podemos ver el aspecto que tendrán estos diálogos en un terminal:
Android nos permite muchísimas opciones para diseñar nuestras pantallas y diálogos. Es
conveniente consultar la documentación oficial para estar actualizado con todas las
opciones.
https://developer.android.com/guide/topics/ui/dialogs
Programación multimedia
y dispositivos móviles
2.5.1. Drawables
Ya conocemos la carpeta de recursos "res" que los proyectos Android tienen dentro del
directorio "app". Dentro de "res" podemos encontrar el directorio "layout" que guarda
los diseños de nuestras pantallas, values, navigation, menu, etcétera. Encontraremos
también la carpeta "drawable". En esa carpeta, el proyecto guardará las imágenes como
recursos drawables que la app utilizará en layouts, gráficos, diálogos e iconos. Un
recurso drawable es una abstracción del SDK para manejar imágenes que puedan ser
mostradas en pantalla. Los drawables pueden cargarse mediante funciones de la API
como getDrawable o ser incluidas en otros recursos XML, como layouts, con algún
atributo como android:drawable o android:icon. Imaginemos que queremos mostrar
una imagen en uno de nuestros layouts: podemos utilizar el componente ImageView,
que es un contenedor de imágenes. El código sería algo como:
<ImageView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/imagen" />
Ya hemos visto cómo presentar las imágenes guardadas en el proyecto como recursos
drawables, pero existen otras opciones, como, por ejemplo, ShapeDrawable. La clase
ShapeDrawable es una subclase de Drawable, y es una buena opción cuando
pretendemos dibujar un gráfico bidimensional relativamente sencillo. En el código de
nuestra app, podremos codificar formas básicas, como cuadrados o círculos, utilizando
las líneas y colores que deseemos. En los objetos ShapeDrawable podemos sobrescribir
su método draw para personalizar el aspecto de la imagen. Veamos un ejemplo:
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.util.AttributeSet
import android.view.View
Hemos creado una subclase de View con dos constructores, uno que se llamará cuando
se incruste la vista en un layout y el otro para ser usado directamente en una activity o
un fragment. Dentro de la clase hemos creado un ShapeDrawable con OvalShape, que
dibujará un óvalo. Como el ancho y el alto son 100, será un círculo. Para personalizar
nuestra vista, sobrescribimos el método onDraw de View. Aquí solo tendremos que
llamar a la función draw del drawable. Veamos cómo podemos utilizar esta vista:
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return CustomDrawableView(requireContext())
}
}
En este caso, la vista ocupará todo el fragment, pero también podríamos utilizar la vista
dentro de un layout junto con otros componentes gráficos:
Programación multimedia
y dispositivos móviles
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NuestroFragment">
<com.ilernaonline.miapp.CustomDrawableView
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_second" />
...
NuestroDrawable hereda de Drawable. Crea un objeto Pain de color rojo, que utilizará
en el método draw que ya conocemos. En el método draw utilizamos el objeto Canvas
que nos pasan por parámetro para dibujar un círculo con el método drawCircle. La clase
Canvas es la responsable de dibujar todas las primitivas de imagen, se trata de un lienzo
virtual que contiene las llamadas a métodos de dibujo para finalmente presentar una
imagen. Los objetos drawables pueden utilizar un bitmap, es decir, una imagen corriente
para definir su apariencia, y/o utilizar la clase Canvas para definir formas básicas como
óvalos y rectángulos. La clase ShapeDrawable que vimos anteriormente no hace más
que utilizar las funciones del Canvas para definir su aspecto.
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="Nuestra imagen" />
Los drawables son una buena fuente de gráficos estáticos para decorar la interfaz
gráfica. ¿Pero qué ocurre si queremos mostrar una lista de productos, cada uno con una
imagen que tenemos en nuestro servidor de internet? Hay muchas formas de cargar
imágenes desde el sistema de disco o desde internet, pero una de las más prácticas es
mediante la librería Glide. Glide es una utilidad que permite manejar de forma rápida y
eficiente imágenes, cargándolas y decodificándolas con gran control sobre la memoria
y el cacheo en disco. Un problema clásico en Android relativo a la carga de bitmaps
desde un archivo era el control de la memoria. Recordemos que la memoria en los
terminales móviles está mucho más limitada que en otro tipo de máquinas. Sin embargo,
Glide cuenta con los mecanismos necesarios para que la carga, manipulación y
presentación de imágenes sea eficiente. Además, si lo que pretendemos es presentar
imágenes almacenadas en la nube, su sistema de cacheo puede ayudarnos a ahorrar
datos y ganar velocidad.
Para utilizar cualquier librería, lo primero que tenemos que hacer es añadirla a la lista
de dependencias de nuestro archivo de compilación build.gradle:
dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}
Utilizar sus funciones es muy sencillo. Imaginemos que en nuestro layout tenemos un
elemento ImageView:
<ImageView
android:id="@+id/imageView"
android:layout_width="200dp"
android:layout_height="200dp"
Debemos recordar, en este caso, que para acceder a internet necesitamos declarar el
permiso en el manifest de nuestro proyecto. De otro modo, el permiso nos será
denegado y la app fallará:
<uses-permission android:name="android.permission.INTERNET" />
https://github.com/bumptech/glide
• keyCode: un entero relacionado con una constante que nos indicará la tecla
pulsada. Si pulsamos la tecla Ctrl y hacemos clic sobre la clase KeyEvent en el
código anterior, Android Studio abrirá el código fuente de la clase. Aquí
podremos ver la definición de todas las funciones y constantes, como, por
ejemplo, KEYCODE_CAMERA, KEYCODE_A, KEYCODE_Z, etcétera.
También podríamos necesitar manejar los botones de medios. Estos botones son
botones físicos que tienen algunos dispositivos periféricos, como los auriculares o
joysticks, que se conectan al móvil. Al presionar estos botones, la aplicación recibirá un
KeyEvent, como hemos visto anteriormente. El código de estos botones, sin embargo,
comienza con KEYCODE_MEDIA en lugar de con KEYCODE.
2.7.1. Animaciones
Creemos un nuevo proyecto. En el menú de Android Studio, pulsemos File > New > New
project… > Empty Activity. Nuestra app tendrá una sola clase, llamada MainActivity, que
hace uso de un layout activity_main. En la carpeta "res", tendremos también el
directorio "drawable" con los archivos del icono de la app. Vamos a añadir un nuevo
recurso en esta carpeta. Pulsamos con el botón derecho sobre la carpeta y elegimos
New > Drawable Resource File, introducimos el nombre "ilerna" y aceptamos.
Introducimos el código:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="107dp"
android:height="57dp"
android:viewportWidth="107"
android:viewportHeight="57">
<path
android:name="one"
android:fillAlpha="1"
android:fillColor="#F00"
android:pathData="M53,11.6c-28.9,4.6 -52.6,8.3 -52.7,8.4 -0.2,-0 -0.3,7.7 -0.3,17l0,17 3.7,-0.6c2.1,-0.3 26.1,-4 53.3,-
8.3l49.5,-7.7 0.3,-17.2c0.1,-9.5 -0.1,-17.1 -0.5,-17.1 -0.5,0.1 -24.4,3.9 -53.3,8.5zM96.9,21.2c1.8,4 3.5,7.9 3.8,8.5 0.3,0.8 -0.3,1.3 -
1.6,1.3 -1.2,-0 -2.4,-0.7 -2.7,-1.5 -1.1,-2.8 -9.4,-1.1 -9.4,1.9 0,0.8 -0.9,1.6 -2,1.9 -1.1,0.3 -2,0.1 -2,-0.3 0,-0.8 6.5,-16.4 7.6,-18.1 1.4,-
2.3 3.4,-0.3 6.3,6.3zM80,24.9c0,7.5 -0.3,9 -1.7,9.6 -1.3,0.4 -2.5,-0.3 -4.3,-2.6 -1.4,-1.8 -3.5,-4.4 -4.7,-5.8l-2.2,-2.6 -0.1,6.1c0,4.6 -
0.4,6.3 -1.5,6.8 -1.4,0.5 -1.5,-0.7 -1.3,-8.7 0.3,-8.3 0.5,-9.2 2.3,-9.5 1.4,-0.1 3.1,1.4 6,5.4l4,5.6 0.3,-5.3c0.3,-6.3 0.8,-7.9 2.2,-7.9 0.6,-
0 1,3.5 1,8.9zM58.8,21.2c1.7,1.7 1.5,6.7 -0.3,7.4 -2,0.8 -1.9,2 0.5,5.4 1.9,2.6 1.9,2.8 0.3,3.5 -1.7,0.6 -3.3,-0.7 -5.9,-5.1 -1.5,-2.6 -3.4,-
0.9 -3.4,3.2 0,2.4 -0.5,3.4 -1.5,3.4 -1.2,-0 -1.5,-1.7 -1.5,-8.9l0,-9 2.8,-0.4c5.4,-0.8 7.8,-0.7 9,0.5zM43,23.5c0,1.4 -1.4,2 -6.5,2.7 -
1.1,0.2 -2.1,1.1 -2.3,2.2 -0.3,1.5 0.2,1.7 4,1.4 3.3,-0.2 4.3,-0 4.3,1.2 0,1 -1.3,1.7 -4,2 -3.2,0.4 -4.1,0.9 -4.3,2.8 -0.3,2.2 -0.2,2.3 4.9,1.6
4.1,-0.5 5.1,-0.4 4.7,0.6 -0.5,1.5 -2.7,2.3 -9,3.3l-3.8,0.6 0,-8.9c0,-9.8 -0.6,-8.8 6.5,-10.3 4,-0.9 5.5,-0.7 5.5,0.8zM20.8,33.1l-0.3,7.1
3.8,-0.6c2.8,-0.5 3.7,-0.3 3.7,0.8 0,1.5 -1.2,2 -7.2,3.1l-3.8,0.6 0,-9.1c0,-8.5 0.1,-9 2.1,-9 1.9,-0 2,0.5 1.7,7.1zM13,36c0,8.3 -0.1,9 -2,9 -
1.9,-0 -2,-0.7 -2,-9 0,-8.3 0.1,-9 2,-9 1.9,-0 2,0.7 2,9z"
/>
</vector>
Este es un drawable creado al importar una imagen vectorial. Podemos copiar este
código directamente, hay otros en la web. Si queremos ver todo el proceso, no hay más
que descargar el logo de Ilerna de la web o cualquier otra imagen que deseemos. Luego
utilizaremos un editor de imágenes para exportarlo a una imagen vectorizada SVG.
Entonces importamos la imagen en Android Studio con la opción de menú File > New >
Vector Asset:
Programación multimedia
y dispositivos móviles
Luego editamos el código para añadir algunos campos más. Estos nos servirán para
modificar en el animador los valores transparencia y color del drawable:
android:name="path_ilerna"
android:fillAlpha="1"
android:fillColor="#F00"
Una vez que tenemos el drawable, crearemos una animación. Pulsamos con el botón
derecho sobre la carpeta "res" y elegimos New > Directory. Establecemos su nombre
como "animator". Ahora pulsamos sobre la carpeta "animator", elegimos New >
Animator Resource File y usamos el nombre "animacion", con el código:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="2500"
android:propertyName="fillColor"
android:valueFrom="#F00"
android:valueTo="#00F" />
<objectAnimator
android:duration="2500"
android:propertyName="fillAlpha"
android:valueFrom="0.2"
android:valueTo="1" />
</set>
El animator especifica la duración del efecto, la propiedad sobre la que actuará y de qué
a qué valor pasará dicho atributo. Solo nos falta un archivo recurso que una el drawable
con el animator. Pulsamos con el botón derecho sobre la carpeta drawable y escogemos
New > Drawable Resource File con el nombre "ilerna_animacion". Su código será:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ilerna">
<target
android:name="path_ilerna"
android:animation="@animator/animacion" />
</animated-vector>
Android nos proporciona muchos mecanismos para animar la interfaz gráfica, tanto que
podríamos estar horas viendo todos ellos. En cualquier caso, es conveniente echar un
Programación multimedia
y dispositivos móviles
https://developer.android.com/training/animation/overview
https://github.com/android/animation-samples
2.7.2. Sonidos
Quizá los sonidos disponibles en el generador de tonos sean demasiado simples para el
efecto que buscamos. Si necesitamos sonidos más complejos, utilizaremos archivos que
hayamos creado con algún editor de audio o descargado de internet. Para reproducir
varios sonidos de forma rápida y eficiente, lo mejor es utilizar un SoundPool. Este objeto
guardará en memoria los sonidos y los reproducirá cuando se lo pidamos, mezclándolos
en el canal correspondiente.
Programación multimedia
y dispositivos móviles
Veamos cómo funciona con un ejemplo. Hemos creado una activity, y en su layout
tenemos dos botones, btnSound1 y btnSound2. Vamos a definir las variables que
usaremos en el código:
private var soundPool: SoundPool? = null
private var audioManager: AudioManager? = null
private val streamType = AudioManager.STREAM_MUSIC
private var isLoaded = false
private var sound1 = 0
private var sound2 = 0
private var volume = 0f
btnSound1.setOnClickListener {
playSound(sound1)
}
btnSound2.setOnClickListener {
playSound(sound2)
}
}
}
Ahora creamos el SoundPool con los atributos que acabamos de definir. También
establecemos el número máximo de streams, es decir, el número máximo de sonidos
que podemos reproducir en un momento dado. Si reproducimos un sonido antes de que
el anterior haya terminado, se mezclarán unos sobre otros hasta llegar al máximo.
Después de crear el SoundPool, establecemos un listener para saber cuándo termina de
cargar cada archivo de sonido. Cargar en memoria un archivo de sonido puede tardar
más o menos, dependiendo del hardware y de lo pesado que sea el fichero. Aquí hemos
simplificado un poco, porque cada vez que se cargue un sonido se llamará al listener,
pero quizá la variable isLoaded debería establecerse a true solo cuando todos los
archivos se hayan cargado, y no solo el primero. Pero sigamos adelante.
Ahora encontramos el código de carga de cada sonido. Vemos que los recursos los
adquirimos desde raw, pues los proyectos de Android no tienen una carpeta específica
para recursos de sonido. Lo siguiente es establecer la acción que se llevará a cabo con la
pulsación de cada botón. En ambos casos, utilizaremos la siguiente función:
private fun playSound(sound: Int) {
if(isLoaded) {
val leftVolumn = volume
val rightVolumn = volume
val priority = 1
val loop = 0
val rate = 1f
soundPool!!.play(sound, leftVolumn, rightVolumn, priority, loop, rate)
}
}
En ella, utilizamos el SoundPool para reproducir el audio identificado con el valor entero
que guardamos al cargar el respectivo archivo de audio. Eso es todo, podemos probar a
pulsar repetidamente ambos botones. Escucharemos cómo cada sonido se monta sobre
sí mismo y sobre el otro varias veces sin el menor problema. Cuando terminemos con el
objeto, deberíamos liberar la memoria, por ejemplo, sobrescribiendo el método onStop
de la actividad:
override fun onStop() {
super.onStop()
soundPool?.release()
soundPool = null
}
Programación multimedia
y dispositivos móviles
Los servicios son componentes que pueden realizar tareas de larga duración en segundo
plano. La ejecución en background o segundo plano significa que el usuario no percibe
la existencia de dichas tareas, ya que no hay interfaz gráfica asociada. No debemos
confundir la ejecución en background, es decir, sin interfaz gráfica, con la ejecución en
paralelo. La ejecución en paralelo significa que dos o más tareas pueden lanzarse al
mismo tiempo, ejecutándose de forma independiente la una de la otra. De este modo,
un retardo en una tarea no influye a la otra. Para llevar a cabo una ejecución en paralelo
o asíncrona, necesitaremos otros mecanismos que veremos más adelante, como hilos o
coroutines.
• Foreground o primer plano: un servicio de este tipo puede llevar a cabo tareas
de larga duración, pero se ve obligado a mostrar un indicador para que el usuario
sepa que está activo. Ese indicador no es más que una notificación en la barra de
notificaciones. Por ejemplo, en el caso de nuestra app de reproducción musical,
haríamos que se mostrase un icono en la barra de notificaciones indicando que
la app está activa. Quizá la notificación tuviese además algunos controles para
detener la reproducción, pasar a la siguiente pista o cerrar la aplicación.
Programación multimedia
y dispositivos móviles
• Background o segundo plano: un servicio de este tipo realiza una tarea sobre la
que el usuario no tiene ningún control, como, por ejemplo, un cálculo complejo
o la compresión de los mensajes en la app de mensajería. Debemos tener en
cuenta que, a partir de la versión 26 de la API de Android, los servicios en
background tienen muchas limitaciones. Android quiere mantener la seguridad
y la privacidad, y matará un servicio en background que lleve demasiado tiempo
corriendo inadvertidamente por el usuario. Por eso, debemos tener cuidado al
utilizar este tipo de servicios si no queremos que el sistema nos los cierre antes
de que hayan terminado su tarea.
Veamos un ejemplo de foreground service, que es uno de los más usados. Imaginemos
que deseamos calcular todos los números primos en un rango definido por el usuario.
Primero creamos el proyecto mediante File > New > New Project… > Empty Activity.
Vamos a añadir un par de EditText para que el usuario introduzca el rango de enteros
desde y hasta. Crearemos además un TextView para mostrar el resultado y, por último,
dos botones que controlarán el cálculo. El layout terminaría siendo algo como:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/txtDesde"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:hint="Desde"
android:inputType="number"
android:text="1"
app:layout_constraintEnd_toStartOf="@+id/txtHasta"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/txtHasta"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:hint="Hasta"
android:inputType="number"
android:text="90000"
Programación multimedia
y dispositivos móviles
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/txtDesde"
app:layout_constraintTop_toTopOf="@+id/txtDesde" />
<Button
android:id="@+id/btnCalcular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Calcular"
android:focusedByDefault="true"
app:layout_constraintEnd_toStartOf="@id/btnCancelar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtDesde" />
<Button
android:id="@+id/btnCancelar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Cancelar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnCalcular"
app:layout_constraintTop_toTopOf="@+id/btnCalcular" />
<TextView
android:id="@+id/txtContenido"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="32dp"
android:layout_marginRight="32dp"
android:scrollbars="vertical"
android:text=""
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnCalcular" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ahora crearemos un objeto que será el cerebro del cálculo de números primos:
object Primos {
private const val TAG = "Primos"
endTime = System.currentTimeMillis()
return primos
}
btnCalcular.setOnClickListener {
txtContenido.movementMethod = ScrollingMovementMethod()
txtContenido.text = ""
txtContenido.scrollY = 0
btnCancelar.setOnClickListener {
Primos.cancelar = true
}
}
super.onResume()
EventBus.getDefault().register(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventBusEvent(res: String) {
txtContenido.text = res
}
}
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
createNotificationChannel()
startForeground(1, notification)
Thread {
run {
val res = Primos.formateaPrimos(desde, hasta)
EventBus.getDefault().post("PrimosService: $res")
stopSelf()
}
}.start()
return START_NOT_STICKY
}
companion object {
const val CHANNEL_ID = "PrimosChannel"
}
}
https://developer.android.com/guide/components/services
https://developer.android.com/guide/components/broadcasts
Programación multimedia
y dispositivos móviles
Los programas, ya sean de escritorio, web o móvil, necesitan guardar datos de una
ejecución a otra para su correcto funcionamiento. Quizá necesiten almacenar opciones
de usuario, quizá guarden logs de depuración con errores y alertas, quizá estemos
hablando de un juego en el que debemos guardar los jugadores y los puntos que han
conseguido en cada partida. Veamos los métodos más usuales para guardar información
en el dispositivo móvil.
2.9.1. Persistencia
Vemos que lo primero es acceder a una instancia de SharedPreferences cono uno de los
dos métodos que hemos visto. Después, llamaríamos a edit para ponernos en modo
escritura. Ahora podríamos llamar a los métodos putX necesarios para guardar todas
Programación multimedia
y dispositivos móviles
nuestras variables. Por cada tipo de datos, tendremos una llamada a la clase. Cuando
hallamos terminado, llamaremos a apply, que dará por finalizada la edición, aunque el
proceso de escritura definitivo lo ejecutará en un hilo distinto para no paralizar la
interfaz gráfica de la activity. Si fuesen pocos campos, podríamos llamar a comit, pero
salvo que sea imprescindible, preferiremos utilizar el método apply. Veamos ahora
cómo podríamos recuperar los datos desde el archivo:
val sharedPref = getPreferences(Context.MODE_PRIVATE)
val valorInt = sp1.getInt("claveInt", 0)
val valorString = sp1.getString("claveString", null)
val valorBoolean = sp1.getBoolean("claveBoolean", false)
//...
Y eso es todo, es sencillo y rápido. Si no son demasiados valores y son básicamente pares
de clave-valor, este será el método que utilizaremos.
Si no nos bastase con eso, siempre podríamos escribir y leer de un archivo de disco. Será
algo más tedioso, pero tendremos mayor libertad. Veamos un ejemplo: crearemos una
función para escribir y otra para leer del archivo:
private fun writeToFile(data: String) {
try {
val file = openFileOutput("configuracion.txt", Context.MODE_PRIVATE)
val outputStreamWriter = OutputStreamWriter(file)
outputStreamWriter.write(data)
outputStreamWriter.close()
}
catch(e: IOException) {
Log.e("Error", "File write failed: ", e)
}
}
Cuando creamos una base de datos desde nuestra aplicación, básicamente estamos
creando un archivo que el sistema guardará en la carpeta privada de la app, de la misma
forma que con los archivos del almacenamiento interno. De este modo se mantiene la
privacidad de los datos, pues el directorio de la aplicación no es accesible para otros
usuarios o aplicaciones. Desde las primeras versiones de iOS y Android, la mejor forma
de mantener una base de datos local ha sido mediante SQLite. Aunque existen muchos
otros, el sistema de gestión de base de datos SQLite está escrito en C, por lo que es muy
eficiente, es simple pero completo y ocupa muy poco.
Hoy en día sigue siendo el preferido y está preinstalado en el sistema. Sin embargo,
debemos tener algunas consideraciones antes de usar SQLite de forma directa, porque
los sistemas relacionales como este tienen algunos inconvenientes. Su lenguaje de
consulta, llamado SQL (Structured Query Language), es potente, pero nada tiene que
ver con los lenguajes de programación orientados a objetos actuales, de modo que
existe un desacople entre el código de la aplicación y el que entiende la base de datos.
Para solucionar ese problema, podemos usar una librería ORM (Object Relational
Mapping) que traducirá de un lenguaje al otro dentro de la aplicación sin esfuerzo por
nuestra parte. Existen muchas librerías que nos ayudan a usar SQLite, pero actualmente
la preferida es Room.
Programación multimedia
y dispositivos móviles
Antes de continuar, estudiemos cómo es el lenguaje SQL que utilizan todas las bases de
datos relacionales. Sus comandos se clasifican en dos grupos: DDL (Data Definition
Language) y DML (Data Manipulation Language). El DDL se utiliza, como su nombre
indica, para definir la estructura de los datos. Sus comandos son: CREATE, ALTER, DROP
y TRUNCATE para crear, modificar y eliminar estructuras como las tablas. El DML sirve
para modificar los datos dentro de las estructuras ya existentes, y tiene los comandos:
SELECT, INSERT, UPDATE y DELETE para recuperar, insertar, actualizar y borrar los datos
dentro de las tablas.
avatar TEXT,
cinturon INTEGER,
);
jugador_id INTEGER,
fecha INTEGER,
puntos INTEGER,
nivel INTEGER
);
La tabla jugadores almacenará a los jugadores que han echado una partida hasta ahora.
Sus campos serán: jugador_id, un identificador único para identificar al jugador;
nombre, que será el alias del jugador en la interfaz gráfica del juego; avatar, que será
una URL o ruta del sistema de archivos hacia una imagen que identifique gráficamente
al jugador en el juego; y un cinturon, que no es más que el nivel que ha alcanzado el
jugador en el juego por puntos o niveles superados. Por otro lado, la tabla partidas
guardará los datos de todas las partidas jugadas por los diferentes jugadores, y sus
campos serán: partida_id, que es el identificador único de cada partida; jugador_id, que
Programación multimedia
y dispositivos móviles
Imaginemos que un nuevo jugador echa una partida: mediante el DML de SQL
guardaríamos esa nueva información:
Con el comando INSERT INTO introducimos los datos del jugador Ninja Rookie con el
código identificador 69 en la tabla jugadores, y la partida que ha jugado con el
identificador 3 y los 30 puntos obtenidos hasta el nivel 2 del juego se guardan en
partidas. Como SQLite no tiene un tipo de datos para fechas, usaremos un número
entero, como el número de segundos desde 1970, una forma clásica de contar el tiempo
en computación. De ahí la función strftime, que convierte la fecha now a segundos %s
y lo pasa a entero. Cuando después quisiéramos recuperar los datos del jugador 69,
haríamos la consulta:
Como puede observarse, SQL es sencillo y potente, solo requiere algo de estudio y
práctica. Si tuviésemos todos los datos desperdigados en un archivo del disco sería muy
costoso acceder a cualquiera de ellos sin tener que leerlo todo y decodificarlo antes. Con
SQL pedimos lo que queremos y el sistema gestor de BBDD nos lo devuelve. Sin
embargo, como programadores, no seremos nosotros los que hablemos con la BBDD,
será nuestra aplicación. Veamos entonces cómo podríamos crear y usar una base de
datos SQLite desde nuestra aplicación Android con las funciones integradas de bajo
nivel, es decir, sin utilizar ningún ORM como Room que nos facilite las cosas.
Programación multimedia
y dispositivos móviles
Lo primero es definir las estructuras que almacenarán nuestros datos, las tablas. Para
ello heredamos de la clase SQLiteOpenHelper, que será la responsable de mantener esas
estructuras en el archivo de base de datos:
class JuegoDbHelper(context: Context)
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
Como vemos, necesita un contexto, el nombre del archivo donde se almacenará la BBDD
y la versión actual, que como en nuestro caso será la primera, podríamos definir como
1. Después sobrescribimos la función onCreate para definir las tablas que darán forma a
nuestra base de datos:
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(SQL_CREATE_JUGADORES)
db.execSQL(SQL_CREATE_PARTIDAS)
}
Se nos pasa un objeto db de base de datos y llamamos a su execSQL, una función que
ejecutará el código SQL que se le pase. Las constantes string SQL_CREATE_JUGADORES
y SQL_CREATE_PARTIDAS que más tarde mostraremos son el código SQL para crear las
tablas que deseamos. Si mientras creamos la BBDD desde nuestra aplicación resulta que
ya existía otra con una versión anterior, se llamará a onUpdate, que será la encargada
de actualizar los datos antiguos a la nueva estructura:
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(SQL_DELETE_JUGADORES)
db.execSQL(SQL_DELETE_PARTIDAS)
onCreate(db)
}
Para no complicar el código, y puesto que solo tenemos una versión de nuestras tablas,
hacemos lo más sencillo, que es eliminar las tablas antiguas y volverlas a crear con la
nueva estructura, sin tener en cuenta que perderemos los datos de la versión previa en
caso de que los hubiera. El código completo sería:
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
Hemos definido los comandos SQL como constantes string para tenerlos bien definidos
solo en un lugar y no desperdigados por el código. Ahora podríamos introducir datos en
nuestra nueva base de datos. Como programamos con Kotlin, nuestros datos serán
objetos de alguna clase. Definamos nuestra clase Jugador, asociada a la tabla jugadores:
data class Jugador(
val id: Int,
val nombre: String,
val avatar: String?,
val cinturon: Int?)
Nada más que un data class de Kotlin con los campos de nuestro jugador. Añadamos
una función a la clase para insertar el propio objeto en la BBDD:
fun introducirEnBBDD(db: SQLiteDatabase) {
// Juntamos todos los campos en una colección ContentValues
val values = ContentValues().apply {
put("nombre", jugador.nombre)
put("avatar", jugador.avatar)
put("cinturon", jugador.cinturon)
}
// Insertamos la fila de datos, y el sistema devuelve el id
id = db?.insert(JuegoDbHelper.TABLE_JUGADORES, null, values)
}
jugador.insertarEnBBDD(db)
Primero creamos nuestra clase de ayuda para BBDD. Con ese objeto creamos nuestra
base de datos db. Luego creamos el Jugador, y llamamos a su método insertarEnBBDD
con el objeto db.
Como podemos observar, utilizar las funciones de bajo nivel para manejar nuestra base
de datos es posible, pero requiere mucho código para cada tabla. Pasar un objeto de
Kotlin a una fila de base de datos SQL es incómodo y repetitivo, entre otras cosas porque
para cada acción de BBDD necesitamos desgranar los campos del objeto en pares clave-
valor. Además, necesitamos conocer el lenguaje SQL para definir las tablas y sus
relaciones, y para codificar las consultas de selección, actualización y borrado.
Por fortuna, existe Room, una librería con la que crear y acceder a los datos de nuestra
BBDD SQLite de forma fácil, eficiente y orientada a objetos. Veamos cómo utilizar este
ORM. Lo primero es añadir la librería en Gradle para que esté disponible en nuestro
proyecto:
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
@Entity(tableName = "jugadores")
data class JugadorEntity(
@PrimaryKey val id: Int,
val nombre: String,
val avatar: String?,
val cinturon: Int?
)
@Entity(tableName = "partidas")
data class PartidaEntity(
@PrimaryKey val id: Int,
val jugador_id: Int,
val fecha: Long,
val puntos: Int?,
val nivel: Int?
)
datos; ahora definiremos las operaciones sobre los datos. Para ello utilizamos un objeto
DAO (Data Access Object) mediante la anotación de Room @Dao en nuestra interfaz
JuegoDao:
import androidx.room.*
@Dao
interface JuegoDao {
//SELECT
@Query("SELECT * FROM jugadores ORDER BY nombre")
suspend fun getJugadores(): List<JugadorEntity>
@Query("SELECT * FROM partidas ORDER BY fecha DESC")
suspend fun getPartidas(): List<PartidaEntity>
@Query("SELECT * FROM jugadores WHERE id = :idJugador")
suspend fun getJugadorById(idJugador: Int): JugadirEntity
//INSERT
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addPartida(partida: PartidaEntity)
//DELETE
@Query("DELETE FROM partidas")
suspend fun deletePartidas()
}
Este es un ejemplo de DAO en el que hemos definido tres funciones de selección, una
de inserción y otra de borrado sobre nuestras dos tablas, pero podemos crear fácilmente
todas las que sean necesarias. Vemos que sobre cada función existe una anotación de
Room que indica el tipo de comando y la orden SQL que ejecutar. También podemos
preguntarnos qué significa la palabra clave suspend que hay delante de la definición de
cada función. Esa palabra reservada indica que la función puede suspender la ejecución
del código. Entenderemos mejor qué significa en un capítulo posterior, cuando
estudiemos el multiproceso. Tengamos en cuenta, simplemente, que el acceso a la BBDD
es muy lento, igual que el acceso a un archivo, a la red o a la cámara, por lo que tenemos
que usar algún mecanismo por el que la interfaz gráfica de la aplicación siga
respondiendo al usuario mientras otro código realiza alguna operación pesada como
una consulta a base de datos o una llamada a un servicio web.
@Database(
entities = [JugadorEntity::class, PartidaEntity::class],
version = 1,
exportSchema = false)
abstract class JuegoDb : RoomDatabase() {
abstract val dao: JuegoDao
companion object {
private const val NAME = "juego.db"
fun buildDefault(context: Context) =
Room.databaseBuilder(context, JuegoDb::class.java, NAME)
.fallbackToDestructiveMigration()
.build()
}
}
Programación multimedia
y dispositivos móviles
Para crear nuestra clase de BBDD heredamos de RoomDatabase, que declara un objeto
interno DAO y una función factoría que devuelve una instancia de la clase. Al crear la
base de datos mediante las funciones de Room, podemos utilizar diferentes opciones.
Por ejemplo, en lugar de tener una BBDD permanente mediante un archivo, podemos
crear una BBDD en memoria que existirá solo mientras la app esté activa. También
podemos definir la forma en la que migrar los datos cuando cambie la versión, etcétera.
Para conocer todas las opciones de la clase Room, investiga un poco más en:
https://developer.android.com/reference/androidx/room/Room
Y ese es todo el código que necesitamos. Una vez definidas las entidades y la base de
datos, es muy sencillo insertar y consultar nuestros datos. Ya podemos olvidarnos de
tablas y consultas, todo será programación orientada a objetos:
val dao = JuegoDb.buildDefault(application).dao
val jugador = dao.getJugadorById(1)
dao.addPartida(PartidaEntity(0, 1, System.currentTimeMillis(), 10, 1))
En la primera línea, creamos una base de datos con buildDefault con una referencia a la
aplicación como contexto. De la base de datos obtenemos el objeto DAO, que nos servirá
para manipular los datos. En la segunda línea obtenemos un objeto JugadorEntity desde
la base de datos mediante la función del DAO getJugadorById. En la tercera, añadimos
una línea a la tabla de partidas mediante la función del DAO addPartida.
Utilizando el ORM Room, hemos creado una interfaz que nos aísla de toda la
complejidad de usar una BBDD relacional en Android. Para pasar a producción, nuestro
ejemplo aún necesitaría añadir algunos mecanismos, como la actualización de los datos
a una nueva versión, el control de excepciones, las llamadas asíncronas, etcétera, pero
gracias a la funcionalidad y a la abstracción de Room todo será sencillo y ordenado. Para
una mejor comprensión y dominio, la documentación de Android es muy interesante,
echa un vistazo tanto al núcleo de SQLite en Android como al ORM Room:
https://developer.android.com/training/data-storage/sqlite
https://developer.android.com/training/data-storage/room
Programación multimedia
y dispositivos móviles
En ocasiones, una base de datos local puede no ser suficiente para nuestros propósitos.
Imaginemos una aplicación de chat que guarde las conversaciones y los contactos. Si
almacenásemos estos datos en una BBDD local, un mismo usuario no podría acceder a
ellos desde dos terminales distintos. Unas conversaciones estarían en el móvil y las otras
en la tablet, y sería un verdadero engorro. Para que el usuario se conecte desde
cualquier dispositivo y acceda a todos sus datos indistintamente, tendríamos que
realizar un complejo sistema de sincronización entre las bases de datos locales de todos
los dispositivos, lo que implicaría un servidor en internet con la base de datos central y
un software de control muy complejo.
Hoy en día, hay sistemas como los descritos bien diseñados, eficientes y disponibles de
forma gratuita y/o de pago. Sus API de desarrollo son bastante sencillas para los
programadores, y el usuario solo necesitará una conexión a internet para acceder a sus
datos desde cualquier terminal. En ocasiones, estos sistemas permiten el cacheo y el
almacenamiento local cuando el dispositivo ha perdido acceso a la red, para luego
sincronizarse cuando obtenga conexión de nuevo. Hablamos por ejemplo de servicios
en la nube como Firebase o Backendless. Firebase es una potente herramienta diseñada
por Google, por lo que parece el candidato ideal para el uso en Android. Las bases de
datos Firebase tienen opciones gratuitas y también de pago, como muchos otros
servicios en la nube, dependiendo del volumen de tráfico y otros servicios añadidos.
Tanto Firebase como Backendless y otros permiten un modo básico gratuito que nos
permitirá hacer pruebas o apps sencillas.
Las bases de datos de Firebase son no relacionales (o NoSQL), lo que significa que no
tienen una estructura de tablas y filas ni utilizan SQL como lenguaje de definición o
acceso a datos. Sus tablas son más bien colecciones de objetos con campos anidados de
forma jerárquica en estructura de árbol de directorios que tendremos la posibilidad
definir como creamos oportuno. Para acceder al servicio de base de datos de Firebase,
necesitamos crear una cuenta en la consola de desarrollo disponible en
https://console.firebase.google.com. Desde la consola podremos gestionar usuarios,
diferentes modos de autenticación de usuario, reglas de seguridad, bases de datos,
almacenamiento de archivos, hosting de webs estáticas, funciones, etcétera.
Programación multimedia
y dispositivos móviles
class Fire {
private val CHATS: String = "ListaChats"
private val CONTACTOS: String = "ListaContactos"
data class Chat(val id: String, val otrosDatos: String)
data class Contacto(val id: String, val otrosDatos: String)
private var fbdb: FirebaseDatabase? = null
En este ejemplo, se utiliza el ID único de cada usuario para crear una rama bajo la que
se almacenarán sus datos. Para eso utilizamos la función getCurrentUserID, que obtiene
una instancia de FirebaseAuth y accede al campo uid. Cuando obtengamos una instancia
de FirebaseDatabase, podremos acceder al elemento uid bajo el que almacenaremos
todos los datos de ese usuario. Podríamos organizar los datos de otro modo, pero de
esta forma separamos completamente los datos entre usuarios, evitando posibles
filtraciones.
En resumen, la API de Firebase nos permite acceder a los elementos de una colección
agrupados en forma de árbol. En este caso particular, decidimos que las ramas
principales fuesen los identificadores de usuario, y debajo de cada una de las ramas de
usuario guardaremos las colecciones de chats y contactos de cada usuario. Es una
simplificación, y Firebase nos permitirá funcionalidades más completas y complejas,
pero es suficiente para hacernos una idea.
https://firebase.google.com/docs/database/android/start
Programación multimedia
y dispositivos móviles
Incluso antes de que existiesen los móviles, incluso antes de que los ordenadores
tuviesen varios cores o multiprocesadores, los desarrolladores de software
desarrollaron la programación asíncrona. Actualmente, todos nuestros smartphones
disponen de un procesador con varios núcleos; sería una pena no sacarles mayor partido
que ejecutar programas en un solo proceso.
En programación llamamos hilos de proceso a caminos por los que el código puede fluir
y ser ejecutado. Cuando creamos un nuevo hilo, nuestro flujo de proceso se bifurca
como un río: por un lado, seguirá el hilo principal, y por otro, el código de acceso a la
base de datos, por ejemplo. En un punto determinado, tendrán que sincronizarse para
pasarse la información necesaria uno al otro. Así será como mantendremos responsivo
el hilo principal, y, consecuentemente, la interfaz gráfica de nuestra app, para que el
usuario no quede paralizado y pueda seguir interactuando con nuestro código. En
Android tenemos bastantes formas diferentes de programación asíncrona, veamos
algunas de las más comunes:
• Java Thread API: la más básica y antigua en Java, que, por supuesto, también
podremos utilizar en Kotlin. La vimos en un ejemplo anterior, con la creación de
un Thread y su ejecución. Es potente y bien conocida, sin embargo, no es la mejor
que podremos utilizar en Android. Debido a ser la más básica, debemos conocer
muy bien su programación o cualquier pequeño fallo terminará en un problema
grave, como un deadlock y el cuelgue de nuestra app. Es difícil de depurar, y
existen en Android opciones mucho más sencillas y potentes.
Thread {
run {
val res = Primos.formateaPrimos(desde, hasta)
EventBus.getDefault().post("PrimosService: $res")
stopSelf()
}
}.start()
Programación multimedia
y dispositivos móviles
• AsyncTask: es una clase sencilla de utilizar, de ahí su gran uso durante años. Sin
embargo, su funcionalidad es algo limitada, y se corre el peligro de crear una
laguna de memoria si creas un AsyncTask dentro de la clase de una activity.
Veamos un ejemplo, crearemos una clase PrimosAsyncTask:
import android.os.AsyncTask
import org.greenrobot.eventbus.EventBus
Veamos un ejemplo para aclarar cómo funcionan las coroutines. Imaginemos que
necesitamos realizar un cálculo complejo que podría paralizar la ejecución del main
thread. Como solo necesitamos realizar el cálculo si nuestra app está activa, no
utilizaremos un servicio, simplemente un sistema de programación asíncrona.
Imaginemos que tenemos un módulo que calcula los números primos en un rango:
object Primos {
var cancelar = false
<EditText
android:id="@+id/txtDesde"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:ems="10"
android:inputType="number"
android:text="1"
app:layout_constraintEnd_toStartOf="@+id/txtHasta"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/txtHasta"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:ems="10"
android:inputType="number"
android:text="90000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/txtDesde"
app:layout_constraintTop_toTopOf="@+id/txtDesde" />
<Button
android:id="@+id/btnCalcular"
Programación multimedia
y dispositivos móviles
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Calcular"
app:layout_constraintEnd_toStartOf="@id/btnCancelar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtDesde" />
<Button
android:id="@+id/btnCancelar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Cancelar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnCalcular"
app:layout_constraintTop_toBottomOf="@+id/txtDesde" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtResultado"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:scrollbars="vertical"
android:scrollbarSize="50dp"
android:text=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnCalcular" />
</androidx.constraintlayout.widget.ConstraintLayout>
Veamos cómo llamar desde nuestra activity a la función formateaPrimos sin congelar el
hilo principal. El código de la actividad será:
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
…
progressBar.visibility = View.GONE
btnCalcular.setOnClickListener {
//GlobalScope.launch {
lifecycleScope.launch {
progressBar.visibility = View.VISIBLE
progressBar.visibility = View.GONE
Programación multimedia
y dispositivos móviles
}
}
btnCancelar.setOnClickListener {
Primos.cancelar = true
progressBar.visibility = View.GONE
}
}
Como vemos, salvo por unos detalles, el código parece síncrono, como si no utilizase
hilos. Simplemente, llamamos a la función calcular con el rango establecido por el
usuario y el resultado lo asignamos al campo txtResultado. Sin embargo, si nos fijamos,
veremos que la función calcular es una función suspendida. Vemos cómo la función hace
uso de withContext, que crea un contexto de ejecución diferente: en este caso,
utilizando Dispatchers.Default, utilizará un hilo de trabajo para llevar a cabo la llamada
a Primos.formateaPrimos.
Para llamar a una función suspendida, debemos hacerlo desde un contexto de coroutine.
En este ejemplo hemos utilizado el lifecycleScope de la actividad, de modo que, si la
actividad termina, también lo hace la coroutine, liberándose los recursos necesarios.
Podríamos haber utilizado un contexto global con GlobalScope, pero tendríamos que
estar pendientes de cancelar la coroutine cuando fuese necesario, de otro modo,
seguiría ejecutándose sin que nos diésemos cuenta, malgastando recursos. Como
vemos, las coroutines son mucho más fáciles de utilizar que otros tipos de programación
asíncrona. Debemos recordar, no obstante, añadir las librerías necesarias en Gradle:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
Hay muchos detalles que aprender para usar correctamente las coroutines:
https://developer.android.com/kotlin/coroutines
Programación multimedia
y dispositivos móviles
https://developer.android.com/guide/topics/connectivity/nfc/nfc
Veamos cómo programar una aplicación que busque los dispositivos Bluetooth que
tenga a su alcance. Abrimos Android Studio y ejecutamos File > New > New Project… >
Empty Activity > Finish. Tendremos la actividad MainActivity y su layout activity_main.
Primero vamos a añadir en el build.gradle del módulo de aplicación las siguientes
librerías:
def coroutines_version = '1.3.7'
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// EventBus
implementation 'org.greenrobot:eventbus:3.2.0'
// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'
Las primeras dos instrucciones implementation son para poder utilizar coroutines en
nuestra app, de modo que podamos llamar a funciones de BT sin que el hilo principal se
bloquee. Después incluimos EventBus para enviar mensajes desde una clase a otra. Por
último, incluimos la librería de Android para utilizar las listas reciclables. El RecyclerView
es un componente de interfaz gráfica que nos permite presentar una lista de elementos
de forma eficiente.
Programación multimedia
y dispositivos móviles
Para manipular el dispositivo Bluetooth del terminal, nuestra app tendrá que registrar
algunos registros en el manifest. Además, definiremos una clase App:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ilernaonline.bluetooth">
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Como la hemos definido en el manifest, tendremos que crear una clase Application para
nuestra aplicación. Pulsemos con el botón derecho sobre el nombre del paquete de la
app y elijamos File > New > Kotlin File/Class. El código es:
import android.app.Application
companion object {
private var _instance: App? = null
val instance: App
get() = _instance!!
}
}
<Button
android:id="@+id/btnEscanear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Programación multimedia
y dispositivos móviles
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Escanear"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnVisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="VISIBLE"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/btnEscanear" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/lstEscaner"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnEscanear" />
<ProgressBar
android:id="@+id/progressBarEscaneo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/lstEscaner"
app:layout_constraintEnd_toEndOf="@+id/lstEscaner"
app:layout_constraintStart_toStartOf="@+id/lstEscaner"
app:layout_constraintTop_toTopOf="@+id/lstEscaner" />
<Button
android:id="@+id/btnPareados"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Pareados"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lstEscaner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/lstPareados"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnPareados" />
</androidx.constraintlayout.widget.ConstraintLayout>
Programación multimedia
y dispositivos móviles
Antes de ponernos con la actividad, vamos a crear un objeto que llevará a cabo todo el
trabajo de activar y escanear dispositivos Bluetooth. Pulsamos con el botón derecho
sobre el nombre del paquete de nuestra aplicación y escogemos New > Kotlin File/Class.
Lo llamaremos MiBluetooth, y lo codificaremos así:
import android.app.Activity
import android.bluetooth.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.core.content.ContextCompat.startActivity
import org.greenrobot.eventbus.EventBus
object MiBluetooth {
fun activar() {
adapter.enable()
Programación multimedia
y dispositivos móviles
adapter = appContext.getSystemService(Context.BLUETOOTH_SERVICE).adapter
No debemos preocuparnos por la manera tan sofisticada del código, hace más o menos
lo mismo, simplemente utilizamos by lazy para no obtener el objeto hasta que no sea
necesario. El resto de funciones utilizarán este objeto: por ejemplo, la función activar
no hace más que adapter.enable.
Para escanear los dispositivos Bluetooth cercanos, el proceso es algo más complicado.
Primero debemos crear un BroadcastReceiver, que es un objeto que recibe llamadas del
sistema o de otras clases. Es un mecanismo del sistema Android bastante parecido a
EventBus, de hecho, podríamos utilizar BroadcastReceiver en lugar de EventBus, pero
este último es más moderno, cómodo y eficiente. Una vez creado el BroadcastReceiver,
lo programamos para que solo reciba ciertos mensajes mediante el IntentFilter. En
nuestro caso, nos interesan los mensajes: ACTION_NAME_CHANGED, que recibiremos
Programación multimedia
y dispositivos móviles
btnVisible.setOnClickListener {
MiBluetooth.visibilizar(this, CINCO_MINUTOS)
}
btnEscanear.setOnClickListener {
btnEscanear.isEnabled = false
progressBarEscaneo.visibility = View.VISIBLE
MiBluetooth.comenzarEscaner(applicationContext)
}
btnPareados.setOnClickListener {
btnPareados.isEnabled = false
lstPareados.adapter = BluetoothListAdapter(MiBluetooth.listarPareados())
btnPareados.isEnabled = true
}
progressBarEscaneo.visibility = View.GONE
val layoutManager1 = LinearLayoutManager(this)
lstEscaner.layoutManager = layoutManager1
val layoutManager2 = LinearLayoutManager(this)
lstPareados.layoutManager = layoutManager2
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onBluetoothDevice(device: BluetoothDevice) {
Programación multimedia
y dispositivos móviles
lstEscaner.adapter = BluetoothListAdapter(MiBluetooth.escaneados)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onBluetoothEscaneoTerminado(escaneo: MiBluetooth.EscaneoTerminado) {
btnEscanear.isEnabled = true
progressBarEscaneo.visibility = View.GONE
lstEscaner.adapter =
BluetoothListAdapter(MiBluetooth.escaneados)
}
companion object {
private const val CINCO_MINUTOS = 300
}
}
En onCreate establecemos las acciones para los botones. Para el botón Visible, llamamos
a MiBluetooth.visibilizar, que pedirá al sistema Android que haga visible el dispositivo
actual durante el tiempo especificado. Para el botón Escanear, llamamos a la función
MiBluetooth.comenzarEscaner, que explicamos antes. Para obtener los dispositivos ya
pareados, llamamos a MiBluetooth.listarPareados. Observemos cómo el resultado se
utiliza como parámetro de un objeto que aún no hemos visto, BluetoothListAdapter,
pero luego hablaremos de él y de los RecyclerView.
Tras establecer las acciones para los botones, vemos cómo creamos un layout que
estableceremos en sendos RecyclerView. Esto es así debido a la gran capacidad de
adaptación que tiene este control. En nuestro caso, nos basta con el layout más sencillo,
pues solo le pedimos a cada elemento de la lista que muestre un texto plano.
Por último, solo nos queda entender cómo funciona el BluetoothListAdapter. Esta clase
será la responsable de administrar los elementos de los RecyclerView. Veamos su
código:
import android.bluetooth.BluetoothDevice
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
Programación multimedia
y dispositivos móviles
class BluetoothListAdapter(
private val dataSet: List<BluetoothDevice>)
: RecyclerView.Adapter<BluetoothListAdapter.ViewHolder>() {
Esta clase recibe una lista de objetos, en nuestro caso, una lista de BluetoothDevices. La
eficiencia de los RecyclerView reside en que, para mostrar una lista de cientos de
elementos, el componente solo tiene en memoria la representación gráfica de los
objetos que pueden verse, y no más. Para ello, define la clase ViewHolder, que es la vista
de un elemento. Cuando el usuario haga scroll sobre la lista, el RecyclerView irá
reutilizando (reciclando) los ViewHolder con los datos de los nuevos elementos ahora
visibles. Vemos cómo en onCreateViewHolder utilizamos otro layout para definir qué
aspecto tendrán los elementos de la lista. En nuestro caso, el layout del elemento es:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
tools:text="Bluetooth Fake Name"
/>
</FrameLayout>
Simplemente, un campo de texto que recibirá el nombre del Bluetooth. Como vemos,
las funciones relativas al Bluetooth son bastante sencillas gracias a las clases del SDK de
Android.
Programación multimedia
y dispositivos móviles
Veamos un ejemplo de cómo podemos escanear dispositivos BLE y listar sus servicios.
Para ello, modificaremos el proyecto del punto anterior. Primero, tendremos que añadir
un par de elementos en nuestro manifest:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
Es extraño, pero por algún motivo Android asocia el Low Energy con la localización, así
que, si necesitamos las funciones de BLE en nuestra app, tendremos que requerir el
permiso. Además, declaramos el uso de la característica BLE, aunque no lo hacemos
obligatorio, por lo que podrá seguir instalándose en móviles sin BLE. Ahora añadiremos
el objeto de utilidad:
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.*
...
object MiBluetoothLE {
private const val tag = "MiBluetoothLE"
private const val REPORT_DELAY = 10000L
appContext!!.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
fun stopScan() {
adapter.bluetoothLeScanner?.flushPendingScanResults(scanCallback)
adapter.bluetoothLeScanner?.stopScan(scanCallback)
}
Vemos que las funciones de Bluetooth LE no son muy diferentes a las que vimos en el
Bluetooth Classic. De hecho, utilizamos el mismo servicio del sistema BluetoothAdapter.
Esta vez, para escanear los dispositivos utilizaremos la función startScan del objeto
bluetoothLeScanner que nos proporciona el adapter. En realidad, también podríamos
realizar la búsqueda con el Classic y mirar uno por uno si el dispositivo encontrado es
Classic, LE o Dual. Se dice que un dispositivo Bluetooth es Dual cuando soporta ambos
protocolos. Veamos cómo sería el layout de la nueva activity:
Programación multimedia
y dispositivos móviles
<Button
android:id="@+id/btnEscanear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/escanear"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/lstEscaner"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnEscanear" />
<ProgressBar
android:id="@+id/progressBarEscaneo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/lstEscaner"
app:layout_constraintEnd_toEndOf="@+id/lstEscaner"
app:layout_constraintStart_toStartOf="@+id/lstEscaner"
app:layout_constraintTop_toTopOf="@+id/lstEscaner" />
</androidx.constraintlayout.widget.ConstraintLayout>
}
private fun onError(error: Int) {
progressBarEscaneo.visibility = View.GONE
Toast.makeText(this, R.string.error_scan_ble, Toast.LENGTH_LONG).show()
}
override fun onResume() {
super.onResume()
btnEscanear.isEnabled = MiBluetoothLE.tieneBLE
checkPermissionsForBluetoothLowEnergy()
}
private fun checkPermissionsForBluetoothLowEnergy() {
if(checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED)
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
}
}
El cliente es nuestro navegador web, que pide su contenido a los distintos servidores
por los que navega. Cliente es nuestra app de mensajería cuando se comunica con el
servidor para enviar o recibir mensajes. Incluso en los juegos en red tenemos la app
instalada como cliente y el servidor que sincroniza el mundo de los jugadores entre
todos ellos.
En realidad, el servidor no tendría por qué ser tampoco una máquina potente escondida
en algún lugar de internet. El patrón cliente-servidor también sirve para la comunicación
entre dos iguales, por ejemplo, dos móviles. Podría ser sobre internet, o sobre
Bluetooth, o sobre cualquier otro medio. Simplemente, es un marco que facilita el
establecimiento y control de la conexión. Uno de los pares actuaría de cliente, que
iniciará la conexión, mientras que el otro esperará pacientemente peticiones de
conexión desde el cliente.
y en la activity:
Programación multimedia
y dispositivos móviles
Tan simple como esto. Obtenemos una instancia de la clase SmsManager y llamamos a
sendTextMessage con el teléfono y el mensaje como parámetros. El mensaje se enviaría
de inmediato. El único problema es que no recibiríamos aviso de que se haya enviado ni
recibido. Para obtener esta confirmación, necesitaríamos un BroadcastReceiver para
saber si se ha enviado y otro para saber si se ha recibido por el destinatario. Veamos
cómo sería:
private val brEnviado = object : BroadcastReceiver() {
override fun onReceive(arg0: Context?, arg1: Intent?) {
when(resultCode) {
Activity.RESULT_OK ->
Toast.makeText(baseContext, "SMS enviado", Toast.LENGTH_SHORT).show()
SmsManager.RESULT_ERROR_GENERIC_FAILURE ->
Toast.makeText(baseContext, "Fallo desconocido", Toast.LENGTH_SHORT).show()
SmsManager.RESULT_ERROR_NO_SERVICE ->
Toast.makeText(baseContext, "Sin servicio", Toast.LENGTH_SHORT).show()
SmsManager.RESULT_ERROR_NULL_PDU ->
Toast.makeText(baseContext, "Null PDU", Toast.LENGTH_SHORT).show()
SmsManager.RESULT_ERROR_RADIO_OFF ->
Toast.makeText(baseContext, "Radio off", Toast.LENGTH_SHORT).show()
}
}
}
private val brEntregado = object : BroadcastReceiver() {
override fun onReceive(arg0: Context?, arg1: Intent?) {
when (resultCode) {
Activity.RESULT_OK ->
Toast.makeText(baseContext,"SMS delivered", Toast.LENGTH_SHORT).show()
Activity.RESULT_CANCELED ->
Toast.makeText(baseContext,"SMS not delivered", Toast.LENGTH_SHORT).show()
}
}
}
Programación multimedia
y dispositivos móviles
Nada complicado. Veamos ahora cómo recibir SMS. Lo primero, los permisos en
manifest:
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
if(pdus != null) {
val isVersionM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
msgs = arrayOfNulls(pdus.size)
for(i in msgs.indices) {
msgs[i] = SmsMessage.createFromPdu(pdus[i] as ByteArray, format)
strMessage += "SMS desde " + msgs[i]?.originatingAddress
strMessage += " :${msgs[i]?.messageBody}"
EventBus.getDefault().post(strMessage)
}
}
}
Programación multimedia
y dispositivos móviles
Como hemos visto, normalmente Android nos comunicará eventos del sistema
mediante los BroadcastReceiver. No hay mucho más, tras recibir el SMS, podríamos
analizar el mensaje para el uso que deseemos o, simplemente, mostrarlo al usuario.
Como hemos visto en el punto anterior, el envío y recepción de mensajes cortos de texto
es bastante sencillo, y puede utilizarse para la identificación de usuario o sencillamente
para comunicar un mensaje. El envío y recepción de mensajes multimedia no es mucho
más complicado, si bien su uso es escaso. Los MMS, como sabemos, pueden incluir
imágenes además de texto. Pero veamos por encima qué clases y funciones
necesitaríamos en el caso de necesitar manejar este tipo de mensajes. La clase
Telephony es la responsable de manejar todas las actividades relativas al teléfono, como
la lista de APN, SMS y MMS, llamadas, etcétera. La app deberá crear y registrar un
BroadcastReceiver para obtener notificaciones del sistema de tipo
Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION, que indicará que se ha recibido
un MMS. También debemos tener en cuenta que tanto SMS como MMS son
almacenados en las bases de datos del sistema. De este modo, tampoco sería necesario
registrar un BroadcastReceiver, sino que podríamos leer directamente esas tablas con
los mensajes recibidos.
https://developer.android.com/reference/android/provider/Telephony
Las conexiones a internet que utilizan la mayoría de las apps hoy en día utilizan los
protocolos HTTP y HTTPS. En principio, el protocolo HTTP se ideó como una capa
superior al TCP/IP que serviría para compartir documentos con enlaces de hipertexto
por los que fuese fácil navegar. Fue el surgimiento de la web, el sistema de comunicación
que más difundió el uso de internet en todo el mundo. El protocolo HTTP permitía que
el navegador, una aplicación cliente que entendía y formateaba documentos HTML,
hiciese peticiones a servidores en internet, y estos devolvían los documentos que
guardaban en sus discos duros. Pero, con el tiempo, los servidores web empezaron a ser
más complejos. Ya no solo devolvían documentos estáticos, sino que podían
componerlos en el momento de la petición, compilando información de sus bases de
datos o de otros servidores conectados. Los servidores web pasaron de ser archivadores
Programación multimedia
y dispositivos móviles
La razón por la que el protocolo HTTP es tan usado en las apps móviles es debido a la
potencia de los actuales servicios web. Mediante servicios web, por ejemplo, una app
puede informar al usuario sobre la temperatura que hace en su zona: la app pide al
sistema que le informe sobre la posición GPS del terminal; entonces la app se conecta a
internet y hace una petición a un servidor web meteorológico; este consulta sus datos y
transforma la latitud y longitud en una temperatura, la formatea y la devuelve a la app;
finalmente, la app formatea y muestra al usuario la información. Existen millones de
posibilidades, y millones de apps que utilizan millones de servicios web. Incluso las
entidades financieras se unieron a esta tecnología. Con el surgimiento del HTTP seguro
o HTTPS, los dispositivos pueden validar la autenticidad de los mensajes y encriptar las
comunicaciones con los servidores web.
Android nos permite comunicarnos a través de internet con los protocolos más básicos:
sockets de TCP/IP o UDP. Pero como no queremos reinventar la rueda, utilizaremos
alguna de las muchas librerías disponibles que construyen por nosotros la pila de
protocolos necesarios para llegar a utilizar servicios web. En la práctica, al menos de
momento, las librerías más utilizadas son OkHttp como capa base y, sobre ella, Retrofit
o Volley, que nos ayudarán aún más a convertir peticiones HTTP en meras funciones que
podremos llamar cómodamente desde nuestro código. Para transformar la información,
podremos utilizar librerías como GSON, que traduce JSON a un objeto en Kotlin.
Pero mejor veamos un ejemplo. Crearemos un nuevo proyecto de Android Studio File >
New > New project… > Empty Activity. Primero, vamos a añadir las librerías que
necesitamos en build.gradle:
def coroutines_version = '1.3.7'
def okhttp_version = '4.7.2'
def retrofit_version = '2.9.0'
def gson_version = '2.8.1'
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
Programación multimedia
y dispositivos móviles
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05"
// Network
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$gson_version"
Hemos añadido las librerías para las coroutines, porque las llamadas a internet pueden
tardar y no queremos paralizar el main thread. Después añadimos las librerías para
acceso a internet OkHttp y Retrofit. Como haremos uso de internet, debemos declarar
el permiso en el manifest:
<uses-permission android:name="android.permission.INTERNET" />
En esta aplicación vamos a utilizar el servicio web que nos ofrece GitHub. Vamos a definir
solo una función de las muchas que sirve. Lo que vemos a continuación es una especie
de descripción que le pasaremos a Retrofit para que sepa cómo hablar con el servicio de
GitHub:
import retrofit2.Response
import retrofit2.http.*
interface GithubApi {
@GET("/repositories")
suspend fun getRepoList(): Response<List<RepoEntity>>
}
Básicamente, le estamos diciendo a Retrofit que utilizaremos un servicio web con una
sola función, y que se accede a ella mediante una petición GET /repositories. Podemos
ver los datos que devuelve el servicio en nuestro navegador con la URL
https://api.github.com/repositories. El protocolo HTTP dispone de varias acciones, y la
más utilizada es GET, que solicita el documento que se le pide. Existen otras como PUT
y POST para subir datos, DELETE para borrar, etcétera. Ahora veamos cómo utilizar esta
interfaz, una vez que hemos informado a Retrofit sobre el servicio que utilizar:
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object GithubApiImpl {
private const val GITHUB_API_URL = "https://api.github.com/"
Vemos cómo Retrofit traduce automáticamente esos datos devueltos por el servidor
web de GitHub a un objeto que aún no hemos definido. Es tan fácil porque utiliza la
librería GSON para hacer el trabajo duro. Nosotros no tendremos más que definir el
objeto con algunas anotaciones para que GSON sepa cómo iniciar, con los datos que le
llegan, cada campo del objeto:
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class RepoEntity(
@SerializedName("id")
val id: String?,
@SerializedName("name")
val name: String?,
@SerializedName("description")
val description: String?
)
Programación multimedia
y dispositivos móviles
No tenemos por qué utilizar todos los campos que nos devuelve el servicio, solo los que
nos interesen. El parseador GSON olvidará el resto. Veamos cómo utilizar esta clase
desde nuestra activity:
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.launch
Como hemos visto, con la ayuda de Retrofit hemos diseñado un mecanismo para
consultar al servicio web y traducir su respuesta en un objeto con el que trabajar en
nuestro código.
soluciones: con un buen diseño, cualquier página o aplicación web puede adaptarse a
pantallas pequeñas de diversos tamaños. Es lo que se conoce como responsive design o
diseño responsivo.
https://search.google.com/test/mobile-friendly
Para desarrollar una web responsiva, debemos tener en cuenta algunos puntos:
básicamente dos tipos de pruebas: las pruebas unitarias (unit tests), que prueban
módulos puros de lenguaje sobre la máquina virtual, es decir, módulos que no tengan
llamadas al sistema de Android, y las pruebas instrumentales (instrumental tests), que
comprueban módulos más complejos con llamadas al sistema operativo y que deben ser
ejecutadas sobre un terminal, físico o emulador.
En ambos tipos pueden utilizarse, además, otras herramientas que pueden facilitarnos
las tareas. Por ejemplo, dado que las pruebas unitarias son más rápidas y efectivas de
ejecutar, quizá querríamos utilizar siempre este tipo. ¿Pero qué ocurre si nuestro código
tiene algunos objetos del sistema, como una activity? Podríamos utilizar mocks o clases
que imitan ser otras más complejas, de modo que al final el código de prueba no tenga
ninguna llamada real al sistema. Existen frameworks como Mockito que nos ayudan en
esas tareas. Para las pruebas instrumentales, también disponemos de ayudas, como las
herramientas Espresso y UI Automator.
Cuando no nos basta con comprobar el código puro, sino que deseamos comprobar
cómo funciona junto con el sistema Android, entonces tendremos que utilizar pruebas
instrumentales. Un ejemplo sería cuando deseemos probar las funciones de la interfaz
gráfica, o de una parte del hardware. Estas pruebas se ejecutan en un dispositivo móvil
o emulador con su sistema operativo Android. Al tener que instalarse en el dispositivo
cada vez que se ejecutan las pruebas, son menos eficientes que las pruebas unitarias,
pero más reales. Veamos un ejemplo:
@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest {
@Test fun useMainActivity() {
val scenario = launchActivity<MainActivity>()
scenario.moveToState(Lifecycle.State.CREATED)
scenario.onActivity { activity ->
assertEquals(View.GONE, activity.progressBar.visibility)
}
}
}
Al utilizar algunas funciones de testeo, quizá tengamos que añadir librerías a nuestro
build.gradle. En este ejemplo, para usar el launchActivity necesitamos:
// Tests
androidTestImplementation 'androidx.test:core-ktx:1.2.0'
2.16.3. Documentación
...
}
https://github.com/Kotlin/dokka
Programación multimedia
y dispositivos móviles
Veremos algunos ejemplos de cómo podemos emplear el multimedia para mejorar las
capacidades de nuestras apps. Estudiaremos algunas de las clases más interesantes,
como MediaPlayer, SoundPool o MediaRecorder y comprobaremos qué características
las hacen más convenientes en según que casos de uso. Comprender sus
funcionalidades nos permitirá en el futuro escoger los mecanismos más adecuados para
llevar a cabo la tarea que necesitemos en nuestras propias apps.
Por último, debemos considerar las fuentes de los datos multimedia, cómo acceder a
esas fuentes de la manera más eficiente. Los datos están en el almacenamiento interno,
en el externo, en internet o llegarán desde los sensores del terminal, la cámara o el
micrófono.
nuestra app, puede estar trabajando con un editor de textos o chateando por una app
de mensajería. Si en un momento dado necesita detener la reproducción o cambiar la
canción, puede lanzar comandos a nuestra aplicación, ya sea mediante la activación de
nuestra activity o mediante botones en una notificación que habremos lanzado en la
barra de notificaciones. Entendemos por ello que nuestra app de audio se ejecutará
como un servicio en segundo plano. Sin embargo, si el usuario está viendo un videoclip
en otra de nuestras apps, es lógico suponer que nuestra app tendrá simplemente una
vista enlazada al reproductor y se ejecutará solo en primer plano. En el caso de una app
captadora de vídeo o audio, podrían plantearse diferentes alternativas. Normalmente,
si grabamos vídeo, necesitaremos una app en primer plano que muestre en pantalla lo
que captamos a través de la cámara. Si adquirimos audio, podríamos plantearnos que el
servicio corra en background o no, dependiendo del análisis de necesidades que
queramos cubrir. Veamos cada caso en los siguientes puntos.
Además, con el uso de algunas clases de Android podríamos conseguir que el sistema
funcionase con más de un cliente. Imaginemos que no solo deseamos controlar la pista
actual que se está reproduciendo en el servidor mediante los controles de nuestra app,
sino que, además, deseamos permitir que otra app instalada en un smartwatch pueda
controlar el servidor. El sistema Android nos facilita el trabajo mediante el uso de las
clases MediaBrowser y MediaBrowserService. Si no necesitamos que otras aplicaciones
o módulos de terceros accedan a nuestro sistema para reproducir audio, podríamos
utilizar un servicio mucho más sencillo, heredando directamente de la clase Service.
Programación multimedia
y dispositivos móviles
https://github.com/android/uamp
Una app que muestre vídeos será en principio más sencilla, ya que no tendrá sentido
separar los controles del propio reproductor. Normalmente, el reproductor estará
enlazado a una ventana en la que volcará las imágenes del vídeo. No obstante, también
deberíamos diferenciar en el código qué parte es la interfaz gráfica y qué otra parte se
dedica al control del media, como la carga, la codificación, etcétera. En el caso de una
app reproductora de vídeo, además, la parte gráfica tiene diferentes posibilidades para
enlazar la salida del reproductor hacia la interfaz gráfica. Podríamos, por ejemplo,
utilizar un elemento VideoView en nuestro layout y cargar en él los vídeos directamente,
utilizando un MediaController para permitir que el usuario pueda controlar la
reproducción a su gusto.
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Esta sería la opción más sencilla, pero no permitiría tanto control como utilizando
directamente los objetos MediaPlayer o ExoPlayer. Más adelante veremos arquitecturas
más complejas que nos permitirán un mayor control sobre la reproducción de vídeo.
Programación multimedia
y dispositivos móviles
Ahora podremos llamar a una aplicación que permita la grabación de vídeo, es decir, a
una aplicación preinstalada que responda a una acción
MediaStore.ACTION_VIDEO_CAPTURE. Nuestro código sería:
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.core.content.FileProvider
import java.io.File
En el caso de una app captadora de audio, utilizar una app externa sería igual de sencillo
que en el caso anterior. Veamos un ejemplo:
Programación multimedia
y dispositivos móviles
import android.content.Intent
import android.provider.MediaStore
companion object {
private const val REQUEST_ID = 1337
}
}
Pero esto no son más que soluciones rápidas a un problema complejo. Tendremos
muchos más recursos si comprendemos las librerías disponibles que nos ofrece el
sistema, antes que utilizar las pocas funcionalidades que anuncian otras aplicaciones ya
instaladas.
Vamos a aprovechar esta sección para comentar brevemente la arquitectura del propio
sistema Android, porque, aunque no sea imprescindible, nos será de ayuda al hablar de
algunos conceptos de bajo nivel, como los códecs. Como vemos en la imagen, el
framework de aplicación es el nivel más alto: es el SDK, con el que los programadores
de apps nos comunicaremos. El IPC sirve para que podamos acceder a los servicios de
Android con mayor facilidad.
Programación multimedia
y dispositivos móviles
Tenemos dos tipos de servicios: los servicios del sistema y los servicios de los media.
Aquí podemos encontrar la cámara, el MediaPlayer y otros. Estos servicios, a su vez, se
comunican con el HAL. El hardware abstraction layer o HAL es una definición estándar
de las llamadas al hardware que los fabricantes de móviles deben cumplir para que el
sistema Android pueda funcionar en ellos. Más abajo tenemos el núcleo de Linux, que
es el código central de todo el sistema operativo. Ahí encontraremos los drivers que
controlarán la cámara, la pantalla, el micrófono y el sonido. Cuando desde nuestra app
preguntemos al sistema sobre un códec, es posible que haga todo este recorrido hacia
abajo al interior del sistema antes de respondernos.
En este punto es imprescindible hablar sobre la clase MediaPlayer. Este objeto nos
permitirá la reproducción de audio y vídeo de forma sencilla a la vez que eficaz. Admite
gran cantidad de formatos distintos y es eficiente en relación con la memoria y el
consumo de batería. Como es una librería del sistema, no necesitamos ningún tipo de
instalación para comenzar a utilizarla en nuestro código. La versatilidad de MediaPlayer,
unida a la funcionalidad que pueden facilitar otras clases del SDK adheridas a ella, la
hacen idónea para cualquier tarea multimedia de nuestra app. Sin embargo, aunque
funcione bien en cualquier tipo de proyecto, en algunos casos tendremos disponibles
clases más adecuadas a una funcionalidad concreta.
Como vemos, podemos utilizar simplemente la primera línea e incluirlo todo. También
podremos reducir el tamaño de nuestra app si solo añadimos los componentes que
vayamos a utilizar en nuestro proyecto en el resto de líneas. Más adelante veremos
cómo utilizar algunas de las funcionalidades de ExoPlayer en un ejemplo práctico.
Programación multimedia
y dispositivos móviles
3.4.2. Hardware
https://developer.android.com/guide/topics/manifest/uses-feature-element
Antes de empezar a codificar, debemos pensar desde dónde nos llegarán los datos
multimedia, pues, dependiendo de cada caso, la forma de acceso cambiará. Por ejemplo,
para capturar vídeo, podremos acceder directamente al hardware de la cámara o utilizar
una app intermedia. Si queremos reproducir un vídeo, quizá es estático y lo
incrustaremos en el APK. Podríamos hacerlo guardándolo en la carpeta
app/src/main/res/raw o en la carpeta app/src/main/assets. La forma de acceso sería
diferente en cada caso. Debemos tener en cuenta también otras diferencias entre los
dos directorios. Al estar "raw" dentro de "res", significa que todos los archivos que
dejemos allí se tratarán como un recurso y se les asignará un identificador que algunas
funciones aceptarán fácilmente sin tener que abrir y leer el archivo nosotros mismos. Al
ser un recurso, podremos también guardar varios distintos según el lenguaje del
terminal o el tamaño de pantalla. Nada de eso es aplicable a la carpeta "assets", pero
esta tiene otras ventajas. Por ejemplo, en la carpeta "assets" podemos crear nuestro
Programación multimedia
y dispositivos móviles
videoView.requestFocus()
videoView.start()
A diferencia de la toma de imágenes fijas, que puede ser más o menos veloz, la captación
de audio o vídeo requiere que el proceso sea suficientemente rápido o se perderán
datos y la toma carecerá de la calidad suficiente. Por fortuna, los procesos que llevan a
cabo las librerías multimedia integradas tienen en cuenta dichos aspectos por nosotros.
Por ejemplo, cuando utilizamos MediaPlayer para reproducir un audio, será este el que
Programación multimedia
y dispositivos móviles
genere los hilos de proceso adecuados para llevar a cabo sus tareas de decodificación,
cacheado, buffering y reproducción, sin que todo eso afecte al hilo principal de la app.
En algún caso excepcional, la velocidad de estas clases no será suficiente y quizá nos
veamos obligados a utilizar el NDK y saltarnos así algunos pasos innecesarios en los
procedimientos habituales del reproductor. Más probablemente, necesitaremos utilizar
alguna otra clase de las librerías multimedia más adaptada a esa necesidad particular.
Por otra parte, el tiempo también significa tamaño. Si comenzamos a grabar un audio a
una velocidad de muestreo de 8 kbps, significa que, cada segundo, nuestro archivo de
audio aumentará 8 kbits, por lo que debemos tener en cuenta el tamaño de disco
disponible en el dispositivo antes de empezar una grabación. Normalmente, las librerías
multimedia del sistema nos permitirán establecer límites de tiempo y/o tamaño del
archivo resultante de la captación de vídeo o audio. En el siguiente punto
comprobaremos lo explicado con un ejemplo.
Necesitamos los permisos para grabar audio y para guardar el audio en un archivo de
disco externo. Además, necesitamos un dispositivo con micrófono. En nuestro layout,
añadiremos dos botones, uno para comenzar y detener la grabación y otro para
reproducir el archivo de audio resultado:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnGrabar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Grabar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
Programación multimedia
y dispositivos móviles
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnReproducir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="reproducir"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnGrabar" />
</androidx.constraintlayout.widget.ConstraintLayout>
btnGrabar.setOnClickListener {
onGrabar()
}
btnReproducir.isEnabled = false
btnReproducir.setOnClickListener {
val file = getArchivoSalida()
val reproductor = MediaPlayer()
reproductor.setDataSource(file?.path)
reproductor.prepare()
reproductor.start()
}
}
requestPermissions(arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE),
6969)
}
grabadora = MediaRecorder()
grabadora.setMaxDuration(15000)//15 segundos max
grabadora.setMaxFileSize(1024*1024)//1 Mb max
grabadora.setOnErrorListener { mr: MediaRecorder, what: Int, extra: Int ->
showMensaje("Error: Error en $mr, ocurrió $what con $extra")
onGrabar()
}
Programación multimedia
y dispositivos móviles
companion object {
private const val tag = "Activity"
private const val archivo = "grabacion.3gp"
}
}
Programación multimedia
y dispositivos móviles
Antes de empezar la grabación, debemos llamar a prepare, que compilará todas las
opciones que le indicamos previamente y preparará la grabación. Ahora sí, podemos
llamar a start y comenzar a captar audio del micro.
con la configuración por defecto, listo para ser configurado de nuevo más tarde.
Después comprobamos que el archivo de audio exista y tenga datos. De ser así,
activaremos el botón btnReproducir, que utilizará el MediaPlayer para reproducir el
audio, como vimos antes.
Como hemos visto en el ejemplo, las clases de las librerías multimedia son tan potentes
que no tenemos por qué preocuparnos por el multiproceso ni por la configuración y el
acceso al hardware. Las clases nos presentarán mecanismos para informarnos del
estado del proceso mientras ellas hacen todo el trabajo duro en background.
Vamos a crear una app que reproduzca tanto videoclips como audioclips. En el código
podremos elegir entre varios formatos, y veremos que no es necesario cambiar la
configuración del MediaPlayer para que siga funcionando. En el directorio "res/raw"
insertaremos varios clips de audio y vídeo para reproducirlos desde nuestro código
indistintamente:
En Android Studio, elegimos la opción File > New > New Project… > Empty Activity.
Diseñaremos una interfaz gráfica sencilla del siguiente modo:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
Programación multimedia
y dispositivos móviles
android:id="@+id/lblAudio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/audio_clip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/btnPlayAudio" />
<Button
android:id="@+id/btnPlayAudio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="@string/play"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStopAudio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/stop"
app:layout_constraintStart_toEndOf="@+id/btnPlayAudio"
app:layout_constraintTop_toTopOf="@+id/btnPlayAudio" />
<TextView
android:id="@+id/lblVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/video_clip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/btnPlayVideo" />
<Button
android:id="@+id/btnPlayVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:text="@string/play"
app:layout_constraintStart_toEndOf="@+id/lblVideo"
app:layout_constraintTop_toBottomOf="@+id/btnPlayAudio" />
<Button
android:id="@+id/btnStopVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/stop"
app:layout_constraintStart_toEndOf="@+id/btnPlayVideo"
app:layout_constraintTop_toTopOf="@+id/btnPlayVideo" />
<VideoView
android:id="@+id/videoView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnPlayVideo" />
</androidx.constraintlayout.widget.ConstraintLayout>
<string name="pause">Pause</string>
<string name="stop">Stop</string>
<string name="audio_finish">Audio clip terminado</string>
<string name="video_finish">Video clip terminado</string>
<string name="video_clip">Vídeo Clip:</string>
<string name="audio_clip">Audio Clip:</string>
</resources>
////////////////////////////////////////////////////////////////////////////////////
/// AUDIO
private fun initAudio() {
mp = MediaPlayer.create(this, R.raw.audio_midi)
//mp = MediaPlayer.create(this, R.raw.audio_ogg)
mp.setOnCompletionListener { mp ->
stopAudio()
Toast
.makeText(this, getString(R.string.audio_finish), Toast.LENGTH_LONG)
.show()
}
btnPlayAudio.setOnClickListener {
if(mp.isPlaying)
pauseAudio()
else
playAudio()
}
btnStopAudio.setOnClickListener {
stopAudio()
}
btnPlayAudio.text = getString(R.string.play)
btnStopAudio.isEnabled = false
}
private fun playAudio() {
mp.start()
//
btnPlayAudio.text = getString(R.string.pause)
btnStopAudio.isEnabled = true
}
private fun stopAudio() {
mp.stop()
mp.prepare()
mp.seekTo(0)
//
btnPlayAudio.text = getString(R.string.play)
btnStopAudio.isEnabled = false
}
private fun pauseAudio() {
mp.pause()
//
btnPlayAudio.text = getString(R.string.play)
Programación multimedia
y dispositivos móviles
////////////////////////////////////////////////////////////////////////////////////
/// VIDEO
private fun initVideo() {
videoView.setOnCompletionListener {
stopVideo()
Toast
.makeText(this, getString(R.string.video_finish), Toast.LENGTH_LONG)
.show()
}
btnPlayVideo.setOnClickListener {
when {
videoView.isPlaying -> pauseVideo()
videoView.currentPosition != 0 -> resumeVideo()
else -> playVideo()
}
}
btnStopVideo.setOnClickListener {
stopVideo()
}
btnPlayVideo.text = getString(R.string.play)
btnStopVideo.isEnabled = false
}
private fun playVideo() {
// Archivo en directorio res/raw (Nota: no debemos escribir la extensión)
//videoView.setVideoURI(Uri.parse("android.resource://$packageName/raw/video_mp4"))
//videoView.setVideoURI(Uri.parse("android.resource://$packageName/raw/video_ogv"))
videoView.setVideoURI(Uri.parse("android.resource://$packageName/raw/video_3gp"))
val mediaController = MediaController(this)
mediaController.setMediaPlayer(videoView)
videoView.setMediaController(mediaController)
videoView.requestFocus()
videoView.start()
//
btnPlayVideo.text = getString(R.string.pause)
btnStopVideo.isEnabled = true
}
private fun resumeVideo() {
videoView.start()
//
btnPlayVideo.text = getString(R.string.pause)
}
private fun pauseVideo() {
videoView.pause()
//
btnPlayVideo.text = getString(R.string.play)
}
private fun stopVideo() {
videoView.stopPlayback()
//
btnPlayVideo.text = getString(R.string.play)
btnStopVideo.isEnabled = false
}
}
Primero definimos una variable de tipo MediaPlayer que nos servirá para reproducir el
audio. Para el vídeo, utilizaremos el componente VideoView que ya está compuesto de
una instancia de MediaPlayer en su interior. En onCreate llamamos a dos funciones, una
para iniciar el audio y otra para el vídeo.
Podemos comentar una de ellas, y será lo único que necesitemos reproducir un tipo u
otro: la clase MediaPlayer utilizará el códec correspondiente a cada uno cuando lo
reproduzca.
3.6.2. MIDI
Ya hemos visto cómo podemos reproducir clips de audio, incluidos clips MIDI, pero
merece especial atención, además, la capacidad del sistema Android para controlar
dispositivos MIDI conectados a él mediante USB o Bluetooth LE. Observemos cómo,
cuando conectamos nuestro terminal Android a un ordenador de sobremesa mediante
un cable USB, en las opciones de conexión aparecerá MIDI:
Programación multimedia
y dispositivos móviles
https://developer.android.com/reference/android/media/midi/package-summary
https://developer.android.com/ndk/guides/audio/midi
Programación multimedia
y dispositivos móviles
Presentamos aquí otras clases menos conocidas, quizá por ser más complejas, de bajo
nivel o específicas para un determinado propósito:
• MediaCodec: clase que nos permite acceder a los códecs del sistema.
• MediaMuxer: una clase que nos permite la mezcla de streams de vídeo y audio.
Soporta codificación de salida MP4, Webm y 3GP.
https://developer.android.com/reference/android/media/package-summary
Programación multimedia
y dispositivos móviles
Plantearemos aquí algunos ejemplos de clases multimedia que aún no hemos probado
anteriormente:
Ejemplo 1: SoundPool
Esta app cargará en memoria algunos clips de audio y los reproducirá a elección del
usuario. Cada sonido tendrá asociado un botón que el usuario podrá presionar para
reproducirlo. Primero, veamos el aspecto del layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:text="Sonido1"
android:tag="1"
android:onClick="onClick"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sonido2"
android:tag="2"
android:onClick="onClick"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button1"
app:layout_constraintTop_toTopOf="@+id/button1" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Sonido3"
android:tag="3"
android:onClick="onClick"
app:layout_constraintStart_toStartOf="@+id/button1"
app:layout_constraintTop_toBottomOf="@+id/button1" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sonido4"
android:tag="4"
android:onClick="onClick"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button3"
app:layout_constraintTop_toTopOf="@+id/button3" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
Programación multimedia
y dispositivos móviles
android:text="Sonido5"
android:tag="5"
android:onClick="onClick"
app:layout_constraintStart_toStartOf="@+id/button3"
app:layout_constraintTop_toBottomOf="@+id/button3" />
<Button
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sonido6"
android:tag="6"
android:onClick="onClick"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button5"
app:layout_constraintTop_toTopOf="@+id/button5" />
<Button
android:id="@+id/button7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Sonido7"
android:tag="7"
android:onClick="onClick"
app:layout_constraintStart_toStartOf="@+id/button5"
app:layout_constraintTop_toBottomOf="@+id/button5" />
<Button
android:id="@+id/button8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sonido8"
android:tag="8"
android:onClick="onClick"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button7"
app:layout_constraintTop_toTopOf="@+id/button7" />
<Switch
android:id="@+id/swtBucle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="En bucle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button7" />
<EditText
android:id="@+id/txtCanales"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:ems="10"
android:hint="Núm. Canales"
android:text="8"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/swtBucle" />
</androidx.constraintlayout.widget.ConstraintLayout>
No son más que ocho botones, un interruptor y una entrada de texto. Todos los botones
llaman a la misma acción cuando son presionados: onClick. Hemos añadido el atributo
tag para diferenciar los distintos botones en el código. El interruptor hará que los
sonidos se reproduzcan una sola vez o en bucle. La caja de texto permitirá al usuario
crear un SoundPool con un número de canales diferente: por ejemplo, si introduce 3,
solo se escucharán tres sonidos al mismo tiempo, aunque se pulsen cuatro botones
Programación multimedia
y dispositivos móviles
data class Sonido(val id: Int, var boton: Button?=null, var cargado: Boolean=false)
private val sonidos = HashMap<Int, Sonido>()
private val sonidosRes = arrayListOf(
R.raw.animal_bark_and_growl,
R.raw.animal_hiss_and_rattle,
R.raw.crow_call,
R.raw.distant_dog_barking,
R.raw.dog_barking,
R.raw.dog_growling,
R.raw.dog_snarling,
R.raw.mouse_squeaking
)
private lateinit var botonesRes: ArrayList<Button>
private var actVolume = 0f
private var maxVolume = 0f
private var volume = 0f
volumeControlStream = AudioManager.STREAM_MUSIC
Android dispone de varios canales para que el usuario pueda establecer diferentes
valores de volumen en cada uno de ellos. Es posible que quiera escuchar música al
máximo pero el canal de notificaciones dejarlo en silencio o a otro volumen diferente.
Después crearemos una instancia de la clase SoundPool con el número de canales
obtenido del usuario. El número de canales es el número máximo de audios que la clase
mezclará al mismo tiempo. También establecemos un listener para detectar cuándo ha
terminado de cargarse cada clip de audio. Cuando SoundPool termine de decodificar y
cargar en memoria uno de los audios, se llamará a esta función, donde habilitaremos el
botón correspondiente al sonido. Con el bucle for, iteramos sobre la lista de sonidos que
hemos guardado en la carpeta de recursos "res/raw", para cargarlos uno a uno en el
SoundPool. El id que nos devuelve lo utilizaremos para nuestra lista de control.
Todos los botones llaman a la misma función onClick cuando son pulsados. Esta función
obtiene el valor del tag del botón, lo convierte a entero y le resta uno. De esta manera,
cada botón estará asociado a un índice de la lista de sonidos. Una vez obtenido el sonido
que queremos reproducir, llamamos a la función play, o playLoop si el conmutador de
bucle está activo. Ambas funciones son prácticamente iguales: comprueban que el
sonido se haya cargado en memoria y llaman al método play del SoundPool. Este
método acepta primero el identificador de carga de sonido que se requiere reproducir;
en segundo y tercer lugar, el volumen del canal izquierdo y derecho respectivamente de
un sistema estéreo, con valores de 0 como mínimo a 1 como máximo, en nuestro caso,
queremos que ambos canales suenen con el mismo volumen; el cuarto parámetro es la
prioridad del sonido que reproducir, con 0 como valor mínimo de prioridad. En el
supuesto de que el número de canales sea inferior o igual a los sonidos que se están
reproduciendo, un nuevo sonido no podrá mezclarse sin más: SoundPool calculará qué
sonido debe dejar de reproducir para incluir el nuevo, y ahí la prioridad de cada sonido
entra en el cálculo.
El quinto parámetro indica si queremos que el sonido se reproduzca una vez o en bucle
de forma constante. Aquí es donde nuestras dos funciones difieren, ya que play utiliza
un valor de 0, que indica tocar solo una vez, mientras que playLoop utilizará un valor de
-1 para indicar que queremos que se toque de forma continua. El sexto parámetro es la
velocidad de reproducción, con un mínimo de 0,5 (reproducción lenta o grave) y un
Programación multimedia
y dispositivos móviles
Hoy más que nunca, este protocolo es importante. El Realtime Transport Protocol nos
permite enviar y recibir audio y vídeo a través de internet. Puede utilizarse para llamadas
telefónicas de VoIP, para escuchar música de radios digitales en internet o para ver
contenidos de vídeo desde alguna plataforma como Netflix o similar. Muchos servicios
de emergencia han abandonado ya el uso de sus radios y walkie-talkies para dar paso a
las nuevas tecnologías. El uso de apps de PTT (push to talk) sobre smartphones (PTT over
Cellular o PoC) permite el envío en tiempo real de audio y vídeo de uno a uno o en
grupos, permite asignar prioridades, etcétera.
En cualquier caso, el protocolo que utilizarán todas esas herramientas para el envió de
streams multimedia será uno basado en RTP. Por otra parte, existirá el RTP Control
Protocol (RTPC), que se encarga del control del flujo y de calidad del servicio (QoS),
ayudando a la sincronización de los diferentes streams. En cada caso concreto existirán
otros protocolos para el correcto funcionamiento del servicio. Por ejemplo, en el caso
del PTT o llamadas de VoIP, se utilizará además el protocolo SIP para realizar el inicio de
sesión de los agentes involucrados en la comunicación.
El protocolo RTP lleva en su interior el payload, es decir, los datos de audio o vídeo que
tendrán que llegar en tiempo, de modo que el cliente no perciba retardos o saltos en la
reproducción. Para ello, los paquetes RTP llevan timestamps y números de secuencia y
cuentan con mecanismos que detectan la pérdida de paquetes de datos y otros
problemas clásicos de la comunicación en tiempo real, sobre todo cuando se utiliza UDP
como protocolo de internet.
El protocolo puede utilizarse tanto de uno a uno, por ejemplo, en una llamada telefónica
de VoIP, como de uno a varios, en el caso de un proveedor de servicios multimedia a
través de IP multicasting. Como comentamos antes, para el establecimiento de la sesión
entre dos o más agentes que deseen compartir datos multimedia en tiempo real, puede
utilizarse un protocolo como SIP, pero también existen otros, como H.232, RTSP o Jingle.
Estos mecanismos podrán utilizar el Session Description Protocol (SDP) para especificar
los parámetros de la sesión, el formato del audio o vídeo que transmitir, etcétera. Cada
stream multimedia creará una sesión RTP, incluso puede separarse el vídeo del audio en
diferentes sesiones, permitiendo así que el cliente seleccione cada componente por
separado, por ejemplo, para el idioma de una película.
Programación multimedia
y dispositivos móviles
Ya hemos visto los protocolos básicos para la transmisión en tiempo real de contenido
multimedia, pero la realidad puede complicarse bastante más. Por ejemplo, los
proveedores de contenidos multimedia requieren la seguridad de que nadie podrá
consumir sus productos sin haber pasado antes por caja. También aseguran, de este
modo, los copyright de las obras transmitidas por internet, al impedir que los streams
sean copiados y reproducidos libremente. Herramientas de digital right management
(DMR) como Widevine se aplican a los streams para encriptar e impedir la copia de los
datos involucrados. La clase MediaPlayer es compatible con la reproducción de
contenido protegido por DRM a partir de Android 8 (API 26), aunque también
disponemos de otras clases como MediaDrm en caso de que necesitemos mayor control
sobre los procesos DRM. Veamos un ejemplo de cómo sería el código Android:
mediaPlayer?.apply {
setDataSource()
setOnDrmConfigHelper() // opcional, para otras configuraciones
prepare()
drmInfo?.also {
prepareDrm() //Prepara el DRM para la fuente actual
getKeyRequest() //Pide una prueba de clave al servidor de licencias
provideKeyResponse() //Procesa la prueba de clave para iniciar sesión
}
// MediaPlayer listo para reproducir
start()
// ...play/pause/resume...
stop()
releaseDrm()
}
Como vemos, las librerías de Android nos permitirán codificar las más avanzadas
técnicas multimedia. Sí es cierto que puede llegar a ser complejo según el caso, por ello,
también tenemos la opción de utilizar ExoPlayer, un potente reproductor que podemos
incrustar en nuestra app. ExoPlayer nos permite diferentes niveles de complejidad. Ya
tenemos una idea de lo compleja que resulta la reproducción en streaming, veamos
cómo ExoPlayer nos puede facilitar en gran medida nuestro trabajo, con el siguiente
ejemplo:
import android.net.Uri
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
exoPlayer = ExoPlayerFactory.newSimpleInstance(this)
dsf = DefaultDataSourceFactory(this, Util.getUserAgent(this, "ExoPlayer"))
Puesto que los datos llegan de servidores en internet, debemos incluir en nuestro
manifest el permiso android.permission.INTERNET.
Son muchas las clases de las librerías multimedia que nos permiten controlar la
reproducción de audio y vídeo. Por ejemplo, tenemos la clase VolumeShaper para
insertar atenuaciones de volumen al comienzo, al final o de transición entre clips de
audio. Podemos crear instancias de VolumeShaper desde objetos MediaPlayer o
AudioTrack y configurar con él las atenuaciones que deseemos mediante los parámetros
que definen la curva de sonido.
https://developer.android.com/guide/topics/media/volumeshaper
https://developers.google.com/cast/docs/developers
Una característica esencial que nos permite manejar Android es el control del volumen
de audio. Ya vimos en un ejemplo anterior cómo la llamada al método
setVolumeControlStream nos permite especificar el canal en el que reproduciremos
nuestros clips. Estudiamos los diferentes canales que permite Android para controlar
por separado el volumen de las notificaciones, la música, los tonos de llamada, etcétera.
De este modo, cuando el usuario modifique el volumen mientras nuestra activity está
activa en pantalla, el volumen afectará al canal que hayamos elegido y a la reproducción
de nuestros clips, estén actualmente en play o en pausa.
Por otra parte, podemos evitar cambios drásticos de sonido al desconectar el jack de
audio o unos cascos inalámbricos. Al desconectarse estos periféricos, la salida de audio
se redirecciona a los altavoces del dispositivo móvil, pudiendo causar inconvenientes
para el usuario. Dependiendo de la aplicación, podremos elegir una estrategia como
pausar el audio, o dejar que siga sonando, como en el caso de un videojuego. De todos
modos, el sistema nos avisará mediante un intent con una acción de tipo
ACTION_AUDIO_BECOMING_NOISY. En nuestra app, tendremos que crear un
BroadcastReceiver que admita esta acción y actúe en consecuencia. Veamos un ejemplo:
private val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver()
private val callback = object : MediaSession.Callback() {
override fun onPlay() {
registerReceiver(myNoisyAudioStreamReceiver, intentFilter)
}
override fun onStop() {
unregisterReceiver(myNoisyAudioStreamReceiver)
}
}
private class BecomingNoisyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
// Aquí pausamos el playback, o la acción que creamos oportuna
Log.e(tag, "BecomingNoisyReceiver:onReceive:")
}
}
}
Programación multimedia
y dispositivos móviles
Dependiendo del nivel de control que deseemos, es posible que tengamos que
movernos hacia las clases de más bajo nivel para poder configurar más detalles de la
reproducción. En cualquier caso, Android tendrá una librería adecuada para cada
problema, solo tendremos que evaluar qué nivel de control deseamos.
Programación multimedia
y dispositivos móviles
Actualmente, hay una gran variedad de motores gráficos, como Ogre (que es open
source), Doom Engine, Quake Engine, Unity, CryEngine, Source engine, Unreal
engine, Game Maker, etc.
• Según la licencia:
El objetivo de los motores gráficos es trasladar las ideas creativas de los diseñadores a
la pantalla, facilitando así la plasmación gráfica del juego.
Unity puede importar archivos desde Maya: solamente hay que colocar un archivo .mb
o .ma en la carpeta correspondiente del proyecto. Para ello, tendremos que asegurarnos
de tener Maya instalado en el ordenador. Puede importar:
Cuando importamos formatos en 3D, solemos recurrir a dos tipos de archivos: los
formatos de archivo 3D importado (.OBJ) o archivos propios de la aplicación 3D (por
ejemplo, un archivo de blender como .Blend). Esto ofrece muchas ventajas, ya que solo
se exportan los datos que se necesitan. También podemos importar mediante
conversión de archivos como, por ejemplo, blender, cinema 4D, .ma, etcétera.
Dicha estructura estará formada por una serie de bloques con una función específica
dentro del juego:
En este bloque se controlarán también las colisiones y los sprites. Mediante el uso
de librerías como OpenGL se modelan y diseñan los diferentes personajes dentro
del juego. A través de esta, es posible generar animaciones en personajes
haciendo uso de sprites (imágenes con transparencia). Esto se conoce como
renderizado de canvas.
Estos frames son procesados uno detrás de otro ofreciendo al usuario una sensación
de continuo movimiento y animación. Todos estos bloques de frames dan como
resultado la escena del juego en los diferentes momentos del tiempo.
En la actualidad existen una gran variedad de motores gráficos, como: Ogre (que es
open source), Doom Engine, Quake Engine, Unity, CryEngine, Source engine,
Unreal engine, Game Maker, etc.
- Librerías gráficas: según sus facilidades para el desarrollo y uso (SDL, XNA,
DirectX, OpenGL).
- Motores: si el motor ya tiene un desarrollo visual completo o requiere scripts
de programación para la utilización de elementos visuales. Por ejemplo,
OGRE, Unreal o id Tech son algunos de los que requieren del uso de scripts
de apoyo para la funcionalidad.
- Herramientas de creación especializadas: algunos de los motores se han
desarrollado con un carácter exclusivo, orientados en su finalidad, como
pueden ser videojuegos u otros tipos. Por ejemplo: GameMaker o ShiVa son
para el desarrollo exclusivo de aplicaciones de juegos. Por su parte, Unity es
posible utilizarlo para diferentes géneros.
• Según la licencia:
Como otros tipos de frameworks, los motores de juegos se crearon con la intención de
ofrecer un marco de trabajo que permitiese a los desarrolladores evolucionar en la
creación del producto final, sin depender de los detalles tecnológicos más frecuentes en
su desarrollo. Con un motor de juegos podremos centrarnos en la creación artística de
nuestro juego, sin perder el tiempo en complejas técnicas de renderizado, de
optimización de velocidad, de algoritmos físicos (saltos, caída de objetos, palancas,
colisiones, etc.), de algoritmos de IA o del problema de compatibilidad entre
dispositivos.
Además, también necesitaríamos: una librería para administrar y reproducir los sonidos;
un módulo para acceder a internet y sincronizar los datos entre jugadores, si queremos
un juego en red; doble o triple codificación, en caso de que queramos que sea
compatible con diferentes dispositivos.
Como vemos, el desarrollo es arduo, complejo y extenso, con un gran trabajo detrás. Un
motor de juegos nos ahorrará la mayoría de estos problemas que pueden surgir durante
el desarrollo de cualquier tipo de juego. Todos los mecanismos que solucionan esos
problemas ya han sido implementados para que no tengamos que preocuparnos una y
otra vez por ellos.
Programación multimedia
y dispositivos móviles
Desde los primeros videojuegos, los programadores empezaron a tomar nota de los
problemas y necesidades que se repetían una y otra vez. ¿Por qué reinventar la rueda?
Cuando un nuevo motor de juegos aparece y consigue un hueco en el mercado, es muy
probable que los demás se hayan quedado anticuados o este disponga de mejores
capacidades o sea más sencillo o más potente. Así, podremos escoger el que mejor se
adapte a nuestras necesidades, pero empezar desde cero nuestro proyecto sin un motor
de juegos sería tremendamente arriesgado.
En cualquier caso, todos ellos tendrán sus pros y sus contras, pero definitivamente
utilizar cualquiera de ellos será mejor que el arduo e incierto camino del desarrollo de
un videojuego desde cero, sin un motor que nos facilite el fascinante pero duro camino
de la creación de juegos.
El uso de un lenguaje u otro viene definido por el tipo de juego que se quiere desarrollar.
Los juegos 2D o plataformas, que trabajan con sprites sencillos, normalmente están
basados en el lenguaje C. En el caso de juegos cuya complejidad gráfica es mayor, sobre
todo cuando se va a trabajar con objetos tridimensionales y sus propiedades, la mayor
parte de programadores usan C++, C# y Java. Al requerir del uso de una máquina virtual,
en ocasiones son menos elegidos, aunque su potencia en desarrollo de juegos también
es alta.
Una de sus tareas es establecer la comunicación y aprovechar todos los recursos que una
tarjeta gráfica ofrece.
• Librerías: todas aquellas librerías de las que se hace uso para el desarrollo de
figuras, polígonos, luces y sombras, etc.
• Motor físico: es el encargado de gestionar las colisiones, animaciones, scripts de
programación, sonidos, físicos, etc.
• Motor de renderizado: es el encargado de renderizar todas las texturas de un
mapa, todos los relieves, suavizado de objetos, gravedad, trazado de rayas, etc.
Estos componentes recogen de manera global todos los elementos que aparecen dentro
de un juego. Cada uno de estos elementos forma parte de un conjunto de recursos que
en todo motor gráfico se pueden encontrar:
• Assets: todos los modelos 3D, texturas, materiales, animaciones, sonidos, etc. Este
grupo representa todos los elementos que formarán parte del juego.
• Renderizado: todas las texturas y materiales en esta parte hacen uso de los
recursos diseñados para el motor gráfico. Esto mostrará el aspecto visual y
potencial de un motor gráfico.
• Sonidos: es necesario configurar dentro del motor cómo serán las pistas de audio.
El sonido del videojuego dependerá de la capacidad de procesamiento de estos
sonidos. Algunas de las opciones configurables son: modificación del tono,
repetición en bucle de sonidos (looping), etc.
• Inteligencia artificial: es una de las características más importantes que puede
desarrollar un motor gráfico. Esto añade estímulos al juego, permitiendo que el
desarrollo del mismo suceda en función de una toma de decisiones definida por
una serie de reglas. Además, define el comportamiento de todos los elementos
que no son controlados por el jugador usuario, sino que forman parte de los
elementos del juego.
• Scripts visuales: no solo es posible ejecutar porciones de código definidas en el
juego, sino que además se pueden ejecutar en tiempo real dentro del aspecto
gráfico del juego.
Programación multimedia
y dispositivos móviles
Como se ha podido comprobar, las tareas que componen un motor gráfico exigen la
utilización de un gran número de recursos dentro del equipo. De ahí que, cuanto mayor
sea la capacidad de procesado y velocidad de una tarjeta gráfica, mejor será el resultado
de la escena de un juego. Para reducir el coste de esto, algunos motores emplean una
serie de técnicas que permiten renderizar los terrenos o los materiales y que no
consumen recursos, sino que aparecen dentro del espacio visual, lo cual se conoce como
culling.
Los motores gráficos son un aspecto clave dentro de un juego. Han sido creados
exclusivamente para el desarrollo de los mismos, y hoy en día son la herramienta
fundamental de creación de videojuegos. La evolución de los juegos y el entretenimiento
está ligada a la evolución de los motores de juegos.
MECANIM es el sistema de Unity que permite la animación a objetos 3D. Este sistema
posee animation clips estructurados en el Animator controller, los cuales nos permitirán
crear animaciones de modelos 3D. Además, Unity también contiene el sistema Avatar,
que permite otorgar características especiales a los personajes humanoides. Estas tres
funcionalidades se integran en el Animator component.
194
Programación multimedia
y dispositivos móviles
Animación 2D
Para esto nos hará falta nuestro personaje 2D con las imágenes de la animación
separadas en poses clave.
Por cada pose se diseña una imagen del personaje: una imagen para cuando salte, otra
para cuando duerma, otra para cuando hable… y así tendremos una o varias imágenes
para cada acción que realice. No obstante, habrá acciones que requieran más de una
imagen, ya que en cada movimiento de un salto, por ejemplo, el personaje mantendrá
una postura distinta.
Para crear la animación donde el personaje corra, por ejemplo, deberemos seleccionar
todas las imágenes referidas a esta acción y arrastrarlas a la escena. Entonces se creará
una ventana que posibilitará la creación de esa animación a la que tendremos que
nombrar. En este caso, le daremos el nombre de Correr.
De todas formas, existe otra manera de trabajar con animaciones, y es haciéndolo con
un único sprite dividido en partes (es decir, la cabeza por un lado, el brazo por otro,
etcétera).
195
Programación multimedia
y dispositivos móviles
Creando un sprite múltiple en Unity, podemos crear cada parte como un sprite
independiente y juntarlo para formar nuestro personaje, del cual podríamos ir
animando pieza a pieza creando fotogramas clave en una línea de tiempo.
En cuanto a estos dos métodos, no hay uno mejor ni otro peor, simplemente son
diferentes y podremos usarlos según el estilo que busquemos para nuestro proyecto.
• División de animaciones: los modelos con los que trabajaremos pueden ser de
varios tipos:
- Animaciones predivididas: exportadas una a una, divididas con su nombre
adecuado.
- Animaciones sin dividir: en estos casos, tenemos un clip de duración extensa
donde podemos definir el rango de frames que corresponde a cada secuencia
de animación.
Una vez creados los clips de animación, estos pueden servir para animar objetos o
propiedades a través de la ventana de animación de Unity.
196
Programación multimedia
y dispositivos móviles
Para representar los estados, a menudo se utiliza un diagrama en el que los nodos
representan los estados y las flechas las transiciones. Así, cada estado tendrá un
motion que se reproducirá cuando la máquina esté en ese estado.
197
Programación multimedia
y dispositivos móviles
Estas variables ya os sonarán de la UF anterior. Las podemos usar para que los
estados de máquina cambien cuando lleguen a cierto requisito (como, por ejemplo,
cuando nuestra variable float llegue a 8.5; podemos hacer que nuestro state
maquine pase de andar a correr).
Al final de esta unidad formativa veremos ejemplos de cómo realizar cada una de
las animaciones anteriormente descritas.
En Unity, el grafo de escena nos muestra una visión global de los elementos que
compondrán la escena, una de las múltiples de las que puede componerse el juego.
Entendemos la escena como un nivel de nuestro juego, de modo que podremos crear y
diseñar diferentes escenas, así como pasar de una a otra según el jugador vaya
avanzando en su aventura. Dentro del grafo de escena crearemos los elementos gráficos
que la componen. Algunos serán meramente decorativos, como fondos, nubes, etc.;
otros tendrán una función activa en el juego, como ascensores, enemigos, etc.
Como vemos en la siguiente imagen, desde la ventana de escena podemos probar cómo
se vería el juego en funcionamiento pulsando la tecla Play:
El grafo de escena presenta una barra de herramientas con algunas opciones que nos
ayudarán en el diseño. Por ejemplo, la opción 2D nos permite observar la escena como
se verá en la pantalla del juego. Si la desactivamos, veremos una representación
tridimensional de las capas que componen la escena, la profundidad:
Podremos navegar por la escena 3D mediante el botón derecho y la rueda central del
ratón, entre otras opciones. De este modo, podremos ver qué capas están delante de
otras y ordenar la escena en la coordenada Z de profundidad. Otras opciones del menú
de herramientas se refieren a la activación de los puntos de luz, del sonido, el número
de elementos ocultos de la escena o el tipo de vista (shaded, wireframe, etc.), entre
otras.
199
Programación multimedia
y dispositivos móviles
En Unity, de nuevo, esto es una tarea sencilla. A cada elemento que creamos en la
escena podemos asociarle un elemento collider. Este elemento tendrá la forma que
queramos darle y se moverá junto al elemento asociado de forma invisible mientras
hace su trabajo; comunicándonos con un evento de colisión siempre que toque a otro
collider. Dependiendo de nuestro juego, utilizaremos colliders 2D o 3D. Tengamos en
cuenta que las matemáticas de cálculo de colisiones son complejas, de modo que
cuantos más elementos se encuentren en escena, el cálculo completo requerirá más
recursos y tardará un mayor tiempo en completarse. En terminales móviles antiguos,
muchos elementos en escena podrían saturar el sistema.
200
Programación multimedia
y dispositivos móviles
Unity nos ofrece múltiples opciones para la forma de los colliders. Lo mismo sucederá
en una escena 3D, en la que tendremos diferentes formas básicas de objetos de colisión,
como cilindro, esfera, cubo, etc. Para asociar un collider al elemento actual
seleccionado, pulsaremos el botón Add component al final del Inspector, escogeremos
la opción Physics 2D y elegiremos el collider que creamos que se adapta mejor al juego.
En Unity, el principal elemento del módulo físico es el rigidbody. El rigidbody podrá tener
asociado un collider, y tendrá un objeto transform que permitirá establecer la posición
y el ángulo del elemento dentro de la escena. Desde el código, podremos manipular
nuestros objetos rigidbody para mover los personajes dentro de la escena, y sus colliders
nos indicarán cuando se tocan. Los rigidbody tienen un atributo bodyType que definirá
su movimiento físico en la escena. Existen tres tipos:
201
Programación multimedia
y dispositivos móviles
contra ellos. En cualquier caso, solo colisionarás con objetos dynamic. Son los
elementos más sencillos y menos exigentes con el sistema.
Además de los elementos rigidbody y de los colliders, el motor físico de Unity dispone
de otros componentes menores para la mejora de las simulaciones físicas:
Los ordenadores siguen siendo máquinas, poco más que mecanismos de relojería
fabricados con semiconductores. Su funcionamiento es programable, pero la mayoría
de los programas que desarrollamos para ellas son lineales, predecibles, limitados. Son
muy potentes y la velocidad de sus cálculos supera con creces la de nuestro cerebro. Sin
embargo, aún no son capaces de resolver problemas que a nosotros nos parecen
triviales, no muestran creatividad ni personalidad. La IA (inteligencia artificial) es un área
del desarrollo informático que pretende traspasar estas capacidades humanas, a las
máquinas.
202
Programación multimedia
y dispositivos móviles
Cada vez pueden desarrollarse aventuras gráficas más potentes, jugadores automáticos
(non-player characters) que actúan con su propia personalidad, grupos de agentes
inteligentes que se comunican y actúan como manadas de animales, o escuadrones de
guerreros que avanzan por terrenos complejos, sin que su comportamiento y su
movimiento esté programado de antemano. Todo esto se lo debemos a los miles de
algoritmos de IA que matemáticos y programadores han ido construyendo durante
décadas. Como desarrolladores, es bueno poseer un conocimiento básico de algunos de
esos algoritmos. Pero como hemos mencionado en varias ocasiones, no tiene sentido
reinventar la rueda. Además, debemos tener en cuenta que los cálculos necesarios para
estos algoritmos requieren muchos recursos del sistema, y tendremos que hacerlos
compatibles con el proceso de renderizado. Por ello, los algoritmos tienen que ser lo
más eficientes posibles.
Algunos motores como Unity incluso se apuntan a las últimas tecnologías en machine
learning, que permitirán que nuestros agentes pasen por fases de aprendizaje que les
hagan más inteligentes o, al menos, más eficientes en sus tareas. Sus librerías son tan
potentes que incluso empresas no relacionadas con los videojuegos las utilizan para
desarrollar y probar nuevos algoritmos de IA. Veamos algunas características del módulo
de IA de Unity que podemos aprovechar en nuestros proyectos:
203
Programación multimedia
y dispositivos móviles
Cualquier videojuego estaría incompleto sin un tema musical de fondo y algunos efectos
de sonido asociados a los principales eventos de la aventura. Los eventos de sonido
conseguirán que el jugador se involucre aún más con el entorno del juego y que sea
204
Programación multimedia
y dispositivos móviles
Unity pone a nuestra disposición potentes mecanismos para el control de audio, como
el sonido espacial 3D, mezcladores de sonido en tiempo real, efectos predefinidos, etc.
El sistema de sonido de Unity soporta la mayoría de formatos, y dispone de mecanismos
para que los efectos de sonido sean más realistas. Por ejemplo, para simular que el
sonido proviene de cada elemento en la escena, asociaremos el sonido al elemento, y
Unity calculará su distancia y su velocidad hacia el jugador, de modo que pueda
modificar el volumen y la frecuencia relativa al efecto Doppler. También podemos añadir
otros efectos, como eco o reverberación, de forma manual a lugares predefinidos como
túneles o pozos.
En Unity, podemos importar archivos de audio con formatos AIFF, Wav, MP3 y Ogg. No
tendremos más que arrastrar los archivos desde el explorador hacia el panel de
proyecto, como haríamos con cualquier otro recurso. Unity convertirá el archivo en un
audio clip, que podrá ser arrastrado a un objeto audio source o utilizado desde un script.
Como música de ambiente, Unity permite también importar archivos de tipo .xm, .mod,
.it y .s3m como tracker modules, que darán lugar a objetos audio clip que usaremos de
la misma forma que los efectos de sonido.
• Audio clip: contiene los datos de sonido, ya sean mono, estéreo o multicanal, usado
directamente por audio source.
• Audio source: reproduce un audio clip en la escena, ya sea hacia un audio listener o
a través de un audio mixer.
• Audio listener: actúa como un micrófono, obteniendo el sonido de cualquier audio
source de la escena y emitiéndolo hacia los altavoces.
• Audio mixer: puede llamarse desde un audio source y proporciona un
procesamiento más complejo del sonido generado desde un audio source.
No es ninguna novedad que los videojuegos pueden disfrutarse en grupo, estén los
jugadores en la misma sala o en la punta opuesta del planeta. Gracias al avance de las
redes de comunicaciones, el ancho de banda permite compartir en tiempo real todo tipo
de datos. En el caso de los videojuegos, los jugadores pueden compartir el estado
absoluto del juego entre todos ellos, de modo que navegan por un entorno virtual
sincronizado.
205
Programación multimedia
y dispositivos móviles
Unity pone a nuestra disposición dos tipos de API para el desarrollo de videojuegos
multijugador en red: una de alto nivel, con las funcionalidades más comunes cubiertas
de modo sencillo, y otra de bajo nivel, que nos permitirá mayor control, pero cuya
complejidad requerirá de mayores conocimientos. Veamos algunas de las
funcionalidades de la API de alto nivel:
206
Programación multimedia
y dispositivos móviles
Las funciones básicas utilizadas por los motores gráficos son aquellas que permiten
trabajar con elementos visuales, como son puntos, rectas, planos o polígonos.
Proveen de los recursos fundamentales en un juego como sonidos y música. El
apartado de modelado de personajes deberá recoger el uso de sprites para 2D y el
uso de modelos (assets) para el desarrollo de plataformas de juego 3D.
207
Programación multimedia
y dispositivos móviles
Otra API que ofrece este mismo tipo de gráficos 3D es Direct 3D. Ofrece una API 3D de
bajo nivel, en la que se pueden encontrar elementos básicos como: sistemas de
coordenadas, transformaciones, polígonos, puntos y rectas.
En la industria de los juegos para dispositivos móviles tienen aceptación tanto los
juegos 2D como los 3D, por lo que el abanico de posibilidades es ilimitado.
208
Programación multimedia
y dispositivos móviles
A diferencia de las aplicaciones, los juegos, por lo general, no suelen ser de código
abierto, por lo que no es posible añadir de forma legal nuevas modificaciones al juego si
no se forma parte del equipo de desarrollo o se es el autor de uno de ellos.
209
Programación multimedia
y dispositivos móviles
5. Desarrollo de juegos 2D y 3D
En este tema se estudiará el desarrollo de los juegos 2D y 3D, sus principales entornos de
desarrollo, la integración del motor de juegos en dicho entorno, conceptos de
programación 3D, sus fases de desarrollo y las propiedades de los objetos. Asimismo, se
mostrarán las diferentes aplicaciones, tanto de las funciones del motor gráfico como del
grafo de escena, así como el análisis de ejecución y optimización del código.
Existen diferentes tipos de entornos que están orientados a un tipo de juegos, ya sea para
2D o para 3D. También es posible encontrar diferentes entornos dependiendo de la
complejidad del juego a realizar.
Si el objetivo es realizar juegos sencillos cuya interfaz no sea muy exigente para
plataformas en 2D, es posible hacer uso de entornos como:
Cuando el juego a desarrollar requiere de una potencia gráfica mayor, como es el caso
del 3D, es necesario que los entornos de desarrollo sean, a su vez, más completos.
Algunos de los más importantes son:
210
Programación multimedia
y dispositivos móviles
• Unity 3D: hoy en día, Unity es una de las herramientas más utilizadas en el mundo
de los juegos, así como una de las mejor valoradas. Unity permite exportar un juego
creado en cualquiera de los distintos dispositivos. Unity está basado en el lenguaje
C#. Tiene un motor propio para el desarrollo de la parte gráfica, lo que permite llevar
a cabo un desarrollo muy completo de todas las escenas de un juego. Es posible
configurar todos los elementos necesarios, como pueden ser: la iluminación, las
texturas, los materiales, los personajes, los terrenos, los sonidos, las físicas, etc.
• Unreal Engine: junto con Unity 3D, es uno de los entornos más conocidos y
valorados dentro del mundo del desarrollo de juegos. Permite la configuración y
diseño de recursos gráficos avanzados de la misma forma que Unity.
Para ello, el primer paso es, una vez seleccionado el proyecto, ir al menú de edición y
seleccionar Preferencias. Esto mostrará la ventana de preferencias de Unity y, dentro de
ella, en el apartado de Herramientas externas, se podrán visualizar los distintos
parámetros de configuración del compilador. Es posible elegir el editor para la
programación de las líneas de código, así como la ruta en la que se encuentra en el equipo
el SDK de Android instalado.
Una vez que se ha seleccionado esta ruta, Unity será capaz de compilar un proyecto para
Android. Para realizar la compilación, debemos seleccionar el Menú configuración de la
compilación. En esta ventana se podrá elegir cuál será la plataforma de compilación, en
este caso Android. Ello realizará todo el proceso de renderizado de gráficos, así como de
programación para dicha plataforma. En este punto se añadirán las escenas deseadas
para compilar. Una vez escogida la plataforma, se tiene que seleccionar la opción de
Compilar.
Android no permite compilar sin un identificador de paquete, por lo que será necesario
definir dicho identificador que, posteriormente, será usado por Google Play para su
211
Programación multimedia
y dispositivos móviles
publicación. Dentro del apartado de Ajustes del proyecto se especificarán todos los
apartados del paquete.
Por último, una vez completados todos estos apartados, se generará en el equipo un
.apk (extensión de las aplicaciones en Android), que será el archivo ejecutable que se
instalará en el dispositivo deseado.
212
Programación multimedia
y dispositivos móviles
• Box collider: se trata de una colisión en forma de cubo. Estos generalmente son
empleados en objetos de forma cúbica, como, por ejemplo, una caja o un cofre.
• Capsule collider: se trata de una cápsula de forma ovalada formada por dos
semiesferas.
• Mesh collider: son colisionadores más precisos que van asociados a objetos 3D ya
diseñados. Ello permite crear un colisionador ajustado completamente a la forma
del objeto.
• Sphere collider: es un colisionador básico de forma esférica. Suele ser aplicado en
objetos esféricos, como pelotas, piedras, etc. Este efecto tiene un gran impacto en
objetos, los cuales aparecen rodando en la escena o se están cayendo, por ejemplo.
A través del inspector de creación de un agente se definen las propiedades que van a
caracterizar al mismo, como son:
• Radio: radio que el personaje tendrá a la hora de moverse para evitar colisiones.
• Altura: define la altura máxima de los obstáculos por los que el personaje podrá
acceder pasando por debajo de ellos.
• Velocidad: velocidad máxima en unidades por segundo que tendrá el personaje.
• Aceleración: aceleración del movimiento y acciones del personaje.
• Área: definirá el camino que tomará el personaje y cuáles no podrá escoger.
213
Programación multimedia
y dispositivos móviles
Sin embargo, las coordenadas de pantalla pueden cambiar con la resolución del
dispositivo y con la orientación de la pantalla. Por ejemplo, las coordenadas de la
interfaz de usuario son iguales a las de pantalla, con la peculiaridad de que el origen se
encuentra en la esquina superior izquierda. Además, incluso en un videojuego 3D
podemos necesitar textos, etiquetas o botones bidimensionales sobre la escena, los
cuales nos indican la vida que nos queda, los puntos acumulados, etc.
Por último, las coordenadas del mundo, o de escena, son coordenadas tridimensionales
de la escena completa. Estas coordenadas nos ayudarán a ordenar los modelos
tridimensionales en la escena, sin importar dónde esté la cámara.
214
Programación multimedia
y dispositivos móviles
5.3.2. Modelos 3D
Veamos cómo podemos utilizar el Asset Store para importar un nuevo personaje a
nuestro juego. Junto a la pestaña de Scene, normalmente encontraremos la de Game y
luego el Asset Store. Al pulsar la pestaña, podremos navegar por la web y utilizar su
buscador. En el campo de búsqueda escribimos Robot; en All categories, seleccionamos
3D y en Pricing hacemos clic en Free assets. Podemos elegir ese robot tan simpático
llamado Space Robot Kyle. Dentro de la ficha del elemento, podremos ver todas sus
215
Programación multimedia
y dispositivos móviles
5.3.3. Formas 3D
Los modelos 3D creados con complejas aplicaciones de diseño aportan gran valor a
nuestro juego. A pesar de ello, su uso puede ser ineficiente en algunos casos.
216
Programación multimedia
y dispositivos móviles
Veamos un ejemplo. En Unity, selecciona el menú GameObject > 3D Object > Cube. En
la escena aparecerá un nuevo objeto 3D en forma de cubo. Más tarde podremos añadir
texturas, materiales, propiedades físicas, colliders o cambiar sus dimensiones desde el
Inspector y sus atributos.
217
Programación multimedia
y dispositivos móviles
de un edificio de oficinas. Para ello, no tendríamos más que añadir las texturas y
materiales necesarios para que su aspecto fuese el requerido. De igual modo, podríamos
rotarlo y colocarlo en el punto de la escena donde queremos ese edificio.
Todas esas transformaciones de cada objeto serán procesadas por el motor de render,
que hará todo el trabajo duro por nosotros y devolverá una representación
bidimensional relativa a la cámara de la escena tridimensional diseñada. En tiempo de
ejecución, podremos controlar con nuestros scripts las transformaciones que queremos
para cada objeto. De este modo, podremos mover los enemigos por la escena, controlar
los movimientos mecánicos de otros objetos, como plataformas y ascensores, y
responder a la entrada del usuario para mover al jugador y a la cámara, que deberá
seguirlo.
Veremos que en la lista aparece un nuevo elemento de tipo script con el nombre que le
hayamos dado. Si pulsamos sobre el nombre del script con el botón derecho, podemos
escoger la opción Edit script del menú contextual que aparece. Rellenemos el archivo de
forma que quede así:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
218
Programación multimedia
y dispositivos móviles
void Start()
yAngle = 10;
void Update()
Hemos añadido las variables float xAngle, yAngle y zAngle, que al ser públicas podrán
ser accedidas y modificadas desde el Inspector. El método Start se ejecuta cuando la
escena carga el objeto que lleva el script. En este caso, hemos iniciado la variable con un
valor de 10, que utilizaremos más tarde en la función Update. La función Update se llama
una vez por cada frame, es decir, una vez por cada ciclo de renderizado. En este método,
obtenemos el parámetro transform del objeto en el que estamos incrustados, y
llamamos a la función Rotate, de modo que el cubo rotará. Como parámetros, la función
Rotate admite los ángulos de los tres ejes de coordenadas sobre los que debe rotar, y el
espacio sobre el que debe hacerlo: en este caso Space.Self hará que rote sobre sí mismo,
y no sobre las coordenadas del mundo. Guardemos el script y ejecutemos la escena
pulsando al icono de Play; veremos el cubo rotar sobre su eje Y. Ahora podemos cambiar
los valores de todos los ángulos y ver cómo reacciona el cubo en tiempo real. El motor
de Unity se encargará de la renderización:
219
Programación multimedia
y dispositivos móviles
Una vez documentada la historia, es necesario separar el juego en partes. Cada una
de estas partes conformará las pantallas del juego. Se debe definir asimismo cuál será
el aspecto del menú dentro de las pantallas, así como la colocación de los objetos
dentro de la misma.
2. Diseño del código: en esta fase se especifican todas las capas de las que se compone
el juego. Se trata de separar todos los aspectos básicos del juego de la funcionalidad
del mismo. Esto es lo que se conoce como framework.
El framework definirá cómo será el manejo de las ventanas del juego. Permite
asegurar que los objetos ocupan el espacio correcto dentro de la ventana que
corresponde. También se encargará del manejo de eventos de entrada de usuario.
Estos, en la mayoría de los casos, serán recogidos del teclado o ratón del equipo.
220
Programación multimedia
y dispositivos móviles
Otra de las tareas del framework es el manejo de ficheros, en los que se llevarán a
cabo las tareas de lectura y escritura como, por ejemplo, guardar las preferencias y
puntuaciones del juego.
3. Diseño de los assets: es una de las fases más complicadas y que mayor repercusión
tiene en un juego. Hace referencia a la creación de los diferentes elementos o
modelos que se pueden utilizar dentro del juego: los personajes, los logos, los
sonidos, los botones, las fuentes, etc.
4. Diseño de la lógica del juego: en esta fase es donde se define cómo se comportará el
juego. Se aplicarán las reglas ya diseñadas, así como la programación del
comportamiento de cada uno de los eventos del juego.
5. Pruebas: una de las fases más importantes. Es en este momento cuando se realiza
una comprobación de toda la aplicación con el fin de valorar el comportamiento del
juego y la aplicación correcta del resto de fases.
6. Distribución del juego: una vez finalizadas las fases de desarrollo, el objetivo es que
el producto sea distribuido. Para ello, es necesario exportar este juego de la misma
forma que una aplicación.
221
Programación multimedia
y dispositivos móviles
Efectos de posprocesamiento
1. Primero tendremos que entrar en la Asset Store de Unity y bajarnos el Asset post-
processing stack. Para entrar en la Asset Store, tenemos que ir a Window > Asset
Store o pulsar Ctrl + 9.
2. Cuando esté listo, nos aparecerá una pestaña de Importar, la cual seleccionaremos.
Ya estaremos listos para crear un posprocesado.
3. Tendremos que hacer dos cosas: primero crearemos un perfil de posprocesado (en
la pestaña Project hacemos clic derecho y después Create > Postprocesing profile).
Veremos que al pulsar este nuevo archivo tendremos varias opciones de
posprocesados, como el fog o el motion blur. Sin embargo, hagamos lo que
hagamos no sucederá nada, ya que tendremos que asignarlo. Seguidamente,
seleccionamos la cámara y añadimos en componentes Post-processing behaviour.
Comprobaremos entonces que este dejará un espacio. Ahí añadiremos el perfil de
posproceso que hemos creado anteriormente. Eso sería todo.
222
Programación multimedia
y dispositivos móviles
En el Inspector del Post processing profile encontraremos el efecto motion blur. Este
efecto simula el desenfoque de una imagen cuando los objetos principales se mueven
más rápido que el tiempo de exposición de la cámara.
El motion blur utiliza dos técnicas principales, pero nos centraremos en la simulación de
velocidad de obturación, la cual imita el desenfoque de una cámara. Esta es costosa y
no es compatible con algunas plataformas (como en realidad virtual), pero proporciona
un efecto fuerte de desenfoque y tiene alta calidad.
La profundidad de campo es otro de los efectos que encontraremos. Este simula las
propiedades de enfoque de la lente de una cámara, lo que le aporta un efecto de
realidad, pues las cámaras solo pueden enfocarse plenamente en un objeto y aquellos
más lejanos o cercanos siempre están algo desenfocados. A este respecto, es un efecto
que aporta altas dosis de realidad y una sensación de profesionalidad.
Encontramos también el efecto anti-aliasing, que aporta suavidad a los gráficos porque
huye del aliasing o dientes de sierra. Su finalidad es alisar y redondear los acabados de
los polígonos mediante algoritmos diseñados exclusivamente para ello.
En nuestro caso, el algoritmo que veremos será el del anti-aliasing rápido y aproximado
(FXAA). Es la técnica recomendada para dispositivos móviles y plataformas que no
admitan vectores de movimiento. Además, proporciona una compensación entre el
rendimiento y la calidad del borde sin difuminar demasiado el acabado de las líneas.
Otro de los efectos que queremos mencionar es la oclusión ambiental. Esta busca una
similitud con la realidad oscureciendo zonas como pliegues o agujeros. Sin embargo, es
recomendable usarlo en equipos de escritorio o en consolas, ya que consume bastantes
recursos.
También cabe destacar la intensidad o grado de oscuridad producido por defecto; la alta
precisión, que alterna el uso de una textura de profundidad de mayor precisión con la
ruta de reproducción hacia adelante; y la opción solo ambiente.
223
Programación multimedia
y dispositivos móviles
Por último, cabe mencionar el efecto niebla (fog), el cual se utiliza para simular niebla o
neblina en imágenes al aire libre, ya que crea un espacio de pantalla que se basa en la
textura de profundidad de la cámara.
El test del producto es una técnica que realizan las compañías a través de especialistas
del mercado. Se presenta el producto (generalmente un prototipo) a un grupo de
consumidores que lo probará y dará su opinión para crear conclusiones.
Los probadores beta o beta tester son usuarios con conocimientos avanzados en el
campo de los videojuegos o que se dedican a hacer test de las versiones beta, es decir,
de las versiones que aún no están finalizadas del todo o que son prototipos. El objetivo
es que detecten errores para que los creadores los puedan subsanar.
224
Programación multimedia
y dispositivos móviles
Asimismo, la fase de testeo del producto puede ser interna o externa. Es interna cuando
es la propia compañía la que cuenta con varios beta tester en plantilla, los cuales suelen
encontrarse en el departamento de QA (calidad). Por el contrario, el proceso es externo
cuando se buscan game tester fuera de la compañía.
Una vez pasada la fase del testeo del videojuego, este debe obtener la certification
testing, la cual es necesaria para poder comercializarlo. Actualmente, ser game tester es
un empleo que requiere experiencia y conocimientos avanzados en el sector.
Los materiales son definiciones de cómo se debe renderizar la superficie. Los shaders
son scripts pequeños que contienen los cálculos matemáticos y los algoritmos para
calcular el color de cada píxel renderizado, los cuales se basan en el input de iluminación
y la configuración del material. Además, cada uno de ellos tiene unas propiedades que
aparecen en el Inspector cuando se mira un material. Un shader define el método para
renderizar un objeto y puede especificar diferentes métodos dependiendo del hardware
de los gráficos del usuario final. Por otro lado, las texturas son imágenes bitmap que
pueden representar aspectos de la superficie de un material como la rugosidad o el
color.
Asimismo, para crear un nuevo material deberemos ir a Assets > Create > Material. Los
nuevos materiales se asignan al Standard shader y, una vez aplicados a un objeto (para
aplicarlo podemos arrastrarlo desde el Project view hasta la scene o la jerarquía),
podremos cambiar las propiedades en el Inspector.
Unity tiene categorías de shader integradas con propósitos concretos (por ejemplo,
Nature para árboles y terren, o Toon para el renderizado tipo caricatura, entre otros).
Efectos de posprocesamiento
226
Programación multimedia
y dispositivos móviles
1. Primero tendremos que entrar en la Asset Store de Unity y bajarnos el Asset Post
Processing Stack. Para entrar en la Asset Store tenemos que ir a Window > Asset
store o pulsar Ctrl+9.
2. Cuando esté listo, nos saldrá una pestaña de Importar, la cual seleccionaremos. Ya
estaremos listos para crear un posprocesado.
227
Programación multimedia
y dispositivos móviles
Cada uno de los objetos representados dentro de una escena tiene unas determinadas
propiedades. Estas son:
Estas son algunas de las propiedades básicas de los objetos. Estas serán configuradas de
forma particular para cada objeto en función de la posible interacción dentro de una
escena.
228
Programación multimedia
y dispositivos móviles
229
Programación multimedia
y dispositivos móviles
Cada uno de estos nodos será la representación de una acción del personaje. Estos
reflejarán las transiciones entre los estados más básicos.
Cuando se está editando un archivo dentro del proyecto, este aparecerá como una
pestaña. El editor de texto permite añadir breakpoints en los márgenes, al lado de cada
una de las líneas de código que se desea. Una vez seleccionados estos puntos de parada,
comienza la depuración del código a través del botón Debug. Esto ejecutará el código,
quedando parado en el primer punto de parada encontrado en el código. Esto permite
ver los valores que han tomado todas las variables hasta ese momento.
Además, es posible navegar entre los distintos puntos de parada para comprobar el
correcto comportamiento de la aplicación.
Otra herramienta que es útil dentro del IDE de Unity es el Unity test runner. Esta
herramienta comprueba el código de programación en busca de errores antes de
realizar una compilación. Esto puede ser útil para corregir errores de sintaxis, por
ejemplo.
230
Programación multimedia
y dispositivos móviles
El código tiene que estar lo más limpio posible, lo que ayudará posteriormente a la
corrección y mejora de algunas funciones. En proyectos con un desarrollo de código muy
extenso, esto puede suponer un problema de optimización muy grande.
Las funciones declaradas deben estar bien definidas y no deberán existir varias
funciones cuyo comportamiento sea el mismo.
231