Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Programación en Java con Android Studio
Este libro se dirige a aquellos desarrolladores que deseen dominar el desarrollo de aplicaciones Android. Cubre todas
las fases del desarrollo de aplicaciones para smartphones y tabletas Android y requiere únicamente poseer
conocimientos básicos en programación orientada a objetos y en el lenguaje Java.
Tomando como ejemplo el desarrollo de una aplicación de gestión de DVD, los distintos capítulos de este libro permiten
al lector descubrir progresivamente las nociones imprescindibles para la construcción de una aplicación de calidad
profesional. De este modo, se explica cada noción presentada, se ilustra con ejemplos de código y se sitúa en su
contexto. Desde la instalación del entorno de desarrollo Android Studio hasta la publicación de la aplicación final en
Play Store de Google, se invita al lector a utilizar los componentes más extendidos de la plataforma: componentes de
texto, botones, actividades y fragmentos, y también listas, ventanas emergentes, paneles de navegación, barra de
acciones, etc.
Cubriendo todas las versiones de Android hasta Oreo, el libro presenta los distintos métodos que permiten hacer que
la aplicación sea compatible con todos los terminales Android y describe cómo implementar el modelo Master/Detail
para ofrecer una experiencia de usuario óptima en cualquier tipo de pantalla, desde el smartphone más pequeño hasta
las tabletas más recientes.
Las tareas asíncronas (AsyncTask), verdaderas piedras angulares del desarrollo Android, se presentan con detalle y se
utilizan a lo largo de todo el libro para gestionar bases de datos SQLite, desarrollar servicios, consultar servicios web
mediante la biblioteca dedicada Volley e interpretar el formato JSON.
El lector aprenderá también a sacar provecho de potentes herramientas como los filtros de intenciones, los
BroadcastReceiver y las intenciones implícitas, y adquirirá todo el conocimiento necesario para enviar y recibir SMS,
manipular la cámara de fotos, los sensores y el uso de la geolocalización, sin olvidar la gestión del Bluetooth y la
interacción con los dispositivos Bluetooth Low Energy. Los estilos, las imágenes redimensionables, los elementos
visuales definidos en XML o las animaciones de transición permitirán al desarrollador ajustar el aspecto visual de sus
aplicaciones. Se guiará también al lector para migrar su aplicación a la última versión de Android.
Como acompañamiento al libro, es posible descargar el código del proyecto para cada capítulo en esta página,
permitiendo al lector explorar con más detalle las nociones presentadas.
Los capítulos del libro:
Prólogo – Entorno de desarrollo – Principios básicos de Android – Preparación del proyecto LocDVD – Consulta e
introducción de datos – Persistencia de datos – Controles avanzados – Los fragmentos – Navegación y ventanas
emergentes – Tareas asíncronas y servicios – Redes e Internet – Explotar el teléfono – Salir de la aplicación – Diseño
avanzado – Imágenes, sonido y vídeo – Uso de Bluetooth Low Energy – Android 8 Oreo – Publicar una aplicación
Sylvain HEBUTERNE
Sylvain HÉBUTERNE es Arquitecto Android. Especializado en la programación orientada a objetos desde hace 15 años,
diseña aplicaciones Android a título personal y para diversas agencias de comunicación, compañías de ingeniería
informática y startup. Estos proyectos, muy diversos, le permiten explotar todo el potencial de la plataforma Android, e
incluso las funcionalidades más avanzadas propuestas por las últimas versiones.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Introducción
Smartphones, tabletas, y ahora también relojes, televisores, objetos conectados, etc., hacen que la popularidad de la
plataforma Android no pare de aumentar. Con más de 1,4 millones de aplicaciones, Google Play Store sirve más de 2
millones de descargas cada mes, con unos beneficios totales estimados de 7 000 millones de dólares en 2014.
Para los desarrolladores, y puede que todavía más para los futuros desarrolladores, adquirir las competencias
necesarias de la plataforma Android es un desafío importante, tanto por el enorme crecimiento de su universo como
por la amplitud de su campo de aplicación.
Los requisitos previos para abordar el desarrollo de aplicaciones con Android no son muy diferentes de las
competencias clásicas demandadas en el mundo de la programación. Además de conocer la sintaxis de Java, solo se
necesitan nociones sobre bases de datos y conocimientos básicos sobre programación orientada a objetos para
iniciarse con éxito en el estudio de esta plataforma.
Las exigencias en términos de hardware no son prohibitivas: las herramientas de desarrollo se proveen de manera
gratuita, existe un emulador que permite no tener que comprar un terminal Android, al menos durante la fase de
aprendizaje, y el ticket de entrada para publicar aplicaciones en Play Store es tan solo de veinte euros.
En definitiva, los únicos requisitos previos reales para abordar sin dificultad los capítulos de este libro son disponer de
competencias académicas en programación y algo de tiempo.
Los objetivos fijados para este libro son múltiples: si bien, evidentemente, el principal objetivo es descubrir el
framework Android, la comprensión de la filosofía de la plataforma es un componente esencial de los capítulos de este
libro.
Así, incluso aunque al principio puede resultar algo desconcertante, los hábitos de los desarrolladores Android se han
respetado a lo largo de los ejemplos de código de este libro. Las clases anónimas, por citar tan solo un ejemplo, que
se utilizan masivamente en el código del propio framework y en los ejemplos y tutoriales que abundan en Internet, se
manipulan y explican desde los primeros capítulos.
Desde un punto de vista práctico, el hilo conductor de este libro es el desarrollo de una aplicación de gestión de DVD,
LocDVD. Esta aplicación nos va a permitir estudiar los principales objetos a disposición de los desarrolladores,
comprender su uso y dominar su manipulación en un marco profesional.
LocDVD permite, en particular, visualizar una lista de DVD, agregar DVD introduciendo la información correspondiente,
importar datos desde un archivo de texto, realizar consultas a bases de datos de Internet…
Incluso aunque algunos párrafos y un capítulo se alejen de esta regla para explicar ciertas nociones que sobrepasan
el ámbito de la aplicación, el conjunto del libro se basa en su desarrollo.
Los dos primeros capítulos están dedicados a la instalación del entorno de desarrollo y a su uso.
El siguiente capítulo presenta el administrador de recursos, que es una potente herramienta muy sencilla de utilizar
que aporta una solución al problema recurrente de la fragmentación en Android.
El capítulo Consulta e introducción de datos plantea las bases del desarrollo en Android con la manipulación de los
componentes de visualización e introducción de texto. Los primeros puntos específicos de la plataforma, tales como la
rotación automática de la pantalla, se abordan también en este capítulo.
A continuación, se presenta con detalle la persistencia de datos con la creación de una base de datos SQLite y la
manipulación de datos mediante consultas SQL y objetos proporcionados por la plataforma. La copia de seguridad de
datos no estructurados y el acceso al sistema de archivos se abordan también y se utilizan para llenar la base de
datos creada.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El capítulo Controles avanzados realiza un estudio detallado del objeto ListView, uno de los componentes más
importantes para una aplicación, que permite mostrar un gran número de elementos. Este capítulo explica a su vez
cómo manipular distintos controles compuestos y proporciona todas las claves al desarrollador para crear sus propios
componentes reutilizables.
El siguiente capítulo presenta los fragmentos, elementos imprescindibles para quien quiera llenar las pantallas de las
tabletas. La aplicación LocDVD los utiliza para proporcionar una experiencia de usuario agradable tanto en
smartphones como en tabletas.
Para estructurar la aplicación, el capítulo Navegación y ventanas emergentes detalla las distintas posibilidades
ofrecidas por la plataforma: se integran en el proyecto los menús y un panel de navegación. Se implementan también
ventanas emergentes y su personalización, que permiten reforzar la comunicación entre la aplicación y el usuario.
El capítulo Tareas asíncronas y servicios es una etapa importante para la aplicación. La programación asíncrona es, en
efecto, omnipresente en Android, y la comprensión de los mecanismos sobre los que se basa resulta esencial para
cualquier desarrollador que quiera construir aplicaciones de calidad. Se realiza también un primer contacto con los
receptores de eventos, que se profundizará en los siguientes capítulos.
El acceso a los servicios web se aborda en el capítulo Redes e Internet, mediante la presentación de la biblioteca
Volley, desarrollada por Google para simplificar las peticiones de red y su implementación. Se propone una
implementación práctica, para ofrecer a los usuarios la posibilidad de buscar un DVD a partir de una base de datos de
referencia.
El capítulo Explotar el teléfono describe con detalle cómo sacar partido de las capacidades de los terminales y sus
múltiples sensores embebidos. El envío y la recepción de SMS se implementan en el marco de la aplicación, dado que
este aspecto es representativo de la integración de una aplicación en el ecosistema propuesto por Android.
A continuación, se propone salir de la aplicación para conquistar la pantalla de bienvenida de los terminales Android.
Además de desarrollar un widget, este capítulo detalla el procedimiento para crear notificaciones y presenta las
herramientas que se proporcionan para interactuar con las redes sociales.
El capítulo Diseño avanzado ofrece las claves para mejorar gráficamente la aplicación LocDVD. Se explican los estilos,
las imágenes redimensionables, los elementos visuales XML y las animaciones para permitir al desarrollador
responder eficazmente a las demandas cada vez más complejas en términos de diseño.
El siguiente capítulo muestra el uso de la cámara de fotos y describe los métodos disponibles para gestionar los
elementos multimedia compatibles con la plataforma.
Dejando de lado momentáneamente la aplicación LocDVD, el capítulo Uso de Bluetooth Low Energy aborda un asunto
algo más complejo exponiendo las clases dedicadas a las comunicaciones Bluetooth Low Energy, y abre así la puerta
a los objetos conectados.
El penúltimo capítulo, por su parte, está dedicado a Android 8 Oreo y a las novedades que incorpora esta versión del
sistema operativo.
El último capítulo, para terminar, ofrece todas las claves para preparar la publicación de una aplicación en Play Store.
El código del proyecto que acompaña al libro puede descargarse desde la página Información para cada capítulo,
permitiendo al desarrollador explorar con más detalle las nociones expuestas.
¡Feliz lectura!
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Arquitectura de Android
1. Presentación de Android
El nacimiento de Android se debe a una startup con el mismo nombre, que desarrollaba este sistema operativo para
cámaras de fotografía digital. Adquirido por Google en 2005, Android se ha convertido en el ecosistema que
conocemos actualmente, disponible para smartphones, tabletas, objetos conectados, televisores… ¡e incluso
coches!
Las evoluciones del sistema operativo son rápidas: de la versión 1.0 en septiembre de 2008, unas 40 versiones
sucesivas se han publicado después; la versión 8 (la más reciente hasta la fecha de este libro) se ha publicado en
agosto de 2017.
Las principales versiones reciben el nombre de un dulce, los nombres siguen el orden alfabético: desde Apple Pie
para la versión 1 hasta Nougat para la versión 7, la versión 8 se llamará Oreo. La siguiente tabla muestra la lista de
versiones principales (las que han dado pie a un cambio de nombre).
Además del nombre y de la versión (Lollipop versión 5.0, por ejemplo), cada versión se caracteriza completamente
por su nivel de API (API Level en inglés), que evoluciona con cada una de ellas. Este nivel de API se utiliza por parte
de los desarrolladores para distinguir, si fuera necesario, la versión de Android sobre la que se encuentra instalada
la aplicación.
Algunas de estas versiones cuentan con mejoras más significativas que otras. Destacamos, en particular, las
siguientes versiones:
l Honeycomb: versión específica para tabletas.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
l Ice Cream Sandwich: unificación de los sistemas operativos para tableta y smartphone, aparición de la noción de
fragmento (se dedica un capítulo completo a esta noción fundamental).
l Lollipop: la máquina virtual Dalvik se reemplaza por ART (Android Runtime).
l Marshmallow: modificación significativa del mecanismo de permisos.
A diferencia de otras plataformas móviles, existen numerosos fabricantes que diseñan sus terminales para funcionar
con el sistema operativo Android. Con cada nueva versión de Android, cada fabricante determina por su parte si
proporciona o no una actualización de sus terminales para aprovechar las ventajas de la última versión del sistema.
Si bien esto deja una gran libertad a los fabricantes, las consecuencias son más problemáticas para los
desarrolladores de aplicaciones: dada la diversidad de terminales, deben hacer malabares con varias versiones de
sistema operativo. Esta situación, una de las características de Android de la que a menudo se mofan sus
competidores, se denomina fragmentación.
Para ayudar a los desarrolladores a decidir respeto a qué versión del sistema operativo soportar, Google provee,
cada quince días, estadísticas acerca de las tasas de instalación de cada versión; estas estadísticas, disponibles en
la dirección https://developer.android.com/about/dashboards/index.html, se basan en las conexiones a Play Store.
Por ejemplo, a 15 de septiembre de 2017, el reparto era el siguiente:
Nombre Porcentaje de instalación
Gingerbread 0,6 %
Ice Cream Sandwich 0,6 %
Jelly Bean 7 %
KitKat 13 %
Lollipop 29 %
Marshmallow 32 %
Nougat 16 %
Si observamos con detenimiento este reparto, la fragmentación, a este nivel, no supone más problema que el
siguiente: la proporción de terminales equipados con una versión de Android anterior a Ice Cream Sandwich (ICS)
puede considerarse como despreciable (menos del 1 %). Recordemos que ICS representa la unificación entre el
sistema operativo para tabletas y el sistema operativo para smartphones, y la aparición de los fragmentos; para los
desarrolladores, ICS ha supuesto una actualización importante, que aporta un número significativo de
modificaciones para la arquitectura de las aplicaciones. El paso de ICS a Jelly Bean, y después a KitKat, ha tenido,
en comparación, menos impacto para los desarrolladores.
Por otro lado, Android garantiza una compatibilidad ascendente entre las versiones del sistema: una aplicación
desarrollada para Gingerbread funcionará sobre ICS y sobre KitKat. Veremos además, más adelante, que Android
permite a los desarrolladores controlar esta compatibilidad ascendente.
El capítulo Preparación del proyecto LocDVD aborda otro aspecto de la fragmentación y que supone un reto
permanente para los desarrolladores: la diversidad de terminales y de sus características, especialmente en lo
relativo a la visualización.
2. Arquitectura
El sistema operativo Android se basa en un núcleo Linux, actualizado regularmente según las versiones del sistema:
si las primeras versiones utilizaban la versión 2.6.x del núcleo de Linux, las actuales últimas versiones (Android 8) se
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
basan en la versión 4.4 de Linux.
Esta primera capa se encarga de gestionar las capas básicas (gestión de los procesos, de la memoria, de la capa de
hardware), así como de los permisos de usuario; retomaremos este tema en la siguiente sección.
La segunda capa presenta las principales bibliotecas del sistema: gestión de la visualización (2D y 3D), motor de
base de datos (SQLite), gestión del audio y del vídeo...
Vienen, a continuación, las bibliotecas Java, que se usan directamente por los desarrolladores para crear
aplicaciones: son estas bibliotecas las que utilizará con este libro para desarrollar la aplicación de gestión de DVD.
Finalmente, una última capa completa esta estructura: las aplicaciones. Estas aplicaciones pueden ser, en efecto,
aplicaciones de terceros descargadas del almacén de aplicaciones oficial, y también aplicaciones instaladas por
defecto, tales como la aplicación de inicio (también llamada Launcher), el navegador web, las aplicaciones de SMS y
telefonía, etc. Todas estas aplicaciones están comúnmente desarrolladas en Java.
Debe saber por último que, incluso aunque este libro no aborda el tema, es posible desarrollar en C/C++ utilizando
el NDK (Native Development Kit): esta posibilidad solo resulta interesante en situaciones específicas donde mejorar
el rendimiento, el cual es algo escaso, resulta primordial (desarrollos de motores de juegos, de análisis de la señal,
por ejemplo).
3. Play Store
Acompañando al propio sistema operativo, Google ha puesto a disposición de los usuarios todo un ecosistema
basado en un almacén de aplicaciones: Play Store.
Llamada originalmente Android Market, Play Store es la ubicación de preferencia para distribuir aplicaciones Android;
el usuario debe, además, indicar explícitamente que acepta la instalación desde fuentes externas para poder utilizar
otros almacenes de aplicaciones.
Las aplicaciones propuestas en Play Store (¡varios millones de aplicaciones disponibles!) son o bien gratuitas o bien
de pago. Existe una amplia tendencia a reemplazar las aplicaciones de pago por un mecanismo de compra integrada
(InApp purchase, en inglés), mecanismo más flexible y que resulta más lucrativo para los desarrolladores.
El capítulo Publicar una aplicación detalla el proceso de publicación de una aplicación en Play Store.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Android Studio
En mayo de 2013, Google publica un entorno de desarrollo en versión beta, dedicado a Android: Android Studio.
Disponible en su versión final desde diciembre de 2014, Android Studio reemplaza al dúo Eclipse/ADT (Android
Developer Tools), que era hasta el momento la principal herramienta de desarrollo de los desarrolladores.
Android Studio está disponible para Windows, Mac OS X y Linux y necesita Java Development Kit (JDK) en versión 7,
disponible en la siguiente dirección: www.oracle.com/technetwork/java/javase/downloads/index.html.
La descarga de Android Studio se realiza desde la siguiente dirección: http://developer.android.com/sdk/index.html. La
instalación es sencilla y depende del sistema operativo.
Una vez realizada la descarga, hay que seguir las instrucciones detalladas a continuación para instalar el entorno de
desarrollo. Durante el proceso de instalación, es importante observar la ubicación donde se instala el kit de desarrollo
de Android (SDK, Software Development Kit, en inglés), pues a lo largo de este libro se utilizan varias herramientas
proporcionadas por el SDK. El asistente le permite modificar la carpeta de instalación.
1. Instalación en Windows
Para lanzar la instalación de Android Studio, ejecute el archivo ejecutable descargado. Un asistente de instalación le
guiará a lo largo del proceso.
Haga clic en el archivo Android-Studio-Bundle.exe. Se abre una pantalla de bienvenida, que le
invita a lanzar la instalación.
Si el asistente no encuentra la carpeta de instalación de JDK, una ventana emergente le pedirá su
ubicación y le proporcionará el vínculo para instalar el kit de desarrollo.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
La siguiente pantalla le pide que indique los elementos que debe instalar. Compruebe que están
marcados todos los elementos de la lista.
Para llevar a cabo la instalación, debe aceptar el contrato de licencia de uso. Haga clic en el botón I Agree
para proseguir.
La siguiente pantalla le permite modificar las carpetas de instalación de Android Studio y del SDK Android.
Observe la ubicación de la carpeta que contendrá el SDK; las herramientas necesarias para la instalación de ciertos
elementos complementarios se encuentran en la raíz de esta carpeta.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Haga clic en Next y seleccione la carpeta correspondiente al menú Inicio de Windows.
Una vez haga clic en Next, arranca la instalación.
Cuando termine la instalación, compruebe que la opción Start Android Studio esté marcada y haga clic en
Finish.
2. Instalación en Max OS X
En Mac OS, ejecute el archivo .dmg descargado y luego mueva Android Studio a la carpeta Application. Abra a
continuación Android Studio y siga el asistente de instalación.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
3. Instalación en Linux
En Linux, descomprima el archivo zip descargado en la carpeta de instalación que desee. Para lanzar Android Studio,
sitúese en la carpeta AndroidStudio/bin/ y ejecute el archivo studio.sh desde una ventana de Terminal.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Complementos
Durante la instalación de Android Studio, solo se instalan los elementos realmente imprescindibles para el
funcionamiento del entorno de desarrollo. En particular, solo se instala una única versión de SDK (la más reciente).
Veremos, en la siguiente sección, que es necesario disponer de varias versiones de SDK Android para crear
terminales virtuales que ejecuten diferentes versiones de Android.
Entre las herramientas instaladas por defecto se encuentra Android SDK Manager, que permite, como su nombre
indica, gestionar la instalación de distintas versiones de Android.
Android SDK Manager está accesible a través de Android Studio, que hay que ejecutar por primera vez.
Abra Android Studio si todavía no lo hubiera hecho tras la instalación. Tras una primera ventana de
presentación, que hay que pasar haciendo clic en el botón Next, se abre una ventana emergente que le va
a preguntar qué tipo de instalación desea llevar a cabo. Seleccione la instalación Standard y haga clic en
Next.
La aplicación presenta, a continuación, un resumen de la configuración de Android Studio y le informa de
datos complementarios que deben descargarse. Haga clic en el botón Finish.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
La descarga de los complementos arranca automáticamente, y una ventana de progreso informa al
desarrollador de las tareas en curso.
Tras finalizar la descarga, haga clic en el botón Finish para mostrar la ventana de bienvenida de Android
Studio.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Esta ventana presenta, en la parte izquierda, la lista de proyectos Android Studio (actualmente vacía), y a la derecha
un conjunto de vínculos que permiten lanzar un asistente para crear un nuevo proyecto (es el objetivo del siguiente
capítulo), importar proyectos, etc., y, lo que nos interesa aquí, configurar Android Studio.
Haga clic en Configure y, a continuación, seleccione la opción SDK Manager en el menú desplegable.
Cuando se abre, el programa se conecta automáticamente con los servidores de Google para descargar la última
información acerca de los paquetes disponibles.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
La pantalla de la aplicación presenta, a continuación, una lista de los paquetes que pueden descargarse e indica,
para cada elemento, si se encuentra instalado o no, y si debe actualizarse. Por defecto, solo se ha instalado la última
versión de Android disponible (en este caso, la versión 8).
Haga clic en la opción Show Package Details, en la parte inferior derecha de la lista, para mostrar, para
cada versión, el detalle de los elementos instalados o que pueden instalarse.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Destacaremos, a la derecha de la pestaña SDK Platforms, la pestaña SDK Tools, que presenta la lista de
herramientas proporcionadas por Google para asistir al desarrollo. Encontramos, entre otros, los siguientes
elementos:
l Android Emulator: el emulador de Android, que permite simular el funcionamiento de un dispositivo Android en el
puesto de desarrollo.
l Android Support Library: conjunto de bibliotecas de soporte, proporcionadas por Google, que ayudan a gestionar los
problemas derivados de la fragmentación.
l Google Play services: que permite a los desarrolladores utilizar los servicios proporcionados por Google (Google
Maps, por ejemplo).
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Como previsión para la siguiente sección, instale las versiones 6.0 y 5.1 de Android:
Sitúese en la pestaña SDK Platforms y muestre los detalles para los paquetes (marcando la opción Show
Package Details).
En la lista, navegue hasta la entrada Android 6.0 (marshmallow).
Marque las opciones correspondiente a Google APIs, Android SDK Platform 23, Sources for Android 23,
ARM EABI v7a System Image y Google APIs ARM EABI v7a System Image.
Navegue, a continuación, hasta la entrada Android 5.1 (Lollipop).
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Marque las opciones Android SDK Platform 22, ARM EABI v7a System Image y Google APIs ARM EABI
v7a System Image.
Observe que Google proporciona tres versiones de cada imagen del sistema Android (utilizado por el emulador): una
versión ARM, que es la seleccionada, una versión Intel x86 Atom y una versión Intel x86 Atom_64. Estas dos últimas
versiones están diseñadas específicamente para puestos de desarrollo equipados con un procesador Intel x86 (la
versión Atom_64 está diseñada para sistemas de 64 bits). Si bien son más rápidas que la versión ARM, que es la
versión estándar, exigen que el procesador de la máquina host soporte la tecnología Virtualization Technologie (VT).
Haga clic en el botón Apply.
Se muestra una ventana que le presenta la lista de paquetes que se van a instalar. Haga clic en la opción
Accept y, a continuación, en el botón Next.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Comienza la descarga, que puede tardar cierto tiempo según la velocidad de conexión a Internet.
Cuando se descargan y se instalan los paquetes, se muestra un mensaje (¡bastante discreto!) en la parte
inferior de la pantalla de la aplicación y el botón Finish se habilita, permitiéndole cerrar el asistente de
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
instalación.
El entorno de desarrollo se encuentra, ahora, instalado. El siguiente capítulo, tras una rápida presentación de
conceptos clave propios de las aplicaciones Android, aborda la creación de un primer proyecto y las herramientas de
depuración.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
Conceptos básicos de una aplicación Android
Antes de crear un primer proyecto, es útil repasar algunas características fundamentales de una aplicación Android.
1. Contexto de ejecución/Seguridad
Android está basado en el núcleo de Linux, de modo que es nativamente multiusuario. Sin embargo, en esta
especialización del sistema Linux, son las aplicaciones las que tienen el rol de usuario: cuando una aplicación se
instala en un dispositivo, se crea automáticamente un usuario para esta aplicación, y se asignan permisos de
acceso a los archivos de la aplicación para que solo este usuario pueda acceder a ellos.
De la misma manera, cada aplicación se ejecuta en un proceso Linux específico con el objetivo de garantizar un
funcionamiento multitarea óptimo.
Existen varios mecanismos que permiten a una aplicación comunicarse con otras; estos mecanismos se basan en
una noción llamada Intención. Las intenciones se abordarán con detalle a lo largo de los próximos capítulos.
Para garantizar una seguridad óptima, se define un mecanismo de autorización de acceso, que obliga a la aplicación
a indicar explícitamente los recursos sensibles que utiliza. Por ejemplo, para acceder a los contactos del
smartphone, la aplicación debe presentar la autorización READ_CONTACT (o WRITE_CONTACT para poder modificar
los contactos).
La lista de autorizaciones así declaradas se presenta al usuario cuando decide instalar la aplicación.
Desde el punto de vista del desarrollador, estas autorizaciones se denominan «permisos».
2. Paquete (Package)
Ya sea para la instalación de una versión de prueba o en el marco de una aplicación distribuida a través de Play
Store (o cualquier otro almacén), las aplicaciones se proveen al sistema en forma de paquetes (packages, en inglés).
Un paquete debe tener un nombre único y debe estar firmado por su propietario. En un escenario de depuración, el
entorno de desarrollo firma temporalmente la aplicación para permitir su instalación.
Cada paquete contiene el conjunto de elementos que constituyen la aplicación: código fuente compilado en
bytecode, archivos de recursos (imágenes, vídeos, sonidos) embebidos, archivos de especificación propios del
sistema (archivo AndroidManifest.xml, que se aborda a continuación, por ejemplo).
3. Archivo AndroidManifest.xml
Cada aplicación contiene, obligatoriamente, un archivo llamado AndroidManifest.xml, que incluye toda la
información que necesita el sistema para instalar y ejecutar la aplicación.
Este archivo contiene, entre otros, los permisos necesarios para ejecutar la aplicación, la versión mínima de Android
soportada por la aplicación, las actividades de la aplicación (que veremos en la siguiente sección) y eventuales
restricciones acerca de la naturaleza del dispositivo, etc.
El archivo AndroidManifest.xml se estudia a lo largo de este libro y algunas de sus características se
presentan con más detalle en el capítulo Publicar una aplicación.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
4. Las actividades
Las actividades forman realmente el esqueleto de una aplicación que presenta una interfaz de usuario cualquiera
(formulario de introducción de datos, vídeo o videojuego): cada pantalla que se presenta al usuario es una subclase
de la clase Activity del framework Android.
Es primordial tener en mente dos reglas importantes:
l En un instante t, solo se encuentra en curso una única actividad.
l Para distribuir una aplicación, es necesario que esta contenga al menos una actividad.
Por otro lado, cada actividad debe inventariarse en el archivo AndroidManifest.xml, pues en caso contrario se
producirá un error en tiempo de ejecución de la aplicación cuando se invoque esta actividad. En este archivo
AndroidManifest.xml debe indicarse también al sistema qué actividad debe lanzarse tras el inicio de la
aplicación.
Veremos esta noción de actividad desde la creación del primer proyecto; el ciclo de vida de las actividades, concepto
esencial para construir aplicaciones totalmente funcionales, se aborda con detalle en el capítulo Consulta e
introducción de datos, sección Ciclo de vida de una actividad.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Un primer proyecto: Hello World
Android Studio proporciona un asistente para la creación de proyectos de aplicación. Esta sección presenta las
distintas etapas de este asistente y a continuación se detendrá en los elementos que configura automáticamente. La
siguiente sección ofrecerá un primer vistazo de la depuración de la aplicación mediante las herramientas
proporcionadas por el entorno de desarrollo.
Abra Android Studio: se muestra la ventana de inicio.
Haga clic en el primer asistente: Start a new Android Studio project.
El asistente muestra una primera pantalla que permite introducir información acerca del nombre del proyecto.
Introduzca, para cada campo del formulario, la siguiente información:
l Application name: es el nombre que se mostrará para la aplicación cuando se instale en un terminal. Esta información
puede cambiarse fácilmente con posterioridad.
Escriba, por ejemplo, HelloWorld.
l Company Domain: Android Studio utiliza esta información para construir el nombre del paquete de la aplicación. Es
una característica ofrecida por Android Studio; el nombre escrito se recuperará con cada ejecución del asistente de
creación de proyecto.
Escriba ejemplo.com.
Por defecto, Android Studio crea un nombre de paquete utilizando el nombre de dominio invertido de la compañía y el
nombre de la aplicación.
El nombre del paquete, es este caso, debe ser com.ejemplo.helloworld.
Por supuesto, es posible modificar este nombre haciendo clic en el vínculo Edit, en la parte derecha de la ventana del
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
asistente. Sin embargo, deben respetarse ciertas reglas:
n Está prohibido el uso de mayúsculas, pues podría causar problemas en el despliegue o durante el uso de ciertos
servicios de Google.
n Es necesario que exista al menos un punto (.), y cada segmento creado con este separador no puede estar vacío.
Para este primer proyecto, se recomienda dejar el nombre propuesto por defecto.
l Marcando la opción Include C++ support es posible indicar al asistente que el proyecto integrará secciones de código
escritas en C++ (desarrollo nativo). Deje esta opción desmarcada, pues este libro no aborda este aspecto de Android,
reservado a ciertos casos específicos.
l El último campo de este formulario permite informar la ubicación donde se almacenará el conjunto del proyecto.
Android Studio recomienda, y nos lo recuerda si se diera el caso, no utilizar caracteres especiales en la ruta de acceso
al proyecto.
De manera clásica, el botón situado a la derecha del campo permite abrir un explorador de archivos para seleccionar
una carpeta.
Haga clic a continuación en el botón Next para acceder a la siguiente pantalla.
La siguiente pantalla permite seleccionar los tipos de dispositivos sobre los que funcionará la aplicación.
Se muestra una lista donde es posible seleccionar varias entradas con todos los tipos de dispositivos susceptibles de
ejecutar una aplicación Android: smartphones y tabletas (se trata, hablando con precisión, de un único tipo de
dispositivo), televisores Android, objetos conectados (aquí llamados Wear), TV y Android Auto, otros dominios donde
está presente la plataforma Android.
Para cada tipo de dispositivo, hay que seleccionar a su vez el SDK mínimo requerido; se corresponde con la versión de
Android mínima con la que debe estar equipado el dispositivo para permitir la instalación de la aplicación.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Esta información puede modificarse posteriormente; veremos las implicaciones que tiene la selección de SDK en
posteriores capítulos.
Seleccione, en esta pantalla, únicamente Phone and Tablet, y seleccione el SDK API 22: Android 5.1
(Lollipop), que se corresponde con uno de los SDK instalados en el capítulo anterior.
Observe que Android Studio le indica el porcentaje de dispositivos compatibles según la API seleccionada, y le
presenta un gráfico explicitando este porcentaje si hace clic en el vínculo Help me choose. Las secciones Gestionar la
fragmentación y Manejar varias versiones de API del capítulo Preparación del proyecto LocDVD están vinculadas
directamente a esta noción.
Haga clic a continuación en Next para mostrar la siguiente pantalla. Se instala un conjunto de elementos si
fuera necesario. Haga clic, tras esta instalación, en el botón Next.
El asistente muestra ahora la información necesaria durante la creación del proyecto para cada uno de los campos
seleccionados en la pantalla anterior: aquí, únicamente smartphones y tabletas.
La pantalla permite seleccionar entre varias actividades (Activity, tal y como hemos visto antes) para agregar a la
aplicación. Entre las posibles opciones, el asistente propone en particular las siguientes soluciones:
l Add No Activity: indica al asistente que no agregue ninguna actividad por defecto.
l Basic Activity: indica que se agregará una actividad en su forma más simple.
l Bottom Navigation Activity: indica que se agregará al proyecto una actividad que conlleva una barra de navegación
(situada en la parte inferior de la pantalla), lo que incluye toda la lógica de procesamiento de la barra de navegación.
l Empty Activity: indica que se agregará una actividad en su forma básica. Esta plantilla es muy parecida a la de Basic
Activity.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
l Fullscreen Activity: indica que se agregará una actividad que integra el código que permite a la aplicación pasar al
modo «pantalla completa» (sin barra de notificación visible).
l Google AdMob Ads Activity: se agregará una actividad que integrará el código necesario para la gestión de la
publicidad de la red AdMob (agencia publicitaria de Google).
l Google Maps Activity: indica que se agregarán los elementos necesarios para la integación de Google Maps.
l Login Activity: indica que se agregará a la aplicación una actividad que integra un mecanismo de conexión.
El código que se genera automáticamente por Android Studio tras la ejecución del asistente depende del tipo de
actividad seleccionada.
Seleccione, para esta toma de contacto con Android, Basic Activity.
Haga clic, a continuación, en Next.
El asistente pide ahora introducir cierta información para la actividad que se creará:
l Activity Name: es el nombre de la actividad, que se corresponde con el archivo Java que se generará.
El asistente propone el nombre MainActivity. Deje este nombre por defecto.
l Layout Name: todavía no se ha abordado la noción de layout. De momento, consideremos que un layout es un archivo
de descripción de la interfaz, en formato XML.
El asistente propone el nombre activity_main. Deje este nombre por defecto.
l Title: el título que se dará a la actividad.
Reemplace el título por defecto por Actividad Principal.
l Menu Resource Name: esta noción se explicará más adelante en este libro. De momento debe saber que, con
Android, es posible definir menús en archivos XML.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Deje el nombre por defecto, menu_activity_main.
l Use a Fragment: los fragmentos se estudiarán más adelante en este libro: considere, de momento, que un fragmento
es una subactividad. Deje la opción desmarcada.
Haga clic en Finish para terminar el asistente y lanzar la generación automática del código.
El asistente se cierra, y una ventana de progreso indica que Android Studio genera el código y lanza una primera
compilación.
Cuando el proyecto está completamente generado y compilado, Android Studio carga el proyecto y presenta un
espacio de trabajo por defecto, que es parecido a la siguiente captura de pantalla.
l La zona izquierda presenta, en forma de árbol, la estructura del proyecto.
l La zona central muestra un conjunto de paneles específicos para el archivo en edición: el archivo
content_main.xml, que es uno de los archivos que describen la composición en la página (layout, en inglés) de la
actividad que el asistente ha generado tras nuestra petición.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
No detallaremos aquí el contenido de la zona izquierda: el capítulo Preparación del proyecto LocDVD se dedica
principalmente a las nociones que se trabajan en esa área.
De la misma manera, los siguientes capítulos permitirán ver con detalle la noción de layout; de momento, he aquí una
breve explicación de lo que presenta Android para este tipo de archivo:
l La zona izquierda de la imagen que representa una pantalla de smartphone es la paleta de controles que pueden
agregarse a un layout.
l La zona principal presenta una previsualización de la pantalla de la actividad.
l El panel situado en la parte inferior derecha de la zona principal, llamado Component Tree, muestra el árbol de
controles integrados actualmente en el layout y sus propiedades correspondientes.
Haciendo clic en la pestaña Text, a la derecha de la pestaña Design (debajo del panel Componant Tree) se muestra
el modo texto:
l Android Studio muestra, tras un cierto tiempo de cálculo, algunas líneas de código XML y, a la derecha, una
previsualización de la representación esperada.
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
En Android, la representación de las pantallas se define, generalmente, en archivos XML.
Haciendo clic en la pestaña MainActivity.java, en la parte superior de la zona principal, a la izquierda de la pestaña
content_main.xml, se habilita la visualización del código del archivo MainActivity.java: se trata del código de
la actividad, tal y como ha sido generado.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Sin entrar en los detalles del código presentado (¡pues es el objetivo de varios capítulos de este libro!), es
interesante destacar que los archivos activity_main.xml y menu_main.xml, archivos definidos en el
asistente, se mencionan en el código de la actividad: la segunda instrucción del método onCreate() para el archivo
activity_main.xml y la primera instrucción del método onCreateOptionMenu() para el segundo.
Los archivos se indican sin extensión ni comillas: son, de hecho, constantes generadas.
Veremos, tras configurar un terminal virtual, cómo lanzar la ejecución de esta primera aplicación, y aprenderemos a
utilizar las herramientas de depuración que proporciona Android Studio.
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Preparar un terminal virtual
El SDK de Android proporciona una herramienta que permite emular terminales Android en su equipo. Esta
herramienta, llamada Android Virtual Device (AVD) Manager, es un preciado aliado que evita, parcialmente, poseer un
número importante de terminales físicos.
AVD Manager está accesible desde Android Studio: para abrirlo, haga clic en el icono que representa un smartphone y
la cabeza de bugDroid (se resalta el icono en la siguiente captura de pantalla): es el cuarto icono por la derecha de la
barra de herramientas.
Si ya hay configurado uno o varios terminales virtuales, AVD Manager los presenta en una lista. En caso contrario,
como ocurre aquí, el asistente de creación de terminales virtuales se abre automáticamente: haga clic en el botón
Create Virtual Device, que se muestra en la parte central de la ventana del asistente, para crear un primer terminal
virtual.
La primera pantalla del asistente presenta una lista de distintos perfiles de terminales, clasificados por categoría
(televisores, smartphones, objetos conectados, tabletas).
También es posible definir nuestros propios perfiles de terminales.
Situándose en la categoría Phone, seleccione un modelo de terminal Nexus, por ejemplo el Nexus 5X. A
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
continuación haga clic en Next.
El asistente muestra a continuación la lista de versiones de Android, organizada en tres pestañas: Recommended,
x86 Images y Others Images.
l La pestaña Recommended muestra las versiones más recientes del sistema operativo.
l La pestaña x86 Images presenta una lista de todas las imágenes del sistema Android optimizadas para los
procesadores de tipo x86.
l La pestaña Others Images, por último, presenta la lista de imágenes de Android compatibles con cualquier tipo de
procesador. Es la lista más completa.
Observe que si una imagen de Android no está disponible en su puesto (no se ha descargado con Android SDK
Manager), existe un vínculo que le permite descargarla.
Seleccione, en la pestaña Recommended, la opción Lollipop (API Level 22). Si la imagen del sistema no está
presente en el puesto de desarrollo, como ocurre en la siguiente captura de pantalla, hay que descargarla
antes de poder hacer clic en el botón Next.
Haga clic en el vínculo Download correspondiente a la imagen de sistema Lollipop. Tras la descarga (que
puede llevar cierto tiempo, pues las imágenes de sistema ocupan varios cientos de megabytes), haga clic
en el botón Finish de la ventana de descarga.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Tras refrescar la ventana, el botón Next aparecerá como habilitado: haga clic en él para mostrar la siguiente pantalla.
Esta ventana permite modificar todos los parámetros del terminal:
l AVD Name (Nombre del terminal): es el nombre que se mostrará en la lista.
l Modelo de terminal emulado (Nexus 4, Nexus S, etc.).
l Versión de Android ejecutada por el terminal.
l Orientación de la pantalla del terminal tras su arranque.
l Emulated Performance: la opción Graphics permite indicar si debe utilizarse la tarjeta gráfica del equipo host
(opción Hardware de la lista desplegable) o no (opción Software); seleccione la opción automatic para dejar esta
elección al sistema operativo.
l Device Frame: marcar esta opción permite indicar si se desea agregar una carcasa en forma de smartphone (o
tableta, según el modelo de terminal seleccionado) a la ventana del emulador.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
El asistente permite, a su vez, haciendo clic en el botón Show Advanced Settings, modificar la configuración del
terminal: el número de cámaras y su soporte, la calidad de la conexión de red, la memoria RAM del terminal virtual, el
número de CPU, la presencia o no de un soporte de almacenamiento externo.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Es necesario realizar ciertas puntualizaciones respecto a las opciones más importantes:
l Camera: es posible emular hasta dos dispositivos de fotos/cámaras para un terminal. La emulación se lleva a cabo
bien a través de un objetivo animado, bien utilizando una webcam en el ordenador host.
l Network: el emulador también puede emular las propiedades de la conexión de red de los dispositivos. Es posible, así,
simular un uso con 4G, 3G, etc.
l Keyboard: este parámetro indica si el emulador debe utilizar el teclado del ordenador host para introducir texto o
presentar un teclado virtual de Android.
En el caso de este primer terminal, deje todos los valores por defecto y haga clic en el botón Finish para crear el
terminal.
Una vez creado el terminal, se muestra la pantalla que presenta la lista de terminales, la cual incluye el terminal que
acabamos de crear.
En esta pantalla, para cada terminal mostrado, se presentan varias opciones:
l El icono «Play» permite arrancar el terminal virtual.
l El icono «Edición» permite modificar los parámetros del terminal, de la misma manera que durante su creación.
l Por último, una lista desplegable permite duplicar el terminal, eliminar los datos de usuario que contenga (opción Wipe
Data), abrir una ventana del explorador de archivos en la ubicación donde se encuentra almacenada la imagen (opción
Show on Disk), ver un resumen de la configuración del terminal (View Details) y eliminar el terminal (Delete).
Haga clic en «Play» para arrancar el terminal virtual: según la potencia del ordenador host, el arranque del terminal
emulado puede ser más o menos rápido.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
El emulador muestra un terminal Nexus 5X funcional, ¡listo para probar las aplicaciones que desarrolle!
Cabe destacar, sin embargo, a diferencia de lo que ocurre con los terminales físicos, que la aplicación Play Store no
está disponible en el emulador: por este motivo, entre otros, el uso de un terminal físico sigue siendo imprescindible
para los desarrolladores.
La siguiente sección está dedicada a la configuración de un terminal físico.
Existen varias aplicaciones de emulación de terminales Android, cada una con sus fortalezas y debilidades. Además de
AVD, que tiene la ventaja de ser completamente gratuita, cabe mencionar el emulador Genymobile
(http://www.genymobile.com/ ), de la compañía Genymotion (compañía francesa). Es particularmente rápido, algo más
complejo de instalar, y solo es gratuito para un uso personal.
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Configurar un terminal físico
Incluso si los dispositivos virtuales de AVD permiten probar de una manera sencilla una aplicación, es indispensable
realizar pruebas en una situación real:
l Por una parte, no todas las funcionalidades pueden probarse a través de los dispositivos virtuales. El «multitouch», y
por extensión todas las manipulaciones gestuales, no son fáciles de emular, por ejemplo. También es conveniente
garantizar que la ergonomía de la aplicación esté adaptada correctamente al uso con el dedo: tamaño de las zonas
sobre las que se puede hacer clic y de los botones… El cursor del ratón no reproduce correctamente la facilidad a la
hora de activarlos.
l Por otra parte, los dispositivos virtuales no ofrecen una buena precisión de la velocidad de ejecución de una aplicación.
A menudo son más lentos que los dispositivos físicos, y no permiten realizar pruebas de carga muy potentes, o trabajar
en una situación real (donde, en general, existen varias aplicaciones en ejecución).
l Por último, los terminales virtuales funcionan en una situación «ideal»: jamás se produce una interrupción (llamada,
SMS), raramente pierden el acceso a la red (salvo si se emula esta situación por parte del desarrollador) y no se
producen manipulaciones «parásitas», como por ejemplo el caso típico de una rotación de pantalla producida por un
movimiento imprevisto por parte del usuario.
Todos los terminales físicos que trabajan con Android pueden utilizarse para probar una aplicación, y ofrecen las
mismas capacidades que los terminales físicos: acceso a los logs, ejecución paso a paso, etc.
El vínculo entre el terminal físico y el equipo de desarrollo se asegura mediante la herramienta ADB (Android Debug
Bridge), proporcionada por el SDK Android.
Típicamente, el terminal físico está vinculado al equipo a través de un cable USB, incluso aunque ADB soporta WiFi.
La primera tarea que debe realizarse es la instalación de los drivers del dispositivo en el equipo. Esta instalación
depende del sistema operativo y de la marca del dispositivo: consulte las instrucciones proporcionadas por el
fabricante de su terminal para llevar a cabo esta operación.
A continuación, hay que indicar al terminal Android que debe autorizar la depuración a través de USB. Esta
manipulación sencilla se lleva a cabo desde la pantalla de configuración del dispositivo, utilizando una función oculta
de Android.
En la pantalla de configuración, seleccione la opción Sistema.
Acceda a la pantalla Información del teléfono.
En esta pantalla, sitúese en la lista para ver la fila correspondiente a Número de build.
Pulse x veces sobre esta fila. El número de pulsaciones necesarias depende del dispositivo, pero en
general basta con entre 4 y 7 pulsaciones. Un mensaje le indicará, normalmente, «¡está a 3 pasos de ser
desarrollador!», y después otro le indicará: «¡Ahora es desarrollador!».
Una vez obtenido este mensaje, vuelva a la pantalla anterior; y en la opción Sistema aparece una nueva
opción Opciones de desarrollador.
Seleccione esta opción. Se muestra una pantalla que le permite, en la opción Depuración, habilitar la
depuración a través de USB.
Observará que existen muchas opciones disponibles en este apartado: mostrar todos los mensajes ANR (Application
Not Responding), ver la memoria utilizada, etc. Los activaremos si es necesario a lo largo de este libro.
Una vez realizadas estas manipulaciones, su terminal físico Android está listo para utilizarse en el marco de sus
desarrollos.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Depuración, traza
Para terminar esta primera toma de contacto con Android Studio, queda por lanzar la ejecución de la aplicación
HelloWorld.
En Android Studio, haga clic en el menú Run y seleccione a continuación la opción Run app. También es
posible hacer clic en el icono de ejecución (el clásico botón «play» verde situado en el centro de la barra de
herramientas) o utilizar la combinación de teclas [Shift][F10].
Se abre una ventana emergente, que presenta la lista de terminales Android, físicos (Connected
Devices) o virtuales (Available Virtual Devices), detectados. Desde esta ventana es posible
seleccionar qué terminal se utilizará para ejecutar la aplicación que se quiere probar.
Una opción (Use same selection for future launches) permite especificar que se utilizará el mismo
terminal en las siguientes ejecuciones, lo que evita tener que indicar cada vez qué terminal se desea
utilizar. Así, la ventana emergente de selección de terminal de pruebas no se volverá a mostrar, siempre
y cuando no cambie la lista de dispositivos disponibles.
Si no hay disponible ningún terminal en la lista, tendrá que crear uno haciendo clic en el botón Create New
Virtual Device.
Seleccione un terminal y haga clic en OK. El paquete se instala en el terminal y la aplicación se abre
automáticamente.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Android Studio proporciona un entorno completo de herramientas para ayudar al desarrollador: visualización de logs,
ejecución paso a paso, análisis de la memoria, etc.: vamos a revisar aquí las principales herramientas, los aspectos
más específicos se abordarán con detalle a lo largo de los siguientes capítulos.
1. Mensajes Toast
Incluso aunque propiamente hablando no sea una herramienta de depuración, los mensajes Toast los utilizan a
menudo los desarrolladores durante la fase de depuración de una aplicación. Se trata de mensajes simples que se
muestran por pantalla y que están previstos inicialmente para informar de forma rápida al usuario.
Un mensaje Toast abre una ventana independiente que muestra un mensaje sin formato, durante un periodo de
tiempo más o menos corto.
La implementación de un mensaje Toast es muy sencilla: basta con invocar el método estático makeText de la
clase Toast y el método show para mostrar el mensaje deseado.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Los parámetros del método makeText son los siguientes:
l Context context: representa el contexto en el que se ejecuta la aplicación. Context es una interfaz,
implementada por varias clases del framework; la más utilizada es la clase Activity.
l CharSequence text: la cadena de caracteres que se va a visualizar.
l int duration: el tiempo durante el cual el mensaje se mostrará por pantalla al usuario. Los dos posibles valores
son las constantes estáticas Toast.LENGTH_SHORT o Toast.LENGTH_LONG.
Así, para mostrar un mensaje al usuario o para mostrar un mensaje de depuración durante la fase de pruebas,
basta con encadenar las llamadas a ambos métodos makeText y show:
2. Logcat
Android proporciona un mecanismo, llamado Logcat, de log integrado en la plataforma, y una clase Java que permite
escribir en los archivos de log.
Logcat permite visualizar, en tiempo real, el archivo de log del terminal conectado, ya sea un terminal físico o virtual.
La ventana Logcat está accesible haciendo clic en el menú Android, en la parte inferior de la ventana principal del
entorno de desarrollo (destacada por un marco negro en la siguiente captura de pantalla), seleccionando a
continuación la pestaña Android Monitor y seleccionando finalmente la pestaña logcat.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
El framework Android expone la clase Log, que permite escribir en el archivo de logcat.
Se proporcionan varios niveles de log: desde Verbose (verboso, el menos importante) hasta Error (error, el más
importante). La ventana logcat permite filtrar los mensajes que se muestran según su importancia. A cada nivel de
importancia le corresponde un método:
Así, el método Log.v permite registrar un mensaje categorizado como verboso. El método Log.e permite, por su
parte, registrar un mensaje de error en logcat.
Cabe destacar que el primer parámetro de estos métodos es una etiqueta que se muestra en función del mensaje
propiamente dicho, en la ventana de log. La práctica recomienda que, para cada clase definida para una aplicación,
el desarrollador declare una cadena de caracteres estática TAG, que se utiliza a continuación para los mensajes de
log.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
[...]
Log.e(TAG,"Error: valor nulo" );
[...]
}
3. Otras herramientas
Además de la visualización de logs de una aplicación, Android Studio proporciona a su vez diversas herramientas
que permiten analizar el rendimiento de la aplicación.
Entre estas herramientas, las más útiles son sin duda las de análisis de consumo de memoria y de uso del
procesador (CPU).
Android proporciona, para estos dos indicadores, gráficos actualizados en tiempo real, que permiten analizar de una
manera muy fina el uso de la memoria y los eventuales picos de CPU.
Es posible acceder a estos gráficos desde la pestaña Monitors, a la derecha de la pestaña logcat vista
anteriormente.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Gestionar la fragmentación
Si bien el desarrollador debe tener en cuenta las distintas versiones del sistema operativo que equipa a los
terminales Android, también debe preocuparse de otra particularidad de la plataforma: los dispositivos que trabajan
con Android están equipados con pantallas que presentan características muy variadas.
Así, si bien los smartphones con configuraciones más modestas están equipados con pantallas que miden entre 3 y 4
pulgadas (una pulgada equivale a 2,54 cm), las tabletas más avanzadas están equipadas con pantallas que miden
hasta 10 pulgadas o más. Los primeros ofrecen, generalmente, una resolución de pantalla de 480 por 800 píxeles,
mientras que las tabletas o smartphones de alta gama presentan una resolución que puede superar los 1080 píxeles
por 1920; la versión 4.3 de Android aporta, a este respecto, el soporte de pantallas con resolución 2160 por 4096
píxeles. Los primeros smartphones Android poseían generalmente una resolución de pantalla QVGA, ¡que se
corresponde con 200 por 320 píxeles!
1. Densidad de pantalla
La combinación de estos dos parámetros, tamaño y resolución de pantalla, produce una característica
suplementaria, la densidad de píxeles, que se expresa en puntos por pulgada (dot per inch en inglés, o dpi).
Un terminal dotado de una pantalla grande pero con una resolución reducida tendrá una densidad de píxeles
reducida, mientras que de, manera opuesta, un smartphone con una pantalla de tamaño reducido pero con una
gran resolución mostrará una densidad de píxeles elevada.
Categoría de pantalla Densidad media (en dpi)
ldpi 120
mdpi 160
hdpi 240
xhdpi 320
xxhdpi 480
xxxhdpi 640
Estas diferencias de densidad deberían impactar en la visualización de imágenes por pantalla: si una imagen mide,
por ejemplo, 120 píxeles de largo, debería mostrarse con una dimensión visible de 1 pulgada en una pantalla de
clase ldpi, pero de 0,5 pulgadas en una pantalla hdpi. La representación y la organización en la página serán, de
este modo, distintas en función del terminal utilizado para la visualización.
Para ayudar a los desarrolladores a proveer la misma experiencia de usuario en terminales con características
diversas, Android proporciona una solución simple y fácil de implementar, basada en el administrador de recursos.
2. Los recursos
En Android, los recursos representan el conjunto de elementos integrados en la aplicación que esta necesita para
poder funcionar: los sonidos, las imágenes, los vídeos… son recursos. Veremos, en la sección Internacionalización,
que los textos presentados al usuario también son recursos, así como los archivos de descripción de la interfaz de
una actividad (recuerde que estos archivos se denominan layout).
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Todos los recursos se almacenan en una subcarpeta de la carpeta raíz /res (del inglés resource), en función de su
naturaleza:
l Las imágenes están organizadas en una carpeta /drawable.
l Las animaciones se encuentran, por su parte, en una carpeta /anim.
l Los archivos de descripción de interfaz se encuentran en la carpeta /layout.
l etc.
Veremos, a lo largo de este libro, distintos tipos de recursos.
Android, para dar respuesta a la problemática de la densidad de pantalla, proporciona, además de una carpeta
genérica para cada tipo de recurso, carpetas específicas para cada categoría de pantalla. Para las imágenes, por
ejemplo, se utilizan las siguientes carpetas:
/res/drawable: imágenes de dimensión estándar.
/res/drawable-ldpi: imágenes utilizadas por pantallas de densidad 120 dpi.
/res/drawable-mdpi: imágenes utilizadas por pantallas de densidad 160 dpi.
/res/drawable-hdpi: imágenes utilizadas por pantallas de densidad 240 dpi.
/res/drawable-xhdpi: imágenes utilizadas por pantallas de densidad 320 dpi.
...
De este modo, el desarrollador puede, si lo desea, proveer a la aplicación una versión de cada elemento visual
adaptado a cada densidad de pantalla.
A la inversa, si el desarrollador no provee una versión para cada densidad de pantalla, el sistema realizará un
escalado de los elementos visuales según la densidad de pantalla del terminal que ejecuta la aplicación.
Por otro lado, para facilitar el uso de recursos, y administrar simplemente las múltiples versiones de un mismo
recurso, Android introduce un nivel de abstracción para la nomenclatura de estos recursos.
Como hemos visto brevemente en el proyecto HelloWorld a propósito de los archivos XML de layout, a cada
recurso se le asigna un identificador único generado por el sistema.
Estos identificadores únicos son de tipo final int, y se definen en las subclases de la clase estática R (de
recurso), a cada tipo de recurso le corresponde una subclase. Además, cumplen la siguiente regla de nomenclatura:
l Si el recurso es un archivo, se utilizará el nombre del archivo como identificador del recurso.
l Si el recurso es un valor, una cadena de caracteres, por ejemplo, entonces se utilizará el nombre indicado en la
propiedad Name del valor.
En el caso de que el recurso deba indicarse en el código Java, el nombre del recurso se define según el siguiente
esquema:
[Nombre del paquete.]R.tipo.nombre
Por ejemplo, en el proyecto HelloWorld definido en el capítulo anterior, el archivo de definición de la organización en
página, el archivo de layout principal de la actividad es el archivo activity_main.xml. En el código fuente se
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
hace referencia a él con el nombre R.layout.activity_main, R pertenece al paquete
com.ejemplo.helloworld.
De la misma manera, el mensaje «Hello world!» se define en el archivo de recursos strings.xml como veremos
en la sección Internacionalización, y se utilizará con el nombre R.string.hello_world, si debe utilizarse en el
código Java.
Si un recurso debe utilizarse en los archivos de layout (en formato XML), se usa el formato @[Nombre del paquete:]
tipo/nombre. Por ejemplo, el archivo activity_main.xml utiliza la cadena de caracteres
@string/hello_world.
3. Densityindependent pixel
La variedad de densidades de pantalla de los terminales introduce otro problema: el dimensionamiento de los
componentes de una interfaz.
Por ejemplo, si se planea tener una pantalla para mostrar un botón de un tamaño específico, este tamaño, si se
expresa en píxeles, sería dependiente del terminal que lo muestre, puesto que no hay el mismo número de píxeles
por pulgada para dos pantallas de diferente densidad.
Para evitar este problema, Android introduce una nueva unidad para el dimensionamiento: el densityindependent
pixel (píxel independiente de la densidad).
Esta unidad se expresa en dip o dp, y permite abstraerse de la densidad de las pantallas durante la fase de
construcción de una interfaz.
Cuando las dimensiones de un elemento de la interfaz se definen en dip, el sistema realiza una conversión en
función de la densidad efectiva de la pantalla según la siguiente fórmula:
px = dip * (dpi/160)
px representa la dimensión en píxeles; dip, la dimensión indicada por el desarrollador en densityindependent
pixels, y dpi representa la densidad de la pantalla del terminal host.
Así, para un botón cuyo ancho esté fijado a 100 dip, el sistema determinará las siguientes dimensiones para cada
densidad de pantalla:
Es habitual utilizar el sufijo dp en lugar de dip para especificar una dimensión en densityindependent pixels. Esto
permite asegurar que no habrá confusión con la unidad dpi (dot per inch, punto por pulgada).
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Manejar varias versiones de API
El administrador de recursos, además de la caracterización por densidad de pantalla, permite también distinguir los
recursos según el nivel de API del terminal.
Si bien esto se utiliza con poca frecuencia para los elementos visuales y los layouts, esta caracterización, es por el
contrario, muy útil para los archivos de definición de estilo: lo veremos en el capítulo dedicado al diseño avanzado, los
estilos dependen en gran medida de la versión de API del sistema.
Para caracterizar una carpeta de recursos por la versión de API es necesario, de manera similar a la densidad,
agregar el número de versión de API correspondiente, prefijada por v.
Por ejemplo, un archivo de definición de estilo específico de la versión 14 de la API (Ice Cream Sandwich) se
almacenará en la carpeta /values-v14/.
El valor v proporcionado será válido para todas las API iguales o superiores a este valor, siempre y cuando no haya
valores definidos para API superiores. Por ejemplo, en el caso de definir las siguientes carpetas:
/res/values-v11/
/res/values-v14/
/res/values-v21/
Los valores definidos en values-v11 se utilizarán en sistemas con API 11, 12 y 13. Los valores definidos en
values-v14 se utilizarán, por su parte, en sistemas con API 14 a 20, los valores values-v21 se utilizarán para
todos los sistemas a partir de la API 21.
Alternativamente, si la distinción del nivel de API debe realizarse en el código Java, se utilizará el valor de la
constante android.os.Build.VERSION.SDK_INT:
if(android.os.Build.VERSION.SDK_INT>=
android.os.Build.VERSION_CODES.GINGERBREAD) {
// código para los terminales con GingerBread o superior
} else {
// código para los terminales previos a GingerBread
}
Observe que todas las versiones están disponibles como constantes de la clase estática
android.os.Build.VERSION_CODES.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Bibliotecas de soporte
El ámbito de la fragmentación del sistema Android ha obligado a Google a implementar un conjunto de bibliotecas
llamadas de soporte (Support Libraries, en inglés), que permiten a las versiones más antiguas de Android aprovechar
las últimas novedades de la plataforma o, al menos, simular el comportamiento de estas novedades. Estas API no se
distribuyen con las actualizaciones de Android, sino que las integran los desarrolladores en sus aplicaciones,
permitiendo así a los desarrolladores proporcionar un comportamiento unificado a los usuarios, ya utilicen una versión
antigua o bien reciente del sistema Android.
Si bien en un principio (la primera API de soporte data de marzo de 2011) el objetivo de las bibliotecas de soporte era
aportar a las versiones antiguas las mismas funcionalidades que las últimas, Google ha agregado progresivamente
funcionalidades propias de las bibliotecas de soporte para ayudar al desarrollo. Google habla, en estas API, de clases
de ayuda (Helpers); las primeras API estaban específicamente vinculadas a la compatibilidad ascendente (backward
Compatibility).
Las dos principales bibliotecas de soporte se llaman v4 y v7appcompat:
l v4: esta biblioteca obtiene su nombre del hecho de que en un origen aportaba un conjunto de funcionalidades a los
sistemas con Android API 4 (¡Android 1.6!). Ahora utilizable a partir de la API 9 (Gingerbread), esta biblioteca ofrece,
entre otras, la posibilidad de utilizar los fragmentos (la utilizaremos en el capítulo Los fragmentos) en las versiones
antiguas de Android, y proporciona un control de usuario para implementar una navegación por panel de navegación
(Drawer layout, que se aborda en el capítulo Navegación y ventanas emergentes, sección El navigation drawer). Está
muy extendida en el entorno Android y es omnipresente en los ejemplos de desarrollo proporcionados por Google.
l v7appcompat: también utilizable a partir de Android 2.3 (API 9), esta biblioteca permite soportar la barra de acción
(ActionBar, que se aborda en el capítulo Navegación y ventanas emergentes, sección Los menús), e integra un conjunto
de elementos que permiten implementar aplicaciones con el estilo Material Design, estilo implementado por Google con
Android 5 (Lollipop). Cabe destacar que v7appcompat (llamada a menudo «appcompat») hace referencia a v4, pero
no la integra.
Veremos, a lo largo de este libro, cómo integrar y utilizar ambas bibliotecas de soporte.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Internacionalización
La plataforma Android es multiidioma: cada usuario puede, en pocos segundos, cambiar el idioma de su sistema sin
tener que reiniciar el terminal.
De la misma manera, veremos en el capítulo dedicado a la publicación que el desarrollador define la lista de países en
los que estará disponible su aplicación.
Para ayudar a construir aplicaciones completamente multilingües, Android adopta el mismo principio que para las
densidades de pantalla: cada carpeta de recursos puede caracterizarse por el código de idioma al que está
destinada.
Esta caracterización, unida a la noción de recursos aplicados a las cadenas de caracteres, proporciona un mecanismo
muy simple para internacionalizar una aplicación, tanto desde su diseño como posteriormente.
En efecto, Google desaconseja de forma encarecida utilizar directamente cadenas de caracteres, ya sea en los
archivos de layout o en el código Java. En vez de ello, estas cadenas de caracteres se definen como valores de tipo
String y se almacenan generalmente en un archivo strings.xml.
Así, si se desea proveer una versión en español de la aplicación HelloWorld del capítulo anterior (disponible, de
momento, solo en inglés), bastará con agregar una carpeta values-es, copiar a continuación el archivo
strings.xml y traducirlo.
Archivo /values/strings.xml:
<resources>
<string name="app_name">HelloWorld</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
</resources>
Archivo /values-es/strings.xml:
<resources>
<string name="app_name">HelloWorld</string>
<string name="hello_world">¡Hola mundo!</string>
<string name="action_settings">Configuración</string>
</resources>
Observe que el nombre de la aplicación, que se ha dejado voluntariamente en inglés, en la entrada app_name
puede omitirse en el archivo dedicado al idioma español.
El código que utiliza estos recursos podría ser el siguiente:
TextView txtHello;
[...]
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
txtHello.setText(R.string.hello_world);
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Generalización
Existen muchos otros calificadores soportados por la plataforma. La siguiente tabla presenta los calificadores
utilizados más a menudo; la lista completa está disponible (en inglés) en la siguiente dirección:
http://developer.android.com/guide/topics/resources/providingresources.html (consultar la tabla «table 2»).
Idioma en Idioma definido por el terminal del usuario. Además del
es idioma, también es posible calificar el código de región, a
enrUS partir de las dos letras siguientes a la letra r (de región).
Orientación de la port Orientación de la pantalla: es posible proveer recursos
pantalla land distintos según si la pantalla está en modo vertical (port) o
apaisado (land, de landscape).
Es posible combinar los distintos calificadores que se pueden agregar a las carpetas de recursos; la única restricción
es respetar el orden indicado en la tabla anterior (o el orden definido en la documentación oficial indicada en el enlace
citado anteriormente). Cada calificador está separado por el carácter «».
Por ejemplo:
res/drawable-port-hdpi/
res/values-es-v11/
res/values-en-v14/
Sin embargo, está prohibido combinar varios calificadores de una misma propiedad. Por ejemplo, la
carpeta /res/values-v11-v14/ no cumple la regla.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Noción de sabor
Incluso aunque esta noción no está concebida, propiamente hablando, para resolver la fragmentación, conviene
mencionar esta característica aportada por Android Studio: los sabores (flavors, en inglés).
AndroidStudio, o con más exactitud Gradle la herramienta de construcción utilizada por AndroidStudio, permite en
efecto producir distintas variantes (los flavors) de una misma aplicación, sin duplicación de código.
Los sabores se utilizan típicamente para producir, a partir de un mismo proyecto, una versión gratuita y una versión
de pago de una aplicación: abordaremos con detalle esta funcionalidad en el capítulo dedicado a la publicación de una
aplicación. Muy fácil de usar, potente (es posible realizar ramas condicionales en función de los sabores), esta
característica también puede ser un recurso para gestionar casos complejos derivados de la fragmentación.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Preparación del proyecto LocDVD
El presente libro se basa en el desarrollo de una aplicación de gestión de DVD. Esta aplicación debe permitir a sus
usuarios visualizar la lista de sus DVD, consultar la ficha detallada de un DVD y autorizar a los usuarios a introducir los
DVD que poseen.
La aplicación debe, a su vez, ser capaz de buscar un DVD a partir de una base de datos de referencia accesible a
través de un servicio web, y proporciona al usuario la posibilidad de personalizar la pantalla de bienvenida del
terminal mostrando el último DVD incorporado en la base de datos.
Si bien el diseño de la aplicación es, en un primer momento, deliberadamente básico, los últimos capítulos tendrán
como objetivo proporcionar una experiencia de usuario más agradable mejorando la presentación de las distintas
pantallas de la aplicación.
Los primeros capítulos se corresponden, particularmente, con los terminales de tipo smartphone. El capítulo Los
fragmentos abordará las problemáticas vinculadas a las tabletas, y permitirá implementar todos los mecanismos
ofrecidos por la plataforma Android para desarrollar una aplicación compatible con smartphones y tabletas,
mecanismos que se retomarán en los siguientes capítulos.
La solución se desarrolla a partir de un proyecto «vacío», creado con el asistente de creación de proyectos de Android
Studio, tal y como hemos visto en el capítulo anterior. Las etapas de creación del proyecto se presentan a
continuación:
Abra Android Studio. Por defecto, se abre automáticamente el último proyecto cargado; aquí, HelloWorld.
En el menú File, seleccione la opción Close Project. El proyecto se cierra en el entorno de desarrollo, y se
muestra la ventana Welcome to Android Studio.
Como con el proyecto HelloWorld, seleccione la opción Start a new Android Studio Project.
El nombre que debe indicar para la aplicación es LocDVD. En el resto del libro, la aplicación utiliza
com.ejemplo.locdvd como nombre de paquete: se corresponde con el nombre de dominio
ejemplo.com. No existe ninguna restricción para cambiar el nombre del dominio y personalizarlo: el
código deberá simplemente adaptarse al nombre del paquete. Esta adaptación se resume en las
declaraciones del paquete en cada clase que se creará a lo largo del proyecto.
La aplicación estará, como el proyecto HelloWorld, destinada a smartphones y tabletas y deberá
basarse en la versión IceCreamSandwich de Android (API 14).
A su vez, es preferible agregar una actividad por defecto a la aplicación utilizando, por ejemplo, la
plantilla Empty Activity. El nombre de actividad sugerido por Android Studio es perfectamente válido.
Una vez terminado el asistente, Android Studio genera el código por defecto, ¡que se modificará a partir
del siguiente capítulo!
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Introducción
Las actividades son la base de una aplicación Android: toda pantalla que se presente a los usuarios está basada en
una actividad.
Este capítulo realiza una presentación completa de las actividades: uso, ciclo de vida, vínculo entre el código de la
actividad y el archivo de descripción de interfaz.
El capítulo aborda a continuación los componentes de creación de interfaces más sencillos: controles de visualización
y de introducción de datos textuales, así como los botones.
Por último, la sección Configurar una pantalla, gestionar la representación adaptativa describirá cómo construir una
interfaz que se adapte a la representación del terminal.
Cuando termine este capítulo, la aplicación LocDVD permitirá introducir y visualizar la información correspondiente a
un DVD.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Las actividades: ciclo de vida de una pantalla
La primera actividad que se va a agregar al proyecto es la que se encarga de mostrar y consultar una ficha de DVD.
1. Creación de una nueva actividad
Para crear la actividad de visualización de un DVD, hay que situarse, en el explorador del proyecto de Android
Studio, en la carpeta app/java/com.ejemplo.locdvd y crear un archivo Java en esta carpeta.
Abra el proyecto LocDVD en Android Studio.
Vaya al árbol del proyecto en modo Proyecto.
Haga clic con el botón derecho en la carpeta com.ejemplo.locdvd y seleccione la opción New Java
class.
Se abre una ventana popup, que le permite introducir un nombre para la nueva clase. Introduzca el
nombre ViewDVDActivity y, a continuación, haga clic en el botón OK.
Android Studio genera el archivo ViewDVDActivity.java y lo muestra en el editor de Java.
De momento, el archivo contiene únicamente el código de definición de la clase.
package com.ejemplo.locdvd;
import android.app.Activity;
No es raro declarar un constructor para una actividad: este constructor no lo invocaría el sistema. En su lugar, debe
seguir el ciclo de vida de una actividad y sobrecargar los métodos expuestos correspondientes a los estados de
este ciclo de vida (que va desde la creación hasta la destrucción del objeto Activity). La siguiente sección
presenta este ciclo de vida.
2. Ciclo de vida de una actividad
Para evitar llamadas redundantes y errores, es obligatorio conocer el ciclo de vida de la actividad. Ciertas acciones,
en efecto, solo pueden llevarse a cabo cuando la actividad se encuentra en un estado particular.
La siguiente tabla describe los distintos estados de una actividad, en orden de invocación.
Método invocado Descripción
onCreate Se invoca durante la creación de la actividad.
onStart Se invoca una vez creada o recreada la actividad.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
onResume Se invoca cuando se presenta la actividad al usuario.
onPause Se invoca cuando otra actividad va a pasar a primer plano. Queda pendiente del
método onResume.
onStop Se invoca cuando la actividad ya no se presenta al usuario.
Aun con riesgo de producir un error en tiempo de ejecución, es obligatorio, cuando se sobrecarga un método del
ciclo de vida de la actividad, invocar explícitamente el método de la clase madre.
El siguiente código muestra un ejemplo completo de sobrecarga de estos métodos.
package com.ejemplo.locdvd;
import android.app.Activity;
import android.os.Bundle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
3. Inicialización de la actividad
La inicialización de una actividad se realiza habitualmente en el método onCreate: este método se invoca, en
efecto, al inicio del ciclo de vida, y se invoca una única vez (a diferencia de los métodos onStart y onResume, que
pueden invocarse varias veces).
Si bien esta no es la única posibilidad, la interfaz de una actividad se define, por lo general (al menos parcialmente)
en un archivo XML de layout.
El método onCreate, que es el primer método invocado por el sistema, es el encargado de «vincular» el cógido
Java con este archivo de layout.
Este vínculo se establece a través del método setContentView de la clase Activity:
setContentView(int layoutID);
El parámetro de tipo int que se pasa como parámetro al método corresponde al identificador del recurso (el
archivo XML de layout) generado automáticamente, como se indicaba en el anterior capítulo. El archivo de layout es
un recurso de tipo layout. Por tanto, el identificador tendrá la forma R.layout.identificador.
La siguiente sección decribe la creación del archivo de layout y el vínculo con la actividad.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Construcción de una pantalla de consulta de datos
El archivo de layout de la actividad ViewDVDActivity debe almacenarse en la carpeta /res/layout/.
Hay que definir este archivo, vincularlo con la actividad y, a continuación, construir la interfaz de visualización.
Sitúese en la carpeta app/src/main/res/layout en el explorador del proyecto Android Studio. Ya debe
existir un archivo activity_main.xml, generado por Android Studio y correspondiente a la actividad
MainActivity creada también por Android Studio.
Haga clic con el botón derecho y seleccione la opción New Layout resource file.
Se abre una ventana popup que le pregunta el nombre del archivo y el elemento raíz del layout (Root
element). Escriba activity_viewdvd y deje la sugerencia para la ocpión Root element (que debe ser
LinearLayout).
Es importante aplicar una conversión de nombre bastante estricta para los archivos de layout, pues, en efecto, es
imposible crear subcarpetas dentro de la carpeta layout. Por ello, la carpeta contendrá, por lo general, una lista
significativa de archivos que, sin una regla de nomenclatura, se vuelve rápidamente ingobernable.
Para los archivos de layout, como con todos los recursos, es obligatorio utilizar solo caracteres en minúsculas.
Android Studio crea el archivo y lo abre directamente en modo Design.
1. Creación de la interfaz
Inicialmente, la interfaz de la aplicación es muy sencilla; se completará más adelante.
Esta pantalla contiene la información básica acerca del DVD: título de la película, año de aparición, actores
principales y resumen.
La pantalla se ilustra en la siguiente maqueta.
Observe que el número de actores principales es, a priori, desconocido. Hay que prever que la interfaz debe
adaptarse y presentar un número variable de campos correspondientes al nombre de los actores.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
La primera etapa consiste en implementar el componente que muestra el título del DVD.
Abra (haciendo doble clic en el nombre del archivo), si no lo estuviera, el
archivo activity_viewdvd.xml en el editor, en modo Design.
Android Studio adapta la visualización en curso y presenta una visualización previa de la pantalla,
actualmente vacía.
En la lista llamada Palette se presenta un conjunto de componentes que pueden situarse en la página.
En esta lista, seleccione el componente TextView, en el apartado Widget.
Deslice el elemento hasta la pantalla, situándolo en la parte superior de esta.
Se agrega un componente TextView. Como está seleccionado, está enmarcado por un rectángulo
redimensionable.
En el panel Properties, a la derecha de la visualización previa, se muestran algunas propiedades del
componente seleccionado. Para mostrarlas todas, haga clic en la opción View all properties, situada en la
parte inferior del panel Properties.
El título del DVD está, de esta manera, situado demasiado arriba. Hay que agregar un margen por
encima del componente para que no se «pegue» al borde de la pantalla.
En la entrada layout_marginTop de la lista, escriba el valor 16 dp.
Las dimensiones se indican en density independent pixels, como vimos en el anterior capítulo.
El control se encuentra, ahora, separado de la parte superior de la pantalla, como queríamos. También hay que
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
mostrar el texto centrado en el control TextView:
Seleccione la propiedad layout_gravity de la lista (puede realizar una búsqueda entre las propiedades
haciendo clic en la lupa situada en la parte superior de la lista) y haga clic en la flecha gris para ampliar la
lista de opciones.
Se presentan varios posibles valores para configurar la gravedad: top, bottom, left, right, etc. Los valores
son casillas de selección, que pueden combinarse entre sí. Seleccione aquí los valores top y
center_horizontal.
Guarde los cambios haciendo clic en el icono Save all (situado en la parte superior derecha de la barra de
herramientas) o utilizando la combinación de teclas [ctrl]s.
a. Código fuente
Ahora que hemos agregado un primer componente, es interesante ver el código que se ha generado para este
elemento.
Muestre el código del layout seleccionando la pestaña Text del editor. Android Studio presenta el código
XML del layout a la izquierda y la visualización previa a la derecha.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
El código presenta una primera etiqueta LinearLayout y, como etiqueta hija, una etiqueta TextView,
correspondiente al componente que acabamos de agregar.
Cada una de estas etiquetas incluye un conjunto de propiedades que se detallarán más adelante en este capítulo.
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_gravity="top|center_horizontal"
android:text="TextView" />
</LinearLayout>
La primera etiqueta, LinearLayout, se corresponde con el elemento raíz indicado durante la creación del
archivo de layout. Un LinearLayout es un componente, igual que el componente TextView, con la diferencia
de que es un contenedor de vistas.
Existen varios contenedores de vistas disponibles en Android:
l LinearLayout: componente que sitúa los componentes hijos que contiene uno a continuación de otro. Según el
valor de la propiedad orientation, los componentes se sitúan unos debajo de otros
(orientation="vertical") o unos al lado de otros (orientation="horizontal"). Los
componentes hijos no pueden superponerse. Este es el contenedor de vista más sencillo de manipular.
l RelativeLayout: componente que permite un posicionamiento relativo de los componentes hijos. Los
componentes pueden posicionarse unos respecto a otros (encima, debajo, al lado, etc.) o respecto al propio
contenedor (encima, debajo, a la izquierda, a la derecha). Los componentes hijos pueden superponerse.
l FrameLayout: componente que no realiza ningún posicionamiento de los componentes hijos. Estos componentes
se sitúan en la parte superior derecha del contenedor, superponiéndose.
l TableLayout: como su nombre indica, este componente permite definir una tabla. Los componentes hijos deben
situarse en contenedores TableRow.
b. Las propiedades
Las propiedades de los componentes también se definen en el archivo XML de layout. El namespace, definido
aquí con el prefijo android, definición realizada por el primer elemento del archivo XML, exige que todas las
propiedades estén prefijadas con android.
La siguiente tabla muestra las principales propiedades comunes a todos los componentes. Las propiedades
específicas de cada componente se detallarán durante su uso, a lo largo del libro.
Propiedad Descripción
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
background Elemento que se utilizará para el fondo de la vista. Puede ser una imagen
( drawable) o un color.
clickable Indica si el elemento debe reaccionar ante el clic del usuario.
id Identificador del componente.
visibility Visibilidad del componente.
La lista completa de propiedades comunes a los componentes de la plataforma está disponible en la siguiente
dirección: http://developer.android.com/reference/android/view/View.html#lattrs
Los contenedores de vista definen también los parámetros de layout, que deben informarse para todos los
componentes hijos.
Los posibles valores para estas propiedades son los siguientes:
l Un valor predefinido entre los siguientes:
n match_parent: el componente recibe las mismas dimensiones que el componente padre.
n wrap_content: el componente recibe la dimensión más pequeña necesaria para mostrar el conjunto de su
contenido.
n fill_parent: parecido a match_parent, deprecado desde la API level 8.
c. Propiedades específicas de los componentes LinearLayout y TextView
El componente LinearLayout posee una propiedad adicional, ya mencionada, que permite indicar el «sentido»
del layout. Esta propiedad, orientation, puede recibir los valores horizontal o vertical.
Cabe destacar que esta propiedad es obligatoria si el LinearLayout posee más de un componente hijo.
También cabe destacar la propiedad layout_gravity, utilizada anteriormente, que especifica cómo debe
posicionarse el componente respecto a su padre. Los valores utilizados habitualmente son los siguientes:
l top: el componente se posicionará en la parte superior de su contenedor.
l bottom: el componente se posicionará en la parte inferior de su contenedor.
l left: el componente se posicionará a la izquierda.
l right: el componente se posicionará a la derecha.
l center_vertical: el componente estará centrado en la dimensión vertical.
l center_horizontal: el componente estará centrado en la dimensión horizontal.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
l center: el componente se situará en el centro de su contenedor.
Es posible combinar estos valores, utilizando el carácter «|» como separador (sin dejar ningún espacio).
Por ejemplo:
android:layout_gravity= "center_horizontal|top"
El componente TextView posee una gran cantidad de propiedades que permiten, entre otros, especificar el
comportamiento del texto que mostrará.
La siguiente tabla describe las principales propiedades:
Propiedad Descripción
drawableLeft Identificador del elemento visual que se ha de configurar en la ubicación indicada.
drawableRight
drawableTop
drawableBottom
gravity Indica cómo debe posicionarse el contenido en el interior del componente.
lines Indica el número de líneas mostradas.
maxLines Indica el número máximo de líneas que se han de mostrar.
minLines Indica el número mínimo de líneas que se han de mostrar.
singleLine Fuerza que el componente muestre una única línea.
text Texto que se mostrará.
textColor Color del texto.
textSize Tamaño del texto. Se recomienda definir el tamaño en scaledpixels (notación
sp): el tamaño de letra tendrá en cuenta, además del tamaño indicado, las
opciones de tamaño de letra que haya configurado el usuario.
typeface Tipografía que se va a utilizar (los posibles valores son normal, sans sans
serif, serif, monospace).
d. Definir un identificador
Cada componente puede contar con un identificador cuando se declara en un archivo de layout. Este identificador
permite obtener una referencia hacia el componente en el código Java.
La sintaxis para definir un identificador es la siguiente: @+id/identificador
l El carácter @ indica al sistema que el resto de la cadena representa un recurso.
l El símbolo + indica que el identificador que sigue debe crearse.
l id/ define que el recurso agregado será de tipo identificador.
l El identificador propiamente dicho se indica tras el carácter /.
Por ejemplo:
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:id="@+id/textView" // el identificador será textView
e. Construcción de la interfaz
La interfaz prevista para la visualización de un DVD es simple: el componente LinearLayout basta para
asegurar la representación en la página del conjunto de información que debe presentarse.
La construcción completa de la interfaz puede llevarse a cabo utilizando el modo design (construcción con el
ratón), o bien escribiendo el código directamente en modo text.
A continuación se presenta una primera versión, que tiene en cuenta únicamente la restricción sobre los nombres
de los actores. Observe que a cada componente TextView se le atribuye un identificador único:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="título del dvd"
android:id="@+id/tituloDVD"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="16dp"
android:textSize="22sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="año"
android:textSize="15sp"
android:id="@+id/anyoDVD" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:text="actor 1"
android:id="@+id/actor1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
android:text="actor 2"
android:id="@+id/actor2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:text="resumen"
android:minLines="5"
android:maxLines="15"
android:id="@+id/resumenPelicula" />
</LinearLayout>
La visualización previa de esta primera página ofrece una primera comprobación de la representación gráfica.
2. Vínculo del archivo de layout con el código de la actividad
Además de asignar el archivo de layout a la actividad, que se hace invocando el método setContentView, es
necesario obtener una referencia a cada uno de estos components para manipularlos en el código de la actividad
(por ejemplo, para indicar una cadena de caracteres que se desea mostrar).
La clase Activity presenta, para ello, el método findViewById:
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El método recibe como parámetro el identificador del componente correspondiente y devuelve un objeto de tipo
View que hay que convertir en un componente tipado (para poder invocar a sus propios métodos).
El identificador, como ocurre con todos los recursos en Android, es una constante, subclase de la clase R (de
recurso). Los recursos están clasificados por tipo, de modo que el identificador se define aquí en la subclase id de
R.
En el código Java, los componentes tienen como identificador las siguientes constantes: R.id.identificador.
Por ejemplo:
Teniendo en cuenta las restricciones del ciclo de vida de la actividad, la obtención de las referencias sobre los
componentes del archivo de layout se realiza en el método onCreate. Las propias manipulaciones tienen lugar,
habitualmente, en el método onResume: la visualización se termina en el último momento.
Los componentes se declaran en variables locales a la actividad, para poder invocarlas en el conjunto de la clase
ViewDVDActivity.
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtActor1;
TextView txtActor2;
TextView txtResumenPelicula;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Además del código, hay que importar la clase android.widget.TextView: agregar la instrucción de
importación en el encabezado del archivo o bien manualmente o bien situando el cursor encima del elemento
TextView (que debe estar resaltando en rojo en el código), utilizando la combinación de teclas [Alt][Enter] y
seleccionando a continuación la opción Import class de la lista desplegable, como muestra la siguiente captura de
pantalla.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
Llegados a este punto del proyecto, todavía no se ha creado la clase DVD, de modo que la visualización se probará
primero con valores arbitrarios.
La asignación de datos que se han de mostrar en un componente TextView se realiza invocando el método
setText, que recibe como parámetro o bien una cadena de caracteres o bien un identificador de recurso de tipo
string.
Por ejemplo:
Teniendo en cuenta estos elementos, el código del método onResume es el siguiente:
@Override
protected void onResume() {
super.onResume();
Mejoras
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
En el código del método onResume, hay que destacar que la asignación del texto correspondiente al año de
aparición del DVD plantea un problema para la internacionalización de la aplicación. El texto «Año de aparición»
está, en efecto, integrado directamente en el código, sin utilizar los archivos de recursos: el multiidioma no puede
gestionarse por el administrador de recursos.
Para hacer que esta parte sea fácil de internacionalizar, hay que modificar esta sección de código y utilizar recursos
de tipo String.
Sitúese en la carpeta /res/values.
Abra el archivo strings.xml.
El archivo ya contiene la definición de varios recursos de tipo String.
Agregue un recurso String según el mismo esquema de los recursos ya definidos.
Hay que asignar un nombre explícito al recurso (por ejemplo, el texto al que representa en el idioma por defecto sin
caracteres especiales y reemplazando los espacios por el carácter de «subrayado»).
Es preferible declarar el valor de la cadena entre comillas, para evitar al editor tener que interpretar eventuales
caracteres especiales.
El valor es una cadena de caracteres que puede manipularse en el código Java.
Gracias a las comillas, los caracteres especiales no se interpretan en el administrador de recursos y pueden
gestionarse en Java.
El recurso se define, por ejemplo, de la siguiente manera:
La clase Activity hereda de un método que permite obtener una cadena de caracteres definida en los recursos:
el método getString, que recibe como parámetro el identificador del recurso de tipo String.
Por ejemplo:
El código para informar el año de aparición es ahora el siguiente:
txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion), 2014));
De esta manera, la fase de internacionalización se limita a la traducción del archivo de definición de recursos de tipo
string.
Al final, el código completo de la actividad viewDVDActivity es el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
package com.ejemplo.locdvd;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtActor1;
TextView txtActor2;
TextView txtResumenPelicula;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
@Override
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
3. Inscripción en el manifiesto
Antes de probar la nueva actividad, hay que declararla en el manifiesto de la aplicación: todas las actividades deben
declararse en el archivo AndroidManifest.xml.
Sitúese en la carpeta app/manifests/.
Abra el archivo AndroidManifest.xml.
Android Studio, a diferencia de Eclipse/ADT, no proporciona un editor especializado para la edición del
archivo AndroidManifest.xml: el archivo se edita como cualquier otro archivo XML.
La actividad creada por defecto por el asistente de creación del proyecto ya se encuentra inscrita en el manifiesto.
Esta declaración se realiza, en su forma más básica, agregando una etiqueta activity en la sección application.
La única información obligatoria es el atributo name, que define el nombre de la clase Java de la actividad. Este
nombre es el nombre completo de la clase, en el cual se puede sustituir el nombre del paquete por un punto si es el
mismo que el definido para la aplicación (como ocurre aquí, el paquete se define en la etiqueta manifest).
Por ejemplo, la declaración de la actividad ViewDVDActivity es la siguiente:
<activity android:name=".ViewDVDActivity"/>
Sin embargo, para esta primera prueba, no hay ningún menú o botón que permita acceder a esta actividad. De
modo que resulta necesario declarar que la actividad ViewDVDActivity es la actividad que debe lanzarse al
inicio.
Durante la creación del proyecto, el asistente de creación del proyecto define una actividad y especifica que esta
actividad es la actividad por defecto. El código correspondiente se estudiará más adelante en este libro, cuando se
aborden los filtros de intención.
De momento, basta con mover el código XML utilizado para la actividad.MainActivity para que se aplique a la
actividad ViewDVDActivity.
El archivo AndroidManifest.xml contiene, por tanto, el siguiente código:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ejemplo.locdvd" >
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
</activity>
<activity android:name=".ViewDVDActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
La aplicación está, ahora, lista para su primera ejecución:
Abra la aplicación con Android Studio, en un emulador o el terminal físico que prefiera.
Se muestra la actividad que acaba de crear, y todo funciona correctamente.
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 15 -
Introducción de datos, controles principales
Además de una pantalla de consulta de datos, la aplicación debe permitir al usuario introducir un nuevo DVD. Como
con la pantalla de consulta, hay que crear una actividad y un archivo de layout asociado.
El registro de los datos se abordará en el siguiente capítulo, de modo que la parte esencial del desarrollo para esta
pantalla está, de momento, vinculada a la gestión de los actores y a la verificación de los datos introducidos.
1. Creación del esqueleto de la pantalla
La maqueta de la pantalla es, aunque más compleja que la pantalla de visualización, relativamente sencilla: la
pantalla se basa en un LinearLayout. Además de utilizar el componente TextView, también hay que invocar
aquí el componente de introducción de texto, llamado EditText, así como el componente Button: en la
aplicación, un botón permite registrar los datos y se utiliza un botón para agregar los campos que permiten
introducir los distintos actores.
El componente EditText es muy parecido al componente TextView: EditText hereda, en efecto, de
TextView. Su integración en un archivo de layout es también bastante similar.
Respecto al botón +, si existen varias soluciones, la más sencilla aquí consiste en encapsular ambos componentes,
etiqueta y botón, en un contenedor de vista de tipo LinearLayout cuya orientación esté definida como
horizontal.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El siguiente ejemplo ofrece una implementación mínima:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Actores"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"/>
</LinearLayout>
La distancia entre ambos componentes está gestionada por los márgenes izquierdos y derechos de los
componentes.
Respecto al botón ok, existen varias soluciones. Una de ellas consiste en encapsular el componente Button en un
contenedor de tipo LinearLayout que tomará todo el ancho de la pantalla (especificando el ancho igual al valor
predefinido match_parent), y definir, además de la orientación horizontal, un posicionamiento alineado a la
derecha de los componentes hijos (utilizando la propiedad gravity de LinearLayout).
La implementación mínima de esta solución es la siguiente:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"/>
</LinearLayout>
Conviene observar que se utiliza la propiedad gravity, y no layout_gravity: el contenedor de vista
especifica así que la alineación de su contenido se hace a la derecha.
A diferencia de la pantalla de visualización, varios componentes TextView presentan aquí un texto fijo. En lugar de
especificar el texto en el código Java, es mucho más sencillo integrar directamente el texto a nivel de la declaración
de los componentes en el archivo de layout. La propiedad text permite realizar esta operación.
Como con el código Java, no es buena idea asignar un valor a esta propiedad directamente con el texto
correspondiente: conviene utilizar una definición de recursos de tipo String.
Para hacer referencia a estos recursos en los archivos de layout, debe utilizarse la siguiente sintaxis:
android:text= ”@string/nombre_del_recurso".
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Por ejemplo, tras definir el recurso actores:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/actores"/>
Inicialmente, la pantalla prevé la introducción de un único actor: la posibilidad de introducir varios actores se
gestionará en el código Java de la actividad.
Teniendo en cuenta estas reglas, ahora es posible crear el nuevo archivo de layout, que se llamará, por ejemplo,
activity_adddvd.xml:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/actores"
android:textSize="18sp"
android:textStyle="bold"/>
<Button
android:id="@+id/addDVD_addActor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addDVD_actor1" />
<!-- Zona del resumen -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/resumen"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="8"
android:id="@+id/addDVD_resumen" />
<!-- Zona del botón OK -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="right">
<Button
android:id="@+id/addDVD_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"
android:textSize="18sp"
android:textStyle="bold"/>
</LinearLayout>
</LinearLayout>
Como en cualquier archivo XML, es posible (y recomendable) incluir comentarios en los archivos de layout.
Como con la pantalla de visualización, hay que definir la clase Java de la actividad de introducción de información: el
archivo Java AddDVDActivity.java.
El código de la actividad, en principio, es prácticamente el mismo que para la visualización: el vínculo entre el código
y el archivo de layout se realiza a través del método onCreate, como la recuperación de las referencias hacia los
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
objetos de tipo EditText y Button.
Aquí, los objetos devueltos por el método findViewById() deben convertirse en EditText y en Button,
según su naturaleza en el archivo de layout.
El método onCreate es, de este modo, el siguiente:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
También aquí las referencias a los componentes se han declarado en la clase AddDVDActivity.
2. Gestión de los botones
Una vez implementada la estructura de la pantalla, ahora es necesario gestionar la posibilidad de agregar varios
actores. Para llevar a cabo esta tarea van a ser necesarias dos etapas: en primer lugar, detectar el clic en el botón
+ por parte del usuario y, a continuación, agregar una zona de introducción de información para un nuevo actor.
a. Reaccionar al clic
Cuando el usuario hace clic en un botón, o en cualquier otro control, el sistema produce un evento. Para
interceptar este evento hay que proveer un objeto que implemente la interfaz View.OnClickListener,
interfaz que presenta el método onClick(View v).
La asignación del objeto OnClickListener puede realizarse o bien en el archivo de layout, o bien en el código
Java.
Para realizar esta declaración en el archivo de layout, hay que informar la propiedad onClick del componente
(aquí, un Button), asignándole un método definido en la actividad, de modo que se respeten las siguientes
reglas:
l El método debe declararse como public.
l Debe devolver void.
l Debe recibir un único parámetro, de tipo View. Esta instancia representa el componente que ha producido el
evento.
Otra manera de hacer esto, más extendida, es realizar esta asignación en el código de la actividad, utilizando el
método setOnClickListener del componente correspondiente.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
public void setOnClickListener(View.OnClickListener listener)
Para condensar el código, esta asignación se lleva a cabo, por lo general, utilizando clases anónimas de Java.
setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}) ;
Tan solo queda por invocar un método que se encargará de agregar dinámicamente un componente de
introducción de información para un nuevo actor.
btnAddActor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addActor() ;
}
});
[...]
b. Agregar un componente
Hasta ahora, los elementos que componen una pantalla se han definido siempre en archivos de layout, pero
también es posible agregar uno o varios componentes en tiempo de ejecución.
Para ello, en primer lugar hay que instanciar el componente en el código, y a continuación integrarlo en un
contenedor de vista.
La instanciación se realiza invocando alguno de los constructores de la clase del componente. Aquí, para agregar
un campo que permita introducir un nombre de actor, hay que agregar un componente EditText.
El constructor por defecto recibe como parámetro un objeto de tipo Context, que es una interfaz que representa
el contexto de ejecución de la aplicación. La clase Activity implementa esta interfaz, de modo que la llamada al
constructor tiene el siguiente aspecto.
Una vez instanciado el componente, para poder presentarlo por pantalla es necesario insertarlo en un contenedor
de vista.
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Los objetos de tipo contenedor de vista ( LinearLayout, RelativeLayout, etc.) presentan el método
addView, cuya forma más sencilla es:
addView(View child)
Para la pantalla de introducción de información, hay que agregar un contenedor de vista en el archivo de layout,
asignarle un identificador, obtener una referencia a este contenedor de vista en el código de la actividad y agregar
un componente EditText a petición, utilizando los métodos vistos.
En el archivo de layout, debe modificarse la zona de los actores para integrar este nuevo layout:
[...]
<!-- Zona de los actores -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/actores"
android:textSize="18sp"
android:textStyle="bold"/>
<Button
android:id="@+id/addDVD_addActor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:textSize="20sp"
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout
android:id= »@+id/addDVD_addActorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:id="@+id/addDVD_actor1" />
</LinearLayout>
[...]
Debe declararse una referencia a este nuevo layout en el código de la actividad: esto se realiza de la misma
manera que para cualquier otro componente, utilizando el método findViewById.
La siguiente línea debe agregarse al cuerpo del método onCreate:
addActoresLayout =
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
(LinearLayout)findViewById(R.id.addDVD_addActorLayout);
El objeto LinearLayout addActoresLayout también se declara en la clase AddDVDActivity.
El método addActor (que permite agregar un componente EditText) contiene la declaración de un nuevo
componente EditText y su incorporación al contenedor de vista.
A continuación se muestra el código Java completo de la actividad:
package com.ejemplo.locdvd;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
EditText editTituloPelicula;
EditText editAnyo;
EditText editResumen;
Button btnAddActor;
Button btnOk;
LinearLayout addActoresLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
View.OnClickListener() {
@Override
public void onClick(View v) {
addActor();
}
});
}
3. Prueba de la pantalla de introducción de datos
Antes de probar la pantalla de introducción de datos, no debemos olvidarnos de declarar la actividad
AddDVDActivity en el archivo AndroidManifest.xml, como con la pantalla de visualización.
En principio, esta actividad se declara como la actividad de inicio de la aplicación.
<application
android:allowBackup= »true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
</activity>
<activity android:name=".ViewDVDActivity">
</activity>
<activity android:name=".AddDVDActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
</manifest>
Ejecute a continuación la aplicación para probar la nueva pantalla.
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Configurar una pantalla, gestionar la representación adaptativa
La pantalla configurada en la sección anterior, si bien es globalmente funcional, presenta sin embargo algunos
problemas. Esta sección va a permitir conocer estos problemas y corregirlos.
1. Desplazar pantalla
Si el usuario agrega varios actores, se corre el riesgo de que la zona de introducción del resumen se salga de la
pantalla. En algunos smartphones, es posible sin embargo que el resumen, o al menos el botón OK, no estén
visibles al mostrar la pantalla: esto depende de la resolución de pantalla del dispositivo de prueba.
La plataforma provee, para resolver este tipo de problema, el componente ScrollView, contenedor de vista que,
como su propio nombre indica, se encarga del desplazamiento de la pantalla.
Este componente es ligeramente diferente del componente LinearLayout en el sentido de que acepta un único
componente hijo. Habitualmente, este componente hijo es, a su vez, un contenedor de vista clásico, que contiene
los componentes hijos que constituyen la pantalla.
En el caso del archivo de layout de la actividad de introducción de datos, lo más sencillo es agregar un
ScrollView en el componente padre del LinearLayout raíz actual. Preste atención y no olvide definir el
espacio de nombres en este nuevo componente ScrollView, que se convierte en el primer componente definido
en el archivo de layout.
El componente ScrollView debe tener el mismo ancho que la pantalla y su altura debe adaptarse a su contenido.
El archivo de layout debe, por tanto, modificarse según el siguiente código fuente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
android:text="@string/anyo_de_aparicion_etiqueta"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="100dp"
android:layout_height="wrap_content"
android:id="@+id/addDVD_anyo" />
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:orientation="horizontal"
android:gravity="right">
<Button
android:id="@+id/addDVD_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"
android:textSize="18sp"
android:textStyle="bold"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
No hay que modificar nada en el código Java de la actividad, todo el procedimiento de desplazamiento lo
gestiona íntegramente el propio componente ScrollView.
2. Controlar la entrada
De momento, no se controla la entrada, ni se proporciona una ayuda al usuario en la introducción de datos. Por
ejemplo, cuando el usuario introduce el año de aparición de la película, el teclado adquiere su aspecto por defecto y
muestra las letras: sería más agradable para el usuario que el teclado estuviera directamente en modo de
introducción de cifra, únicamente para este campo.
De la misma manera, cuando el usuario introduce un nombre de actor, el corrector ortográfico subraya los nombres
si no se trata de nombres conocidos por el diccionario interno.
El componente EditText proporciona la propiedad inputType, que permite especificar la naturaleza del texto
que está destinado a capturar.
Hay varias opciones disponibles, que pueden combinarse utilizando el serparador «|». La siguiente tabla muestra
algunas de las posibles opciones, la lista completa está disponible en las listas automáticas de opciones del editor
Android Studio y también en la siguiente dirección:
http://developer.android.com/reference/android/widget/TextView.html#attr_android:inputType.
Valor Descripción
number El contenido es un número.
phone El contenido es un número de teléfono. El formato depende de la configuración del
terminal (español, inglés, etc.).
text El contenido es texto clásico.
textPersonName El texto es un nombre de persona.
textEmailAddress El texto es una dirección de correo electrónico.
textMultiLine El texto tiene varias líneas.
textCapWords Cada palabra empieza por una letra mayúscula.
textCapSentences El texto es una serie de frases. La primera letra de cada frase está en mayúsculas.
De este modo, para el comportamiento de introducción del año de aparición del DVD, la propiedad Input_Type
debe tomar el valor number:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
<EditText
android:layout_width="100dp"
android:layout_height="wrap_content"
android:id="@+id/addDVD_anyo"
android:inputType="number"/>
De la misma manera, el componente «resumen» debe declarar como valor por defecto de la propiedad
Input_Type la constante textCapSentences. Sin embargo, al asignar valor a esta propiedad se modifica el
comportamiento por defecto del componente cuando se declara como multiidioma. De modo que hay que asignar
también el valor textMultiLine.
<EditText
android:layout_width= »match_parent"
android:layout_height= »wrap_content"
android:minLines="8"
android:id="@+id/addDVD_resumen"
android:inputType="textCapSentences|textMultiLine"/>
Tenga precaución, no hay que insertar ningún espacio en blanco entre las constantes, el editor no detectará
correctamente los valores en ese caso.
Para el nombre de los actores, el hecho de que los componentes se gestionen dinámicamente hace que sea
necesario dar un valor a la propiedad InputType en el código de la actividad. Esto se realiza invocando el método
setInputType del componente.
setInputType(int type)
Es posible definir los mismos valores en la interfaz android.text.InputType: si bien tienen los mismos valores
que las constantes que pueden utilizarse en el archivo de layout, no tienen el mismo nombre (son constantes Java,
a diferencia de los archivos de layout).
La siguiente tabla muestra la correspondencia entre ambas.
Archivos de layout Código Java
number TYPE_CLASS_NUMBER
phone TYPE_CLASS_PHONE
text TYPE_CLASS_TEXT
textPersonName TYPE_TEXT_VARIATION_PERSON_NAME
textEmailAddress TYPE_TEXT_VARIATION_EMAIL_ADDRESS
textMultiLine TYPE_TEXT_FLAG_IME_MULTI_LINE
textCapWords TYPE_TEXT_FLAG_CAP_WORDS
textCapSentences TYPE_TEXT_FLAG_CAP_SENTENCES
Por lo tanto, para los actores es necesario definir la propiedad
InputType como una combinación de las
constantes TYPE_TEXT_VARIATION_PERSON_NAME y TYPE_TEXT_FLAG_CAP_WORDS (para que las primeras
letras del nombre y los apellidos estén en mayúsculas):
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME
|
InputType.TYPE_TEXT_FLAG_CAP_WORDS);
Es preciso modificar el método addActor para agregar la llamada a setInputType:
3. Gestionar la rotación de la pantalla
El tercer punto que hay que resolver es más complejo que los anteriores, y puede ser más difícil de detectar si las
pruebas se realizan exclusivamente en el terminal emulado: para visualizarlo, hay que introducir más de un actor
(de modo que la aplicación agregue un componente EditText en tiempo de ejecución) y realizar una rotación de
la pantalla (con la combinación de teclas [Ctrl][F11] en la ventana del emulador): los componentes EditText
agregados desaparecen cuando la pantalla se reajusta.
La explicación está directamente ligada al ciclo de vida de las actividades y al funcionamiento del sistema: cuando la
pantalla sufre una rotación, la actividad se destruye y se recrea a continuación. El mecanismo subyacente incluye,
por defecto, una copia de los datos introducidos en los controles EditText (si se les ha especificado un
identificador), pero no es capaz de guardar los elementos más complejos.
Android provee una solución que admninistra una colección (de tipo clave/valor) que se conserva durante la rotación
de la pantalla y se presenta a la actividad una vez que se crea de nuevo: se trata del parámetro de tipo Bundle
del método onCreate.
Antes de lanzar el proceso de rotación de la pantalla, se invoca el método onSaveInstanceState por la
actividad, que permite al desarrollador guardar los datos que desea almacenar en el objeto Bundle.
a. Guardar los datos
La copia de los datos se realiza, por tanto, sobrecargando el método onSaveInstanceState de la clase
Activity.
@Override
public void onSaveInstanceState(Bundle savedInstanceState)
Este método permite almacenar los nombres de los actores introducidos por el usuario; a continuación habrá que
comprobar la existencia de datos tras la creación de la actividad y volver a crear los componentes EditText
llegado el caso.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
El contenedor de vista
LinearLayout posee dos métodos que van a permitir obtener los componentes
EditText agregados dinámicamente: getChildAt y getChildCount:
l El método getChildAt(int n) devuelve la enésima vista hija del LinearLayout.
l El método getChildCount() devuelve el número de componentes hijos del layout.
Por último, cabe destacar que es necesario, para que la información se guarde correctamente, invocar el método
onSaveInstanceState de la clase madre.
El código de la sobrecarga es, al final, el siguiente:
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
String[] actores = new
String[addActoresLayout.getChildCount()];
for(int i=0;i<addActoresLayout.getChildCount();i++) {
View child = addActoresLayout.getChildAt(i);
if(child instanceof EditText)
actores[i] =
((EditText)child).getText().toString();
}
savedInstanceState.putStringArray("actores",actores);
super.onSaveInstanceState(savedInstanceState);
}
b. Restaurar los datos
De la misma manera, debe modificarse el método onCreate para tener en cuenta el hecho de que el Bundle
contiene datos potencialmente:
[...]
if(savedInstanceState!=null) {
String [] actores =
savedInstanceState.getStringArray("actores");
[...]
}
Tras algunas optimizaciones, en particular sobre el método addActor, el código de la actividad
AddDVDActivity es el siguiente:
package com.ejemplo.locdvd;
import android.app.Activity;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
import android.widget.LinearLayout;
EditText editTituloPelicula;
EditText editAnyo;
EditText editResumen;
Button btnAddActor;
Button btnOk;
LinearLayout addActoresLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addActoresLayout =
(LinearLayout)findViewById(R.id.addDVD_addActorLayout);
btnAddActor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addActor(null);
}
});
else {
// Ningún actor introducido, se muestra un componente editText vacío
addActor(null);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
String[] actores = new String[addActoresLayout.getChildCount()];
for(int i=0;i<addActoresLayout.getChildCount();i++) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
View child = addActoresLayout.getChildAt(i);
if(child instanceof EditText)
actores[i] = ((EditText)child).getText().toString();
}
savedInstanceState.putStringArray("actores",actores);
super.onSaveInstanceState(savedInstanceState);
}
addActoresLayout.addView(editNewActor);
}
}
El archivo de layout es el siguiente:
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:id="@+id/addDVD_anyo"
android:inputType="number"/>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
</LinearLayout>
</LinearLayout>
</ScrollView>
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Introducción
La aplicación LocDVD debe ser capaz de guardar los datos introducidos por el usuario. La plataforma Android provee
varias soluciones para el almacenamiento de los datos. Este capítulo describe estas diferentes soluciones, abordando
en primer lugar la copia en base de datos, a continuación describiendo las posibilidades de copia de seguridad
previstas para las preferencias de usuario y, por último, la lectura y escritura de datos en el sistema de archivos del
terminal.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Creación y modificación de una base de datos
Android integra, de manera nativa, un motor de base de datos, llamado SQLite. Si bien no ofrece todas las
funcionalidades de un motor de base de datos orientado a servidor no soporta procedimientos almacenados, por
ejemplo SQLite presenta todas las herramientas para manipular eficazmente cualquier tipo de datos.
1. Creación de la base de datos
De momento, los datos que se han de guardar en la aplicación son los datos propios de la pantalla de introducción
de un DVD. Su copia se realiza cuando el usuario hace clic en el botón OK de esta pantalla.
El esquema de la base de datos se limita por tanto, inicialmente, a una tabla: Android presenta una API que
gestiona a la perfección las actualizaciones de datos; será muy sencillo enriquecer este esquema a lo largo del
desarrollo.
El paquete android.database.sqlite presenta dos clases esenciales para administrar las bases de datos: la
clase abstracta SQLiteOpenHelper y la clase SQLiteDatabase.
l SQLiteOpenHelper se encarga de la creación y de las modificaciones en la base de datos.
l SQLiteDatabase presenta los métodos que permiten realizar todas las operaciones sobre los datos: consultas
SQL, operaciones de lectura y de escritura de datos. Esta clase se estudia en la sección Manipulación de los datos.
La primera etapa consiste, pues, en crear una clase que herede de SQLiteOpenHelper, donde será preciso
implementar dos métodos:
l onCreate, que se invoca por la plataforma durante la creación de la base de datos.
l onUpgrade, que se invoca para actualizar la base de datos.
La clase heredada debe, también, invocar el constructor de su clase madre, proporcionando el nombre de la base de
datos, así como su número de versión: este número de versión permite al sistema determinar si la base de datos
debe o no actualizarse.
En el explorador del proyecto, cree una nueva clase de Java, llamada LocalSQLiteOpenHelper.
Declare que esta clase herede de SQLiteOpenHelper.
La clase creada contiene ahora el siguiente código:
package com.ejemplo.locdvd;
import android.database.sqlite.SQLiteOpenHelper;
Android Studio puede generar automáticamente las firmas de los métodos que se han de implementar:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
haga clic con el botón derecho en el editor de código Java y seleccione la opción Generate. También
puede utilizar la combinación de teclas [Alt][Insert].
Se abre un menú contextual que permite seleccionar qué código generar: seleccione la opción Implement
Methods. Se abre una ventana popup, que muestra los métodos que se generarán. Haga clic en OK.
Los métodos onCreate y onUpgrade se generan automáticamente.
package com.ejemplo.locdvd;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
@Override
public void onCreate(SQLiteDatabase db) {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
}
}
Como indica el editor de Android Studio, es imprescindible declarar un constructor para la clase
LocalSQLiteDatabase, constructor que debe invocar al constructor de la clase madre.
SQLiteOpenHelper presenta dos constructores, cuyas firmas son las siguientes:
Los parámetros comunes a estos constructores son los siguientes:
l Context context: contexto de ejecución de la aplicación. Este parámetro deberá integrarse en la firma del
constructor de la clase LocalSQLiteOpenHelper.
l String name: se corresponde con el nombre de la base de datos. Este nombre se utiliza como nombre del archivo
de almacenamiento de la base de datos.
l CursorFactory factory: permite proporcionar un cursor específico que se utilizará en sustitución del cursor
por defecto. Si el valor que se pasa aquí es nulo, se utilizará el cursor por defecto.
l int version: indica el número de la versión actual de la base de datos. Este parámetro puede ser interno a la
clase LocalSQLiteOpenHelper. La primera versión debe ser la versión 1.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El segundo constructor agrega al primero un objeto de tipo DatabaseErrorHandler, que permite especificar un
gestor de errores diferente al gestor por defecto.
Tras estas observaciones, el constructor de la clase LocalSQLiteOpenHelper es el siguiente:
La clase LocalSQLiteOpenHelper no presenta ningún error: a continuación es necesario implementar la
creación del esquema de la base de datos en el método onCreate.
La tabla que permite el almacenamiento de los DVD introducidos por el usuario contiene los siguientes campos:
l id: identificador único, que se define como autoincrementado.
l titulo: cadena de caracteres que representa el título de la película.
l anyo: número entero que representa el año de aparición del DVD.
l actores: cadena de caracteres que almacena la lista de actores principales de la película; los nombres están
separados por puntos y coma.
l resumen: cadena de caracteres que representa el resumen de la película.
Para crear esta tabla, en el método onCreate, hay que definir la consulta SQL correspondiente y ejecutarla sobre
la instancia de SQLDatabase que se pasa como parámetro del método onCreate.
La sintaxis de la consulta SQL es estándar; los tipos disponibles para los datos se resumen en la siguiente tabla.
Tipo de dato Descripción
INTEGER Se utiliza para almacenar datos de tipo número entero.
REAL Se utiliza para almacenar datos de tipo número decimal.
TEXT Se utiliza para almacenar cadenas de caracteres.
NUMERIC Tipo específico de SQLite que representa un número. El tipo exacto se define cuando se
almacena el dato ( INTEGER o REAL).
BLOB Se utiliza para almacenar datos de tipo BLOB (Binary Large Object).
Por otro lado, las claves primarias se definen con las palabras clave PRIMARY KEY.
Conviene destacar que, si una columna se declara como INTEGER PRIMARY KEY, se asignará automáticamente un
identificador único si no se pasa ningún identificador durante el registro. Está permitida la palabra clave
AUTOINCREMENT, aunque se recomienda no utilizarla por motivos de rendimiento.
La consulta de creación de la tabla es la siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
"titulo TEXT, anyo NUMERIC, actores TEXT, resume TEXT);";
La consulta se ejecuta invocando el método execSQL del objeto SQLiteDatabase: el funcionamiento de este
método se describe en la siguiente sección.
El código del método onCreate es, finalmente:
@Override
public void onCreate(SQLiteDatabase db) {
String sqlFilTable ="CREATE TABLE DVD(id INTEGER PRIMARY KEY," +
"titulo TEXT, anyo NUMERIC, actores TEXT, resumen TEXT);";
db.execSQL(sqlFilTable);
}
2. Modificación de la base de datos
De momento no es necesario modificar el esquema, de modo que el método onUpgrade incluye únicamente el
código genérico de actualización de la base de datos.
Este método debe poder actualizar el esquema de la base de datos a partir de cualquier versión hasta la última
versión: en efecto, hay que prever que los usuarios pueden no instalar sistemáticamente las últimas versiones.
De este modo, si para una actualización de la aplicación se modifica la base de datos para trabajar con la versión n,
es posible que la aplicación instalada en un terminal posea una versión de base de datos inmediatamente anterior o
bien que sea necesario hacer evolucionar la base de datos sobre varias versiones. Lo más sencillo es, por tanto,
iterar, gracias a un bucle for por ejemplo, sobre los números de versión y, para cada número de versión, realizar la
actualización correspondiente.
A continuación se muestra un ejemplo de código genérico, la implementación real se hará en el capítulo Controles
avanzados, en la sección TimePicker/DatePicker.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for(int i = oldVersion;i<newVersion;i++) {
int versionToUpdate = i+1;
if(versionToUpdate==2) {
// Código para actualizar la base de datos a la versión 2
} else if(versionToUpdate==3) {
// Código para actualizar la base de datos a la versión 3
}
//[...]
}
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Manipulación de los datos
La primera versión del esquema de la base de datos está lista. A continuación hay que implementar todos los
métodos que nos van a permitir manipular los datos. La aplicación necesita poder realizar las siguientes acciones
sobre los datos:
l Consultar la base de datos para obtener la lista de todos los DVD registrados.
l Consultar la base de datos para obtener un DVD a partir de su título.
l Registrar un nuevo DVD.
l Editar el registro de un DVD.
l Eliminar un DVD.
1. Creación de la clase DVD
En primer lugar, hay que definir una clase DVD y, a continuación, crear los métodos que nos van a permitir realizar
las acciones anteriores.
En el explorador del proyecto, cree una nueva clase DVD.
Defina, a continuación, las propiedades de la clase:
l Un campo id, de tipo long.
l Un campo titulo, de tipo String.
l Un campo anyo, de tipo int.
l Un campo actor, de tipo String[].
l Un campo resumen, de tipo String.
En el editor de código, haga clic con el botón derecho y seleccione la opción Generate, seleccione a
continuación getter and setter para generar automáticamente los accesores de las propiedades.
El código de la clase es, de momento, el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
return titulo;
}
Ahora hay que crear los métodos que permitirán manipular los datos.
Todas estas manipulaciones se realizan a través de la clase SQLiteDatabase: lectura de los datos, agregar o
actualización.
La clase abstracta SQLiteOpenHelper, y por tanto las clases que heredan de ella, presenta dos métodos,
getReadableDatabase y getWritableDatabase, que permitirán, respectivamente, obtener una referencia
sobre la base de datos abierta en lectura o en escritura.
El método execSQL, visto durante la creación de la base de datos, permite ejecutar una consulta SQL que no
devuelve datos.
SQLite no soporta los comandos múltiples: es imposible, por tanto, encadenar consultas SQL separándolas por un
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
punto y coma.
2. Consultas de selección
La aplicación debe poder proveer la lista de todos los DVD registrados por el usuario; hay que crear un método
getDVDList en la clase DVD, método estático que devolverá una lista de DVD.
Los métodos query y rawQuery de la clase SQLiteDatabase permitirán consultar la base de datos. El primero
permite indicar un nombre de tabla, las columnas correspondientes, así como distintos parámetros relativos a la
consulta (cláusulas where, groupBy, having, etc.). El segundo método, rawQuery, ejecuta la consulta SQL que
se pasa como parámetro. Ambos métodos devuelven un objeto Cursor, que permite navegar por los resultados de
la consulta.
El método getDVDList invoca a query para obtener el contenido del conjunto de la tabla, ordenado
alfabéticamente según el título:
Cursor cursor =
db.query(true, "DVD", new String[]{"id", "titulo", "anyo",
"actores", "resumen"},null, null,null,null,"titulo", null );
La navegación por los resultados de la consulta se realiza con ayuda de los siguientes métodos de la clase
Cursor:
l boolean moveToFirst(): sitúa el cursor en el primer registro.
l boolean moveToNext(): avanza el cursor un registro.
l boolean isLast(): devuelve true si el registro en curso es el último.
l boolean isFirst(): devuelve true si el registro en curso es el primero.
l boolean moveToPosition(int n): sitúa el cursor en el enésimo registro.
El método getCount(), además, devuelve el número total de registros.
Cabe destacar que el método query devuelve un cursor situado antes del primer registro.
Una primera versión (incompleta) del método getDVDList es la siguiente:
SQLiteDatabase db = helper.getReadableDatabase();
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Cursor cursor = db.query(true, "DVD", new String[]{"id",
"titulo", "anyo", "actores", "resumen"}, null,
null,null,null,"titulo", null );
while (cursor.moveToNext()) {
//[...]
}
cursor.close();
db.close();
//[...]
}
El objeto cursor expone los métodos necesarios para leer los datos. Estos métodos se resumen en la siguiente
tabla:
Método Descripción
double getDouble(int i) int getInt(int Devuelve el valor de la columna con índice i en
i) long getLong (int i) String el tipo indicado. Se produce una excepción si el
getString (int i) ... tipo no se corresponde.
boolean isNull(int i) Devuelve true si la columna i contiene un
valor null.
Aquí, para la clase DVD, es preferible crear un constructor que reciba como parámetro un cursor: esto permite
factorizar mejor el código que se ha de escribir.
Ahora es posible completar el método getDVDList:
while (cursor.moveToNext()) {
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
listDVD.add(new DVD(cursor));
}
cursor.close();
db.close();
return listDVD;
}
No debemos olvidarnos de cerrar el cursor abierto, así como la instancia de SQLiteDatabase.
Sobre el mismo esquema, el método getDVD(int id), que permite obtener un DVD a partir de su identificador en
la base de datos, es sencillo de crear:
if(cursor.moveToFirst())
dvd = new DVD(cursor);
cursor.close();
db.close();
return dvd;
}
3. Consulta de escritura
Para la inserción de datos, la clase SQLiteDatabase presenta el método insert:
Este método recibe como parámetro el nombre de la tabla, una cadena de caracteres que representa un nombre de
columna y un objeto ContentValues.
El segundo parámetro permite indicar el nombre de una columna de la tabla que puede valer null, en caso de que
el objeto ContentValues valga null (SQLite no permite insertar una fila completamente vacía, al menos uno
de los campos debe tener valor, con el valor null).
El objeto de tipo ContentValues almacena, en formato clave/valor, los datos que se han de insertar en la tabla.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Los pares clave/valor se registran mediante el método put del objeto ContentValues. Existe un método put
por tipo de datos soportado.
Además, insert devuelve el identificador del nuevo registro si la creación se ha producido correctamente, o -1 si
se ha producido algún error.
El método insert de la clase DVD, que se encarga de la escritura en base de datos de un nuevo DVD, puede
escribirse de la siguiente manera:
SQLiteDatabase presenta también el método update, que permite actualizar un registro:
Los parámetros son los siguientes:
l String table: el nombre de la tabla.
l ContentValues values: el objeto que contiene los nuevos valores del registro.
l String whereClause: el valor de la cláusula where para la selección de uno o varios registros.
l String [] whereArgs: un array de valores de tipo String, que se utiliza en el caso de que los valores se
reemplacen por el carácter ? en la cláusula where definida anteriormente.
El método devuelve el número de filas modificadas.
El método update de la clase DVD, construido sobre el mismo modelo que el método insert, es el siguiente:
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
ContentValues values = new ContentValues();
values.put("titulo",this.titulo);
values.put("anyo",this.anyo);
if(this.actores!=null) {
String listActores = new String();
for(int i =0;i<this.actores.length;i++) {
listActores+=this.actores[i];
if(i<this.actores.length-1)
listActores+=";";
}
values.put("actores",listActores);
}
values.put("resumen",this.resumen);
String whereClause = "id=" + String.valueOf(this.id);
LocalSQLiteOpenHelper helper = new LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getWritableDatabase();
db.update("DVD", values,whereClause,null);
db.close();
}
Es posible realizar numerosas optimizaciones. El código para descargar presenta algunas mejoras en relación con el
código anterior, en particular respecto a la factorización de la creación del objeto ContentValues.
4. Consulta de eliminación
La clase SQLiteDatabase presenta el método delete para la eliminación de datos:
Funciona sobre el mismo principio que el método update, y recibe como parámetro, además del nombre de la tabla,
una cláusula where y un array de valores.
El método de eliminación de un registro basándose en a su identificador se escribe simplemente:
5. Transacción
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Cuando deben ejecutarse varias operaciones sobre la base de datos (múltiples inserciones, por ejemplo), se
recomienda encarecidamente, por motivos de rendimiento, utilizar procesamientos por lote.
La clase SQLiteDatabase provee los tres métodos siguientes, que permiten realizar transacciones:
El esquema global de un procesamiento por lotes es, en resumen, el siguiente:
db.beginTransaction();
try {
[...]
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Copia de las preferencias de usuario
Cuando los datos que se han de salvaguardar son poco numerosos, y no requieren ningún procesamiento por lotes,
resulta molesto utilizar una base de datos. Android provee un mecanismo de almacenamiento particularmente
adaptado a la copia de seguridad de este tipo de datos: los archivos de preferencias.
Los archivos de preferencias son archivos XML, gestionados por el sistema, que permiten salvaguardar los datos de
tipos básicos, con el formato de pares clave/valor. La clase android.content.SharedPreferences presenta
todos los métodos que permiten manipular estos archivos.
Para obtener una instancia de SharedPreferences, hay que invocar el método getSharedPreferences de la
clase Context.
Los parámetros de este método son los siguientes:
l String name: nombre del archivo XML utilizado para el almacenamiento. Se recomienda usar un nombre de archivo
basado en el nombre único de la aplicación.
l int mode: el modo en que se creará el archivo, entre los siguientes modos:
n Context.MODE_PRIVATE: solo la aplicación tiene permisos de lectura/escritura sobre el archivo.
n Context.MODE_WORLD_READABLE: solo la aplicación tiene permisos de escritura en el archivo. Las demás
aplicaciones del terminal pueden acceder en modo lectura.
n Context.MODE_WORLD_WRITABLE: todas las aplicaciones pueden leer y escribir en el archivo.
Para leer un dato previamente registrado en el archivo, basta con invocar el método getxxx de la clase
SharedPreferences, según el tipo de dato que se ha de leer:
Todas las firmas de estos métodos son iguales: hay que proporcionar el nombre del dato que se ha de leer y un valor
por defecto, que se devuelve si el nombre del dato no se encuentra en el archivo.
La escritura en el archivo se realiza mediante un objeto de tipo SharedPreferences.Editor, que proporciona el
método edit de la clase SharedPreferences. Por ejemplo:
SharedPreferences sharedPreferences=
getSharedPreferences("com.ejemplo.locDVD.prefs",
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El objeto Editor presenta un conjunto de métodos putXXX, que permite registrar una pareja clave/valor, según el
tipo de valor.
Para validar el registro, hay que invocar el método commit() del objeto Editor.
editor.commit();
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Leer y escribir en un archivo
Para mejorar la experiencia de usuario durante la primera ejecución, es interesante proporcionar una aplicación que
incluya ciertos datos de ejemplo. Si bien es posible rellenar previamente la base de datos integrando directamente
algunas consultas SQL en el código, resulta más conveniente integrar un archivo de datos que se leerá durante la
primera ejecución: los datos podrán modificarse fácilmente sin tener que modificar el código.
Hay que implementar los siguientes elementos:
l Integrar un archivo de datos que se distribuye con la aplicación.
l Leer e interpretar el archivo durante el primer arranque de la aplicación.
l Registrar el hecho de que el archivo de datos se ha integrado, para evitar que la aplicación lo haga con cada arranque.
1. Integrar un archivo de datos
El almacenamiento de archivos que no deben interpretarse por el sistema se realiza en una carpeta particular de la
aplicación: la carpeta assets. Esta carpeta se sitúa en la raíz de la aplicación, al mismo nivel que las carpetas res
y src.
La carpeta assets no se crea por defecto, aunque Android Studio provee un asistente para su creación.
Sitúe el cursor en la carpeta main de la solución.
Haga clic con el botón derecho y seleccione la opción New; a continuación la opción Folder, y por último la
opción Assets Folder.
Se abre un asistente, que le permite modificar la ruta de acceso a la carpeta que se creará. Deje la opción
desmarcada para crear la carpeta en su ubicación por defecto y haga clic en Finish.
Se crea la carpeta assets.
El desarrollador puede almacenar cualquier tipo de archivo en esta carpeta: datos binarios, imágenes, archivos de
fuentes, etc. También es posible crear un árbol de carpetas completo.
Dado que los datos a integrar son únicamente textuales, lo más sencillo es crear un archivo de texto, donde cada
línea del archivo represente un DVD y, por tanto, un registro.
Sitúe el cursor en la carpeta assets recién creada.
Haga clic con el botón derecho y seleccione la opción New y, a continuación, File.
Se abre una ventana popup que le permite introducir un nombre para el archivo.
Asígnele el nombre data.txt, por ejemplo.
En una línea de este archivo, los datos de un DVD estarán separados por el carácter «|». Los nombres de los
actores estarán separados por el carácter «,».
Por ejemplo:
[...]
Interstellar|2014|M.McConaughey, A.Hathaway|En un futuro
cercano,[...]
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
[...]
Un archivo con 15 DVD está disponible para su descarga en la solución correspondiente a este capítulo.
2. Leer los datos y registrarlos
Aquí, el archivo contiene los datos textuales, de modo que cada línea representa un registro. Lo más sencillo es, por
tanto, leer el archivo línea a línea.
String readLine()
BufferedReader posee dos constructores. El primer constructor recibe como único parámetro una instancia de
Reader. El buffer tendrá un tamaño de 8192 bytes. El segundo constructor recibe, además, un parámetro que
permite precisar el tamaño del buffer.
BufferedReader(Reader in)
BufferedReader(Reader in, int size)
En el contexto de la aplicación, el tamaño del buffer no tiene una importancia particular, de modo que es posible
utilizar el primer constructor sin problema.
Reader reader;
[...]
BufferedReader bufferedReader = new BufferedReader(reader);
[...]
Para instanciar la clase BufferedReader, es preciso instanciar una clase que herede de la clase abstracta
Reader.
Aquí, se utiliza InputStreamReader, que proporciona, entre otros, el siguiente constructor:
Solo queda por obtener una referencia al archivo data.txt como objeto InputStream. Android provee la clase
AssetManager, que permite manipular los archivos almacenados en la carpeta assets. Es el contexto de la
aplicación el que provee una instancia de AssetManager a través del método getAssets(). La instancia de
AssetManager presenta el método open(String filename), que devuelve un objeto de tipo
InputStream.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Así, en el cuerpo de una actividad, el código que permite abrir el archivo data.txt es el siguiente:
try {
file = getAssets().open("data.txt");
reader = new InputStreamReader(file);
bufferedReader = new BufferedReader(reader);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
La lectura del archivo se hace de manera clásica, iterando sobre la lectura de la línea en curso hasta que esta valga
null.
Tan solo queda por deserializar la línea, instanciar un nuevo DVD, informar sus propiedades y registrarlo en la base
de datos. Todo el código está encapsulado en un método llamado readEmbeddedData, que se agrega en el
cuerpo de la actividad MainActivity:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
dvd.insert(this);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
En un entorno de producción, sería conveniente prever un método de inserción múltiple para la clase DVD.
3. Registrar la lectura del archivo
El método construido en la sección anterior solo debe ejecutarse una vez. Si bien es posible comprobar si ya existe
alguna inserción, lo más sencillo es utilizar las preferencias de usuario vistas anteriormente.
Una vez leído y procesado el archivo, el método readEmbeddedData debe indicarlo en las preferencias. Y, tras el
arranque, el método se invocará únicamente si no existe ninguna referencia a este respecto en el archivo de
preferencias.
El bloque finally se convierte en:
finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",
Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
}
La invocación del método readEmbeddedData está condicionada a la ausencia de la preferencia de usuario
registrada.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Las listas
La primera pantalla de la aplicación, que se presenta al usuario durante el arranque, debe mostrar la lista de DVD
contenidos en la base de datos. Cuando el usuario hace clic en uno de los elementos de la lista, se muestra una
segunda pantalla, que presenta la ficha del DVD creada en el capítulo Principios básicos de Android.
Para mostrar los DVD registrados en la base de datos, sería posible utilizar únicamente los componentes que ya
hemos abordado en los anteriores capítulos: para cada DVD mostrado, bastaría con crear dinámicamente
componentes TextView que mostraran las propiedades del DVD.
En su lugar, Android provee un componente ListView, cuya función es mostrar cualquier tipo de dato en forma de
una lista: este componente está optimizado para gestionar mejor la memoria, y presenta rendimientos que serían
difíciles de igualar construyendo dinámicamente la lista.
1. Integrar una lista
El framework Android propone dos técnicas diferentes para integrar una lista en una pantalla:
l Utilizar la clase ListActivity, que propone una implementación muy sencilla pero con posibilidades limitadas.
l Integrar el componente ListView en un layout e implementar el mecanismo de presentación de los datos.
La primera solución, que recurre a la clase ListActivity, solo permite mostrar dos propiedades como máximo
para cada elemento de la lista. La segunda solución, más completa, ofrece una libertad total al desarrollador en la
visualización: es el método que utilizaremos aquí.
Esta implementación completa utiliza tres elementos:
l El componente ListView, que debe integrarse en un archivo de layout (o instanciarse dinámicamente en el código
de la actividad).
l Un archivo de layout para precisar la visualización de cada elemento de la lista.
l Un adaptador para gestionar los datos.
a. Integrar un componente ListView
El componente ListView es un contenedor de vista, similar al componente LinearLayout, por ejemplo. La
declaración en un archivo de layout sigue, por tanto, exactamente las mismas reglas que para cualquier
componente, al menos para las propiedades comunes a todos los componentes.
Aquí, el componente ListView debe agregarse al layout de la primera pantalla, correspondiente a la clase
MainActivity. Hay que integrar por lo tanto una etiqueta ListView en el archivo de layout
activity_main.xml.
Edite el archivo activity_main.xml en el editor de Android Studio.
Este layout solo debe incluir, de momento, un elemento: el componente ListView. El código XML es,
por tanto, el siguiente:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
android:padding="16dp">
<ListView
android:id="@+id/main_List"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
La visualización previa muestra de forma correcta una lista, que ocupa efectivamente toda la página en pantalla:
Es necesario, como hemos visto en el capítulo Principios básicos de Android, obtener una referencia al objeto
ListView en el código de la actividad (por este motivo se define un identificador para el componente ListView
en el archivo de layout).
En el archivo MainActivity.java, declare una variable global de tipo ListView y realice el vínculo
con el archivo de layout en el cuerpo del método onCreate:
ListView list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
list = (ListView)findViewById(R.id.main_List);
[...]
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
b. Declaración de un layout para los elementos de la lista
En la visualización previa del editor de Android Studio, cada elemento de la lista muestra dos propiedades: una
propiedad principal y un texto más pequeño debajo: esto se corresponde con una implementación estándar.
Para una visualización completamente personalizable, hay que declarar un archivo de layout: el archivo se
vinculará a la lista para el adaptador de datos que veremos más adelante.
Para la aplicación LocDVD, cada DVD que se presenta en la lista debe mostrar el título, el año de la película y el
inicio del resumen de la película: cada uno de estos elementos es de solo lectura, de modo que hay que utilizar
componentes TextView.
En la carpeta res/layout/, cree un archivo de layout, llamado listitem_dvd.xml.
El componente raíz es un LinearLayout, los elementos se posicionan inicialmente unos debajo de otros.
Integre, en este archivo, los tres componentes TextView que muestran las propiedades de cada DVD.
Cada uno de estos componentes TextView debe poder invocarse en el código de la aplicación, de modo que es
necesario asignarles un identificador único.
El título está en negrita, en dos líneas como máximo, y el resumen en cursiva, en tres líneas como máximo.
El código del archivo de layout es el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
El atributo android:ellipsize indica al sistema que el texto debe truncarse si supera el tamaño dedicado al
componente TextView.
c. Implementar un adaptador
El adaptador establece el vínculo entre los datos y el archivo de layout definido en la sección anterior: en efecto,
se invoca desde el componente ListView para mostrar cada elemento de la lista.
El framework Android provee varias clases que proporcionan una implementación parcial de un adaptador de
datos: la clase BaseAdapter (paquete android.widget) es la implementación básica, que debe heredarse
para implementar los métodos abstractos siguientes:
l View getView(int pos, View convertView, ViewGroup parent): este método debe
devolver la vista que muestra el elemento de la lista en la posición pos.
l long getItemId(int pos): debe devolver el identificador del elemento que se muestra en la posición
pos.
l Object getItem(int pos): devuelve el elemento mostrado en la posición pos.
l int getCount(): debe devolver el número de elementos mostrados en la lista.
Cree una clase DVDAdapter, que herede de ArrayAdapter<DVD>.
El constructor de DVDAdapter debe invocar un constructor de la clase madre. Por ejemplo, el siguiente
constructor:
l Context context: representa el contexto de ejecución de la aplicación.
l int resource: identificador del recurso layout utilizado para la visualización de cada elemento, en el caso de
un uso estándar, o bien 1 en caso contrario.
l List<T> objects: elementos de tipo T que muestra la lista.
En el marco de la aplicación LocDVD, la visualización es específica, de modo que el constructor de la clase
DVDAdapter debe informar el contexto de ejecución, así como la lista de DVD que se ha de mostrar.
El constructor de la clase DVDAdapter será, por tanto, el siguiente:
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El método getView debe sobrecargarse para tener en cuenta la visualización específica deseada para cada DVD.
@Override
public View getView(int pos, View convertView, ViewGroup parent)
{
[...]
}
La clase BaseAdapter y las clases que heredan de ella permiten optimizar la visualización de un número
importante de elementos. Para ello, integran un mecanismo de reciclaje de vistas, para evitar desinstanciar
demasiados componentes.
Por ello, la vista pasada como parámetro, convertView, puede ser nula: es el caso cuando la vista pasada para
la visualización del DVD almacenado en la posición pos no es una vista reciclada. Es preciso, por tanto, crear la
vista mediante un objeto de tipo android.view.LayoutInflater, invocando el método inflate.
Aquí, se utiliza el archivo de layout creado para la visualización de un DVD.
LayoutInflater layoutInflater ;
[...]
View view = layoutInflater.inflate(R.layout.listitem_dvd, parent);
LayoutInflater es una clase abstracta: para obtener una instancia de LayoutInflater, hay que invocar el
método getSystemService de la clase Context, pasándole como parámetro que el servicio deseado es de
tipo Context.LAYOUT_INFLATER_SERVICE.
LayoutInflater layoutInflater
=(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
El contexto no se pasa como parámetro al método getView, de modo que hay que guardar una referencia al
objeto Context del constructor de la clase DVDAdapter. La gestión del parámetro convertView es, por
tanto, la siguiente:
View view;
if(convertView==null) {
LayoutInflater layoutInflater =
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_dvd, parent);
} else {
view = convertView;
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Una vez preparada la vista, ya sea una vista reciclada o una vista creada a partir de un LayoutInflater, hay
que obtener las referencias a todos los componentes TextView integrados en el archivo de layout
listitem_dvd. Esto se hace simplemente invocando, como en el marco de una actividad, el método
findViewById de la instancia de View.
Estas referencias permiten asignar las propiedades del DVD que se quiere mostrar a los componentes TextView.
Por ejemplo, para obtener una referencia sobre el componente de visualización del título del DVD:
TextView titulo
=(TextView)view.findViewById(R.id.listItemDVD_titulo);
Por último es necesario, para cada componente, asignarle el texto que se ha de mostrar. Para ello, es preciso
disponer de una referencia sobre el DVD en curso. Conociendo la posición del DVD en la lista, la instancia se
obtiene invocando el método getItem de la base ArrayAdapter<T>:
T getItem(int position)
Aquí, el método getItem devuelve una instancia de DVD:
Una primera implementación del método getView es la siguiente:
@Override
public View getView(int pos, View convertView, ViewGroup parent) {
View view;
if(convertView==null) {
LayoutInflater layoutInflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_dvd,
parent);
} else {
view = convertView;
}
TextView titulo
=(TextView)view.findViewById(R.id.listItemDVD_titulo);
TextView anyo
=(TextView)view.findViewById(R.id.listItemDVD_anyo);
TextView resumen
=(TextView)view.findViewById(R.id.listItemDVD_resumen);
titulo.setText(dvd.getTitulo());
anyo.setText(String.valueOf(dvd.getAnyo()));
resumen.setText(dvd.getResumen());
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
return view;
}
Se ha modificado el constructor de la clase DVDAdapter para almacenar una referencia al contexto de ejecución:
Context context;
2. Vincular el componente ListView con el adaptador
Una vez definido el adaptador, hay que asignarlo al componente ListView. La clase ListView provee, para ello,
el método setAdapter:
La llamada a este método se hace directamente en la actividad que contiene la lista, aquí la clase
MainActivity.java. Según el ciclo de vida de las actividades, la llamada a setAdapter puede hacerse o bien
en el método onCreate o bien en el método onResume: el interés de establecer el vínculo en el método
onResume reside en el hecho de que si la actividad se pone en pausa, el método onResume se invocará cuando
se reactive esta actividad. Si se modifican los datos, el método onResume puede recuperar los datos actualizados
para rellenar la lista.
El constructor de la clase DVDAdapter creado en la sección anterior requiere, como parámetro, un contexto de
ejecución así como una lista de DVD: el contexto de ejecución lo provee la propia actividad, la lista de DVD se
obtiene llamando al método getDVDList creado previamente en la clase DVD.
El cuerpo del método onResume para la clase MainActivity es por tanto el siguiente:
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(this);
DVDAdapter dvdAdapter = new DVDAdapter(this, dvdList);
list.setAdapter(dvdAdapter);
}
Como todos los métodos invocados durante el ciclo de vida de la actividad, no hay que olvidar invocar el método de
la clase madre, para no obtener errores de ejecución.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
En resumen, se han definido los siguientes elementos:
l El layout de la actividad principal, el archivo activity_main.xml:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<ListView
android:id="@+id/main_List"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
l El layout utilizado para mostrar los elementos de la lista, el archivo listitem_dvd.xml:
l El adaptador de datos DVDAdapter.java:
paquete com.ejemplo.locdvd;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
Context context;
public DVDAdapter(Context context, List<DVD> objects) {
super(context, -1, objects);
this.context = context;
}
@Override
public View getView(int pos, View convertView, ViewGroup
parent) {
View view=null;
if(convertView==null) {
LayoutInflater layoutInflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_dvd,
null);
} else {
view = convertView;
}
TextView titulo
=(TextView)view.findViewById(R.id.listItemDVD_titulo);
TextView anyo
=(TextView)view.findViewById(R.id.listItemDVD_anyo);
TextView resumen
=(TextView)view.findViewById(R.id.listItemDVD_resumen);
titulo.setText(dvd.getTitulo());
anyo.setText(String.valueOf(dvd.getAnyo()));
resumen.setText(dvd.getResumen());
return view;
}
l Y, por último, el método onResume de la actividad principal MainActivity.java:
paquete com.ejemplo.locdvd;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ListView;
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
ListView list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
list = (ListView)findViewById(R.id.main_List);
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
}
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(this);
DVDAdapter dvdAdapter = new DVDAdapter(this, dvdList);
list.setAdapter(dvdAdapter);
}
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3. Gestionar el clic en un elemento
Cuando el usuario hace clic en un DVD de la lista, el sistema debe redirigirle a la pantalla que muestra los detalles
sobre el DVD: la pantalla ViewDVDActivity creada en el capítulo Consulta e introducción de datos.
El objeto ListView genera un evento al hacer clic en alguno de los elementos de la lista y propone, como todos
los componentes de la plataforma que pueden reaccionar ante un clic, un método para definir un Listener que
permita reaccionar al clic: el método setOnItemClickListener.
El método recibe como parámetro un objeto de tipo AdapterView.OnItemClickListener. Esta interfaz
expone un método onItemClick, que se invoca cuando el usuario hace clic en algún elemento.
En la actividad principal, hay que definer una clase que derive de AdapterView.OnItemClickListener y
asignarle la lista utilizando el método setOnItemClickListener.
El listener no tiene que modificarse si la actividad se reactiva, de modo que la asignación puede hacerse en el
método onCreate de la actividad.
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View v, int
position, long id) {
}
});
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
El sistema invoca el método onItemClick con los siguientes parámetros:
l AdapterView<> parent: el padre del elemento sobre el que se ha hecho clic, es decir, la propia lista.
l View v: la vista que lleva al elemento de la lista que ha producido la llamada.
l int position: la posición del elemento en el origen de datos del adaptador.
l long id: el identificador del elemento, es decir, el valor devuelto por el método getItemId de la clase
BaseAdapter.
Para conocer el identificador del elemento de la lista sobre el que ha hecho clic el usuario, hay que sobrecargar el
método getItemId del adaptador DVDAdapter.
@Override
public long getItemId(int pos) {
return getItem(pos).id;
}
Cuando los identificadores de los objetos mostrados por una lista no son de tipo long, no es posible utilizar este
procedimiento. En ese caso, se recomienda utilizar la propiedad Tag de la clase View y almacenar en esta
propiedad o bien el identificador del objeto en curso o bien directamente el propio objeto. Esto se hace en el
método getView del adaptador.
El siguiente código muestra este procedimiento:
@Override
public View getView(int pos, View convertView, ViewGroup parent)
{
View view=null;
if(convertView==null) {
LayoutInflater layoutInflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_dvd, null);
} else {
view = convertView;
}
TextView titulo
=(TextView)view.findViewById(R.id.listItemDVD_titulo);
TextView anyo
=(TextView)view.findViewById(R.id.listItemDVD_anyo);
TextView resumen
=(TextView)view.findViewById(R.id.listItemDVD_resumen);
titulo.setText(dvd.getTitulo());
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
anyo.setText(String.valueOf(dvd.getAnyo()));
resumen.setText(dvd.getResumen());
return view;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int
position, long id) {
Object o = view.getTag();
if(o!=null) {
DVD dvd= (DVD)o;
}
}
El método onItemClick debe producir una llamada a ViewDVDActivity y proporcionar a esta actividad el
identificador del DVD clicado por el usuario.
La ejecución de una actividad se lleva a cabo mediante una llamada al método startActivity de la clase
Activity.
El objeto Intent que se pasa como parámetro representa una intención, es decir, una acción que debe ejecutar el
sistema. Para especificar que la acción que se ha de realizar es la ejecución de una nueva actividad, se utiliza el
siguiente constructor de la clase Intent:
l el parámetro paqueteContext representa el contexto de ejecución.
l el parámetro cls representa la clase de la actividad que se ha de lanzar.
El objeto Intent puede almacenar datos serializables, datos que se pasarán a la actividad iniciada. Los datos
deben almacenarse en un objeto de tipo Bundle, en formato de pares clave/valor. Para ello, la clase Intent
expone el método putExtra, del que existe una variante para cada tipo de dato que se ha de almacenar.
Por ejemplo:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
public Intent putExtra (String name, Parcelable value)
public Intent putExtra (String name, Parcelable[] value)
public Intent putExtra (String name, Serializable value)
[...]
El método que permite lanzar la actividad ViewDVDActivity es el siguiente:
No puede haber dos actividades en ejecución al mismo tiempo, de modo que resulta imposible comunicar
directamente una actividad con otra. Por este motivo se utiliza la capacidad de almacenamiento del objeto Intent
para pasar datos de una actividad a otra.
@Override
public void onItemClick(AdapterView<?> parent, View view, int
position, long id) {
startViewDVDActivity(id);
}
Al final, el código de la actividad MainActivity es el siguiente:
paquete com.ejemplo.locdvd;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
ListView list;
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
list = (ListView)findViewById(R.id.main_List);
list.setOnItemClickListener(new
AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View
view, int position, long id) {
startViewDVDActivity(id);
}
});
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
}
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(this);
DVDAdapter dvdAdapter = new DVDAdapter(this, dvdList);
list.setAdapter(dvdAdapter);
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 15 -
dvd.insert(this);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4. Mostrar el DVD seleccionado
La actividad ViewDVDActivity debe modificarse para mostrar los datos del DVD cuyo identificador se ha pasado
como parámetro. Las modificaciones que hay que realizar son las siguientes:
l Extraer el identificador que ha pasado la actividad MainActivity.
l A partir de este identificador, obtener el DVD correspondiente en la base de datos.
l Mostrar los datos del DVD.
Para extraer el identificador del DVD, en primer lugar hay que obtener una referencia a la intención que ha permitido
lanzar la actividad. Esto se realiza invocando el método getIntent() de la clase Activity.
- 16 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Serializable getSerializableExtra(String name)
[...]
Además, el método hasExtra(String name) permite comprobar si la clave name existe en el conjunto de
pares clave/valor.
El identificador del DVD se recupera con el siguiente código:
Se recomienda gestionar la recuperación de los datos del Intent en el método onCreate.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Para obtener una instancia del DVD seleccionado por el usuario, basta, una vez conocido el identificador, con invocar
el método getDVD creado en el capítulo Persistencia de datos:
En la actividad ViewDVDActivity, tan solo queda por modificar el método onResume para mostrar las
propiedades del DVD y, en particular, tener en cuenta el problema específico de los actores, que no se ha
gestionado durante la creación de la actividad.
Basta, como con la introducción de datos, con crear controles TextView dinámicamente para cada actor y
agregarlos a un contenedor de vista (de tipo LinearLayout, por ejemplo).
El código completo de la clase ViewDVDActivity es, al final, el siguiente:
paquete com.ejemplo.locdvd;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 17 -
import android.widget.LinearLayout;
import android.widget.TextView;
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;
DVD dvd;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@Override
protected void onResume() {
super.onResume();
txtTituloDVD.setText(dvd.getTitulo());
txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion),
dvd.getAnyo()));
También debe modificarse el archivo de layout de la actividad para integrar el contenedor de vistas para los actores.
- 18 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_margin="8dp"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="título del dvd"
android:id="@+id/tituloDVD"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="16dp"
android:textSize="22sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="año"
android:textSize="15sp"
android:id="@+id/anyoDVD" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/layoutActores"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:text="resumen"
android:minLines="5"
android:maxLines="15"
android:id="@+id/resumenPelicula" />
</LinearLayout>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 19 -
GridView, lista desplegable
1. Componente GridView
Además del componente ListView, Android proporciona también un contenedor de vista que presenta los datos
en forma de matriz: el GridView.
Este componente funciona exactamente igual que el componente ListView: necesita un adaptador, que es el
mismo que para ListView.
Presenta, sin embargo, la propiedad numColumns, que permite identificar cuántas columnas se mostrarán por fila.
<GridView
android:id="@+id/main_Grid"
android:numColumns="3"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
2. Lista desplegable
La plataforma proporciona un componente que muestra una lista desplegable, llamada Spinner.
Esta interfaz exige, además del método getView, la implementación del método getDropDownView:
El método getDropDownView se invoca para mostrar cada elemento de la lista desplegable. El método getView,
por su parte, se encarga de la representación del elemento seleccionado.
El objeto Spinner genera un evento cuando el usuario selecciona un elemento de la lista: como con la gestión del
clic del componente ListView, hay que implementar la interfaz AdapterView.OnItemSelectedListener, y
asignar una instancia de la implementación al componente invocando el
método setOnItemSelectedListener: deben implementarse los métodos onItemSelected y
onNothingSelected.
spinner.setOnItemSelectedListener(new
AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view,
int position, long id) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
TimePicker/DatePicker
El usuario de la aplicación quiere poder almacenar en la base de datos la fecha del último visionado de cada DVD.
Para ello, hay que agregar un campo en la tabla donde se registran los datos de cada DVD, e incorporar la posibilidad
de introducir esta fecha a través de la aplicación: en lugar de crear una pantalla específica, se ha decidido
simplemente agregar un botón en la ficha de visualización del DVD. Este botón debe provocar la apertura de una
ventana popup que presente un componente de selección de fecha.
1. Agregar un campo fecha de visionado
La fecha de visionado se almacena como un valor entero long en la base de datos: hay que agregar la columna
correspondiente en la tabla DVD.
La tabla se creó en versión 1 en el capítulo Persistencia de datos, y aquí debe pasar a versión 2. De modo que hay
que modificar el archivo LocalSQLiteOpenHelper.java, creado previamente.
Abra el archivo en el editor de Android Studio.
Modifique el valor de la constante DB_VERSION asignándole el valor 2.
Defina un método upgradeToVersion2, con la siguiente firma:
El método upgradeToVersion2 debe agregar una columna al esquema de la tabla DVD. Como para la creación
de la tabla, esto se realiza ejecutando un comando SQL e invocando al método execSQL de la clase
SQLiteDatabase.
El método upgradeToVersion2 es, por tanto, el siguiente:
También hay que agregar la llamada al método upgradeToVersion2 en el cuerpo del método sobrecargado
onUpgrade.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int
newVersion) {
for(int i = oldVersion;i<newVersion;i++) {
int versionToUpdate = i+1;
if(versionToUpdate==2) {
upgradeToVersion2(db);
} else if(versionToUpdate==3) {
// Código para poner la base de datos en versión 3
}
//[...]
}
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
A continuación hay que modificar el método onCreate para agregar la columna fechaVisionado: en efecto, en
el caso de una primera instalación de la aplicación, el esquema de la base de datos se crea en versión 2 y no se
invoca al método onUpgrade.
El código del método onCreate se convierte en el siguiente:
@Override
public void onCreate(SQLiteDatabase db) {
String sqlFilTable ="CREATE TABLE DVD(id INTEGER PRIMARY KEY," +
"titulo TEXT, anyo NUMERIC, actores TEXT, resumen TEXT,
fechaVisionado NUMERIC);";
db.execSQL(sqlFilTable);
}
No se olvide de administrar el nuevo campo en los métodos de consulta, de lectura y de escritura en la base de
datos.
2. Introducir la fecha de visionado
La introducción de la fecha se realiza desde la pantalla de visualización de un DVD. Hay que agregar, en la parte
inferior de la pantalla, un botón que produzca, al hacer clic en él, la apertura de una ventana popup de selección de
fecha. También hay que agregar un campo para mostrar la fecha introducida por el usuario.
Edite el archivo de layout activity_viewdvd.xml.
Agregue un componente Button que tenga el ancho de la pantalla. Su identificador debe ser
setFechaVisionado.
Agregue también un conjunto de componentes para mostrar la fecha de visionado. El modo de
representación es el mismo que para el año de aparición de la película.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/fecha_ultimo_visionado"
android:textSize="15sp"
android:id="@+id/fechaVisionado" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fecha_ultimo_visionado"
android:id="@+id/setFechaVisionado"/>
En la clase de la actividad también hay que declarar el botón agregado al layout.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El clic en el botón debe provocar la ejecución de un nuevo método showDatePicker:
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;
TextView txtFechaUltimoVisionado;
Button setFechaVisionado;
DVD dvd;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setFechaVisionado.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
}
El método showDatePicker utiliza el componente DatePicker, componente de la plataforma que permite
introducir una fecha utilizando los controles estándar de Android.
El constructor utilizado es el siguiente:
DatePickerDialog(Context context,
DatePickerDialog.OnDateSetListener callBack,
int year, int monthOfYear, int dayOfMonth)
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
l Context context: contexto de ejecución.
l OnDateSetListener callback: interfaz cuyo método onDateSet se invoca cuando el usuario selecciona
una fecha.
l int year, int monthOfYear, int dayOfMonth: valores enteros que representan la fecha por defecto
mostrada por el componente DatePicker.
El único punto no abordado hasta ahora es la interfaz OnDateSetListener: esta interfaz permite crear un
mecanismo de callback para, por ejemplo, guardar la fecha introducida por el usuario.
Hay que declarar una variable de tipo OnDateSetListener e implementar el método OnDateSet:
DatePickerDialog.OnDateSetListener onDateSetListener
= new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int
monthOfYear, int dayOfMonth) {
}
};
La firma del método onDateSet comprende, además de una referencia hacia el componente DatePicker que ha
generado el evento, los tres componentes de la fecha seleccionada por el usuario: el año, el mes y el día.
A partir de esta información hay que asignar valor a la propiedad fechaVisionado de la instancia de DVD en
curso y actualizar la visualización.
El método onDateSet es, por tanto, el siguiente:
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear,
int dayOfMonth) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, monthOfYear, dayOfMonth);
dvd.setFechaVisionado(calendar.getTimeInMillis());
dvd.update(ViewDVDActivity.this);
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("dd-MM-yyyy");
txtFechaUltimoVisionado.setText(dateValue);
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Observe que, cuando la referencia this a la actividad en curso está oculta, como ocurre aquí, es posible hacer
referencia a la actividad en curso utilizando ViewDVDActivity.this.
Una vez definida la variable onDateSetListener, tan solo queda por gestionar la fecha por defecto: si la fecha
de visionado del DVD está informada, se utiliza como fecha por defecto. En caso contrario, se utiliza la fecha del día
en curso.
if(dvd.fechaVisionado>0)
calendar.setTimeInMillis(dvd.fechaVisionado);
Por último, hay que lanzar la visualización de la ventana popup invocando al método show() de la clase
DatePickerDialog.
datePickerDialog.show();
El código completo del método showDatePicker es el siguiente:
DatePickerDialog.OnDateSetListener onDateSetListener =
new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int
monthOfYear, int dayOfMonth) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, monthOfYear, dayOfMonth);
dvd.setFechaVisionado(calendar.getTimeInMillis());
dvd.update(ViewDVDActivity.this);
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("dd-MM-yyyy");
txtFechaUltimoVisionado.setText( dateValue);
}
};
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Calendar calendar = Calendar.getInstance();
if(dvd.fechaVisionado>0)
calendar.setTimeInMillis(dvd.fechaVisionado);
datePickerDialog =
new DatePickerDialog(this,onDateSetListener,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
datePickerDialog.show();
}
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Crear nuestro propio componente reutilizable
Es evidente que el componente DatePicker utilizado en la sección anterior encapsula, de hecho, un conjunto de
componentes: botones, zonas de texto, texto editable.
En efecto, la plataforma Android permite a los desarrolladores crear sus propios componentes, ya sea a partir de
componentes existentes o bien creando un componente de varias piezas.
Para crear un componente a partir de componentes existentes de la plataforma, existen dos enfoques posibles: bien
sobrecargar un componente existente, o bien reunir un conjunto de componentes en una clase e integrar la lógica de
funcionamiento.
1. Sobrecargar un componente de la plataforma
Sobrecargar un componente de la plataforma es la solución más sencilla para crear nuestro propio componente.
Utilizado habitualmente para agregar funcionalidades a un componente existente, este método solo requiere, por lo
general, la creación de una clase Java que herede de la clase del componente inicial.
Así, por ejemplo, para extender un componente de tipo TextView, hay que declarar una clase que herede de
android.widget.TextView.
Para poder utilizarlo como los demás componentes de la plataforma, es necesario implementar dos constructores
para la clase:
l Un constructor que reciba como parámetro el contexto de ejecución. Este constructor se utiliza para instanciar el
componente en el código Java.
l Un constructor que reciba como parámetro el contexto de ejecución y una colección de tipo AttributeSet, que
permite obtener una referencia a los parámetros informados en el archivo de layout para el componente.
Por ejemplo:
2. Definir atributos personalizados
El desarrollador también puede definir sus propios atributos para personalizar mejor un componente.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Para ello, hay que definir estos nuevos atributos en un archivo de recursos, llamado habitualmente attrs.xml.
El archivo attrs.xml debe tener la siguiente estructura:
l La etiqueta declare-styleable permite indicar un nombre para la colección de atributos que se define a
continuación.
l Cada etiqueta attr permite definir un atributo especificando su nombre y el tipo de datos esperado.
Por ejemplo, los atributos miAtributo1 y miAtributo2 se definen gracias al archivo attrs.xml siguiente:
A continuación es posible utilizar estos atributos en un archivo de layout. Para ello, hay que declarar en primer lugar
en el archivo de layout, como con los atributos definidos por la plataforma, el espacio de nombres correspondiente a
los atributos: se trata del nombre del paquete de la solución.
xmlns:mis_atributos="http://schemas.android.com/apk/res/
com.ejemplo.locdvd"
Android Studio recomienda, en vez de indicar directamente el nombre del paquete, utilizar un nombre específico de
paquete, res_auto, que se sustituirá en tiempo de compilación por el nombre efectivo del paquete de la solución.
xmlns:mis_atributos="http://schemas.android.com/apk/res-auto"
3. Integrar el componente en un layout
La integración del componente creado en el archivo de layout se hace como con cualquier otro componente de la
plataforma. Sin embargo, hay que tener la precaución de indicar el nombre completo del componente, integrando el
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
nombre del paquete.
Por ejemplo, si se define un componente MiTextView en el proyecto LocDVD, la etiqueta correspondiente será
com.ejemplo.locdvd.MiTextView.
Los atributos definidos en el archivo attrs se agregan a la etiqueta como los atributos de la plataforma, utilizando
el espacio de nombres definido en el encabezado del archivo de layout.
Al final, la integración del componente MiTextView en un archivo de layout toma el siguiente aspecto:
<com.ejemplo.locdvd.MiTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
mis_atributos:miAtributo1="true"
mis_atributos:miAtributo2="cadena de caracteres"/>
</LinearLayout>
Se requieren dos etapas para obtener los valores de los atributos personalizados en el código Java del
componente:
l Obtener un objeto de tipo TypedArray a partir de la instancia de tipo AttributeSet presente en el constructor
del componente.
l Leer cada valor almacenado en el objeto TypedArray.
La clase Context expone el método obtainStyledAttributes, que permite recuperar una instancia de
TypedArray.
l El primer parámetro es el objeto de tipo AtributoSet del constructor del componente.
l El segundo parámetro es un array de enteros que representa el conjunto de atributos personalizados definidos. Este
array se genera en tiempo de compilación, como miembro de la clase R.styleable, y tiene el nombre indicado en
la propiedad name de la etiqueta declare-styleable.
En el caso del componente MiTextView creado más arriba, el código es:
A continuación hay que invocar, para cada atributo personalizado, el método getXXX del objeto TypedArray
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
según el tipo de dato devuelto:
Cada método recibe como parámetro un entero, que representa el identificador único del atributo personalizado
definido.
Se genera, en efecto, un identificador único en tiempo de compilación para cada atributo personalizado. El nombre
de este identificador está compuesto por el valor de la propiedad name de la etiqueta declare-styleable y el
nombre del atributo, separados por el carácter de subrayado _. Los nombres se declaran como propiedades de la
clase R.styleable.
Así, el código que permite recuperar el valor del atributo miAtributo2 del componente MiTextView es el
siguiente:
String valor =
typedArray.getString(R.styleable.misAtributos_miAtributo2);
Al final, el código canónico del componente MiTextView es el siguiente:
paquete com.ejemplo.locdvd;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.TextView;
}
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Presentación
La aplicación LocDVD está destinada a difundirse en smartphones y tabletas. Si para los smartphones las pantallas
implementadas son satisfactorias, no ocurre lo mismo con las tabletas: la lista de DVD aparece en la totalidad de la
pantalla, lo cual resulta poco legible. El espacio disponible en las tabletas puede explotarse de una manera más
eficaz si se utilizan, principalmente, fragmentos.
La noción de fragmento se introdujo en Android con la versión 3 (Android Honeycomb, específica para las tabletas)
para ayudar a los desarrolladores a tener en cuenta las tabletas. Integrados después en Android 4, los fragmentos
se han impuesto progresivamente hasta convertirse en uno de los elementos esenciales para la programación de
aplicaciones en Android.
1. Fragmento (fragment) y actividad
Un fragmentoo (fragment, en inglés) debe considerarse como una subactividad desde el punto de vista de la
programación o, si lo miramos desde el punto de vista de diseñar aplicaciones, como un fragmento de la pantalla
(por oposición a una actividad, que utiliza necesariamente la pantalla íntegra).
Los fragmentos, a diferencia de las actividades, no son elementos autónomos: cada fragmento, para poder
mostrarse, debe estar integrado en una actividad.
Una actividad puede contener uno o varios fragmentos, cada uno de ellos independiente de los demás.
Como hemos visto para los componentes, es posible agregar un fragmento a una actividad de dos maneras: bien
directamente en el código del layout de la actividad, o bien por programación. En este último caso, el layout de la
actividad debe declarar un contenedor de vista al que se asociará el fragmento en tiempo de ejecución.
2. Ciclo de vida
Como con las actividades, es importante conocer el ciclo de vida de los fragmentos, ciclo de vida que es muy
parecido al de las actividades. Las etapas del ciclo de vida se describen en la siguiente tabla.
Método invocado Descripción
onAttach Se invoca cuando el fragmento se vincula a una actividad.
onCreate Se invoca en la creación del objeto.
onCreateView Se invoca para vincular el fragmento a su vista.
onStart Se invoca cuando se ha creado el fragmento.
onResume Se invoca cuando se presenta el fragmento al usuario.
onPause Se invoca cuando el fragmento está en pause (la actividad se pone en pause).
onStop Se invoca cuando el fragmento ya no está visible.
onDestroy Se invoca cuando el objeto fragment se elimina de la memoria.
onDetach Se invoca cuando el fragmento se «desvincula» de la actividad.
Cuando se sobrecarga alguno de los métodos del ciclo de vida, hay que invocar explícitamente al método
correspondiente de la clase madre.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
3. Compatibilidad
La noción de fragmento aparece con la versión 3.0 de Android, versión destinada únicamente a las tabletas. La
versión 4 ha tenido en cuenta, naturalmente, esta nueva noción y la ha hecho disponible para las aplicaciones
destinadas a smartphones.
Para acelerar la transición de las aplicaciones al soporte de las tabletas, Google ha propuesto una primera biblioteca
de soporte, evocada en el capítulo Preparación del proyecto LocDVD, sección Bibliotecas de soporte, API que permite
la integración de los fragmentos en las versiones previas de Android (a partir de Android 1.6): de este modo, los
desarrolladores pueden utilizar fragmentos y garantizar una compatibilidad con los smartphones más antiguos.
l En lugar de utilizar la clase android.app.Fragment, el desarrollador utiliza la clase
android.support.v4.app.Fragment.
l Una nueva clase android.support.v4.app.FragmentActivity reemplaza a la clase
android.app.Activity, que incorpora el soporte de los objetos de tipo
android.support.V4.app.Fragment.
En el resto del libro, utilizaremos las clases android.support.v4.app.*, como recomienda Google. Veremos a
continuación cómo integrar esta biblioteca en el proyecto, en el caso de que Android Studio no lo haga
automáticamente.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Trabajar con los fragmentos
La adaptación de la aplicación a las tabletas empieza con una modificación de la actividad principal MainActivity:
en lugar de gestionar directamente la lista de DVD, MainActivity debe simplemente cargar un fragmento, la
lista se gestionará dentro de este fragmento.
De modo que es preciso, en primer lugar, crear esta nueva clase ListDVDFragment e integrar el código que
gestiona la lista de DVD, y a continuación modificar la actividad MainActivity así como su archivo de layout para
integrar el fragmento creado.
1. Creación del fragmento
En el explorador del proyecto Android Studio, cree una clase ListDVDFragment en el paquete
com.ejemplo.locdvd.
Esta clase debe heredar de android.support.v4.app.Fragment: indique que la clase hereda de
Fragment:
Sitúe el cursor de texto antes del término Fragment y presione [Alt][Enter]: seleccione la opción
Import Class en la lista desplegable que se muestra.
Aparece una segunda lista desplegable, que permite escoger entre dos clases:
android.app.Fragment y android.support.v4.app.Fragment: seleccione la clase de soporte
(la segunda opción).
Android Studio agrega el import correspondiente.
paquete com.ejemplo.locdvd;
import android.support.v4.app.Fragment;
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
public class ListDVDFragment extends Fragment {
Si para las actividades, el vínculo entre el código de la actividad y el archivo de layout se hace en el
método onCreate, para los fragmentos, este vínculo se realiza en el método onCreateView.
l LayoutInflater inflater: este objeto permite crear la vista que se mostrará por pantalla.
l ViewGroup container: representa la vista madre del fragmento.
l Bundle savedInstanceState: como con las actividades, este objeto es distinto de nulo en el caso de que el
fragmento se haya recreado (tras una rotación de la pantalla, por ejemplo).
El código de este método onCreateView difiere ligeramente del código clásico del método onCreate de una
actividad: en vez de invocar setContentView para vincular el archivo de layout, hay que invocar aquí el método
inflate de la instancia de LayoutInflater que se pasa como parámetro, análogamente a como se ha hecho
para el adaptador de la lista de DVD que hemos visto en el capítulo anterior. También hay que devolver la vista
resultante como salida del método.
Cree, en primer lugar, un nuevo archivo de layout, llamado fragment_listdvd.xml.
El contenido de este archivo es la copia del actual archivo de layout de la actividad principal: el
componente ListView debe estar incluido en el fragmento y no en la actividad.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ListView
android:id="@+id/main_List"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_listdvd, null);
return view;
}
Como con el método onCreate de la clase MainActivity, el método onCreateView debe instanciar una
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
referencia al componente ListView integrado en el archivo de layout.
Declare una variable de clase ListView en la clase ListDVDFragment.
La instancia de ListView se obtiene a partir de la vista de resultado de la llamada a inflate.
list = (ListView)view.findViewById(R.id.main_List);
La gestión de la lista de DVD se realiza íntegramente en el fragmento, de modo que hay que mover el código
correspondiente de la clase MainActivity a la clase ListDVDFragment.
Reemplace, en el cuerpo del método onResume de ListDVDFragment, this por la llamada a
getActivity().
El método onResume del fragmento es, al final, el siguiente:
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(getActivity());
DVDAdapter dvdAdapter = new DVDAdapter(getActivity(), dvdList);
list.setAdapter(dvdAdapter);
}
También hay que mover el procesamiento del clic sobre algún elemento de la lista:
Mueva el
método startViewDVDActivity de la clase MainActivity a la clase
ListDVDFragment. También hay que importar las clases utilizadas en este método y reemplazar this
por getActivity().
El método startViewDVDActivity es el siguiente:
Mueva, por último, la declaración del administrador de eventos onItemClick de la lista.
El código es un simple copiapega del método onCreate de la actividad al método onCreateView del fragmento.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Al final, el código de la clase ListDVDFragment el siguiente:
paquete com.ejemplo.locdvd;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.ArrayList;
ListView list;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_listdvd,
null);
list = (ListView)view.findViewById(R.id.main_List);
list.setOnItemClickListener(new
AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View
view, int position, long id) {
startViewDVDActivity(id);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(getActivity());
DVDAdapter dvdAdapter = new DVDAdapter(getActivity(),
dvdList);
list.setAdapter(dvdAdapter);
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
}
}
2. Modificación de la actividad host
Una vez implementado el fragmento, hay que modificar la actividad Main Activity para que tenga en cuenta
este fragmento.
La primera etapa consiste en definir, en el archivo de layout de la actividad, un contenedor de vista que será la vista
madre del fragmento.
Edite el archivo activity_main.xml y reemplace el componente ListView por un contenedor de
vista FrameLayout, que ocupará íntegramente la pantalla de la actividad.
Asigne un identificador al contenedor de vista antes de invocarlo en el código de la actividad: por ejemplo,
el identificador main_placeHolder.
El archivo de layout es, de este modo, el siguiente:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<FrameLayout
android:id="@+id/main_placeHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
La actividad MainActivity debe tener en cuenta la gestión del fragmento. Las operaciones que se han de
implementar tienen como objetivo cargar el fragmento definido en la sección anterior en el contenedor de vistas
implementado.
Para manipular los fragmentos, el framework Android provee la clase FragmentManager. Es posible obtener una
instancia de FragmentManager invocando el método getFragmentManager de la clase Activity:
La biblioteca de soporte, para resolver este problema, introduce una nueva clase para las actividades: la clase
FragmentActivity. Esta clase proporciona el método getSupportFragmentManager, que provee una
instancia de android.support.v4.app.FragmentManager. Este último autoriza la manipulación de los
objetos android.support.v4.app.Fragment.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
De modo que hace falta, en el marco de la aplicación, modificar la actividad MainActivity para que herede de la
clase de soporte FragmentActivity.
Edite la clase MainActivity para que herede de FragmentActivity en lugar de Activity.
import android.support.v4.app.FragmentActivity;
[...]
En la ejecución, la actividad MainActivity debe cargar automáticamente el fragmento ListDVDFragment en el
contenedor de vista previsto. Esta acción se lleva a cabo en un método openFragment que se define a
continuación, y se invocará en el método onResume de la actividad.
Cree un nuevo método openFragment, que recibe como único parámetro una instancia de fragment.
La instancia fragment debe ser de tipo android.support.v4.app.Fragment, para ser compatible con la
clase ListDVDFragment.
La primera instrucción de este método permite obtener una instancia de FragmentManager: para ello hay que
invocar el método getSupportFragmentManager.
El objeto FragmentManager solo se utiliza de manera local, de modo que puede declararse
directamente en el método openFragment.
Las operaciones sobre los fragmentos se realizan a través de una transacción: esto es necesario para hacer
unitarios la carga y el reemplazo del fragmento.
El objeto FragmentManager expone el método beginTransaction, que devuelve un objeto de tipo
FragmentTransaction. Este objeto se encarga de las manipulaciones efectivas sobre los fragmentos, hay que
invocar su método commit para validar la transacción.
FragmentTransaction transaction =
fragmentManager.beginTransaction();
[...]
transaction.commit();
Para cargar un fragmento en un contenedor de vista, el objeto FragmentTransaction proporciona los métodos
add y replace. También proporciona el método remove, que permite retirar un fragmento (es el opuesto al
método add).
El método add permite superponer varios fragmentos en un mismo contenedor de vista, lo que no autoriza el
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
método replace: este último invoca el método remove para eliminar cualquier fragmento cargado en el
contenedor antes de agregar el fragmento deseado. Aquí, es preferible utilizar el método replace, para no tener
que preocuparse de futuras manipulaciones sobre los fragmentos que se implementarán en la siguiente sección.
El método replace expone la siguiente firma:
l int containerViewId: identificador único del contenedor de vista en la que se carga el fragmento.
l Fragment fragment: instancia de fragmento que se ha de cargar en el contenedor de vista.
l String tag: etiqueta que permitirá obtener posteriormente la instancia de Fragment a partir de esta
referencia. Si la etiqueta no es necesaria, se recomienda utilizar la firma sin este parámetro del método replace.
La sección Ejecutar acciones como tarea de fondo, del capítulo Tareas asíncronas y servicios, ilustra el uso de los tags
para los fragmentos.
Utilizando el método replace, invoque la sustitución por el fragmento que se pasa como parámetro de
openFragment en el contenedor de vista creado anteriormente, con el identificador
main_placeHolder.
El código para esta instrucción es el siguiente:
transaction.replace(R.id.main_placeHolder, fragment);
Por último, una actividad, en el marco de uso conjunto con los fragmentos, puede gestionar una pila de fragmentos:
esta pila permite gestionar de manera sencilla el clic en el botón de retorno del terminal. Gracias a la pila de
fragmentos, cuando el usuario presiona el botón de retorno de su terminal, resulta sencillo cargar el fragmento que
estuviera activo previamente en el contenedor de vista.
Es posible agregar un fragmento a la pila invocando el método addToBackStack del objeto
FragmentTransaction.
l El parámetro String name permite precisar un nombre para el estado de la pila, lo que permite volver en
cualquier momento a un estado previo de la pila.
Agregue, tras la instrucción replace, una llamada a addToBackStack pasándole un nombre null.
Una vez completada la transacción, hay que invocar el método commit del objeto
FragmentTransaction. En definitiva, el método openFragment presenta la siguiente forma:
FragmentManager fragmentManager =
getSupportFragmentManager();
FragmentTransaction transaction =
fragmentManager.beginTransaction();
transaction.replace(R.id.main_placeHolder, fragment);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
transaction.addToBackStack(null);
transaction.commit();
}
La carga del fragmento ListDVDFragment debe hacerse en el método onResume de la actividad. Esta carga
consiste simplemente en instanciar un objeto ListDVDFragment e invocar el método openFragment creado
previamente.
@Override
public void onResume() {
super.onResume();
ListDVDFragment listDVDFragment = new ListDVDFragment();
openFragment(listDVDFragment);
}
Las modificaciones han terminado: la lista se gestiona, ahora, en un fragmento, cargado por la actividad
MainActivity.
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Implementación del modelo Master/Detail
De momento, la aplicación LocDVD no es óptima para las tabletas: en efecto, estas, utilizadas principalmente en modo
apaisado, presentan una visualización mucho más ancha que alta. La lista de DVD mostrada en modo apaisado en
una pantalla grande pierde legibilidad.
La plantilla Master/Detail permite resolver este problema de manera sencilla: en vez de dedicar íntegramente la
pantalla a la visualización de la lista, prevé utilizar solo una parte, dejando el resto para la visualización del detalle
del elemento seleccionado en la lista.
Esta plantilla puede resumirse en los siguientes esquemas:
Para los smartphones, el encadenamiento de las pantallas Lista/Detalle es el siguiente:
El usuario visualiza la lista en el conjunto de la pantalla. Cuando selecciona un elemento de la lista haciendo clic en él,
se carga una segunda pantalla que presenta los detalles del elemento seleccionado.
En la vista tableta, una única pantalla muestra, a la vez, la lista y el detalle:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
1. Implementación del layout
Sería posible implementar la visualización previa para las tabletas sin recurrir a fragmentos: para ello habría que
desarrollar un layout específico que muestre al mismo tiempo un componente listView en la parte izquierda de
la pantalla y un conjunto de componentes para la información relativa al DVD seleccionado.
Sin embargo, esto implica desarrollar una versión de la pantalla específica para las tabletas, lo cual representa una
carga de trabajo suplementaria.
Los fragmentos permiten minimizar los desarrollos suplementarios: cada zona de la pantalla se define en un
fragmento y la aplicación muestra uno o dos fragmentos según si el usuario trabaja en un smartphone o en una
tableta.
Hay que implementar, por tanto, un archivo de layout que autorice la visualización de ambos fragmentos uno al lado
del otro y, a continuación, detectar en tiempo de ejecución si hay que mostrar el layout con un único fragmento o el
correspondiente a dos fragmentos.
El administrador de recursos permite implementar fácilmente este esquema de diseño utilizando calificadores de
recursos, como vimos en la sección Gestionar la fragmentación del capítulo Preparación del proyecto LocDVD.
Así, definiendo una carpeta de layout específica para las tabletas, es posible crear dos versiones del archivo de
layout de la página de inicio: uno muestra un único contenedor de vista para mostrar un fragmento, y el otro
propone dos.
En Android Studio, haga clic con el botón derecho en la carpeta /res, en el explorador del proyecto.
Seleccione New y, a continuación, Android resource directory: se abre una ventana popup que
presenta un asistente para la creación de la carpeta de recursos.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Seleccione la opción layout en la lista del campo Resource type.
El nombre por defecto de la carpeta muestra, ahora, layout.
En la lista de calificadores de recursos, seleccione la opción Size y haga clic en el botón que representa
dos flechas orientadas hacia la derecha.
El asistente muestra ahora una lista para seleccionar un calificador de tamaño de pantalla.
Seleccione, en la lista desplegable, la opción Large.
El nombre de la carpeta de recursos es ahora layout-large.
Haga clic en OK para crear la carpeta de recursos.
El explorador del proyecto muestra una nueva carpeta, layout-large, cuando se muestra en modo Project.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Es en esta carpeta, específica para los terminales dotados de pantallas grandes es decir, las tabletas, donde debe
crearse el archivo de layout destinado a las tabletas.
Sitúe el cursor en la nueva carpeta creada y defina un nuevo archivo de layout activity_main.xml.
El nombre del archivo debe indicarse en el nombre del archivo de layout de la pantalla principal de la carpeta por
defecto (la carpeta /res/layout): el administrador de recursos escogerá, en tiempo de ejecución, el archivo que
utilizará según la configuración del terminal host.
Edite este archivo para crear un layout que presente ambos objetos FrameLayout uno al lado del otro.
El código de este layout se presenta a continuación:
El primer FrameLayout se corresponde con el definido para la version smartphone del layout: tiene,
obligatoriamente, el mismo identificador.
El segundo FrameLayout está destinado a incorporar un fragmento para la visualización de los detalles del DVD.
El capítulo Diseño avanzado expone con más detalle el funcionamiento de la propiedad layout_weight.
2. Modificación de la vista detallada
La vista detallada de un DVD debe modificarse, como se ha hecho con la vista de DVD, para integrarse en un
fragmento, en lugar de estar definida como una actividad.
Esta modificación no supone ninguna dificultad particular: como con la lista, hay que realizar los siguientes cambios:
l Renombrar el archivo ViewDVDActivity.java por ViewDVDFragment.java.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
l Renombrar la clase por ViewDVDFragment: Android Studio ofrece la posibilidad de hacerlo simplemente
seleccionando la clase que se ha de renombrar, desplegando el menú contextual clic con el botón derecho,
seleccionando la opción Refactor y, a continuación, Rename.
l Hacer que la clase ViewDVDFragment herede de la clase Fragment, en vez de Activity.
l Cambiar el método onCreate por onCreateView y adaptarlo para el fragmento.
l Reemplazar this por getActivity() para obtener una referencia al contexto de ejecución allí donde sea
preciso.
l La clase Fragment no expone el método getIntent, utilizado para obtener el identificador del DVD que se ha de
visualizar, de modo que se presentará otro método más adelante en este capítulo. De momento, basta con invocar el
método getActivity para obtener una referencia a la actividad madre e invocar el método getIntent sobre
esta referencia.
El código de la clase ViewDVDFragment es el siguiente:
paquete com.ejemplo.locdvd;
import android.app.DatePickerDialog;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;
TextView txtFechaUltimoVisionado;
Button setFechaVisionado;
DVD dvd;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
txtResumenPelicula=
(TextView)view.findViewById(R.id.resumenPelicula);
layoutActores =
(LinearLayout)view.findViewById(R.id.layoutActores);
setFechaVisionado =
(Button)view.findViewById(R.id.setFechaVisionado);
txtFechaUltimoVisionado =
(TextView)view.findViewById(R.id.fechaVisionado);
setFechaVisionado.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
return view;
}
DatePickerDialog.OnDateSetListener onDateSetListener =
new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int
monthOfYear, int dayOfMonth) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, monthOfYear, dayOfMonth);
dvd.setFechaVisionado(calendar.getTimeInMillis());
dvd.update(getActivity());
datePickerDialog = new
DatePickerDialog(getActivity(),onDateSetListener,
calendar.get(Calendar.YEAR),calendar.get(Calendar.MONTH),calendar
.get(Calendar.DAY_OF_MONTH));
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
datePickerDialog.show();
}
@Override
public void onResume() {
super.onResume();
txtTituloDVD.setText(dvd.getTitulo());
txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion), dvd.getAnyo()));
El fragmento que acabamos de crear reemplaza la actividad ViewDVDActivity; hay que modificar el código que
gestiona el clic en un elemento de la lista: en lugar de cargar una nueva actividad, hay que cambiar el fragmento.
Para la comunicación entre un fragmento y la actividad host, se recomienda desarrollar un mecanismo de callback: el
fragmento define una interfaz implementada por la actividad, y se invoca esta interfaz desde el fragmento cuando
se hace clic en algún elemento de la lista.
Edite el archivo ListDVDFragment.java.
Defina la interfaz OnDVDSelectedListener en la clase ListDVDFragment.
Esta interfaz debe ser pública y definir el método OnDVDSelected:
[...]
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Defina a su vez una variable de clase de tipo OnDVDSelectedListener en la clase
ListDVDFragment.
OnDVDSelectedListener onDVDSelectedListener;
Por último, la variable onDVDSelectedListener debe tener valor: lo más fácil es hacerlo en el método
onAttach de ListDVDFragment, y la manera más sencilla es la siguiente:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
[...]
}
La actividad MainActivity debe gestionar el clic en algún elemento de la lista: debe implementar, por tanto, la
interfaz OnDVDSelectedListener.
Edite la clase MainActivity y declare que implementa onDVDSelectedListener.
Defina el método onDVDSelected, dejando el cuerpo del método vacío de momento.
@Override
public void onDVDSelected(long dvdId) {
}
...
Ahora es posible asignar la variable onDVDSelectedListener en la clase ListDVDFragment:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
onDVDSelectedListener = (OnDVDSelectedListener)activity;
}
En un entorno de producción, sería imprescindible encapsular la asignación de onDVDSelectedListener en
un bloque try/catch para gestionar los eventuales errores de tipado.
También es preciso modificar el código de gestión del evento onItemClick de la lista para que, en lugar de
invocar un método que lanza una nueva actividad, invoque el método onDVDSelected de la interfaz
OnDVDSelectedListener:
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int
pos, long id) {
if(onDVDSelectedListener!=null )
DVD selectedDvd = (DVD)v.getTag();
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
onDVDSelectedListener.onDVDSelected(selectedDvd.id);
}
});
Al final, el código modificado de la clase ListDVDFragment es el siguiente:
paquete com.ejemplo.locdvd;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.ArrayList;
ListView list;
OnDVDSelectedListener onDVDSelectedListener;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_listdvd,
null);
list = (ListView)view.findViewById(R.id.main_List);
list.setOnItemClickListener(new
AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View
view, int position, long id) {
if(onDVDSelectedListener!=null ) {
DVD selectedDvd = (DVD)v.getTag();
onDVDSelectedListener.onDVDSelected(selectedDvd.id);
}
}
});
return view;
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(getActivity());
DVDAdapter dvdAdapter = new DVDAdapter(getActivity(),
dvdList);
list.setAdapter(dvdAdapter);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
onDVDSelectedListener = (OnDVDSelectedListener)activity;
}
3. Gestión de fragmentos
Para terminar la adaptación de la aplicación a las tabletas, quedan por implementar dos mecanismos en la actividad
principal:
l La carga del fragmento ViewDVDFragment.
l El paso del identificador del DVD seleccionado al fragmento ViewDVDFragment.
La carga del fragmento depende del terminal seleccionado. En un smartphone, el fragmento ViewDVDFragment
se carga en lugar del fragmento ListDVDFragment; para las tabletas, el fragmento ViewDVDFragment se
carga en el contenedor de vista previsto en el archivo de layout específico de las tabletas.
Defina, en la clase MainActivity, un nuevo método openDetailFragment, que reciba como
parámetro una instancia de Fragment.
Copie y pegue el código del método openFragment.
Este código se corresponde con una carga estándar de fragmento en un contenedor de vistas.
FragmentTransaction transaction =
fragmentManager.beginTransaction();
transaction.replace(R.id.main_placeHolder, fragment);
transaction.addToBackStack(null);
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
transaction.commit();
El administrador de recursos va a escoger, en tiempo de ejecución, el layout correspondiente al terminal en curso.
Basta, para tratar de manera separada los smartphones y las tabletas, con definir qué versión del archivo de layout
se tiene en cuenta.
Para llevar a cabo esta verificación, lo más sencillo es comprobar si el contenedor de vista dedicado a las tabletas
está cargado en memoria: si no lo está, esto significa que la aplicación se ejecuta sobre un smartphone.
if(findViewById(R.id.detail_placeHolder)==null)
// versión smartphone
else
// versión tableta
Aquí, la aplicación se ejecuta en una tableta, de modo que hay que cargar el fragmento en el contenedor de vista
detail_placeHolder.
El código del método openDetailFragment es, por tanto, el siguiente:
FragmentTransaction transaction =
fragmentManager.beginTransaction();
if(findViewById(R.id.detail_placeHolder)==null)
transaction.replace(R.id.main_placeHolder, fragment);
else
transaction.replace(R.id.detail_placeHolder, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
Cuando la vista detallada de un DVD se gestionaba desde una actividad, el paso del identificador de DVD se
realizaba a través de la colección Extras del objeto Intent utilizado para abrir la actividad.
Edite el método startViewDVDActivity de la clase MainActivity.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
Elimine el código actual del método: este código no es válido en el marco de uso del fragmento
ViewDVDFragment.
El método debe, en primer lugar, instanciar la clase ViewDVDFragment e invocar el método
openDetailFragment definido previamente.
openDetailFragment(viewDVDFragment);
}
Tras el código de instanciación del fragmento, defina una instancia de Bundle.
Agregue en el objeto bundle una entrada para el identificador del DVD.
bundle.putLong("dvdId",dvdId);
viewDVDFragment.setArguments(bundle);
El código completo del método startViewDVDActivity es el siguiente:
openDetailFragment(viewDVDFragment);
}
En último lugar, hay que modificar el código que permite recuperar el identificador pasado como parámetro al
fragmento.
Edite la clase ViewDVDFragment.
Hay que eliminar el código que permite obtener una instancia de Intent.
Suprima las dos líneas de código siguientes:
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El método getArguments de la clase Fragment permite obtener una referencia al objeto Bundle
pasado como parámetro. El método getLong de la clase Bundle permite obtener a continuación el valor
asignado previamente.
Preste atención: el objeto Bundle de la firma del método onCreateView de la clase Fragment no es el
mismo que el utilizado por los métodos setArguments y getArguments. Solo se utiliza en el marco de una
destrucción/recreación de un fragmento (tras una rotación de pantalla, habitualmente).
El método onCreateView de ViewDVDFragment, una vez modificado, presenta el siguiente código:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setFechaVisionado.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
return view;
}
Ahora, la aplicación es, capaz de adaptarse a las tabletas y presenta una pantalla optimizada para cada tipo de
terminal.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
Pruebe la aplicación en un smartphone y en una tableta para ver la diferencia de gestión de las pantallas
según el terminal.
Atención si prueba la versión tableta en el emulador de Android: en el momento de redactar este libro, existe un bug
que impide al sistema detectar correctamente qué versión de layout se utiliza en Lollipop. Es preferible definir una
tableta virtual que ejecute Android Jelly Bean (API 17).
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Los menús
Los antiguos terminales Android estaban dotados obligatoriamente de una tecla que permitía mostrar el menú de la
aplicación activa. Desde Android 3.0 (Honeycomb), esta tecla se ha vuelto opcional en todos los terminales
Android: se ha reemplazado por un botón dedicado, situado en la barra de acción.
Google propone, como con la compatibilidad con los fragmentos, una biblioteca de soporte que permite al
desarrollador no tener que preocuparse de eventuales problemas de compatibilidad: esta biblioteca se utiliza en
nuestro proyecto para gestionar el menú de la aplicación.
1. Definición del menú
Como con la organización en la página, que puede hacerse mediante archivos de layout o por programación, los
menús pueden definirse o bien a través de archivos de recursos, archivos en formato XML, o bien completamente
por programación. El primer método es el más extendido y es el que seguiremos en esta sección.
Por defecto, Android Studio genera un archivo de menú tras la creación de una aplicación: este archivo, llamado
menu_main.xml, se almacena en la carpeta /res/menu y presenta la opción settings.
La aplicación LocDVD necesita un menú que presenta dos opciones: la primera debe permitir, tras la confirmación del
usuario, reinicializar la base de datos. La segunda opción debe, por su parte, mostrar información acerca de la
aplicación.
En un marco de trabajo clásico, el desarrollador utilizará el archivo de menú ya creado y lo modificará. En lugar de
hacer esto, nosotros vamos a crear un nuevo archivo de menú para seguir el proceso de principio a fin.
Sitúese en la carpeta /res de la vista de proyecto.
Haga clic con el botón derecho y seleccione la opción New.
En el menú contextual que aparece, seleccione la opción Android Resource File para abrir el asistente de
creación de recursos.
Escriba menu_principal como nombre del archivo (en la opción File name del formulario).
Seleccione, a continuación, para la entrada Resource type, la opción Menu.
Deje los valores por defecto para las demás opciones del formulario.
Haga clic en el botón OK para validar la creación del archivo de menú.
El asistente termina y se crea un archivo menu_principal.xml en la carpeta /res/menu.
El archivo menu_principal.xml que se acaba de crear contiene, de momento, una etiqueta <menu>. Las
entradas del menú deben incluirse en el interior de esta etiqueta; cada opción está representada por una etiqueta
<item>.
La etiqueta item acepta los siguientes atributos:
l android:id: identificador de la entrada de menú.
l android:title: título de la entrada de menú. Se corresponde con el texto que se muestra en la entrada de
menú.
l android:icon: nombre del recurso de imagen que se muestra para la entrada de menú.
l android:onClick: nombre del método de la actividad que se ha de invocar cuando se selecciona la opción de
menú. En este libro, este atributo no se utilizará y se reemplazará por una gestión completamente programática de la
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
opción realizada por el usuario.
l android:showAsAction: opción que permite precisar cómo y dónde debe mostrarse la entrada de menú. Las
posibles opciones se detallan más adelante en esta sección.
Las dos opciones de menú que se van a agregar se mostrarán como texto, sin imagen, y solo deben aparecer si el
usuario hace clic en el botón de menú.
Edite el archivo menu_principal.xml en el editor de recursos de Android Studio.
Entre les etiquetas <menu> y </menu>, agregue una primera etiqueta <item>.
Introduzca el atributo android:id, dándole como identificador único el valor menu_reinicializar.
Introduzca el atributo android:title dándole como valor o bien directamente la cadena de caracteres
"Reinicializar la base de datos" o bien el identificador de un recurso de tipo String que definirá en el
archivo strings.xml para el entorno de producción; debe utilizarse obligatoriamente esta solución.
Introduzca, por último, el atributo android:showAsAction, dándole el valor never.
Se presenta la primera opción del menú, al final, así:
<item
android:id="@+id/menu_reinicializar"
android:title="@string/reinicialice"
android:showAsAction="never" />
Se ha creado una entrada reinicialice en el archivo strings.xml.
El atributo showAsAction permite especificar cómo debe mostrarse la entrada de menú en la barra de acción.
Pueden usarse las siguientes opciones:
l always: la entrada de menú se muestra siempre directamente en la barra de acción.
l ifRoom: la entrada de menú se muestra si el sistema determina que el espacio disponible es suficiente para
mostrarla en la barra de acción.
l never: la entrada de menú no se muestra jamás en la barra de acción, y solo está visible si el usuario hace clic en
el botón de menú de la barra de acción.
Existe una cuarta opción, que puede combinarse con alguna de las otras tres: la opción withText. Si se incluye
esta opción, la entrada de menú se mostrará con el texto definido en el atributo title, incluso aunque se haya
especificado un icono.
Cuando se incluye el atributo android:showAsAction en la etiqueta <item>, el editor de Android Studio
muestra un error (la propiedad aparece subrayada en rojo): puede obtener información acerca del error detectado
situando el puntero del ratón sobre el error y utilizando la combinación de teclas [Ctrl][F1]. Aquí, Android Studio
precisa que hay que utilizar el prefijo ’app’ en vez del tradicional ’android’. También hay que definir el prefijo ’app’ e
indicar que representa el espacio de nombres de la aplicación. Esta modificación permite indicar que se utiliza el
atributo ShowAsAction de la biblioteca de compatibilidad en lugar del atributo estándar de la plataforma.
Defina en primer lugar el espacio de nombres de la aplicación en la etiqueta menú. El espacio de nombres
de la aplicación se define por http://schemas.android.com/apk/res-auto. De modo que hay
que agregar el siguiente atributo:
xmlns:app="http://schemas.android.com/apk/res-auto"
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Modifique a continuación el prefijo del atributo ShowAsAction, reemplazando android por app. La
etiqueta completa del elemento de menú queda así:
<item
android:id="@+id/menu_reinicializar"
android:title="@string/reinicialice"
app:showAsAction="never" />
Cree, a continuación, una segunda entrada de menú, para la opción «Información». Esta entrada de
menú debe tener como identificador el valor menu_informacion y, como texto, el valor «Información».
El archivo de menú se presenta finalmente así:
Se han agregado las siguientes entradas al archivo strings.xml:
2. Inclusión en la actividad
Para permitir al usuario acceder al menú, hay que prever un botón en la barra de acción de la actividad principal,
MainActivity. Pero, por ahora, ¡esta no presenta la barra de acción!
En teoría, la presencia o la ausencia de la barra de acción en una aplicación, o para una actividad, se gestiona a
través del tema aplicado a la aplicación (o a la actividad). Sin embargo, los temas que muestran una barra de acción
no son compatibles con las versiones más antiguas de Android, de modo que es preferible utilizar otro método.
En el capítulo Los fragmentos habíamos visto que, para que los antiguos terminales sean compatibles con los
fragmentos, hay que modificar la clase de la que heredan las actividades. Aquí se va a utilizar el mismo método para
tener en cuenta la barra de acción: la actividad correspondiente debe, en lugar de heredar de
android.app.Activity o de android.support.v4.app.FragmentActivity, heredar de la clase
android.support.v7.app.AppCompatActivity.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Los fragmentos y la barra de acción son dos nociones que se utilizan habitualmente de manera conjunta; la clase
AppCompatActivity hereda de la clase FragmentActivity.
Abra el archivo MainActivity.java en el editor de Android Studio.
Modifiquie la definición de la clase MainActivity para que herede de AppCompatActivity.
Compile la aplicación y ejecútela: la pantalla principal mostrará ahora una barra de acción en la que aparece
únicamente el título de la aplicación, como muestra la siguiente figura:
Para indicar al sistema que la actividad presenta un menu, y para crear este menú, hay que sobrecargar el método
onCreateOptionsMenu de la clase Activity:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
[...]
}
El método onCreateOptionsMenu devuelve un valor booleano: si es verdadero ( true), el menú se creará. Si
devuelve falso, el menú no se creará.
También debe tener en cuenta la creación del menú, es decir, indicar al sistema qué archivo de recursos XML utilizar
para las entradas del menú. Esto se hace invocando el método inflate presentado por un objeto de tipo
MenuInflater.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_principal, menu);
El método onCreateOptionsMenu es, por tanto, el siguiente:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_principal, menu);
return true;
}
Ahora que se ha implementado el menú, hay que preparar la gestión del clic sobre alguna de las opciones de este
menú: el código correspondiente a ambas opciones se estudiará en la sección Mostrar una ventana emergente
estándar, más adelante en este capítulo.
Incluso aunque la gestión del clic en una opción de menú puede hacerse directamente en el atributo onClick de la
entrada del menú como habíamos indicado antes, es preferible realizar esta gestión a nivel del código de la
actividad: esto aporta una mayor flexibilidad y permite centralizar el conjunto del código.
Para ello, hay que sobrecargar el método onOptionsItemSelected de la clase Activity.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
[...]
}
Este método recibe como parámetro una instancia de MenuItem, que representa la entrada del menú que ha
seleccionado el usuario. Debe devolver verdadero si la gestión del clic se ha hecho, y falso en caso contrario.
La clase MenuItem presenta el método getItemId, que devuelve el identificador del menú, tal y como se ha
definido en el archivo de recursos del menú que se ha «inflado». Este identificador, de tipo entero, es fácil de utilizar
en un comando switch para gestionar cada entrada del menú.
El método onOptionsItemSelected es, de momento, el siguiente:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_reinicializar:
// se ha seleccionado la entrada Reinicializar la base de datos
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
return true;
case R.id.menu_informacion:
// se ha seleccionado la entrada Información
return true;
default:
return super.onOptionsItemSelected(item);
}
}
La sección Mostrar una ventana emergente estándar permitirá terminar la gestión del clic mostrando una ventana
emergente como respuesta a la acción del usuario.
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El navigation drawer
Además de los menús, la plataforma Android proporciona una segunda opción para organizar la navegación dentro de
la aplicación: el navigation drawer, que podríamos traducir por la expresión «panel de navegación».
El panel de navegación ha aparecido progresivamente con la versión 4 de Android y se ha convertido en un elemento
esencial en las aplicaciones Android. Consiste en un panel, generalmente oculto, que se muestra por delante de la
pantalla principal cuando el usuario desliza la pantalla de izquierda a derecha.
La compatibilidad con este modo de navegación no es nativa del sistema Android: también aquí hay que utilizar la
biblioteca de soporte android.support.app.v4.
1. Modificación del layout
La primera etapa para integrar un panel de navegación es modificar el archivo de layout de la actividad: el panel se
gestiona a este nivel, y no a nivel del fragmento.
Para indicar al sistema que una actividad incluye un panel de navegación, el layout de la actividad debe contener
como elemento raíz una etiqueta de tipo android.support.v4.widget.DrawerLayout.
Edite el archivo de layout de la actividad principal, es decir, el archivo activity_main.xml.
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/main_placeHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.DrawerLayout>
A continuación, hay que definir las vistas que formarán el contenido del panel de navegación. Es preciso respetar
ciertas reglas:
l El contenido del panel de navegación debe ser el último elemento de la pantalla definido en el archivo de layout: el
panel de navegación debe mostrarse por encima del contenido de la pantalla principal.
l La propiedad layout_gravity debe informarse para el contenedor del panel de navegación.
El propio panel de navegación puede adquirir varias formas: un contenedor de vista clásico, de tipo
LinearLayout por ejemplo, en el que se definirán componentes TextView, cada componente será una entrada
del panel de navegación. Aquí, se ha optado por utilizar un componente ListView para mostrar las entradas del
panel.
Defina, tras el contenedor FrameLayout, un componente ListView.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El ancho de la lista debe ser inferior a la pantalla: Google recomienda un ancho máximo de 320 dp. En el ejemplo, lo
óptimo es utilizar un ancho de 240 dp. La altura ocupa la totalidad de la pantalla.
También hay que indicar un color para el fondo del panel: si no se ha definido ningún color, el panel se muestra
superpuesto a la vista principal.
Informe, para esta lista, la propiedad layout_gravity.
Normalmente, el valor asociado debería ser left (izquierda), de modo que el panel se abre desde la izquierda
hacia la derecha. Sin embargo, en ciertos países se utiliza una escritura de derecha a izquierda, en cuyo caso los
usuarios esperan tener un panel de navegación situado a la derecha. Android provee, para cubrir esta situación, el
valor start: el panel sigue así el sentido de lectura definido en los parámetros del terminal.
Por último, defina un identificador único para el componente DrawerLayout, así como para el
componente ListView.
El archivo de layout de la actividad principal es ahora el siguiente:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_Drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/main_placeHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ListView
android:id="@+id/main_DrawerList"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#000000"/>
</android.support.v4.widget.DrawerLayout>
No debemos olvidarnos de integrar el componente DrawerLayout en el archivo de layout específico para las
tabletas, archivo creado en el capítulo Los fragmentos.
2. Inclusión en la actividad
Todo el mecanismo de apertura y cierre del panel de navegación lo gestiona la plataforma íntegramente: llegados a
este punto, si se ejecuta la aplicación, el panel es funcional. Sin embargo, no se ha definido ninguna entrada, de
modo que está completamente vacío.
Se ha optado por basarse en un componente ListView para visualizar las entradas del panel: de modo que hay
que proporcionar a este componente una lista de elementos. Como hemos visto en el capítulo Controles avanzados,
hay que utilizar un adaptador de datos para completar la lista.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
En vez de utilizar datos propios de una tabla, como se hizo con la lista de DVD, es más fácil crear una lista de
elementos estáticos. Como para las cadenas de caracteres, Android proporciona un método sencillo para gestionar
los arrays de cadenas de caracteres estáticas.
Edite el archivo de recursos de cadenas de caracteres, es decir, el archivo strings.xml (almacenado
en la carpeta /res/values).
Agregue una entrada string-array: debe especificar, en la etiqueta <string-array>, la propiedad
name. Indique el valor drawer_Items para esta propiedad.
En la etiqueta hija, agregue dos entradas <item>: el contenido de la etiqueta representa la cadena de
caracteres. La primera entrada, de momento, es «Inicio», la segunda «Nuevo DVD».
En resumen, el array de cadenas de caracteres se define de la siguiente manera:
<string-array name="drawer_Items">
<item>Inicio</item>
<item>Nuevo DVD</item>
</string-array>
Antes de configurar el adaptador de la lista, es necesario también diseñar un archivo de layout que se pasará al
adaptador. Para simplificar los desarrollos, Android propone una solución en el caso de que los datos que se han de
mostrar sean simples cadenas de caracteres: si un archivo de layout solo contiene un único componente, un
componente TextView, no es necesario definir un adaptador específico; el adaptador genérico ArrayAdapter
es capaz de completar el contenido del componente TextView.
Sitúese en la carpeta /res/layout en el explorador del proyecto de Android Studio.
Cree un nuevo archivo de layout, llamado listitem_drawer.xml. Para ello, haga clic con el botón
derecho, seleccione la opción New y, a continuación, Layout resource file (puede dejar el elemento raíz
tal y como se le propone).
Este archivo de layout debe contener un único componente. Reemplace la etiqueta raíz por una etiqueta
TextView, indicando que el ancho y el alto del componente deben adaptarse al contenido (constante
wrap_content).
También hay que especificar un color para el texto, diferente del color de fondo del panel y, para hacer que el clic
sea más sencillo para el usuario, utilizar un tamaño de letra elevado (el tamaño de letra se define, tradicionalmente,
en unidades sp).
El archivo de layout es el siguiente:
Todos los elementos necesarios para la visualización de las entradas del panel están ahora implementados; tan
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
solo queda por definir el adaptador para la lista.
Edite el archivo MainActivity.java y sitúese en el método onCreate, tras la llamada al método
setContentView.
Hay que declarar un objeto de tipo ListView y vincularlo al componente ListView del panel de
navegación. El identificador de la ListView se ha definido aquí como main_DrawerList, el código es
el siguiente:
ListView listDrawer =
(ListView)findViewById(R.id.main_DrawerList);
Los datos se declaran como recursos de tipo StringArray: hay que definir un array de cadenas de
caracteres y vincularlo a estos recursos. Esto se hace invocando al método getResources de la clase
Activity.
String [] drawerItems =
getResources().getStringArray(R.array.drawer_Items);
Por último, hay que asignar un adaptador de tipo ArrayAdapter<String> para la lista, pasándole el
archivo de layout listitem_drawer y el array de cadenas de caracteres definido previamente:
listDrawer.setAdapter(new ArrayAdapter<String>(this,
R.layout.listitem_drawer, drawerItems));
Al final, el código completo del método onCreate es el siguiente:
@Override
protected void onCreate(Bundle savedInstanciaState) {
super.onCreate(savedInstanciaState);
setContentView(R.layout.activity_main);
ListView listDrawer =
(ListView)findViewById(R.id.main_DrawerList);
String [] drawerItems =
getResources().getStringArray(R.array.drawer_Items);
listDrawer.setAdapter(new ArrayAdapter<String>(this,
R.layout.listitem_drawer, drawerItems));
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
}
El panel de navegación ahora está activo, y las dos entradas deseadas se muestran cuando se abre el panel. Tan
solo queda por gestionar la reacción al clic del usuario, lo que se hace, como vimos en el capítulo Controles
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
avanzados, asignado una instancia de OnItemClickListener al objeto ListView del panel de navegación.
listDrawer.setOnItemClickListener(new
ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int pos,
long id) {
}
});
Al hacer clic en la primera entrada, debe redirigirse al usuario a la página de inicio. Sin embargo, hay que
tener la precaución de vaciar la pila de actividades para que la aplicación sea la misma que tras su
ejecución. Esto se hace asignado un valor concreto a dos propiedades del objeto Intent que se utiliza
para lanzar la actividad:
if(pos==0) {
Intent intent = new Intent(MainActivity.this,
MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
Al hacer clic en la segunda entrada, debe abrirse la vista que permite introducir un nuevo DVD.
if(pos==1)
startActivity(new Intent(MainActivity.this,
AddDVDActivity.class));
Sería necesario, en un entorno de producción, transformar la actividad AddDVDActivity en un fragmento para que el
conjunto de pantallas de la aplicación sea coherente. La solución que tiene disponible para descargar implementa
esta funcionalidad.
El código del método onCreate es ahora el siguiente:
@Override
protected void onCreate(Bundle savedInstanciaState) {
super.onCreate(savedInstanciaState);
setContentView(R.layout.activity_main);
listDrawer.setOnItemClickListener(new
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int pos,
long id) {
if(pos==0) {
Intent intent = new Intent(MainActivity.this,
MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
if(pos==1)
startActivity(new Intent(MainActivity.this,
AddDVDActivity.class));
}
});
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
}
3. Manipular el panel de navegación
Puede ser necesario manipular el panel de navegación por programación: por ejemplo, cuando el usuario hace clic
en una entrada del panel, este debería cerrarse automáticamente.
Para ello, hay que definir una variable global para la clase MainActivity, obtener una referencia sobre el
DrawerLayout del archivo de layout de la actividad e invocar los métodos openDrawer y
componente
closeDrawer para, respectivamente, abrir o cerrar el panel de navegación.
Ambos métodos reciben como parámetro un valor entero que indica en qué dirección debe abrirse y cerrarse el
panel: este parámetro se corresponde con el atributo layout_gravity, informado en el archivo de layout para el
panel. El valor correspondiente a start es la constante android.view.Gravity.START.
En el marco de la aplicación LocDVD, hay que cerrar el panel de navegación cuando se procesa el evento
onItemClick. El código completo de la gestión del panel es, por tanto, el siguiente:
DrawerLayout drawerLayout;
@Override
protected void onCreate(Bundle savedInstanciaState) {
super.onCreate(savedInstanciaState);
setContentView(R.layout.activity_main);
drawerLayout = (DrawerLayout)findViewById(R.id.main_Drawer);
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
ListView listDrawer =
(ListView)findViewById(R.id.main_DrawerList);
String [] drawerItems =
getResources().getStringArray(R.array.drawer_Items);
listDrawer.setAdapter(new ArrayAdapter<String>(this,
R.layout.listitem_drawer, drawerItems));
listDrawer.setOnItemClickListener(new
ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int pos,
long id) {
if(pos==0) {
Intent intent = new Intent(MainActivity.this,
MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
if(pos==1)
startActivity(new Intent(MainActivity.this,
AddDVDActivity.class));
drawerLayout.closeDrawer(android.view.Gravity.START);
}
});
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",Context.MODE_PRIVATE);
if(!sharedPreferences.getBoolean("embeddedDataInserted", false))
readEmbeddedData();
}
A la inversa, puede ser necesario que la actividad esté informada de las manipulaciones del usuario sobre el panel.
La plataforma provee, para ello, una interfaz DrawerListener que expone los siguientes métodos:
l onDrawerOpened(View drawerView)
Se invoca cuando el panel está abierto.
l onDrawerClosed(View drawerView)
Se invoca cuando el panel está cerrado.
l onDrawerStateChanged(int newState)
Se invoca cuadno el panel cambia de estado. Los posibles estados son los siguientes:
n STATE_IDLE: el usuario ya no puede manipular el panel.
n STATE_DRAGGING: el panel está siendo manipulado.
n STATE_SETTLING: el panel ha alcanzado una posición "final" (abierto o cerrado).
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Mostrar una ventana emergente estándar
Es habitual presentar al usuario una ventana emergente para permitirse confirmar una acción importante o
irreversible. Es el caso, aquí, cuando el usuario pide reinicializar la base de datos a partir de la opción del menú.
La plataforma Android dispone de varios tipos de ventana emergente: el capítulo Controles avanzados ha presentado
las ventanas emergentes DatePicker y TimePicker. Las ventanas emergentes que permiten solicitar la
confirmación o informar de un hecho importante se llaman, en el universo Android, AlertDialog.
Una ventana emergente AlertDialog se compone de tres partes: el título, en la parte superior de la ventana
emergente, el contenido propiamente dicho el mensaje comunicado al usuario, que aparece en la zona central y, en
la parte inferior de la ventana emergente, de uno a tres botones. Estos botones están etiquetados como neutro,
positivo o negativo. Sin embargo, no hay que presumir la posición relativa de cada uno de estos botones, pues
depende de la versión de Android tal y como demuestran las siguientes capturas de pantalla (la captura de pantalla
situada a la izquierda se corresponde con Android Jelly Bean y la situada a la derecha, con Android Lollipop).
La API Android provee una clase que simplifica la creación de ventanas emergentes: la clase
AlertDialog.Builder.
AlertDialog.Builder AlertDialog.Builder(Context.context);
Para la aplicación LocDVD, una ventana emergente debe preguntar al usuario si confirma que quiere reinicializar la
base de datos: además del mensaje de confirmación, es necesario mostrar los botones positivo y negativo.
Dado que el menú está contenido en la actividad principal, la ventana emergente debe implementarse en esta
actividad.
Edite el archivo MainActivity.java.
Defina un nuevo método, privado, llamado ensureReInitializeApp, que no reciba ningún parámetro.
En primer lugar hay que definir una instancia de AlertDialog.Builder. Para esta clase se exponen
dos constructores:
AlertDialog.Builder(Context context)
AlertDialog.Builder(Context context, int theme)
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El parámetro Context context es el contexto de ejecución de la aplicación.
El parámetro opcional int theme permite definir un tema específico para modificar el aspecto de la ventana
emergente.
Utilice el primer constructor, el tema por defecto es apropiado aquí. El contexto se obtiene, habitualmente,
de la propia actividad.
Hay que definir el título de la ventana emergente invocando el método setTitle del objeto
AlertDialog.Builder.
setTitle(CharSequence title)
setTitle(int titleId)
En la primera versión de setTitle, hay que pasar una cadena de caracteres, la segunda versión recibe como
parámetro el identificador del recurso de tipo String que se ha de mostrar.
Una vez definido el recurso de tipo String correspondiente, se le asigna valor al título mediante el siguiente código:
builder.setTitle(R.string.confirmar_reinicializacion_title);
El mensaje se informa de la misma manera, invocando el método setMessage. También aquí existen dos
versiones del método disponibles, una que recibe como parámetro una secuencia de caracteres, y otra, el
identificador del recurso String correspondiente.
setMessage(CharSequence message)
setMessage(int messageId)
Como la cadena de caracteres se ha definido en los recursos, el código es simplemente el siguiente:
builder.setMessage(R.string.confirmar_reinicializacion_message);
Hay que implementar los botones positivo y negativo. El botón neutro no se utiliza aquí, basta con no definirlo: no se
mostrará en este caso.
El botón negativo debe indicar «No», y no debe producir ninguna acción, aparte de cerrar la ventana emergente. Por
defecto, el clic en el botón de una ventana emergente la cierra automáticamente, de modo que no hay que prever
ningún tratamiento adicional aparte de asignar el texto del botón.
Hay que invocar el método setNegativeButton de la clase AlertDialog.Builder. También aquí
hay disponibles dos versiones, según si el texto se provee directamente o a través de un recurso de tipo
String.
setNegativeButton(CharSequence text,
DialogInterface.OnClickListener listener)
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
setNegativeButton(int textId, DialogInterface.OnClickListener
listener)
El segundo parámetro, DialogInterface.OnClickListener listener, permite indicar una instancia de
DialogInterface.OnClick Listener que se invocará con el clic.
Defina un recurso de tipo String para el texto del botón negativo y asigne este recurso al botón. Como
no se quiere realizar ninguna acción cuando se hace clic en este botón, hay que pasar el valor null para
el OnClickListener:
builder.setNegativeButton(R.string.no, null);
El botón positivo debe, por su parte, lanzar la reinicialización de la base de datos. Hay que invocar al
método setPositiveButton y proveer, además del texto que se ha de mostrar, una instancia de
OnClickListener; esta instancia puede definirse mediante una clase anónima.
builder.setPositiveButton(R.string.si,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}
);
La recreación de la base de datos se corresponde con una eliminación y una creación posterior de la base de datos.
Edite el archivo LocalSQLiteOpenHelper.java.
La eliminación de una base de datos se realiza invocando el método deleteDatabase de la clase
Context. El método recibe como parámetro el nombre de la base de datos.
El método deleteDatabase es el siguiente:
La creación de la base de datos la gestiona íntegramente el sistema, como se ha indicado en el capítulo
Persistencia de datos. Solo queda invocar el método de MainActivity que rellena la base de datos con
los datos de ejemplo.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
También hay que actualizar la visualización para que la lista de DVD se actualice tras regenerar la base de datos. Lo
más fácil es relanzar la página de inicio, para forzar a la aplicación a consultar de nuevo la base de datos.
Al final, el método onClick tiene el siguiente aspecto:
@Override
public void onClick(DialogInterface dialog, int which) {
LocalSQLiteOpenHelper.deleteDatabase(MainActivity.this);
readEmbeddedData();
Una vez implementados los botones, tan solo queda por mostrar la ventana emergente. Cuando se utiliza el
asistente AlertDialog.Builder, esto se hace en dos etapas: la primera etapa debe invocar el método create
de la clase Builder, que devuelve una instancia de AlertDialog, para pedir al sistema que cree efectivamente la
ventana emergente. La segunda etapa consiste en invocar el método show de la clase AlertDialog.
En definitiva, el método ensureReInitializeApp tiene el siguiente aspecto:
LocalSQLiteOpenHelper.deleteDatabase(MainActivity.this);
readEmbeddedData();
}
});
Tan solo queda por invocar este método en la instrucción switch implementada para la gestión del menú.
@Override
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.menu_reinicializar:
// se ha seleccionado la entrada Reinicializar la base de datos
ensureReInitializeApp();
return true;
case R.id.menu_informacion:
// se ha seleccionado la entrada Información
return true;
default:
return super.onOptionsItemSelected(item);
}
}
Si bien no es necesario aquí, es posible cerrar la ventana emergente por programación: para ello, basta con invocar el
método dismiss de la clase AlertDialog.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Crear ventanas emergentes personalizadas
La opción Información del menú, que se ha definido al principio de este capítulo, debe mostrar una breve descripción
de la aplicación.
El mensaje que se ha de mostrar no es demasiado largo, de modo que resultaría molesto tener que crear un
fragmento únicamente para esto. La mejor opción es mostrar una breve descripción utilizando una ventana
emergente.
Si bien es posible utilizar la ventana emergente AlertDialog no tendría más que un botón para cerrar la ventana
emergente, es más interesante definir un archivo de layout para esta ventana emergente: esto aporta una mayor
flexibilidad en la definición del aspecto de dicha ventana.
El componente TextView debe mostrar el contenido en 10 líneas y debe tener un margen de de 16 dp por cada
lado. También debe mostrar una barra de desplazamiento vertical.
El layout se define así:
Una vez definido el archivo de layout de la ventana emergente, hay que gestionar su visualización.
Edite el archivo MainActivity.java y defina un método privado showInformation que no reciba
ningún parámetro.
Como con una ventana emergente clásica, instancie un objeto de tipo AlertDialog.Builder.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El método setTitle se utiliza para introducir el título de la ventana emergente (la cadena de caracteres
información se ha definido al inicio de este capítulo y puede reutilizarse aquí):
builder.setTitle(R.string.info);
Hay un único botón que permite cerrar la ventana emergente. Puede utilizarse para ello cualquiera de los
tres botones, y debe mostrar el texto «Cerrar».
builder.setPositiveButton(R.string.cerrar, null);
Como con el botón «No» de la sección anterior, no hace falta ninguna otra acción para cerrar la ventana emergente.
No hace falta, por tanto, instanciar un OnClickListener para gestionar el comportamiento al hacer clic.
Ahora hay que asignar el archivo de layout que acabamos de definir a la ventana emergente, e introducir el
texto informativo.
Para especificar un layout, hay que invocar el método setView de la instancia de AlertDialog.Builder.
Existen dos versiones de este método: una recibe como parámetro una instancia de android.view.View; la
segunda, aparecida en la API versión 21, recibe como parámetro el identificador del layout.
Aquí, para mantener una total compatibilidad, se utiliza la primera versión.
setView(int layoutResId)
setView(View view)
Para pasarle una instancia de View que tenga en cuenta el archivo de layout definido, hay que invocar, como hemos
visto para los fragmentos, el método inflate del objeto LayoutInflater:
También hay que especificar el texto que se ha de mostrar. Edite el archivo de recursos strings.xml y
agregue una entrada llamada mensaje_informacion. Escriba un texto que debe, idealmente, ser
bastante largo.
Es posible insertar saltos de línea en el texto utilizando la secuencia de escape \n. A continuación se muestra un
ejemplo de texto:
<string name="mensaje_informacion">
"La aplicación LocDVD es la aplicación que acompaña
al libro ’Desarrolle una aplicación Android’, de Ediciones ENI.
\n\nSe ha desarrollado íntegramente siguiendo el libro,
y aborda los principales dominios de la creación de aplicaciones
para todos los terminales Android: desde la creación del proyecto
hasta la publicación de la aplicación en Play Store,
pasando por la creación y la manipulación de bases de datos,
la adaptación a tabletas y smartphones,
la programación asíncrona, etc.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
\n\n\n ¡Feliz lectura!"
</string>
El texto, definido en el archivo de recursos, hay que asignarlo al componente TextView integrado en el
layout. Como hemos visto, en primer lugar hay que obtener una instancia del componente e invocar a
continuación el método setText de la instancia:
TextView message
=(TextView)view.findViewById(R.id.dialog_message);
message.setText(R.string.mensaje_informacion);
El mensaje que aparecerá es, a priori, más largo de lo que puede mostrar el componente TextView (que
se ha configurado para mostrar 10 líneas de texto), de modo que hay que indicarle al objeto TextView
cómo gestionar el desplazamiento; ¡no lo hace por defecto!
Para ello, hay que invocar el método setMovementMethod, pasándole como parámetro una instancia de la clase
android.text.method.ScrollingMovementMethod:
message.setMovementMethod(
new android.text.method.ScrollingMovementMethod());
Una vez definido completamente el texto, tan solo queda invocar el método setView de la instancia de
AlertDialog.Builder, pasándole el objeto View configurado.
builder.setView(view);
Y, por último, queda por lanzar la creación y la visualización de la ventana emergente personalizada.
builder.create().show();
A continuación se muestra el método showInformation completo:
builder.setTitle(R.string.info);
builder.setPositiveButton(R.string.cerrar, null);
builder.setView(view);
builder.create().show();
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Ahora es posible completar la gestión del clic del usuario en la entrada Información del menú, invocando el
método showInformation:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_reinicializar:
// se ha seleccionado la entrada Reinicializar la base de datos
ensureReInitializeApp();
return true;
case R.id.menu_informacion:
// se ha seleccionado la entrada Información
showInformation();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Ejecutar acciones como tarea de fondo
En cualquier aplicación, el confort y la experiencia de usuario son aspectos esenciales: una aplicación debe, en
smartphones y tabletas, reaccionar inmediatamente a cada solicitud del usuario. Para garantizar una respuesta
óptima, la plataforma Android introduce una regla: de cualquier aplicación que no reaccione a una solicitud del usuario
en un plazo de 10 segundos se dice que no responde. En tal situación, se produce un error ANR, de Application Not
Responding («la aplicación no responde»), y el sistema puede detener la aplicación.
Para aquellas operaciones que pueden potencialmente tomar cierto tiempo (y cuya respuesta, por lo tanto, no es
inmediata), se recomienda de forma especial realizar procesamientos asíncronos: la operación se ejecuta como tarea
de fondo, y el usuario puede observar el progreso de la operación.
Si bien es posible implementar dicho mecanismo utilizando las tipicas clases de gestión de threads de Java, la
plataforma provee una clase abstracta, android.os.AsyncTask, que se encarga de la mayor parte de la
implementación de una solución asíncrona, aliviando el trabajo del desarrollador.
Para simplificar el diseño de una operación de tarea de fondo, AsyncTask separa el procesamiento en varias fases;
cada una está representada por un método.
l void onPreExecute(): este método se ejecuta cuando se lanza la tarea asíncrona. Se ejecuta en el thread
principal, lo que permite manipular los componentes de la interfaz de usuario. No hay que invocar este método
directamente, sino invocar el método execute, que lanza el procesamiento.
l void onPostExecute(Result result): este método se invoca tras el método doInBackground.
Recibe como parámetro el objeto de tipo genérico Result devuelto por doInBackground. Este método se
ejecuta por el thread principal y permite manipular componentes de la interfaz de usuario.
La noción de thread principal/thread de tarea de fondo no es banal: en efecto, es imposible manipular cualquier objeto
que forme parte de la interfaz de usuario en un thread de tarea de fondo: se produce una excepción. Si bien, en el
marco de la clase AsyncTask, esto no supone un problema, los métodos onPreExecute, onPostExecute y onProgressUpdate
se ejecutan en el thread principal, lo que puede plantear problemas en ciertos casos. Para resolver esta limitación, la clase
Activity expone el método runOnUIThread, que, como su propio nombre indica, fuerza la ejecución en el thread principal.
runOnUIThread recibe como parámetro una instancia de la interfaz Runnable. En el capítulo Uso de Bluetooth Low Energy
se muestra un ejemplo, en la sección Conectar un objeto.
En el proyecto LocDVD, la inserción de los DVD de ejemplo es una tarea que puede, potencialmente, llevar cierto
tiempo: aquí hay tan solo unos pocos DVD, pero podríamos imaginar un archivo más extenso, que tomara tiempo en
cargarse. Por ello resulta conveniente transformar este procesamiento en un procesamiento asíncrono.
Edite el archivo MainActivity.java que contiene el método que realiza la inserción de los DVD de
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
ejemplo.
En la clase MainActivity, defina una clase AsyncReadEmbeddedData que extienda de la clase
AsyncTask<String, Integer, Boolean>.
@Override
protected void onPreExecute() {
}
@Override
protected Boolean doInBackground(String... params) {
return false;
}
@Override
protected void onProgressUpdate(Integer... values) {
}
@Override
protected void onPostExecute(Boolean result) {
}
};
El cuerpo de doInBackground es, de momento, el siguiente:
@Override
protected Boolean doInBackground(String... params) {
String dataFile = params[0];
InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
file = getAssets().open(dataFile);
reader = new InputStreamReader(file);
bufferedReader = new BufferedReader(reader);
String line= null;
while((line=bufferedReader.readLine())!=null) {
String [] data = line.split("\\|");
if(data!=null && data.length==4) {
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
DVD dvd = new DVD();
dvd.titulo = data[0];
dvd.anyo = Integer.decode(data[1]);
dvd.actores = data[2].split(",");
dvd.resumen = data[3];
dvd.insert(MainActivity.this);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",
Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
Para informar al usuario del avance de la inicialización, hay que publicar con cada inserción el número de
DVD insertados en la base de datos. Para ello, hay que implementar un contador e invocar, con cada
iteración del bucle while, el método publishProgress:
try {
int counter = 0;
file = getAssets().open(dataFile);
reader = new InputStreamReader(file);
bufferedReader = new BufferedReader(reader);
String line= null;
while((line=bufferedReader.readLine())!=null) {
String [] data = line.split("\\|");
if(data!=null && data.length==4) {
DVD dvd = new DVD();
dvd.titulo = data[0];
dvd.anyo = Integer.decode(data[1]);
dvd.actores = data[2].split(",");
dvd.resumen = data[3];
dvd.insert(MainActivity.this);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
publishProgress(++counter);
}
}
} catch (IOException e) {
e.printStackTrace();
}
doInBackground debe devolver verdadero si la inserción se ha producido correctamente, y falso en caso
contrario. En el marco de la aplicación, resulta interesante además agregar un temporizador entre la
inserción de dos DVD, para visualizar más fácilmente el procesamiento como tarea de fondo. A continuación
se muestra una versión completa del método:
@Override
protected Boolean doInBackground(String... params) {
boolean result = false;
String dataFile = params[0];
InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
int counter = 0;
file = getAssets().open(dataFile);
reader = new InputStreamReader(file);
bufferedReader = new BufferedReader(reader);
String line= null;
while((line=bufferedReader.readLine())!=null) {
String [] data = line.split("\\|");
if(data!=null && data.length==4) {
DVD dvd = new DVD();
dvd.titulo = data[0];
dvd.anyo = Integer.decode(data[1]);
dvd.actores = data[2].split(",");
dvd.resumen = data[3];
dvd.insert(MainActivity.this);
publishProgress(++counter);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",
Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
result = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
El usuario debe estar informado del avance en la inserción de los DVD. La plataforma Android provee, para ello, un
componente integrado, ProgressDialog, que muestra en un popup una ventana emergente de progreso. La
inicialización de esta ventana emergente debe hacerse en el método onPreExecute: este método se ejecuta en el
thread principal y puede acceder a los componentes de la interfaz.
Declare una variable de tipo ProgressDialog:
ProgressDialog progressDialog;
[...]
};
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle(R.string.inicializacion_de_la_base_de_datos);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
}
Cabe destacar que la cadena de caracteres inicializacion_de_la_base_de_datos se ha definido en el
archivo de recursos strings.xml:
<string
name="inicializacion_de_la_base_de_datos">Inicialización de la
base de datos</string>
La ventana emergente de progreso debe cerrarse cuando termine la tarea, es decir, en el método
onPostExecute. Para ello, hay que invocar el método dismiss del objeto progressDialog:
@Override
protected void onPostExecute(Boolean result) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
progressDialog.dismiss();
}
El método onProgressUpdate debe actualizar la ventana emergente indicando el número de DVD insertados en la
base de datos. El método setMessage del objeto progressDialog permite modificar el mensaje mostrado:
@Override
protected void onProgressUpdate(Integer... values) {
progressDialog.setMessage(
String.format(getString
(R.string.x_dvd_insertados_en_la_base_de_datos),
values[0]));
}
La cadena de caracteres x_dvd_insertados_en_la_base_de_datos se ha incluido en el archivo
strings.xml:
El código de definición de la clase AsyncReadEmbeddedData es, completo, el siguiente:
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog
.setTitle(R.string.inicializacion_de_la_base_de_datos);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
}
@Override
protected Boolean doInBackground(String... params) {
boolean result = false;
String dataFile = params[0];
InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
int counter = 0;
file = getAssets().open(dataFile);
reader = new InputStreamReader(file);
bufferedReader = new BufferedReader(reader);
String line= null;
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
while((line=bufferedReader.readLine())!=null) {
String [] data = line.split("\\|");
if(data!=null && data.length==4) {
DVD dvd = new DVD();
dvd.titulo = data[0];
dvd.anyo = Integer.decode(data[1]);
dvd.actores = data[2].split(",");
dvd.resumen = data[3];
dvd.insert(MainActivity.this);
publishProgress(++counter);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bufferedReader!=null) {
try {
bufferedReader.close();
reader.close();
SharedPreferences sharedPreferences =
getSharedPreferences("com.ejemplo.locDVD.prefs",
Context.MODE_PRIVATE);
SharedPreferences.Editor editor =
sharedPreferences.edit();
editor.putBoolean("embeddedDataInserted", true);
editor.commit();
result = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
@Override
protected void onProgressUpdate(Integer... values) {
progressDialog.setMessage(
String.format(getString
(R.string.x_dvd_insertados_en_la_base_de_datos),
values[0]));
}
@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss();
}
};
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
El método readEmbeddedData, en vez de realizar la inicialización, lanza la tarea asíncrona instanciando una
variable de tipo AsyncReadEmbeddedData e invocando el método execute, al que pasa como parámetro el
nombre del archivo de datos.
Para probar la inserción asíncrona de datos, hay que desinstalar, en primer lugar, la aplicación con objeto de forzar
una nueva inicialización.
La primera ejecución de la aplicación muestra cómo la inserción se desarrolla correctamente se muestra la ventana
emergente de progreso pero que, tras esta inserción, la lista de DVD sigue vacía.
En efecto, cuando la inserción de los DVD se hacía en el thread principal, la inicialización de la lista, que se hace en el
método onResume, se producía necesariamente tras la operación de inserción. Ahora que la inserción se realiza
como una tarea de fondo, la aplicación lanza la inicialización de la lista inmediatamente después de la llamada a la
operación de inserción, sin que esta haya terminado.
Para resolver este problema es preciso, cuando termina la inserción, que la tarea de fondo indique a la lista que los
datos se han modificado.
La actualización de la lista solo puede llevarse a cabo en el fragmento que la contiene: de modo que hay que
modificar la clase ListDVDFragment.
De momento, la lista se rellena en el método onResume. Lo más fácil, para hacer que esta operación esté disponible
fuera de la clase, es mover el código del método onResume a un método público.
Edite el archivo ListDVDFragment.java.
Cree un nuevo método público, updateDVDList. Este método no recibe ningún parámetro ni tampoco
devuelve ningún dato.
Mueva el código que configura la lista desde el método onResume hasta el nuevo método
updateDVDList.
El método
onResume, además de llamar al método onResume de la clase madre, debe invocar el método
updateDVDList.
@Override
public void onResume() {
super.onResume();
updateDVDList();
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
}
El método onPostExecute de la tarea asíncrona creada previamente debe invocar updateDVDList. Para ello,
hay que obtener, en onPostExecute, una referencia al fragmento ListDVDFragment, si realmente está
vinculado a la pantalla.
El administrador de fragmentos, la clase FragmentManager, provee el método findFragmentByTag, que
permite buscar un fragmento en la pila de fragmentos a partir de un tag que se define durante la invocación al
método replace (método que carga el fragmento en la página en curso). De modo que hay que modificar la gestión
del fragmento para asignar un valor a esta propiedad tag.
Edite la clase MainActivity.
Modifique el método openFragment, agregando un parámetro de llamada de tipo String:
Modifique la llamada al método replace, para integrar el tag:
El código del método openFragment es, por tanto, el siguiente:
FragmentManager fragmentManager =
getSupportFragmentManager();
FragmentTransaction transaction =
fragmentManager.beginTransaction();
transaction.replace(R.id.main_placeHolder, fragment, tag);
transaction.addToBackStack(null);
transaction.commit();
}
[...]
@Override
public void onResume() {
super.onResume();
ListDVDFragment listDVDFragment = new ListDVDFragment();
openFragment(listDVDFragment, TAG_FRAGMENT_LISTDVD);
}
Ahora es posible modificar el m método onPostExecute, para insertar el código que permite obtener una referencia
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
sobre el fragmento ListDVDFragment, e invocar el método updateDVDList:
@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss();
FragmentManager fragmentManager =
getSupportFragmentManager();
ListDVDFragment listDVDFragment =
(ListDVDFragment)fragmentManager
.findFragmentByTag(TAG_FRAGMENT_LISTDVD);
if(listDVDFragment!=null)
listDVDFragment.updateDVDList();
}
Tras la primera ejecución, una vez reinstalada, la aplicación realiza ahora correctamente la inicialización de la base de
datos y, a continuación, refresca la lista y muestra los DVD insertados automáticamente.
También hay que modificar el código que se encarga de la reinicialización de la base de datos. En efecto, ya no es
necesario recargar la pantalla; la actualización de la lista se realiza directamente tras la inicialización. Basta, por
tanto, con eliminar el código correspondiente a la recarga de la pantalla. El método ensureReInitializeApp es,
ahora, el siguiente:
Cabe destacar que, en vez de definir una clase que extiende AsyncTask<Param, Progress, Result>,
también es posible definir directamente una variable local de tipo AsyncTask. En este caso, la implementación de la
clase se hace utilizando tipos anónimos de Java; el código correspondiente se indica como ejemplo a continuación.
@Override
protected Boolean doInBackground(String... params) {
return null;
}
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
};
Sin embargo, existe una restricción importante que limita el interés de esta manera de trabajar: una variable de tipo
AsyncTask solo puede ejecutarse una única vez. Esta restricción, impuesta por la plataforma, sirve probablemente
para evitar que el sistema tenga que gestionar con detalle el estado de la tarea asíncrona.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
Desarrollar un servicio
Las tareas asíncronas permiten realizar operaciones como tarea de fondo, lo que deja al thread principal libre. Sin
embargo, estas tareas asíncronas están vinculadas a una actividad; no pueden iniciarse de manera autónoma.
Esto puede plantear problemas en ciertos casos: por ejemplo, si la tarea que se debe realizar es larga, no podemos
estar seguros a priori de que la actividad que ha creado la tarea asíncrona siga activa durante todo el procesamiento.
Para resolver este problema, Android ofrece la posibilidad de desarrollar servicios o bien componentes de aplicación
desprovistos de interfaz de usuario.
Estos servicios deben basarse en la clase android.app.Service o en una de sus clases derivadas. Típicamente,
en el marco de la ejecución de una tarea asíncrona larga, hay que utilizar la clase android.app.IntentService.
Esta clase es una especialización de android.app.Service, que integra todo el mecanismo de gestión de la
tarea asíncrona: en efecto, por defecto, un servicio se ejecuta en el thread principal. IntentService se encarga
de implementar la gestión de un thread como tarea de fondo y simplifica enormemente la implementación del servicio.
IntentService es una clase abstracta que hay que extender implementando el método onHandleIntent: este
método se invoca directamente por el sistema cuando arranca el servicio. Su ejecución se produce en un thread
separado.
También hay que proveer un constructor por defecto, que debe obligatoriamente invocar al constructor de la
superclase IntentService, pasándole un nombre de servicio.
La implementación de un servicio basado en IntentService se hace, por tanto, de la siguiente manera:
Cree una clase que extienda IntentService:
Defina un constructor por defecto, que invoque el constructor de la superclase:
public LocDVDIntentService() {
super("LocDVDIntentService");
}
Implemente el método onHandleIntent.
@Override
protected void onHandleIntent(Intent intent) {
El objeto Intent permite pasar parámetros para lanzar el servicio. Por ejemplo, el siguiente código recupera el valor
de un parámetro waitDuration y ejecuta una pausa con una duración correspondiente a este valor:
@Override
protected void onHandleIntent(Intent intent) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
int waitDuration = intent.getIntExtra("waitDuration", 1000);
try {
Thread.sleep(waitDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Como las actividades, los servicios desarrollados deben declararse en el archivo AndroidManifest.xml de la
aplicación.
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
[...]
<service
android:name=".LocDVDIntentService">
</service>
</application>
</manifest>
Para lanzar este servicio, hay que invocar el método startService de la clase Context (o de alguna de las
clases que implementen Context - Activity, por ejemplo), pasándole un objeto de tipo Intent.
Es posible realizar varias llamadas a un servicio; se tratarán secuencialmente, en el orden definido por las llamadas.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Comunicarse con un servicio
Si bien la comunicación entre una actividad y un servicio se realiza a través del objeto Intent que se pasa como
parámetro, implementar una comunicación desde el servicio hacia la actividad que lo ha lanzado es un poco más
complejo.
En primer lugar hay que definir un objeto de tipo Handler, que se encargue de la comunicación entre el servicio y la
actividad. El objeto de tipo Handler debe implementar el método handleMessage:
Este objeto Handler debe, a continuación, pasarse al servicio, en el Intent. Para ello se utiliza un objeto de tipo
Messenger, que puede almacenarse en un Intent.
El servicio puede recuperar el objeto Messenger en el método onHandleIntent:
@Override
protected void onHandleIntent(Intent intent) {
[...]
Messenger mensajero = (Messenger)intent.getParcelableExtra("mensajero");
[...]
}
Tras realizar los procesamientos del servicio en el método onHandleIntent, el servicio puede comunicarse con la
actividad utilizando el objeto de tipo Mensajero obtenido.
Para ello, hay que definir un objeto de tipo Message, pasarle la información que se ha de comunicar en un objeto de
tipo Bundle y enviar el mensaje invocando el método send del mensajero.
Se obtiene una instancia de Message invocando el método estático obtain de la clase Message:
La gestión del objeto Bundle es clásica. El siguiente ejemplo muestra cómo comunicar una cadena de caracteres en
una instancia de Bundle:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Bundle replyData = new Bundle();
replyData.putString("reply","El servicio ha terminado");
El Bundle se asocia, ahora, al mensaje, invocando el método setData:
message.setData(replyData);
El mensaje está listo para enviarse, la llamada al método send debe encapsularse en una instrucción try/catch:
try {
mensajero.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
Este mensaje lo tratará el método handleMessage del objeto Handler definido en la actividad:
@Override
public void handleMessage(Message msg) {
Bundle reply = msg.getData();
String message = reply.getString("reply");
[...]
}
En definitiva, una implementación canónica de IntentService tiene el siguiente aspecto:
import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
public LocDVDIntentService() {
super("LocDVDIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Messenger mensajero =
(Messenger)intent.getParcelableExtra("mensajero");
try {
Thread.sleep(waitDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(mensajero!=null) {
Message message = Message.obtain();
Bundle replyData = new Bundle();
replyData.putString("reply","El servicio ha terminado");
message.setData(replyData);
try {
mensajero.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
La gestión del servicio es la siguiente, en el contexto de una actividad llamada MiActivity:
Intent intent =
new Intent(MiActivity.this, LocDVDIntentService.class);
intent.putExtra("mensajero",mensajero);
intent.putExtra("waitDuration", 10000);
startService(intent);
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Utilizar los receptores de eventos
Además del mecanismo de mensaje/mensajero, existe otra solución, más general, que permite iniciar una
comunicación entre un servicio y, por ejemplo, una actividad. Este mecanismo, los receptores de eventos, se utiliza
con mucha frecuencia en el sistema Android.
Básicamente, un receptor de eventos es un componente de la aplicación cuyo rol es interceptar un evento y
procesarlo.
Los eventos procesados pueden estar o bien generados por el sistema o bien por alguna aplicación. El capítulo
Explotar el teléfono describe cómo procesar un evento generado por el sistema y el uso llamado estático de los
receptores de eventos.
Aquí, en el marco de la comunicación con un servicio, los receptores de eventos permiten implementar una
comunicación asíncrona con una actividad.
1. Definir un receptor de eventos
Un receptor de eventos es un objeto que hereda de la clase abstracta
android.content.BroadcastReceiver. El método onReceive debe implementarse y se invoca tras la
recepción del evento para el que se ha inscrito el receptor de eventos.
@Override
public void onReceive(Context context, Intent intent) {
}
};
OnReceive recibe como parámetro el contexto de ejecución, así como un objeto de tipo Intent, que contiene la
información transmitida.
Una vez definido, un receptor de eventos debe estar vinculado al evento que desea procesar. Para ello, se invoca el
método registerReceiver de la clase Context.
l BroadcastReceiver receiver: el receptor de eventos que se va a asociar al evento.
l IntentFilter filter: el filtro de intención, que determina qué acción generará la invocación del receptor de
eventos.
2. Intención y filtro de intención
La clase Intent se ha mencionado en varias ocasiones hasta el momento: para iniciar una actividad, un servicio, y
para pasar información entre actividades.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Una intención, formalmente hablando, es una acción que se ha de realizar. Android presenta dos tipos de intención:
las intenciones explícitas y las intenciones implícitas.
Una intención explícita define el componente de la aplicación que debe ejecutarse, por ejemplo, para lanzar una
actividad específica o iniciar un servicio. La intención definida en la sección Definir un servicio para lanzar el servicio
LocDVDIntentService es, así, una intención explícita: el servicio que se ha de iniciar está indicado de manera
precisa.
De manera inversa, una intención implícita no define el componente de la aplicación que se va a lanzar, sino que
define qué acción debe llevarse a cabo. El sistema, según esta acción, selecciona el componente de la aplicación que
se va a ejecutar según el filtro o los filtros de intención que expone.
Para ejecutarse cuando el sistema solicita una acción precisa, el componente de aplicación debe indicar al sistema
que es capaz de procesar esta acción: esto se hace con un filtro de intención.
Así, para gestionar los receptores de eventos, el parámetro IntentFilter del método registerReceiver
permite indicar qué acción provocará la ejecución del receptor de eventos.
3. Inscribir el receptor de eventos
En el marco de la comunicación entre el servicio LocDVDIntentService y la actividad MainActivity, hay que
definir una acción específica que invoca el servicio cuando ha terminado los procesamientos y asociar el receptor de
eventos para esta acción.
El servicio, tras el procesamiento, define una intención implícita, aquí llamada LocDVD.ServiceEnded:
La intención definida permite almacenar la información destinada al receptor de eventos:
Una vez definida la intención, el servicio puede producir el evento invocando el método sendBroadcast:
sendBroadcast(resultIntent);
La actividad
MainActivity se encarga de asociar el receptor de eventos con la acción
LocDVD.ServiceEnded:
Al final, la comunicación entre el servicio y la actividad, si se utiliza un receptor de eventos, se implementa de la
siguiente manera:
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
import android.app.IntentService;
import android.content.Intent;
public LocDVDIntentService() {
super("LocDVDIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
int waitDuration = intent.getIntExtra("waitDuration", 1000);
try {
Thread.sleep(waitDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
sendBroadcast(resultIntent);
}
}
La gestión de la comunicación para la clase MainActivity se realiza así:
intent.putExtra("waitDuration", 10000);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
IntentFilter intentFilter = new
IntentFilter("LocDVD.ServiceEnded");
registerReceiver(myBroadcastReceiver,intentFilter);
startService(intent);
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Presentación de Volley
En mayo de 2013, Google presentó la biblioteca Volley para simplificar los desarrollos que utilizaban las
comunicaciones de red. Esta biblioteca, que sustituye al cliente HTTP Apache incluido en la plataforma, simplifica
enormemente el desarrollo de las comunicaciones de red para las aplicaciones:
l Volley gestiona de manera nativa las peticiones asíncronas, evitando al desarrollador tener que implementar todo el
mecanismo asíncrono de consulta y de lectura.
l La biblioteca integra una pila para las llamadas de red y asegura su gestión.
l Integra, a su vez, un mecanismo de caché de datos, caché que puede estar directamente en memoria o que utiliza el
soporte de almacenamiento.
Por otro lado, Volley soporta de manera nativa varios tipos de respuesta: cadena de caracteres, imágenes y flujos
JSON.
1. La aplicación LocDVD
El objetivo de este capítulo es agregar a la aplicación una función de búsqueda de películas a partir de un título.
Para ello, la aplicación utiliza la base de datos de películas gratuita The Movie Database. En la siguiente dirección se
hace una presentación: https://www.themoviedb.org/.
Esta base de datos expone un servicio web que puede consultarse muy fácilmente. Los resultados se proveen en
formato XML o JSON, formato que se usará aquí.
Para utilizar la API, es preciso registrarse y crear una cuenta en el sitio. El conjunto es gratuito y simple; todas las
explicaciones se encuentran en el sitio web, en la siguiente dirección: https://developers.themoviedb.org/3/getting
started. El registro permite obtener una clave de API (API key), clave que hay que enviar con cada consulta realizada
sobre el servicio web.
2. Integrar la biblioteca Volley
Google no distribuye directamente el archivo .jar de la biblioteca: hay que integrar directamente el módulo en el
proyecto Android Studio.
Descargue el código fuente de la API en la siguiente dirección:
https://android.googlesource.com/platform/frameworks/volley
Esta dirección nos redirige a un repositorio Git: el lector que esté familiarizado con Git puede clonar
directamente el proyecto. Como alternativa, basta con seleccionar la rama principal (la última versión
estable del código) haciendo clic en master en el menú Branches, y hacer clic a continuación en el
enlace [tgz] para descargar las fuentes en formato tar.gz.
Descomprima a continuación el archivo en una nueva carpeta de su elección.
En Android Studio, haga clic con el botón derecho en la raíz del proyecto LocDVD. Seleccione la opción
Open Module Settings.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Se abre una ventana, llamada Project Structure, que nos permite agregar un nuevo módulo al proyecto.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Haga clic en el botón + (New Module), en la parte superior izquierda de la ventana. Se abre un asistente
que permite seleccionar el módulo que se va a importar.
Seleccione, en la lista, la opción Import Gradle Project y, a continuación, haga clic en el botón Next.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Seleccione a continuación la raíz de la carpeta descargada (o clonada) para la carpeta fuente en la
ventana del asistente.
El asistente completa automáticamente el nombre del módulo.
Haga clic en el botón Finish para lanzar la importación del módulo en el proyecto.
En la ventana Project Structure, seleccione el módulo app en la parte izquierda, y sitúese en la pestaña
Dependencies: el módulo volley_master debe definirse como dependencia del proyecto. Haga clic en el
botón + en la parte superior derecha de la ventana (botón Add).
Se muestra una lista de tres elementos en la ubicación del botón +.
Seleccione la opción Module dependency en la lista.
En la ventana popup que se abre, seleccione la opción Volley y haga clic en el botón OK.
Por último, haga clic en el botón OK de la ventana Project Structure para terminar el asistente. Android
Studio lanza una compilación: el módulo Volley está ahora integrado en la solución LocDVD.
Puede que se produzca un error Gradle tras la importación del módulo Volley: Gradle DSL method not found: has().
Este error se debe a la diferencia de versiones entre la versión de Gradle utilizada por Android Studio y la utilizada
por Google para distribuir la biblioteca Volley.
Si apareciera este error, basta con editar el archivo bintray.gradle y reemplazar la instrucción publish =
project.has("release") por la siguiente instrucción:
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
publish = project.hasProperty("release")
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Consultar un servicio web
La aplicación LocDVD debe, ahora, permitir al usuario realizar una consulta sobre la base de datos OMDb de películas.
Para ello, hay que agregar una opción en el menú del panel de navegación, y crear un nuevo fragmento que va a
permitir introducir la búsqueda y mostrar los resultados. También hay que tener en cuenta que la aplicación debe
conectarse a Internet.
1. Preparación
Edite el archivo strings.xml, que se encuentra en la carpeta /res/values.
Agregue un elemento a la lista string-array definida en el capítulo Navegación y ventanas
emergentes. Llame a este elemento «Buscar».
<string-array name="drawer_Items">
<item>Inicio</item>
<item>Nuevo DVD</item>
<item>Buscar</item>
</string-array>
Haciendo esto, aparece una entrada suplementaria en el panel de navegación.
Ahora hay que definir el archivo de layout del nuevo fragmento. En el explorador del proyecto, agregue un
archivo de layout: sitúese en la carpeta /res/layout, haga clic con el botón derecho y seleccione la
opción New y, a continuación, Layout resource file. Llame a este archivo fragment_search.xml, e
indique que el elemento raíz es un LinearLayout.
En primer lugar, hay que integrar el formulario de búsqueda: este se compone de un campo que permite
introducir texto y de un botón situado debajo. El identificador del campo de introducción de texto es, por
ejemplo, search_queryText, y el del botón search_queryLaunch. El código correspondiente al
layout es, de momento, el siguiente:
</LinearLayout>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Cabe destacar que la opción de búsqueda se ha agregado en un recurso de tipo String.
Una vez definido el archivo de layout, hay que agregar el código Java del fragmento.
En el explorador del proyecto, agregue una nueva clase Java, llamada SearchFragment. Esta clase
debe, como para los fragmentos implementados en el capítulo Los fragmentos, extender la clase
android.support.v4.app.Fragment.
package com.ejemplo.locdvd;
import android.support.v4.app.Fragment;
[...]
Como vimos en el capítulo Los fragmentos, hay que sobrecargar en primer lugar el método
onCreateView para asociar el fragmento con el archivo de layout. Este método debe definir también las
referencias sobre los componentes del layout: el campo de introducción de texto y el botón de búsqueda.
A continuación se muestra el código correspondiente:
EditText searchText;
Button searchButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
searchText =(EditText)view.findViewById(R.id.search_queryText);
searchButton=(Button)view.findViewById(R.id.search_queryLaunch);
return view;
}
Por último, para terminar la preparación de la pantalla de búsqueda, hay que cargar el fragmento
SearchFragment cuando se selecciona la opción correspondiente en el panel de navegación.
Edite el archivo MainActivity.java y sitúese a nivel de la gestión del clic sobre alguno de los
elementos de la lista del panel de navegación: el método setOnItemClickListener del objeto
listDrawer.
La opción Buscar está situada en tercer lugar, correspondiente al valor pos==2. Al hacer clic en el
elemento correspondiente, hay que instanciar un objeto de tipo SearchFragment y abrirlo invocando el
método openDetailFragment, creado en el capítulo Los fragmentos. A continuación se muestra el
código completo:
listDrawer.setOnItemClickListener(new
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
ListView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int
pos, long id) {
if (pos == 0) {
Intent intent =
new Intent(MainActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
if (pos == 1) {
AddDVDFragment addDVDFragment = new AddDVDFragment();
openDetailFragment(addDVDFragment);
}
if(pos==2) {
SearchFragment searchFragment = new SearchFragment();
openDetailFragment(searchFragment);
}
drawerLayout.closeDrawer(android.view.Gravity.START);
}
});
2. Solicitar permisos
Como se ha indicado en el capítulo Principios básicos de Android, las acciones que son susceptibles de exponer
datos personales del usuario deben indicarse de manera precisa por parte del desarrollador. Así, el sistema
presenta al usuario la lista de permisos requeridos, para que este último esté informado y pueda escoger entre
autorizar o no dichos permisos. Aquí, la biblioteca Volley requiere, como cualquier módulo que quiera conectarse a
Internet, que se autorice el permiso android.permission.INTERNET a la aplicación.
Según la versión de Android ejecutada por el dispositivo correspondiente, la gestión de los permisos es
sensiblemente diferente:
l Para aquellos dispositivos que funcionan con una versión anterior a la versión 6 (Marshmallow), basta con que el
desarrollador precise los permisos en el archivo Manifest. Estos permisos, así listados, se solicitarán durante la
instalación de la aplicación.
l Para los dispositivos que trabajan con Android Marshmallow o superior, la comprobación de los permisos se realiza
cuando el usuario utiliza alguna función de la aplicación que requiere estos permisos: el desarrollador debe
implementar este mecanismo.
En este capítulo, implementaremos el permiso INTERNET para los dispositivos equipados con una versión anterior a
Marshmallow; los dispositivos equipados con Android 6 o superior no necesitan declarar este permiso, como se
explica con más detalle a continuación. En el siguiente capítulo, Explotar el teléfono, se presenta una
implementación detallada de los permisos con Marshmallow.
a. Antes de Marshmallow
Las peticiones de permisos antes de Android Marshmallow son muy fáciles de implementar: basta con declarar
estos permisos en el archivo Manifest.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Para ello, hay que agregar una etiqueta <uses-permission> en el archivo AndroidManifest.xml.
Agregue, antes de la etiqueta <application>, una etiqueta <uses-permission>. La sintaxis
general de la etiqueta es la siguiente:
Aquí, el permiso se llama android.permission.INTERNET. El archivo AndroidManifest.xml debe ser el
siguiente:
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Cabe destacar que es obligatorio agregar esta etiqueta en el archivo Manifest incluso aunque la aplicación esté
dirigida únicamente a dispositivos que funcionan con Marshmallow.
b. A partir de Marshmallow
Desde Marshmallow, los permisos se clasifican en dos grupos: los permisos «normales» y los permisos llamados
«peligrosos» o «sensibles». Si bien los primeros se asignan automáticamente por el sistema, los segundos deben
disponer de un tratamiento específico.
Aquí, para las necesidades de la biblioteca Volley, solo se pide el permiso Internet: este permiso está clasificado
como «normal», de modo que no es necesario implementar ningún procesamiento. No ocurrirá lo mismo cuando
abordemos el envío de SMS en el capítulo Explotar el teléfono.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
3. Consulta del servicio web
La búsqueda se produce cuando el usuario hace clic en el botón searchButton: en el código del fragmento
SearchFragment, hay que asignar una instancia de OnClickListener al botón e invocar un nuevo método
launchSearch.
La asignación de la instancia de OnClickListener puede hacerse en el método onCreateView:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
searchText =(EditText)view.findViewById(R.id.search_queryText);
searchButton=(Button)view.findViewById(R.id.search_queryLaunch);
searchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchSearch();
}
});
return view;
}
El servicio web utilizado es muy fácil de consultar: para obtener información sobre una película a partir de su título,
basta con enviar una petición de tipo GET a la dirección del servicio web correspondiente
(https://api.themoviedb.org/3/search/movie), especificando como mínimo los siguientes parámetros:
l api_key: identificador personal obtenido durante la inscripción al servicio web.
l query: título de la película buscada.
Existen otros parámetros, opcionales, que también se exponen:
l language: código del idioma deseado para los resultados.
l page: número de página en los resultados (los resultados se devuelven de forma paginada).
Por lo tanto, la petición de información mínima es, en resumen, la siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
URLEncoder, que presenta el método estático encode.
El primer parámetro representa la cadena que se ha de codificar; el segundo, el nombre del juego de caracteres que
debe utilizarse para la codificación: UTF-8. Este método puede producir la excepción
UnsupportedEncodingException, de modo que es necesario encapsular el código en una cláusula try/catch.
La construcción de la petición tiene el siguiente aspecto:
try {
String api_key="cfa8a04xxxxxxxxxx59495";
String title = URLEncoder.encode(searchText.getText()
.toString(),"UTF-8" );
String url=
String.format("https://api.themoviedb.org/3
/search/movie?api_key=%s&query=%s&language=es-ES",
api_key,title);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Una consulta al servicio web con un simple navegador de Internet muestra que los datos devueltos tienen el
siguiente formato:
{
"page": página en curso,
"total_results": número de resultados,
"total_pages": número total de páginas,
"results": [{
"vote_count": número de votos para esta película,
"id": identificador en la base de datos,
"video": vídeo ?,
"vote_average": nota media,
"title": "título de la película",
"popularity": popularidad,
"poster_path": "ruta de acceso a la imagen de la película",
"original_language": "idioma original de la película",
"original_title": "título de la película en el idioma original",
"genre_ids": [géneros en los que está clasificada la película],
"adult": película para adultos ?,
"overview": "resumen de la película",
"release_date": "fecha de aparición"
},
{
"vote_count": número de votos para esta película,
"id": identificador en la base de datos,
"video": vídeo ?,
"vote_average": nota media,
"title": "título de la película",
"popularity": popularidad,
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
"poster_path": "ruta de acceso a la imagen de la película",
"original_language": "idioma original de la película",
"original_title": "título de la película en el idioma original",
"genre_ids": [géneros en los que está clasificada la película],
"adult": película para adultos ?,
"overview": "resumen de la película",
"release_date": "fecha de aparición"
},
...
]
}
El servicio web devuelve un objeto de tipo JSON; este objeto contiene una lista de películas.
Volley provee un conjunto de clases para realizar las peticiones que simplifica la recuperación de los datos de los
resultados tipándolos fuertemente. La siguiente tabla muestra, para cada tipo de datos de resultado, la clase
correspondiente:
Tipo de datos devuelto Nombre de la clase
String StringRequest
Imagen ImageRequest
Objeto en formato json JsonObjectRequest
Tabla en formato json JsonArrayRequest
Todas estas clases forman parte del paquete com.android.volley.toolbox.
Los constructores de estas clases están construidos sobre el mismo esquema. Por ejemplo, para la clase
JsonObjectRequest:
l int method: indica el método utilizado, entre las constantes definidas en com.android.volley.Method
(por ejemplo, com.android.volley.Method.GET).
l String url: URL del servicio web que se ha de consultar.
l JSONObject jsonRequest: petición JSON que debe devolverse si es necesaria.
l Listener<JSONObject> listener: clase que se encarga de tratar la respuesta del servicio web.
l ErrorListener errorListener: clase que se encarga de tratar la respuesta en caso de error.
Para implementar la llamada al servicio web, hay que definir en primer lugar una instancia de la clase
com.android.volley.Response.Listener<JSONObject> para tratar la respuesta y una instancia de
com.android.volley.Response.ErrorListener para gestionar los eventuales errores.
Defina una variable de tipo Response.Listener<JSONObject>, llamada jsonRequestListener.
Debe implementarse el método onResponse, la clase Response.Listener<JSONObject> es
abstracta.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
private Response.Listener<JSONObject> jsonRequestListener =
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
}
};
Defina también una variable de tipo ErrorListener. Debe implementarse el método
onErrorResponse:
}
};
Una vez definidas estas variables, es posible instanciar un objeto de tipo JsonObjectRequest:
Para que el sistema ejecute la consulta, hay que agregarla a una pila de tipo
com.android.volley.RequestQueue. Así, la biblioteca Volley optimiza las llamadas a los servicios web: si el
resultado de la consulta está en caché, se devuelve directamente como respuesta. En caso contrario, la consulta se
sitúa en una caché de consultas de red y se procesará cuando exista algún thread disponible para tratarla.
Es posible agregar una consulta a la pila invocando el método add del objeto de tipo RequestQueue.
Volley propone dos soluciones para obtener una instancia de RequestQueue.
Para los casos clásicos, que no requieren una gestión fina del orden de ejecución de las consultas, es posible
invocar el método estático Volley.newRequestQueue:
La clase Volley forma parte del paquete com.android.volley.toolbox.
Si el proyecto exige un procesamiento específico de la pila de consultas, o bien si la gestión de la caché por defecto
no se corresponde con los requisitos, es posible definir un objeto RequestQueue específico invocando alguno de
los constructores de la clase RequestQueue. A continuación se explica el constructor más completo:
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
threadPoolSize, ResponseDelivery delivery)
l Cache cache: instancia que implementa la interfaz com.android.volley.Cache, que se encarga de la
gestión de la caché de peticiones.
l Network network: instancia que implementa la interfaz com.android.volley, que se encarga de las
peticiones HTTP.
l int threadPoolSize: valor entero que especifica el tamaño del pool de peticiones http que el sistema puede
utilizar.
En el marco del proyecto LocDVD, las consultas son bastante simples y no requieren ningún procesamiento
particular para los datos en caché (los resultados, por ejemplo, no varían con el tiempo), de modo que no es
necesario recurrir a un RequestQueue específico.
Cabe destacar que, dado que se provee el código fuente de la biblioteca Volley, se recomienda inspirarse en el
método estático newRequestQueue y en los objetos utilizados para crear una instancia propia de
RequestQueue.
El código que permite gestionar las consultas al servicio web es, de momento, el siguiente:
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
private Response.ErrorListener errorListener =
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// ¡Completar!
}
};
Existe, sin embargo, un problema con esta implementación: en efecto, si el usuario realiza varias búsquedas, cada
búsqueda va a instanciar una nueva RequestQueue, lo cual no resulta óptimo. Es preferible declarar una variable
requestQueue e invocar un método de acceso para obtener una referencia sobre esta variable.
El código modificado es el siguiente (los objetos responseListener y errorListener no cambian, no se
repiten en el siguiente código):
RequestQueue requestQueue;
RequestQueue getRequestQueue() {
if(requestQueue==null)
requestQueue = Volley.newRequestQueue(getActivity());
return requestQueue;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Trabajar con el formato JSON
El objeto de tipo Response.Listener<JSONObject> debe procesar la respuesta del servidor a la consulta
realizada. Los resultados de la búsqueda se presentan, habitualmente, en forma de lista. De modo que hay que leer
e interpretar la respuesta en formato JSON, almacenar los datos de la película en una clase específica y, por último,
definir todos los elementos necesarios para utilizar un componente ListView, como vimos en el capítulo Controles
avanzados.
1. Interpretación del formato JSON
El objeto JSON devuelto por el servicio web contiene un conjunto de propiedades, en particular el conjunto de
películas que satisfacen la consulta, lo que nos interesa principalmente aquí. Esta propiedad, results, incluye un
array de objetos JSON que contienen los datos de cada película.
La primera etapa en la interpretación de la respuesta consiste en recorrer los datos para extraer cada película.
La clase JSONObject expone un conjunto de métodos que permiten extraer datos tipados. Las propiedades de un
objeto JSON se almacenan con el formato (nombre, valor), cada uno de estos métodos recibe como parámetro el
nombre de la propiedad que se ha de extraer. Estos métodos son susceptibles de producir una excepción, de modo
que deben encapsularse en una etiqueta try/catch.
Para extraer un array de objetos JSON, hay que invocar el método getJSONArray del objeto JSONObject.
A continuación hay que recorrer cada elemento del array recuperado con ayuda de un bucle for.
Cada objeto JSON del array se obtiene invocando el método getJSONObject del objeto JSONArray.
Cada elemento del array contiene información relativa a la película, que hay que extraer. Los datos se proporcionan
todos en formato String, de modo que su extracción es muy sencilla:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
String movieId = jsonObject.getString("id");
String overview = jsonObject.getString("overview");
}
Al final, el código para instanciar el Response.Listener es, de momento, el siguiente:
¡Observe que el formato JSON es sensible a las letras mayúsculas! De modo que hay que tener la precaución de
respetar exactamente el nombre de las propiedades.
2. Creación de la lista
Como hemos visto antes, para mostrar los datos extraídos en un componente ListView, hay que definir un
adaptador de datos y asociarlo a la lista.
Para ello, hay que definir una estructura que contenga los datos de las películas.
Defina una clase interna estática Movie, que contenga las propiedades title, releaseDate,
movieId y overview.
[...]
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
[...]
La extracción de los datos en el bucle for creado en la sección anterior, en lugar de instanciar datos de tipo
String, instancia y asigna valor a un objeto de tipo Movie y lo almacena en una estructura de tipo
ArrayList<Movie>:
Antes de definir el adaptador, hay que prever un archivo de layout llamado, por ejemplo, listitem_movie.xml,
para los elementos de la lista. Este debe presentar al usuario el título de la película en dos líneas, el año de
aparición y un botón Más detalles que permita al usuario obtener información adicional acerca de la película
seleccionada.
Una versión del archivo de layout sería, por ejemplo, la siguiente:
<TextView
android:id="@+id/movie_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:lines="2"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp">
<TextView
android:id="@+id/movie_releaseDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
android:layout_alignParentLeft="true"
android:lines="2"/>
<Button
android:id="@+id/movie_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="@string/detalle"/>
</RelativeLayout>
</LinearLayout>
La opción detalle se ha agregado, además, en el archivo de definiciones strings.xml.
También hay que agregar un componente ListView al layout del fragmento SearchFragment.
La organización de la página es, aquí, más completa: la lista debe utilizar todo el espacio disponible en la pantalla y
posicionarse debajo del botón Buscar.
Si bien existen varias soluciones que permiten implementar este tipo de presentación, la más eficaz utiliza un truco
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
relativo a la propiedad layout_weight de los componentes. La propiedad layout_weight permite definir, en
efecto, la proporción de la pantalla que va a ocupar un componente respecto a los demás. Pero, si un único
componente de la pantalla utiliza esta propiedad, ocupará el total del espacio disponible en la dimensión cuyo valor
es cero.
El archivo de layout del fragmento puede definirse de la siguiente manera:
<EditText
android:id="@+id/search_queryText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"/>
<Button
android:id="@+id/search_queryLaunch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/buscar"/>
<ListView
android:id="@+id/search_List"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
El vínculo entre el componente ListView y el código del fragmento se hace, como con los demás componentes, en
el método onCreateView del fragmento SearchFragment:
[...]
ListView searchList;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
[...]
searchList = (ListView)view.findViewById(R.id.search_List);
[...]
}
Es posible definir el adaptador como clase interna al fragmento. El método sobrecargado getView debe utilizar el
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
layout listitem_movie definido más arriba; el código del adaptador sigue el mismo esquema que vimos en el
capítulo Controles avanzados, en la sección Las listas.
A continuación se muestra la declaración del adaptador:
Context context;
public SearchListAdapter(Context context, List< Movie > movies) {
super(context, R.layout.listitem_movie, movies);
this.context = context;
}
@Override
public View getView(int pos, View convertView, ViewGroup parent)
{
View view=null;
if(convertView==null) {
LayoutInflater layoutInflater =
(LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_movie, null);
} else {
view = convertView;
}
titulo.setText(movie.title);
fechaAparicion.setText(movie.releaseDate);
return view;
}
Para terminar con la gestión de resultados, tan solo queda por vincular el componente ListView con una instancia
del adaptador que acabamos de definir.
Este vínculo se establece cuando se rellena la lista de películas, asignando una instancia de SearchListAdapter
a la ListView mediante el método setAdapter del objeto ListView:
SearchListAdapter searchListAdapter =
new SearchListAdapter(getActivity(),listOMdbFilm);
searchList.setAdapter(searchListAdapter);
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El código del fragmento SearchFragment es, de momento, el siguiente:
package com.ejemplo.locdvd;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
EditText searchText;
Button searchButton;
ListView searchList;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
View view = inflater.inflate(R.layout.fragment_search, null);
searchText =(EditText)view.findViewById(R.id.search_queryText);
searchButton=(Button)view.findViewById(R.id.search_queryLaunch);
searchList = (ListView)view.findViewById(R.id.search_List);
searchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchSearch();
}
});
return view;
}
RequestQueue requestQueue;
RequestQueue getRequestQueue() {
if(requestQueue==null)
requestQueue = Volley.newRequestQueue(getActivity());
return requestQueue;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
jsonArray.getJSONObject(i);
Movie movie = new Movie();
movie.title = jsonObject.getString("title");
movie.releaseDate =
jsonObject.getString("release_date");
movie.movieId = jsonObject.getString("id");
movie.overview =
jsonObject.getString("overview");
listOfMovies.add(movie);
}
SearchListAdapter searchListAdapter =
new
SearchListAdapter(getActivity(),listOfMovies);
searchList.setAdapter(searchListAdapter);
} catch (JSONException e) {
Log.e("JSON",e.getLocalizedMessage());
}
}
};
Context context;
public SearchListAdapter(Context context, List< Movie > movies) {
super(context, R.layout.listitem_movie, movies);
this.context = context;
}
@Override
public View getView(int pos, View convertView, ViewGroup parent) {
View view=null;
if(convertView==null) {
LayoutInflater layoutInflater =
(LayoutInflater)context.getSystemService(Context.
LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_movie,
null);
} else {
view = convertView;
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
view.setTag(movie);
TextView titulo =
(TextView)view.findViewById(R.id.movie_title);
TextView fechaAparicion =
(TextView)view.findViewById(R.id.movie_releaseDate);
Button detailButton=
(Button)view.findViewById(R.id.movie_detail);
titulo.setText(movie.title);
fechaAparicion.setText(movie.releaseDate);
return view;
}
}
}
3. Vista de detalle
La vista de detalle debe permitir al usuario visualizar información complementaria acerca de la película seleccionada.
The Movie DataBase provee un segundo servicio web que, a partir del identificador de una película, permite obtener
información detallada acerca de la película: lista de actores principales, resumen de la película, URL del cartel, etc.
En el marco del proyecto LocDVD, la vista detallada debe presentar el cartel de la película así como el resumen. Esta
información, en lugar de presentarse en una pantalla separada, debe mostrarse debajo de la información principal.
La siguiente captura de pantalla muestra la visualización deseada:
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El layout de elementos de la lista debe modificarse: hay que agregar un componente para visualizar el cartel, un
componente para presentar el resumen y un botón (no visible en la captura de pantalla anterior) para cerrar la vista
de detalle.
Estos componentes solo deben visualizarse si se pide la vista detallada; en caso contrario, los componentes no
deben presentarse al usuario.
Edite el archivo de layout listitem_movie.xml.
Para gestionar fácilmente la visibilidad de la vista de detalle, basta con encapsular los componentes de esta vista en
un contenedor de vista, de tipo LinearLayout o RelativeLayout, y asignar valor a la propiedad
visibility de este contenedor.
Defina, tras los componentes ya presentes, un contenedor de vista de tipo RelativeLayout. Su ancho
se corresponde con el de la pantalla, su altura estará restringida a su contenido. Estará, sin embargo, no
visible por defecto. El contenedor, que se manipulará desde el código, debe tener un identificador único,
por ejemplo movie_detailLayout:
<RelativeLayout
android:id ="@+id/movie_detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
</RelativeLayout>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
El componente nativo de Android ImageView, que permite mostrar imágenes, no puede utilizarse aquí: no
gestiona correctamente la descarga de una imagen a partir de una URL. La biblioteca Volley provee un componente
específico para esto, llamado NetworkImageView. Pertenece al paquete com.android.volley.toolbox.
Aquí hay que utilizar este componente. Como con los componentes definidos por el desarrollador (consulte el
capítulo Controles avanzados, sección Crear nuestro propio componente reutilizable), hay que registrar el nombre
completo del componente (incluyendo el paquete) en el archivo de layout.
El contenedor de vista RelativeLayout sitúa sus componentes hijos según un posicionamiento relativo: hay que
indicar que la imagen está posicionada a la izquierda.
El componente NetworkImageView se manipula asimismo en el código, de modo que necesita también un
identificador único, por ejemplo movie_poster.
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/movie_poster"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_margin="8dp"/>
A la derecha de la visualización de la película, un componente TextView debe presentar el resumen de esta. Su
identificador único es movie_plot:
<TextView
android:id="@+id/movie_plot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/movie_poster"
android:layout_margin="8dp"
android:maxLines="20"/>
Por último, el botón que permite cerrar la vista detallada está centrado horizontalmente y se posiciona verticalmente
debajo del texto. Su identificador es movie_closeDetail.
<Button
android:id="@+id/movie_closeDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@+id/movie_plot"
android:text="@string/cerrar"/>
El layout de los elementos de la lista se define, finalmente, así:
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/movie_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:lines="2"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp">
<TextView
android:id="@+id/movie_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:lines="2"/>
<Button
android:id="@+id/movie_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="@string/detalle"/>
</RelativeLayout>
<RelativeLayout
android:id ="@+id/movie_detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/movie_poster"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/movie_plot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/movie_poster"
android:layout_margin="8dp"
android:maxLines="20"/>
<Button
android:id="@+id/movie_closeDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@+id/movie_plot"
android:text="@string/cerrar"/>
</RelativeLayout>
</LinearLayout>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
Al hacer clic en el botón Más detalles, se va a producir la visualización de la vista detallada: hay que asociar, por
tanto, un objeto OnClickListener a este botón, en el código del método getView del adaptador.
Edite el archivo SearchFragment.java y sitúese a nivel del método getView.
Asocie un nuevo objeto OnClickListener al botón, invocando el método setOnClickListener:
detailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
El método getView manipula los componentes de la vista detallada: de modo que hay que obtener las referencias
sobre estos componentes.
Agregue, tras las referencias a los componentes ya definidos, las referencias a los componentes
RelativeLayout, NetworkImageView, TextView y Button:
[...]
TextView titulo =
(TextView)view.findViewById(R.id.movie_title);
TextView fechaAparicion =
(TextView)view.findViewById(R.id.movie_releaseDate);
Button detailButton=
(Button)view.findViewById(R.id.movie_detail);
Button closeButton=
(Button)view.findViewById(R.id.movie_closeDetail);
RelativeLayout detailLayout =
(RelativeLayout)view.findViewById(R.id.movie_detailLayout);
NetworkImageView detailPoster =
(NetworkImageView)view.findViewById(R.id.movie_poster);
TextView detailPlot =
(TextView)view.findViewById(R.id.movie_plot);
[...]
Al hacer clic en el botón Más detalles, se debe mostrar el contenedor RelativeLayout. Para ello, hay que
invocar el método setVisibility de la clase View. Los posibles valores son View.VISIBLE (el componente
es visible), View.INVISIBLE (el componente no es visible pero su ubicación está prevista) y View.GONE (el
componente no es visible y su ubicación no es visible en la pantalla). Aquí, hay que hacer que el componente sea
visible, habiéndose definido como «View.GONE» en su declaración en el layout.
De manera inversa, hay que indicar que el botón Más detalles debe estar oculto (visibilidad View.GONE).
En el método onClick del OnClickListener de detailButton, agregue el código para cambiar la
visibilidad del contenedor RelativeLayout y del botón:
detailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
detailLayout.setVisibility(View.VISIBLE);
detailButton.setVisibility(View.GONE);
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
}
});
El compilador de Android Studio indica varios errores: en efecto, las instancias de detailLayout y de
detailButton están referenciadas en una clase anidada, de modo que deben definirse como final en su
declaración:
closeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
detailLayout.setVisibility(View.GONE);
detailButton.setVisibility(View.VISIBLE);
}
});
Además de cambiar la visibilidad, al hacer clic en el botón Más detalles debe lanzarse la consulta al servicio web,
para obtener la información detallada sobre la película seleccionada por el usuario.
La consulta del servicio web es parecida a la del servicio web de búsqueda, con la diferencia de que, en vez de
definir formalmente las instancias de Response.Listener<JSONObject>, es preferible utilizar instancias de
clases anónimas: estas se definen directamente en el OnClickListener del botón Más detalles, y pueden
manipular los objetos definidos en el método getView, siempre y cuando se declaren como final.
Sitúese en el método OnClick del botón detailButton.
La URL consultada se define en una variable de tipo
String. La dirección del servicio web es
https://api.themoviedb.org/3/movie/%s?api_key=[API_KEY]&language=es-ES, %s debe
reemplazarse por el identificador de la película.
String url =
String.format("https://api.themoviedb.org/3/movie/
%s?api_key=[API_KEY]&language=es-ES", movie.movieId);
Instancie un nuevo objeto JsonObjetRequest. El método HTTP es, como para la consulta anterior,
Request.Method.GET. Los objetos Response.Listener<JSONObject> y
Response.ErrorListener se definen de manera anónima:
JsonObjectRequest jsonObjectRequest;
jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, url, null,
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 15 -
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("DETAIL", error.getLocalizedMessage());
}
}
);
A continuación debe agregarse la consulta a la pila de llamadas, invocando el método add del objeto
RequestQueue:
getRequestQueue().add(jsonObjectRequest);
El método onResponse del objeto Response.Listener<JSONObject> debe extraer de la respuesta JSON el
texto de presentación así como la URL de visualización. Una consulta al servicio web con un simple navegador nos
muestra el formato de respuesta del servicio web (aquí solo se muestran las propiedades que nos interesan):
{
"title": "[título de la película]",
"release_date": "[año de la película]",
[...]
"overview": "[resumen de la película]",
[...]
"poster_path": "[url del cartel]",
[...]
}
El servicio web no devuelve directamente la URL de la imagen de la película, sino solo el nombre del archivo. La URL
debe construirse a partir de esta información, según las indicaciones de la API:
l La URL de base es https://image.tmdb.org/t/p/
l A continuación, hay que agregar un parámetro para el formato de la imagen. Aquí, se utilizará el parámetro w500
(hay disponibles otros formatos: w92,w154,w342,w500,w780).
l Por último, agregar el nombre del archivo.
Instancie dos variables de tipo String y asígneles valor invocando el método getString del objeto
response pasado como parámetro de onResponse:
- 16 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
find TextView detailPlot=(TextView)
view.findViewById(R.id.movie_plot);
[...]
public void onResponse(JSONObject response) {
try {
String posterPath = response.getString("poster_path");
String plot = response.getString("overview");
detailPlot.setText(plot);
} catch (JSONException e) {
Log.e("JSON", e.getLocalizedMessage());
}
}
La URL de la imagen de la película también debe definirse según las reglas establecidas antes:
Además de la URL, hay que proveer una instancia de ImageLoader: esta clase se define por Volley y su principal
rol es gestionar la caché de las imágenes descargadas.
Como con la instancia de RequestQueue, es preferible definir una variable global para la instancia de
ImageLoader y proveer un método de acceso: así, si el usuario pide visualizar varios carteles de películas, se
utilizará la misma instancia de ImageLoader.
ImageLoader imageLoader;
ImageLoader getImageLoader() {
if(imageLoader==null) {
// Completar
}
return imageLoader;
}
Hay que instanciar la clase ImageLoader. El constructor de la clase presenta la siguiente firma:
Si bien el primer parámetro, de tipo RequestQueue, es fácil de proveer, el segundo es más complejo:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 17 -
ImageCache es una interfaz definida por la biblioteca Volley, cuya implementación debe tener en cuenta la gestión
de la caché de imágenes. La definición de ImageCache es la siguiente:
Google recomienda, para la implementación de la caché, utilizar la clase Lru<key,Value>. Esta clase gestiona, en
efecto, una caché, cuyos datos en caché están accesibles mediante una clave.
Defina, en getImageLoader, una instancia de ImageCache:
ImageLoader getImageLoader() {
if(imageLoader==null) {
ImageLoader.ImageCache imageCache = new
ImageLoader.ImageCache() {
};
}
return imageLoader;
}
Implemente los dos métodos, putBitmap y getBitmap:
En la clase anónima definida, defina e instancie un objeto de tipo LruCache<String, Bitmap>: el
primer genérico especifica el tipo de dato de las claves, el segundo genérico indica el tipo de dato que se
pone en caché. El constructor de LruCache recibe un parámetro de tipo int que indica el tamaño
máximo. Aquí, basta con un tamaño igual a 10.
El método putBitmap debe almacenar en la caché la imagen que se pasa como parámetro. Para ello,
- 18 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
hay que invocar el método put de la clase Lru<K,V>:
cache.put(url, bitmap);
De manera inversa, el método getBitmap debe devolver la imagen en caché en función de la clave que se pasa
como parámetro:
return cache.get(url);
Una vez definida la instancia de ImageCache, es posible instanciar la clase ImageLoader:
La implementación completa del método getImageLoader es la siguiente:
ImageLoader imageLoader;
ImageLoader getImageLoader() {
if(imageLoader==null) {
ImageLoader.ImageCache imageCache = new
ImageLoader.ImageCache() {
imageLoader = new
ImageLoader(getRequestQueue(),imageCache);
}
return imageLoader;
}
Ahora es posible finalizar la presentación del cartel de la película.
Sitúese en el cuerpo del método onResponse del objeto Response.Listener<JSONObject>.
Agregue la llamada al método setImageUrl del objeto NetworkImageView:
detailPoster.setImageUrl(url, getImageLoader());
El código del método onClick del botón detailButton es, al final, el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 19 -
@Override
public void onClick(View v) {
detailLayout.setVisibility(View.VISIBLE);
detailButton.setVisibility(View.GONE);
String url =
String.format("https://api.themoviedb.org/3/movie/
%s?api_key=[API_KEY]&language=es-ES", movie.movieId);
JsonObjectRequest jsonObjectRequest;
jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
String posterPath =
response.getString("poster_path");
String plot = response.getString("overview");
detailPlot.setText(plot);
String url =
"https://image.tmdb.org/t/p/w500/" + posterPath;
detailPoster.setImageUrl(url, getImageLoader());
} catch (JSONException e) {
Log.e("JSON", e.getLocalizedMessage());
}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("DETAIL", error.getLocalizedMessage());
}
}
);
getRequestQueue().add(jsonObjectRequest);
}
Para terminar la vista detallada, tan solo queda un aspecto menor por resolver: con el objetivo de optimizar la
gestión de la memoria, el componente ListView recicla las vistas utilizadas para mostrar los elementos de la lista.
En este caso, la visibilidad de varios componentes se modifica con los clics en los botones detailbutton y
closeButton. De modo que, cada vez que se muestra un elemento de la lista, hay que reinicializar la visibilidad
de los componentes eventualmente modificados: así, incluso si la vista es una vista reciclada, el estado inicial de los
componentes se restaura.
Tras la definición de los componentes detailButton y detailLayout, inicialice la visibilidad de los
componentes: detailButton debe estar visible y detailLayout debe tener una visibilidad
View.GONE.
- 20 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
(Button)view.findViewById(R.id.listItemOMdbFilm_detail);
Al final, el código del adaptador de la lista que presenta los resultados de la búsqueda es el siguiente:
Context context;
public SearchListAdapter(Context context, List< Movie > movies) {
super(context, R.layout.listitem_movie, movies);
this.context = context;
}
@Override
public View getView(int pos, View convertView, ViewGroup parent) {
View view=null;
if(convertView==null) {
LayoutInflater layoutInflater =
(LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(R.layout.listitem_movie, null);
} else {
view = convertView;
}
TextView titulo =
(TextView)view.findViewById(R.id.movie_title);
TextView fechaAparicion =
(TextView)view.findViewById(R.id.movie_releaseDate);
final Button detailButton=
(Button)view.findViewById(R.id.movie_detail);
Button closeButton=
(Button)view.findViewById(R.id.movie_closeDetail);
final RelativeLayout detailLayout =
(RelativeLayout)view.findViewById(R.id.movie_detailLayout);
final NetworkImageView detailPoster =
(NetworkImageView)view.findViewById(R.id.movie_poster);
final TextView detailPlot =
(TextView)view.findViewById(R.id.movie_plot);
detailButton.setVisibility(View.VISIBLE);
detailLayout.setVisibility(View.GONE);
titulo.setText(movie.title);
fechaAparicion.setText(movie.releaseDate);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 21 -
detailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
detailLayout.setVisibility(View.VISIBLE);
detailButton.setVisibility(View.GONE);
JsonObjectRequest jsonObjectRequest;
jsonObjectRequest =
new JsonObjectRequest(Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
String posterPath = response.getString("poster_path");
String plot = response.getString("overview");
detailPlot.setText(plot);
} catch (JSONException e) {
Log.e("JSON", e.getLocalizedMessage());
}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("DETAIL", error.getLocalizedMessage());
}
}
);
getRequestQueue().add(jsonObjectRequest);
}
});
closeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
detailLayout.setVisibility(View.GONE);
detailButton.setVisibility(View.VISIBLE);
}
});
return view;
}
- 22 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
4. Posibles optimizaciones
La biblioteca Volley procesa las consultas de red en un thread separado, lo que permite a la aplicación LocDVD
presentar un rendimiento ideal. Sin embargo, la etapa del procesamiento de la respuesta JSON se ejecuta
directamente en el thread principal. Si bien aquí las operaciones realizadas en esta etapa son sencillas, el
procesamiento de una gran cantidad de resultados puede afectar sensiblemente a la fluidez de la interfaz.
En un entorno de producción, sería imprescindible optimizar la estructura del programa a este nivel: para ello, hay
que implementar un procesamiento como tarea de fondo, como vimos en el capítulo Tareas asíncronas y servicios.
Los cambios que deben realizarse en el código son mínimos: hay que instanciar una nueva tarea asíncrona en el
cuerpo del método onResponse del objeto Response.Listener<JSONObject>:
l El método onPreExecute se utiliza para inicializar los datos para el procesamiento como tarea de fondo.
l El método doInBackground, que se ejecuta como tarea de fondo, integra el código que permite crear la lista de
objetos que se muestran en la ListView.
l El método onPostExecute permite, por su parte, vincular los datos tratados a la lista mediante el adaptador.
@Override
public void onResponse(final JSONObject response) {
@Override
protected void onPreExecute() {
[...]
}
@Override
protected ArrayList<OMdbFilm> doInBackground(Void...
params) {
[...]
}
@Override
protected void onPostExecute(ArrayList<OMdbFilm> result) {
[...]
}
}.execute((Void)null);
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 23 -
Integrar un navegador
Si bien el servicio web utilizado por la aplicación LocDVD devuelve los datos en formato JSON, sería posible consultar
un servicio web que devuelva directamente la información en formato HTML.
La plataforma Android proporciona varias soluciones para presentar al usuario datos en formato HTML.
La primera solución consiste en utilizar un componente TextView, cuando los datos que se han de mostrar están
parcialmente formateados en HTML. El componente TextView es capaz, efectivamente, de interpretar algunas
etiquetas HTML, tales como las etiquetas básicas de formato de texto texto en cursiva, en negrita, subrayado, color
del texto y del fondo, así como las etiquetas de representación en la página salto de línea, principalmente.
Un componente TextView no interpreta directamente el código HTML. En su lugar, hay que transformar de forma
explícita el texto HTML en un objeto de tipo Spanned mediante el método estático Html.fromHtml:
El texto transformado se asigna, a continuación, al componente TextView invocando el método setText.
TextView textView;
[...]
textView.setText(Html.fromHtml("Hola a <b>todos</b>"));
Si bien esta solución es adecuada para fragmentos de código HTML simples, no funciona si el código HTML que se
quiere mostrar es más complejo (etiquetas de formato complejas, por ejemplo). En este caso, hay que utilizar un
componente WebView, que permite mostrar una página HTML completa.
Por defecto, el componente WebView utiliza el motor de visualización WebKit para las versiones de Android anteriores
a la 4.4. Desde la versión 4.4, se utiliza el motor Chromium: este último soporta HTML5.
Como cualquier otro componente, la vista WebView puede declararse en un archivo de layout o instanciarse
directamente en el código Java de una actividad o de un fragmento. Alternativamente, también es posible omitir el
archivo de layout instanciando un componente WebView y asignándole esta instancia a la actividad mediante el
método setContentView:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(webView);
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
}
La carga de una URL se hace invocando el método loadUrl del objeto WebView:
webView.loadUrl("http://www.ediciones-eni.com");
Hay que destacar que, por defecto, el componente WebView no interpreta el código JavaScript que pueda exponer la
página HTML. Para indicar al componente que debe interpretar el código JavaScript, hay que modificar los parámetros
del webView. Para ello, hay que obtener una referencia sobre estos parámetros, almacenados en una clase
WebSettings, invocando el método getSettings del componente WebView. Para activar el código JavaScript,
hay que invocar a continuación el método setJavascriptEnabled del objeto WebSettings. Por ejemplo:
webView.getSettings().setJavaScriptEnabled(true);
Una última solución para gestionar una visualización en HTML es, en vez de integrar un componente WebView,
invocar el navegador por defecto del terminal: en este caso, la aplicación se pone en pausa y se muestra el
navegador en primer plano.
Este método, recomendado por Google, es fácil de implementar: basta con utilizar un objeto de tipo Intent,
definiendo la acción Intent.ACTION_VIEW, y pasándole también al constructor la URI correspondiente a la
dirección de la página que se ha de mostrar. El método startActivity permite, a continuación, pedir al sistema
que arranque el navegador del terminal:
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Enviar/recibir SMS
Android permite el acceso a la mayoría de las funciones del terminal. En particular, para los smartphones, es posible
explotar la capacidad del teléfono para enviar y recibir SMS. Dejando de lado, de momento, la aplicación LocDVD, esta
sección presenta en primer lugar el envío de SMS y, a continuación, se centra en la recepción, más compleja de
implementar, para abordar por último el funcionamiento de los sensores presentes en nuestros dispositivos Android.
1. Enviar un SMS
Cualquier terminal equipado con una tarjeta SIM GSM puede enviar SMS, siempre que la conexión GSM esté activa.
Algunos terminales Android no están equipados para la comunicación GSM, de modo que es necesario, antes de
invocar a las clases utilizadas para la gestión de SMS, comprobar la capacidad del terminal. Dependiendo de la
situación, se plantean dos escenarios posibles: o bien la función de envío/recepción de SMS es imprescindible para
la aplicación, o bien es una característica suplementaria cuya ausencia no supone una restricción.
Si la capacidad para enviar/recibir SMS es imprescindible para el funcionamiento de la aplicación, es preferible no
presentar esta aplicación en Play Store para aquellos dispositivos no compatibles. Para ello, hay que agregar la
etiqueta uses-feature en el archivo AndroidManifest.xml de la aplicación:
l El parámetro name permite indicar qué funcionalidad debe estar presente en el terminal. Para enviar/recibir SMS, la
funcionalidad es android.hardware.telephony.
l El parámetro required indica si la funcionalidad es imprescindible o no. En efecto, Play Store también puede
deducir los uses-feature a partir de los permisos solicitados en el archivo AndroidManifest. Por ejemplo,
si el archivo AndroidManifest expone el permiso android.permission.SEND_SMS, Play Store deduce
que la funcionalidad android.hardware.telephony es necesaria para el correcto funcionamiento de la
aplicación. Indicando que la funcionalidad correspondiente no es obligatoria (required=false), la aplicación se
propondrá a aquellos terminales no equipados.
Así, la primera etapa para desarrollar las funcionalidades SMS en una aplicación consiste en registrar estas dos
etiquetas en el archivo Manifest:
l La primera etiqueta, uses-permission, necesita el permiso android.permission.SEND_SMS.
l La segunda, uses-feature, indica si la función «teléfono» es imprescindible para la aplicación.
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-feature android:name="android.hardware.telephony"
android:required="false"/>
Ambas etiquetas son hijas de la etiqueta raíz <Manifest>.
También es posible comprobar la capacidad de enviar SMS en tiempo de ejecución. Para ello, hay que invocar al
método hasSystemFeature de la clase PackageManager:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
El parámetro String name indica qué capacidad del terminal se comprueba. Para el envío/recepción de SMS, esta
capacidad es PackageManager.FEATURE_TELEPHONY.
Para obtener una instancia de la clase PackageManager, hay que invocar el método getPackageManager del
objeto Context. Por ejemplo, en el código de una actividad:
Cabe destacar que esta comprobación no es fiable en los emuladores, que pueden indicar que no son capaces de
enviar SMS cuando en realidad sí pueden o al menos lo simulan, como veremos más adelante.
a. Gestión de permisos
Como hemos indicado en el capítulo Redes e Internet, la gestión de los permisos se ha visto profundamente
modificada con la aparición de Android Marshmallow. En efecto, desde Android 6, los permisos se clasifican en dos
grupos: los permisos llamados normales, y aquellos definidos como peligrosos. Si bien los primeros los concede
automáticamente el sistema, los permisos peligrosos deben solicitarse explícitamente al usuario: el envío y la
recepción de SMS forman parte de este último grupo.
La primera etapa consiste en comprobar si ya se ha concedido el permiso (durante algún uso previo de la
aplicación). La plataforma provee para ello el método estático checkSelfPermission de la clase
ContextCompat. Este método recibe como parámetro la actividad en curso y la cadena de caracteres que
describe el permiso, y devuelve PackageManager.PERMISSION_GRANTED o
PackageManager.PERMISSION_DENIED en función de si el permiso se concede o no.
[...]
if(ContextCompat.checkSelfPermission(this, Manifest.permission.
SEND_SMS)== PackageManager.PERMISSION_GRANTED) {
// el permiso envío SMS ya está concedido
} else {
// el permiso envío SMS no se ha concedido
}
}
}
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Cabe destacar que la clase estática Manifest.permission contiene un conjunto de constantes que definen
los permisos de la plataforma.
Si el permiso no está concedido (checkSelfPermission devuelve PERMISSION_DENIED), hay que
solicitárselo al usuario. Sin embargo, antes de solicitar el permiso, Google recomienda determinar si debe
proporcionarse una explicación suplementaria o no acerca del permiso solicitado. Para ello, la plataforma expone el
método shouldShowRequestPermissionRationale, que devuelve true si debe darse alguna explicación,
es decir, si el usuario ya ha respondido negativamente a la solicitud del permiso. Cabe destacar que, si no se ha
realizado la petición al usuario, o si este ya ha respondido negativamente indicando que no desea que se le
vuelva a preguntar ha marcado la opción «No volver a preguntar», el método devuelve el valor false.
Los parámetros del método shouldShowRequestPermissionRationale son, aquí también, la actividad en
curso y el permiso correspondiente.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)
== PackageManager.PERMISSION_GRANTED) {
}
else {
if (shouldShowRequestPermissionRationale(this,
Manifest.permission.SEND_SMS)) {
// Hay que proveer una explicación suplementaria
}
}
La petición efectiva del permiso debe hacerse invocando el método requestPermissions. Este método recibe
como parámetros la actividad en curso, un array de permisos los permisos solicitados, y un valor entero que
representa la petición.
[...]
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)
== PackageManager.PERMISSION_GRANTED) {
// el permiso está concedido
}
else {
if (shouldShowRequestPermissionRationale(this,
Manifest.permission.SEND_SMS)) {
// Hay que proveer una explicación suplementaria
}
else {
String[] permissions =new String[] {Manifest.permission.SEND_SMS};
requestPermissions(this,permissions , REQUEST_SMS);
}
}
Con el objetivo de procesar la respuesta a la petición de permisos, la actividad debe implementar la interfaz
ActivityCompat.onRequestPermissionsResultCallback. Se invoca el método
onRequestPermissionResult expuesto por esta interfaz.
El método onRequestPermissionResult presenta los siguientes parámetros:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
l int requestCode: corresponde al código de la petición que se pasa como parámetro a
requestPermissions.
l String[] permissions: array de permisos solicitados.
l int[] results: array de resultados de las peticiones de permisos.
El procesamiento de la petición de permisos SEND_SMS es, al final, el siguiente:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
[...]
ensurePermission();
}
canSendSMS =ContextCompat.checkSelfPermission(this,
Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED;
if(!canSendSMS) {
if (shouldShowRequestPermissionRationale
(Manifest.permission.SEND_SMS )) {
// proveer una explicación al usuario
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Petición de permisos");
builder.setMessage("El permiso de envío de SMS es necesario
para la ejecución de esta aplicación. ¿Desea reevaluar su
decisión?");
builder.setNegativeButton("No",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// el usuario no quiere:
// hay que informar que se va a cerrar la aplicación,
}
});
builder.setPositiveButton("Sí",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
askPermissions();
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
});
builder.show();
} else { // No se requiere ninguna explicación
askPermissions();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull
String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if(requestCode==REQUEST_SEND_SMS) {
for(int i =0;i<permissions.length;i++) {
if(permissions[i].equals(Manifest.permission.SEND_SMS) &&
grantResults[i] ==
PackageManager.PERMISSION_GRANTED)
canSendSMS =true;
}
}
}
}
b. Enviar un SMS
La clase
SmsManager se encarga de las operaciones de envío de SMS. En particular, expone el método
sendTextMessage, que permite el envío de un SMS:
l String destinationAddress: número de teléfono destinatario del mensaje.
l String scAddress: permite indicar la dirección del SMSC (Short Message Service Center), el servidor que
procesará el SMS. Indicar null permite utilizar el SMSC por defecto.
l String text: mensaje que se va a enviar.
l PendingIntent sentIntent: permite indicar un objeto de tipo PendingIntent para saber cuándo se
envía el mensaje.
l PendingIntent deliveryIntent: permite indicar un objeto de tipo PendingIntent para saber
cuándo se recibe el mensaje por parte del destinatario.
Para obtener una instancia de SmsManager, hay que invocar el método estático getDefault de la clase
SmsManager:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
SmsManager smsManager = SmsManager.getDefault();
Así, el código más sencillo para el envío de un SMS es el siguiente:
String numero;
String mensaje;
[...]
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(numero, null, mensaje, null, null);
Antes de ver con más detalle el envío de SMS, resulta útil precisar una funcionalidad interesante de los emuladores
Android.
En efecto, si bien estos no se comportan necesariamente como se espera durante una prueba para verificar su
capacidad para enviar/recibir SMS, en la práctica esta funcionalidad sí está soportada.
Así, se asigna un número de puerto a los terminales emulados en un ordenador, número de puerto que está
presente en la ventana del terminal emulado. Es posible utilizar este número de puerto como número de teléfono
para el terminal.
De esta manera, abriendo dos emuladores en el mismo puesto de desarrollo ¡es posible simular
envíos/recepciones de SMS entre dos terminales Android!
Por ejemplo, si al primer terminal arrancado se le asigna el puerto 5554, y al segundo, el puerto 5556, basta con
que el primer terminal envíe un SMS al número 5556 para que el segundo lo reciba correctamente.
Para saber si se ha enviado el SMS, hay que proveer, en la llamada al método sendTextMessage, un objeto de
tipo PendingIntent.
Esta instancia de PendingIntent permite al sistema enviar un evento (tal y como vimos en el capítulo Tareas
asíncronas y servicios, en la sección Utilizar los receptores de eventos). De modo que, además de definir una
instancia de PendingIntent, hay que implementar el mecanismo de suscripción al evento definido por el objeto
PendingIntent.
l Context context es el contexto de ejecución de la aplicación.
l int requestCode permite especificar un código para el evento.
l Intent intent indica qué intención se envía.
l int flags permite precisar el comportamiento del objeto PendingIntent. Por ejemplo, agregar el valor
FLAG_ON_SHOT indica que el objeto PendingIntent solo debe utilizarse una única vez.
Por ejemplo:
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
String SMS_SEND = "SMS_SEND";
Intent sendIntent = new Intent(SMS_SEND);
PendingIntent sendPendingIntent =
PendingIntent.getBroadcast(this,0,sendIntent, 0);
Para que la aplicación reciba la intención, hay que definir un objeto BroadcastReceiver y suscribirlo a esta
intención.
Como hemos visto en el capítulo Tareas asíncronas y servicios, esta suscripción se realiza invocando el método
registrerReceiver del objeto Context. El objeto de tipo IntentFilter que debe pasarse al método
registerReceiver debe filtrar la intención que se pasa como parámetro al método getBroadcast.
El código correspondiente al registro de un receiver para estar informado del envío del SMS es, por ejemplo, el
siguiente:
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
}, new IntentFilter(SMS_SEND));
Las posibles respuestas son:
l RESULT_OK: el envío se ha desarrollado correctamente.
l RESULT_ERROR_GENERIC_FAILURE: se ha producido un error «global».
l RESULT_ERROR_RADIO_OFF: el terminal está en modo avión.
l RESULT_ERROR_NULL_PDU: se ha producido un error al agregar el SMS a la pila de SMS por enviar.
Así, el código genérico para enviar un SMS y estar informado del resultado del envío es el siguiente:
String numero="5556";
String mensaje ="un mensaje";
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
PendingIntent sendPendingIntent =
PendingIntent.getBroadcast(this,0,sendIntent, 0);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int result = getResultCode();
switch (result) {
case RESULT_OK:
// Se ha enviado el SMS
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
// Error general
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
// Error durante el registro del SMS
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
// Error: el terminal está en modo avión
break;
}
}
}, new IntentFilter(SMS_SEND));
La instancia de PendingIntent que se ha de proveer para estar informado de la recepción del mensaje por
parte del destinatario funciona de la misma manera.
2. Recibir un SMS
Para que una aplicación sea capaz de recibir SMS, hay que comprobar que dispone del permiso
android.permission.RECEIVE_SMS. Lo primero es declarar este permiso en el archivo Manifest:
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
También hay que gestionar este nuevo permiso para los dispositivos que ejecutan Android Marshmallow o superior.
El método requestPermissions que hemos visto antes permite solicitar varios permisos en la misma llamada,
de modo que es posible hacer dos peticiones simultáneamente.
@Override
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
[...]
ensurePermission();
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if(requestCode==REQUEST_ALL_PERMISSIONS) {
for(int i =0;i<permissions.length;i++) {
if(permissions[i].equals(Manifest.permission.SEND_SMS) &&
grantResults[i] == PackageManager.PERMISSION_GRANTED)
canSendSMS =true;
if(permissions[i].equals(Manifest.permission.RECEIVE_SMS) &&
grantResults[i] == PackageManager.PERMISSION_GRANTED)
canReceiveSMS =true;
}
}
}
}
Cabe destacar que, incluso si la petición de dos permisos se hace con una única solicitud, el usuario verá dos
ventanas emergentes, cada una correspondiente a un permiso.
Cuando el terminal Android recibe un SMS, produce un evento Broadcast
android.provider.Telephony.SMS_RECEIVED. La aplicación que desea estar informada de los SMS
recibidos por el terminal debe declarar un objeto BroadcastReceiver que filtre esta intención.
Existen dos escenarios posibles en esta situación: o bien la aplicación debe estar informada de la recepción de SMS
incluso si está inactiva, o bien solo debe estar informada de la recepción únicamente si está en ejecución.
En el primer caso, la aplicación debe definir un BroadcastReceiver, y este debe declararse en el archivo
Manifest con el filtro de intención android.provider.Telephony.SMS_RECEIVE.
A continuación se muestra el esquema de definición de un BroadcastReceiver:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@Override
public void onReceive(Context context, Intent intent) {
}
}
La suscripción del BroadcastReceiver en el archivo AndroidManifest es, entonces, la siguiente:
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
<receiver android:name=".SMSReceiver" >
<intent-filter>
<action
android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
Si la recepción de SMS solo debe ser efectiva en caso de que la aplicación esté en ejecución, basta, en vez de
agregar el filtro de intención en el archivo AndroidManifest, suscribir el BroadcastReceiver especificando el
mismo filtro de intención:
registerReceiver(new SMSReceiver(),
new IntentFilter("android.provider.Telephony.SMS_RECEIVED"));
Como vimos en el capítulo Tareas asíncronas y servicios, se invoca el método onReceive del objeto
BroadcastReceiver durante la recepción del SMS.
Este método onReceive expone dos parámetros: el primero es un contexto de ejecución, el segundo es un objeto
Intent que contiene toda la información correspondiente al mensaje recibido.
La extracción de los datos del mensaje se realiza en varias etapas. Los componentes del mensaje están codificados
en un formato específico de los SMS: el formato PDU (de Protocol Description Unit) y se almacenan en los datos
EXTRAS de la intención pasada como parámetro.
De modo que hay que extraer, en primer lugar, los datos de la intención utilizando el siguiente código:
El objeto bundle puede contener varios mensajes: por ejemplo, si el mensaje enviado por el emisor es demasiado
largo, se partirá en varios mensajes, que se recibirán al mismo tiempo por el BroadcastReceiver.
En primer lugar, hay que extraer cada parte del mensaje; el conjunto se almacena en el bundle bajo la clave pdus.
A continuación, para cada parte del mensaje, hay que interpretar el mensaje en formato PDU. Android provee una
clase SmsMessage que permite acceder a los distintos componentes del mensaje sin tener que interpretar el
propio mensaje (el protocolo de decodificación es bastante laborioso de implementar).
Para obtener una instancia de SmsMessage, hay que invocar el método estático createFromPdu de la clase
SmsMessage:
La clase SmsMessage expone los métodos getDisplayOriginatingAddress y getDisplayMessageBody
que proveen, respectivamente, el número del emisor y el contenido del mensaje.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
El código de recepción de un mensaje es, finalmente, el siguiente:
@Override
public void onReceive(Context context, Intent intent) {
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Utilizar los sensores del dispositivo
La mayoría de los terminales Android integran varios sensores: acelerómetro, giroscopio, sensor de temperatura,
etc.; la plataforma permite acceder a las mediciones realizadas por estos sensores.
Como con las características de hardware de los terminales, todos los dispositivos que funcionan con Android no
incluyen todos los tipos de sensores soportados por la plataforma. De modo que hay que comprobar la existencia de
un sensor antes de utilizarlo en una aplicación.
También aquí, igual que con el envío de SMS, existen dos enfoques posibles: o bien reservar la aplicación a los
terminales equipados del sensor específico o bien comprobar en tiempo de ejecución la presencia del sensor.
En el primer caso, hay que utilizar la etiqueta <uses-feature> que hemos visto antes. La siguiente tabla muestra
los nombres de los sensores que se han de utilizar con <uses-feature>. La lista se ha extraído de la tabla
publicada por Google en la siguiente dirección: http://developer.android.com/guide/topics/manifest/usesfeature
element.html#hwfeatures.
Tipo de sensor Valor para utilizar en la etiqueta <uses-feature>
Acelerómetro android.hardware.sensor.accelerometer
Barómetro android.hardware.sensor.barometer
Brújula android.hardware.sensor.compass
Giroscopio android.hardware.sensor.gyroscope
Luminosidad android.hardware.sensor.light
Proximidad android.hardware.sensor.proximity
Contador de pasos android.hardware.sensor.stepcounter
Detector de pasos android.hardware.sensor.stepdetector
Todos los sensores expuestos por la plataforma se manipulan de la misma manera, y la letura de los datos se realiza
siempre según el mismo principio.
SensorManager sensorManager =
(SensorManager)getSystemService(SENSOR_SERVICE);
SensorManager expone el método getSensorList, que devuelve la lista de sensores incorporados al terminal:
El parámetro int
type permite indicar el tipo de sensor cuya lista se pide, un terminal puede disponer de varios
sensores de un tipo concreto. Los valores para el tipo de sensor se indican en la clase
Sensor:
Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_AMBIENT_TEMPERATURE, etc. La lista completa está
disponible en la siguiente dirección: http://developer.android.com/reference/android/hardware/Sensor.html. Para obtener la
lista de todos los sensores, hay que utilizar el parámetro Sensor.TYPE_ALL.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
La clase SensorManager presenta también el método getDefaultSensor, que devuelve el sensor definido por
defecto para el tipo que se pasa como parámetro:
Así, por ejemplo, para obtener el sensor por defecto que pide la presión atmosférica, hay que realizar la siguiente
llamada:
Sensor pressureSensor =
sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
La lectura de los valores de un sensor se realiza utilizando un objeto de tipo SensorEventListener. Esta interfaz
expone los dos métodos siguientes:
El método onAccuracyChanged se invoca con cada cambio de precisión del sensor. La precisión viene determinada
por el parámetro int accuracy, y puede tomar los siguientes valores:
l int SENSOR_STATUS_ACCURACY_HIGH: precisión máxima.
l int SENSOR_STATUS_ACCURACY_LOW: precisión baja.
l int SENSOR_STATUS_ACCURACY_MEDIUM: precisión media.
l int SENSOR_STATUS_NO_CONTACT: el sensor no ha obtenido el evento que debe medir.
l int SENSOR_STATUS_UNRELIABLE: el sensor devuelve valores erróneos.
El método onSensorChanged se invoca con cada medida del sensor. El parámetro de tipo SensorEvent expone
las siguientes propiedades:
l int accuracy: precisión de la medida en curso.
l Sensor sensor: sensor que ha generado la medida.
l long timestamp: hora de la medida, en nanosegundos.
l float[] values: array de valores medidos.
Para asociar un objeto SensorEventListener a un sensor, hay que invocar al método registerListener de
la clase SensorManager:
l SensorEventListener listener: instancia de SensorEventListener que se asocia al sensor.
l Sensor sensor: sensor que se ha de medir.
l int samplingPeriodUs: periodicidad deseada de la medida. Los posibles valores son
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
SENSOR_DELAY_NORMAL (periodicidad estándar), SENSOR_DELAY_UI (periodicidad adaptada a la actualización
de la interfaz de usuario), SENSOR_DELAY_GAME (periodicidad adaptada a los videojuegos), o
SENSOR_DELAY_FASTES (periodicidad mínima). También es posible indicar directamente un valor en
microsegundos. La periodicidad se indica únicamente a título informativo, respetarla o no queda a discreción del
fabricante del terminal.
unregisterListener(SensorEventListener)
unregisterListener(SensorEventListener, Sensor)
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Geolocalizar al usuario
La geolocalización es una característica importante de los dispositivos móviles; se utiliza, por ejemplo, para presentar
al usuario una información en función de su localización, para guiarlo geográficamente, etc. Como con los sensores
vistos anteriormente, la geolocalización está accesible para las aplicaciones, pero su uso está sometido a permisos.
Android proporciona dos tipos de geolocalización, que dependen del tipo de componente utilizado para realizar la
medida:
l En caso de que la geolocalización no requiera una gran precisión, es posible utilizar una geolocalización basada en las
señales GSM y de red.
l En caso de que la geolocalización deba ser más precisa, hay que utilizar una geolocalización basada en un componente
que capte la señal GPS de los satélites.
En el primer caso, de geolocalización poco precisa, hay que exponer el permiso
android.permission.ACCESS_COARSE_LOCATION en el archivo AndroidManifest.xml. En el segundo
caso, debe declararse el permiso android.permission.ACCESS_FINE_LOCATION en el Manifest.
Cabe destacar que el permiso ACCESS_FINE_LOCATION incluye, explícitamente, el permiso
ACCESS_COARSE_LOCATION.
El desarrollador debe tener en mente que el uso del componente GPS de un terminal consume mucha energía. Por
este motivo, se recomienda de forma encarecida utilizar la geolocalización solo cuando sea realmente necesario.
1. LocationManager
La clase base del mecanismo de geolocalización es la clase LocationManager. Como con los sensores, hay que
invocar el método getSystemService para obtener una instancia de LocationManager. El parámetro de
llamada de getSystemService es aquí Context.LOCATION_SERVICE.
LocationManager locationManager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
Tres métodos permiten obtener la medida de la geolocalización del terminal. El uso de uno u otro método depende
del escenario seleccionado por el desarrollador:
l O bien la aplicación solo necesita conocer la última posición conocida.
l O bien la aplicación debe realizar una única petición de posición.
l O bien la aplicación debe realizar medidas de posición a intervalos regulares.
Para obtener el valor de la última posición conocida, hay que invocar el método getLastKnownLocation:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
PASSIVE_PROVIDER (la medida usa el último proveedor de medida utilizado por el sistema o cualquier otra
aplicación).
El método devuelve un objeto de tipo Location, que se estudiará más adelante en esta sección.
Para obtener una única medida, la clase LocationManager provee, desde la versión 9 de la API (Android 2.3), el
método requestSingleUpdate. Este método expone varias firmas:
La principal diferencia entre estas versiones reside en la manera de seleccionar el proveedor de la medida. Entre las
dos primeras versiones presentadas aquí, el proveedor se selecciona directamente, como con el método
getLastKnownLocation. Las dos últimas versiones, en vez de indicar directamente un proveedor, reciben como
parámetro un objeto de tipo Criteria, que permite informar directamente qué tipo de medida se desea obtener:
el sistema escogerá él mismo qué proveedor de medida utilizará según la medida deseada.
Según la versión seleccionada, los resultados de la medida se explotarán bien por una instancia de
LocationListener, interfaz que debe implementarse, o bien por un mecanismo de PendingIntent, en cuyo
caso hay que implementar el mismo tipo de mecanismo que vimos antes para la lectura de SMS.
La interfaz LocationListener presenta los siguientes métodos:
l void onProviderEnabled(String provider): se invoca cuando el servicio de geolocalización se
activa (por el usuario).
Cuando se devuelve la medida a través de un objeto de tipo LocationListener, el método
requestSingleUpdate pide también un objeto de tipo Looper en aquellos (raros) casos donde sea necesario
realizar el procesamiento de la respuesta en un thread separado. Esta situación, compleja de gestionar, supera el
marco de este libro, de modo que se utilizará el valor null que permite no precisar ningún Looper específico.
En caso de que el desarrollador desee no indicar directamente un proveedor para la geolocalización, es posible
precisar los criterios de selección del proveedor, a través de un objeto de tipo Criteria.
La clase Criteria permite precisar qué tipo de medida se desea, informando los valores para los posibles
criterios. Los métodos expuestos por la clase Criteria permiten, por ejemplo, indicar una precisión deseada, un
nivel de consumo de energía, etc.:
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Criteria criteria = new Criteria();
criteria.setPowerRequirement(Criteria.POWER_LOW);
En caso de que se pidan varias medida de geolocalización, la clase LocationManager presenta el método
requestLocationUpdates. Presenta las siguientes firmas:
Los parámetros de llamada son similares a los parámetros del método requestSingleUpdate. Los parámetros
suplementarios propuestos permiten indicar criterios para que la medida se envíe a la instancia de
LocationListener o bien invocar al objeto PendingIntent.
Estos criterios son los siguientes:
l long minTime: permite indicar un retardo mínimo entre dos medidas, en milisegundos. Es primordial indicar aquí
un valor realista, lo más grande posible para evitar un consumo de energía excesivo.
l float minDistance: permite especificar una distancia mínima entre dos medidas para que se envíe la medida.
Si la distancia indicada es cero, no se tendrá en cuenta el criterio minDistance.
Cuando se realizan las medidas, o como muy tarde cuando la actividad que utiliza estas medidas se pone en pausa,
es imprescindible detener la medición. Esto se realiza invocando simplemente el método removeUpdates de la
clase LocationManager. Existen dos versiones de este método, dependiendo de si las medidas son observadas
utilizando un LocationListener o un PendingIntent.
2. Location
Los resultados de la medida se proveen por el sistema mediante una clase Location. Esta clase, además de
contener la información correspondiente a las medidas, provee un conjunto de métodos que permiten calcular
distancias (entre dos instancias de Location, por ejemplo, o entre dos posiciones expresadas en
latitud/longitud).
La siguiente tabla muestra los datos relativos a las medidas proporcionados por el objeto Location:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Método Información proporcionada
float getSpeed() Devuelve la velocidad de desplazamiento medida, en metros por
segundo.
Cabe destacar que únicamente la información correspondiente a la latitud, la longitud y la hora de la medida se
devuelven obligatoriamente en el objeto Location. Los demás datos son susceptibles de no estar disponibles.
La explotación por una instancia de LocationListener para una medida está basada, por ejemplo, en el
siguiente código:
@Override
public void onLocationChanged(Location location) {
float accurracy = location.getAccuracy();
double longitude = location.getLongitude();
double latitude = location.getLatitude();
long time = location.getTime();
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Desarrollar un widget
Los widgets pueden verse, en el universo Android, como partes de aplicaciones que se muestran directamente en el
escritorio del terminal. Permiten, en general, presentar una información breve, actualizada regularmente, propia de la
aplicación que provee el widget.
La aplicación LocDVD debe disponer de un widget que muestre, a intervalos regulares, una película al azar de entre la
lista de DVD de la aplicación. Por otro lado, cuando el usuario haga clic en la película, el widget debe actualizarse y
mostrar otra película.
Los widgets se basan en la clase android.appwidget.AppWidgetProvider, clase que debe sobrecargarse,
según el mismo principio de las actividades y los fragmentos. Sin embargo, como se detalla a continuación, el
funcionamiento de un widget y su implementación son muy diferentes de los de una actividad clásica o incluso de un
fragmento. La clase AppWidgetProvider es, de hecho, una subclase de la clase BroadcastReceiver (que
vimos, en particular, en el capítulo Tareas asíncronas y servicios, en la sección Utilizar los receptores de eventos).
La interfaz de visualización de un widget se basa, como para los demás componentes de una aplicación, en un
archivo de layout. Sin embargo, conviene tener en cuenta que, si bien son compatibles con los componentes clásicos,
no todos los componentes de la plataforma son compatibles con los widgets; veremos más adelante cuál es el
motivo.
Defina, en la carpeta /res/layout, un archivo de layout para el widget. Su nombre es, por ejemplo,
widget_layout.xml.
El layout es muy simple: un primer componente TextView muestra el título del DVD, y un segundo
TextView se encarga de mostrar, debajo, el resumen de la película. El título debe estar escrito en
caracteres de 22 sp de tamaño, el resumen en tamaño 14 sp. También hay que prever un color
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
diferente para el fondo del layout (propiedad background del contenedor de vistas raíz) y para el texto
(propiedad textColor de los componentes TextView).
El archivo de layout es el siguiente:
</LinearLayout>
El widget se basa en la clase android.appwidget.AppWidgetProvider, que debe extenderse.
Declare una nueva clase Java, LocDVDWidget, en el paquete por defecto de la aplicación.
Esta clase debe extender AppWidgetProvider:
package com.ejemplo.locdvd;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
El ciclo de vida de un widget está representado por un conjunto de métodos para sobrecargar. A continuación se
muestran los principales métodos:
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Método Ciclo de vida
onEnabled Se invoca cuando se sitúa una primera instancia del widget en el escritorio del terminal.
onDisabled Se invoca cuando se elimina la última instancia del widget del escritorio.
onUpdate Se invoca a intervalos regulares para la actualización del widget.
onDelete Se invoca cuando se elimina una instancia del widget del escritorio.
El método onUpdate es el método que se encarga de la parte esencial del mecanismo de funcionamiento del widget:
es, en efecto, este método el encargado de mostrar el contenido del widget. La firma de onUpdate est la siguiente:
l Context c: representa el contexto de ejecución del widget.
l AppWidgetManager manager: este objeto va a permitir actualizar el contenido del widget.
l int[] appWidgetIds: array de identificadores de los widgets situados en el escritorio.
Este último parámetro ilustra la primera diferencia entre un widget y una actividad: el usuario puede, sin restricción
alguna, situar varias instancias del mismo widget en su escritorio. El sistema asocia un identificador a cada instancia
del widget.
La plataforma invoca el método onUpdate a intervalos regulares; este intervalo lo especifica el desarrollador. Pero si
se sitúan varias instancias del mismo widget en el escritorio, se realizará una única llamada a onUpdate en cada
intervalo de tiempo. El método onUpdate debe encargarse, entonces, de actualizar todos los widgets y no solo el
que ha generado la llamada a onUpdate.
@Override
public void onUpdate(Context context, AppWidgetManager
appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Para que todos los widgets estén actualizados, hay que integrar un bucle que recorra todos los elementos del array
appWidgetIds.
Defina un bucle for para recorrer el array:
En primer lugar, hay que definir el layout del widget. Los widgets, para ello, en lugar de utilizar directamente un
objeto de tipo LayoutInflater (como los fragmentos), deben utilizar una instancia de la clase RemoteViews. El
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
constructor de la clase RemoteViews es el siguiente:
Los parámetros son el nombre del paquete de la aplicación y el identificador del layout utilizado para la visualización
del widget. El nombre del paquete, en vez de estar escrito directamente, puede obtenerse invocando el método
getPackageName de la clase Context.
Defina una instancia de RemoteViews:
RemoteViews remoteViews =
new RemoteViews(context.getPackageName(),
R.layout.widget_layout);
Cabe destacar que, para no tener que instanciar a un objeto RemoteViews en cada iteración, es preferible definir
este objeto antes del bucle for.
La manipulación de los componentes del layout se realiza a través del objeto RemoteViews. Así, para dar valor al
texto mostrado por un componente TextView, hay que invocar el método setTextViewText del objeto
RemoteViews:
Esta necesidad de utilizar el objeto RemoteViews para la manipulación de los componentes del layout del widget
ilustra las limitaciones de los widgets: no todos los componentes son compatibles, solo es posible integrar en un
layout aquellos componentes que puedan manipularse a través de un objeto RemoteViews.
Cada widget debe mostrar el título y el resumen de una película tomada al azar de la lista. A continuación se muestra
una propuesta de código que permite seleccionar un DVD:
También aquí es preferible definir la lista Arraylist<DVD> fuera del bucle for.
Utilizando el método setTextViewText, introduzca el texto mostrado por los dos componentes
TextView definidos en el archivo de layout del widget:
remoteViews.setTextViewText(R.id.widget_title,
selected.getTitulo());
remoteViews.setTextViewText(R.id.widget_summary,
selected.getResumen());
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El código completo del método onUpdate es, llegados a este punto, el siguiente:
@Override
public void onUpdate(Context context, AppWidgetManager
appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Una vez asignado valor a los componentes TextView, hay que implementar el mecanismo que permite actualizar el
texto cuando el usuario hace clic en el widget. La clase RemoteViews presenta, para ello, el método
setOnClickPendingIntent, que permite definir un objeto PendingIntent que se difundirá al hacer clic en un
componente:
El objeto PendingIntent permite especificar al sistema una intención que se difundirá posteriormente (aquí,
cuando el usuario haga clic en un componente TextView).
PendingIntent ser obtiene aquí invocando el método estático getBroadcast de la clase
La instancia de
PendingIntent:
La intención que se difundirá debe permitir realizar la actualización del widget. Para ello, hay que indicar la acción
AppWidgetManager.ACTION_APPWIDGET_UPDATE a la intención y agregar a la colección Extras de la
intención el array de identificadores de los widgets.
Esta intención es una intención explícia (como las intenciones utilizadas para lanzar una nueva actividad, por
ejemplo). La clase que se lanzará es la clase del widget.
Defina un objeto Intent, asignando la siguiente acción específica:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Intent intent= new Intent(context,LocDVDWidget.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
appWidgetIds);
Una vez definido el objeto Intent, es posible instanciar el objeto PendingIntent:
PendingIntent pendingIntent =
PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
A continuación hay que invocar el método setOnClickPendingIntent para asociar el objeto
PendingIntent al clic sobre los componentes TextView:
remoteViews.setOnClickPendingIntent(R.id.widget_title,
pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.widget_summary,
pendingIntent);
Con esto termina la actualización del mecanismo que gestiona el clic. Queda por especificar al sistema que hay que
actualizar el widget. El objeto AppWidgetManager pasado como parámetro al método onUpdate presenta, para
ello, el método updateAddWidget:
Los parámetros del método son el identificador del widget que se ha de actualizar y el objeto RemoteViews que
contiene la vista que se ha de mostrar.
Invoque el método siguiente mediante la instancia de AppWidgetManager:
appWidgetManager.updateAppWidget(widgetId, remoteViews);
El código completo del método onUpdate es, al final, el siguiente:
@Override
public void onUpdate(Context context, AppWidgetManager
appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
RemoteViews remoteViews =
new RemoteViews(context.getPackageName(),
R.layout.widget_layout);
ArrayList<DVD> list = DVD.getDVDList(context);
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Intent intent= new Intent(context,LocDVDWidget.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
appWidgetIds);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.widget_title,
pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.widget_summary,
pendingIntent);
appWidgetManager.updateAppWidget(widgetId, remoteViews);
}
}
Una vez definido el código del widget, quedan dos etapas por realizar: definir un archivo de configuración e inscribir el
widget en el archivo AndroidManifest.
El archivo de configuración especifica al sistema las características del widget: sus dimensiones, el periodo de
actualización, etc. Tiene el aspecto de un archivo XML, almacenado en la carpeta /res/xml del proyecto.
Sitúe el cursor en la carpeta /res del proyecto LocDVD.
Haga clic con el botón derecho, seleccione la opción New y, a continuación, Android resource directory: se
abre una ventana emergente.
Seleccione, en la lista Resource type, el valor xml. El nombre de la carpeta vale, automáticamente, xml.
Haga clic en ok para confirmar la creación de la carpeta.
Se crea la carpeta xml.
El archivo de configuración debe crearse en esta carpeta:
Cree un nuevo archivo XML, llamado, por ejemplo, widget_config.xml, y ábralo en el editor.
Los parámetros de configuración se definen en una etiqueta appwidget-provider. Hay que indicar también el
namespace android, como con los archivos de layout.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Defina la etiqueta appwidget-provider en el archivo XML de configuración:
Hay que introducir la siguiente información en el widget:
l Anchura mínima del widget.
l Altura mínima del widget.
l Periodo de actualización.
l Archivo de layout inicial del widget.
l Categoría de widget (permite especificar si el widget debe ocupar espacio en el escritorio o en la pantalla de bloque do).
Para calcular las dimensiones de un widget, hay que utilizar una fórmula que define la dimensión en dp en función del
número de casillas ocupadas por el widget:
Así, si el widget debe ocupar cuatro casillas de altura, su anchura mínima debe ser de 4*742 dp, es decir, 294 dp.
Introduzca la anchura mínima del widget, utilizando la propiedad android:minWidth.
La altura mínima, que corresponde a la propiedad android:minHeight, debe ser de 72 dp (es decir,
una casilla):
El periodo de actualización se expresa en milisegundos. Una restricción del sistema impone que este periodo no sea
inferior a los 30 minutos.
Especifique un periodo de actualización de 30 minutos, utilizando la propiedad
android:updatePeriodMillis.
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="1800000"
/>
La propiedad android:initialLayout permite especificar el layout que se mostrará durante el tiempo
en que el sistema actualiza por primera vez el widget. Aquí, puede utilizarse el mismo layout que el definido
para el método onUpdate.
El widget puede situarse o bien en el escritorio del terminal o bien sobre la pantalla de bloqueo. La
propiedad correspondiente, android:widgetCategory, puede tomar los valores home_screen,
keyguard o ambos, separándolos por el carácter «|». Por ejemplo:
Hemos terminado la configuración mínima del widget. Es posible definir otras opciones a este nivel; la lista completa
está disponible en la siguiente dirección:
https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo.html
Los widgets, como las actividades y los servicios, deben registrarse en el archivo AndroidManifest.xml.
Edite el archivo AndroidManifest.xml del proyecto LocDVD.
</receiver>
Para que el widget esté actualizado, hay que asociarle el filtro de intención
android.appwidget.action.APPWIDGET_UPDATE:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
<receiver android:name=".LocDVDWidget" >
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
Por último, hay que especificar el nombre del archivo de configuración, utilizando la siguiente
etiqueta <meta-data>:
<meta-data
android:name="android.appwidget.provider"
android:resource="identificador_del_archivo_de_configuración" />
Aquí, la declaración del widget en el archivo Manifest se hace, al final, de la siguiente manera:
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_config" />
</receiver>
El widget está completamente definido, es posible instalarlo tanto en el escritorio como en la pantalla de
bloqueo del terminal.
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Utilizar la barra de notificaciones
Utilizar la barra de notificaciones del terminal es una manera sencilla de informar al usuario: inmediatamente visibles,
rápidas de implementar, las notificaciones son una herramienta potente para el desarrollador.
Inicialmente, una notificación está compuesta por un icono, un título en una línea, una segunda línea de texto que
utiliza una tipografía de tamaño inferior, así como una fecha. La versión 4 de Android (API 14) incorpora la posibilidad
de definir notificaciones extendidas, que presentan además una imagen (con una altura máxima de 256 dp), o bien
distintos controles para la visualización del texto.
Incluso aunque esta característica no es realmente interesante para la aplicación LocDVD, permite enviar una
notificación al usuario cuando pide mediante un clic en un botón visitar la ficha detallada del DVD.
Edite el archivo de layout de la ficha de visualización de un DVD, el archivo activity_viewdvd.xml.
Agregue, debajo del botón Modificar la fecha de última visualización, un botón Enviar una notificación.
Además, el botón debe tener como identificador el valor setNotification.
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enviar_una_notificación"
android:id="@+id/setNotification"/>
Conviene destacar que la cadena de caracteres enviar_una_notificación debe definirse en el archivo
strings.xml.
Visualice el código del fragmento ViewDVDFragment en el editor de código. Hay que agregar una
referencia al botón setNotification:
Button setNotification =
(Button)view.findViewById(R.id.setNotification);
Defina un objeto OnClickListener para el botón. El evento onClick debe producir la ejecución de un
nuevo método privado, sendNotification:
setNotification.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendNotification();
}
});
Declare el nuevo método en la clase ViewDVDFragment:
Como con las ventanas emergentes, vistas en el capítulo Navegación y ventanas emergentes, el framework Android
provee una clase para ayudar a crear notificaciones: la clase Notification.Builder (del paquete
android.app.Notification).
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
En el cuerpo del método sendNotification, instancie un objeto Notification.Builder. El
constructor recibe como único parámetro el contexto de ejecución, que se obtiene, en un fragmento,
invocando el método getActivity.
Al título de la notificación se le da valor invocando el método setContentTitle del objeto
Notification.Builder. El título de la notificación es, aquí, el título del DVD en curso:
builder.setContentTitle(dvd.getTitulo());
Por su parte, al texto de la notificación se le da valor mediante el método setContentText:
builder.setContentText(dvd.getResumen());
Para mostrar la notificación, es imprescindible proveer un icono de dimensiones 32 por 32 píxeles. Android Lollipop
agrega una restricción suplementaria: el icono debe estar en blanco y negro.
Defina un icono para la notificación (o utilice el que se provee con el código fuente correspondiente al
presente capítulo: archivo ic_notification.png de la carpeta drawable), y asigne valor al icono de
la notificación invocando el método setSmallIcon del objeto Builder:
builder.setSmallIcon(R.drawable.ic_notification);
Se han definido los elementos imprescindibles para la visualización de la notificación. El objeto
Notification.Builder, una vez definida la notificación, devuelve un objeto Notification invocando el
método getNotification para aquellos sistemas que funcionan con la API 16 o inferior, y utilizando el método
build para los sistemas que funcionan con la API 17 o superior.
Defina un objeto Notification, invocando el método correspondiente a la API del terminal:
Notification notification;
if(Build.VERSION.SDK_INT<16)
notification = builder.getNotification();
else
notification = builder.build();
Para enviar la notificación hay que utilizar un objeto de tipo NotificationManager. Se obtiene una instancia de
NotificationManager gracias al método getSystemService del objeto context. El servicio es, aquí,
Context.NOTIFICATION_SERVICE.
Defina una instancia de NotificationManager:
NotificationManager notificationManager =
(NotificationManager)getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
El envío de notificaciones se realiza invocando el método notify del objeto NotificationManager:
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
public void notify (int id, Notification notification)
El primer parámetro es el identificador de la notificación. Si el identificador es el mismo que una notificación de la
aplicación actualmente mostrada en el terminal, se reemplaza por la nueva aplicación.
Invoque el método notify. Aquí, según la lógica de la aplicación, el identificador puede construirse a partir
del identificador del DVD en curso.
notificationManager.notify((int)dvd.id, notification);
El código completo del método sendNotification es, de momento, el siguiente:
builder.setContentTitle(dvd.getTitulo());
builder.setContentText(dvd.getResumen());
builder.setSmallIcon(R.drawable.ic_notification);
Notification notification;
if(Build.VERSION.SDK_INT<16)
notification = builder.getNotification();
else
notification = builder.build();
NotificationManager notificationManager =
(NotificationManager)getActivity().
getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify((int)dvd.id, notification);
La ejecución muestra que la notificación se lanza correctamente cuando el usuario hace clic en el botón. Queda por
implementar la ejecución automática de la aplicación cuando el usuario hace clic en la notificación.
Como con la gestión del clic en un widget, la acción que sigue al clic se gestiona con un objeto PendingIntent (del
paquete android.app).
[...]
Intent intent = new Intent(getActivity(), MainActivity.class);
[...]
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
El objeto PendingIntent debe producir la apertura de una actividad. En este caso hay que invocar el método
estático getActivity de la clase PendingIntent para obtener una instancia de PendingIntent.
Defina un objeto PendingIntent, utilizando el método getActivity de la clase PendingIntent:
PendingIntent pendingIntent =
PendingIntent.getActivity(getActivity(), 0, intent, 0);
El objeto PendingIntent se asocia a la notificación mediante el Notification.Builder, invocando
el método setContentIntent:
builder.setContentIntent(pendingIntent);
El código completo del método sendNotification es, ahora, el siguiente:
builder.setSmallIcon(R.drawable.ic_notification);
Notification notification;
if(Build.VERSION.SDK_INT<16)
notification = builder.getNotification();
else
notification = builder.build();
NotificationManager notificationManager =
(NotificationManager)getActivity().
getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify((int)dvd.id, notification);
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Compartir contenido y utilizar las redes sociales
Las redes sociales están integradas naturalmente con los terminales Android, y los usuarios esperan a que cada
aplicación permita compartir contenido en una o varias redes sociales.
Si bien las principales redes sociales proporcionan, para integrar esta compartición de contenido en una aplicación,
sus propias API, Google provee una solución que aprovecha la potencia ofrecida por las intenciones.
Esta solución no es perfecta; las opciones de compartición no explotan todas las posibilidades de cada red social,
pero permite sin embargo implementar una funcionalidad de compartición rápidamente y garantizando que el
mantenimiento será sencillo.
La primera etapa consiste en agregar una opción en el menú accesible a través de la barra de navegación de la
aplicación. Esta barra de acción, implementada en el capítulo Navegación y ventanas emergentes, utiliza la biblioteca
de compatibilidad android.support.v7. La opción Compartir que se va a agregar debe tener en cuenta esta
biblioteca.
Edite el archivo de menú de la actividad principal, es decir, el archivo menu_principal.xml.
Hay que agregar una entrada a este menú, entrada que debe mostrar la etiqueta Compartir. El código
correspondiente es, por ahora, el siguiente:
<item
android:id="@+id/menu_compartir"
android:title="@string/compartir"
/>
Para indicar al sistema que la entrada de menú debe iniciar una acción de compartición de contenido, hay que
informar la propiedad actionProviderClass asignándole el valor
android.widget.ShareActionProvider. El código correspondiente debería ser el siguiente:
<item
android:id="@+id/menu_compartir"
android:title="@string/compartir"
android:actionProviderClass="android.widget.ShareActionProvider"/>
Sin embargo, este código plantea varios problemas:
l En primer lugar, la funcionalidad ShareActionProvider se ha introducido con la API 14 de Android, y no es
compatible con los terminales más antiguos.
l Por otro lado, la barra de menú está construida utilizando la biblioteca de soporte. El ShareActionProvider
tampoco es compatible con la barra de acción de la biblioteca de soporte.
Para resolver estos problemas de compatibilidad, Google ha incorporado a la biblioteca de soporte la posibilidad de
utilizar un ShareActionProvider específico de esta biblioteca. Esto es lo que debemos implementar.
Para poder utilizar el ShareActionProvider proporcionado por la biblioteca de soporte, hay que agregar, como
con el panel de navegación (Navigation Drawer), el namespace de la solución.
En la etiqueta raíz del archivo de definición del menú, agregue la definición del namespace como muestra el
siguiente código:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
xmlns:app=http://schemas.android.com/apk/res-auto
Una vez agregado el namespace, es posible integrar el ShareActionProvider de soporte. El código
correspondiente es el siguiente:
<item
android:id="@+id/menu_compartir"
android:title="@string/compartir"
app:actionProviderClass=
"android.support.v7.widget.ShareActionProvider"
/>
Para que la opción esté visible para el usuario, es preferible que se muestre siempre en la barra de acción. Para ello,
hay que agregar la propiedad showAsAction de la biblioteca de soporte (utilizando, para ello, el
namespace definido previamente).
<item
android:id="@+id/menu_compartir"
app:showAsAction="always"
android:title="@string/compartir"
app:actionProviderClass=
"android.support.v7.widget.ShareActionProvider"
/>
Una vez integrada la opción Compartir del menú en el archivo de definición del menú, hay que definir el código que
permite llevar a cabo la compartición.
Edite la clase MainActivity.java: la barra de menú, así como el menú, están definidos en la actividad
principal de la aplicación.
La gestión del botón Compartir debe hacerse durante la creación del menú por parte del sistema, es decir, en el
método sobrecargado onCreateOptionsMenu de la actividad.
Sitúese en el cuerpo del método onCreateOptionsMenu.
Hay que obtener una referencia al objeto ShareActionProvider. En caso de utilizar la biblioteca de soporte, esto
se hace invocando el método estático getActionProvider de la clase MenuItemCompat (del paquete
android.support.v4.view):
El método recibe como parámetro una instancia de la entrada de menú correspondiente para la compartición. Esta
instancia puede obtenerse a partir del objeto Menu que se pasa como parámetro de onCreateOptionsMenu.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Una vez definida la instancia de MenuItem, es posible invocar el método getActionProvider:
ShareActionProvider shareActionProvider =
(ShareActionProvider)
MenuItemCompat.getActionProvider(shareItem);
Importe la clase ShareActionProvider del paquete android.support.v7.widget.
La información que se va a compartir se provee al objeto ShareActionProvider a través de una intención
implícita, cuya acción es Intent.ACTION_SEND.
Defina un objeto Intent, con la acción Intent.ACTION_SEND:
intent.setType("text/plain");
El asunto de la compartición se especifica en el Extra cuya etiqueta es Intent.EXTRA_SUBJECT. El
cuerpo del mensaje se especifica mediante la etiqueta Intent.EXTRA_TEXT:
Por último, se asocia el objeto Intent a la instancia de ShareActionProvider:
shareActionProvider.setShareIntent(intent);
El código completo del método onCreateOptionsMenu es, ahora, el siguiente:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_principal, menu);
shareActionProvider.setShareIntent(intent);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
return true;
}
Durante la ejecución, aparece el nuevo botón Compartir: el sistema ha integrado automáticamente el icono estándar
de compartición de Android en la barra de acción, como muestra la siguiente captura de pantalla:
Haciendo clic en la opción de menú Compartir, presenta la lista de aplicaciones susceptibles de compartir la
información enviada.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Implementar un tema, utilizar los estilos
Si bien el diseño gráfico de una aplicación es un dominio reservado a los diseñadores e infografistas, es el
desarrollador quien se encargará de aplicar el diseño gráfico definido a la aplicación.
Android provee un conjunto de herramientas que hacen esta tarea menos laboriosa y que van a permitir modificar el
aspecto gráfico de la aplicación LocDVD.
Para crear una aplicación homogénea, resulta esencial que los elementos que se muestran por pantalla tengan el
mismo aspecto si cumplen la misma función. Así, por ejemplo, es imprescindible que las etiquetas de los campos de
formulario tengan, todas, las mismas características.
El capítulo Principios básicos de Android muestra cómo es posible modificar el aspecto de un texto por pantalla:
cambiar el tamaño del texto, modificar su color, el color de fondo, etc. Sin embargo, resulta molesto tener que aplicar
estas mismas modificaciones a todos los elementos de la misma naturaleza en la aplicación. Por otro lado, cualquier
cambio, incluso menor, en el diseño gráfico impone al desarrollador tener que revisar el conjunto de archivos de
layout.
Inspirándose en las hojas de estilo de HTML, Android propone la noción de estilo, que permite sistematizar la
aplicación de los cambios sobre los componentes mostrados.
En la plataforma Android, los estilos se definen en un archivo XML. Este archivo, cuyo nombre puede configurarse a
elección del desarrollador, debe almacenarse en la carpeta /res/values.
Los estilos son recursos: el archivo XML debe tener como etiqueta raíz la etiqueta <resources>. Cada estilo se
define mediante una etiqueta <style>; el atributo name es obligatorio. Las propiedades de un estilo se definen en
etiquetas <item>, etiquetas hijas de la etiqueta <style>. En resumen, la estructura de declaración de un estilo es
la siguiente:
<style name="nombre_del_estilo_1">
<item name="nombre_de_la_propiedad_1">valor_de_la_propiedad_1</item>
<item name="nombre_de_la_propiedad_2">valor_de_la_propiedad_2</item>
...
</style>
<style name="nombre_del_estilo_2">
...
</style>
Todas las propiedades expuestas por los componentes de la plataforma pueden utilizarse para definir un estilo. Por
ejemplo, si un estilo debe definir que el tamaño del texto es de 16 sp, debe asignar valor a la propiedad
android:textSize:
<style name="mediumText">
<item name="android:textSize">16sp</item>
</style>
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Se recomienda encarecidamente, cuando se define una jerarquía de estilos, que el estilo situado en la parte superior
de la jerarquía herede del estilo definido por la plataforma: en caso contrario, ¡hay que garantizar que todas las
propiedades de estilo utilizadas en todos los componentes estén definidas por el desarrollador!
Es posible asignar un estilo a tres niveles: para cualquier aplicación, para una actividad específica o para un
componente. El nombre del estilo es @style/nombre_del_estilo.
Para especificar el estilo para el conjunto de la aplicación, hay que utilizar el atributo android:theme de la etiqueta
<application> en el archivo AndroidManifest.xml. Por ejemplo:
[...]
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
[...]
Si debe aplicarse un estilo particular a todos los componentes de una actividad, hay que informar el atributo
android:theme en la etiqueta <activity> correspondiente en el archivo AndroidManifest:
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme">
Por último, para aplicar un estilo a un componente, hay que informar el atributo style (sin el prefijo android) en la
etiqueta del componente:
<TextView
android:id="@+id/listItemDVD_anyo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/AppTheme"
/>
Por defecto, en la creación de la solución LocDVD, Android Studio ha agregado un estilo para el conjunto de la
aplicación. Este estilo, llamado AppTheme, se indica en la etiqueta <application> del archivo
AndroidManifest.xml.
Si se mira la definición del estilo, parece que este estilo está prácticamente vacío, y hereda sus propiedades del estilo
Theme.AppCompat.Light.DarkActionBar: la aplicación, que usa la biblioteca de soporte, debe utilizar
necesariamente un estilo compatible (hija de Theme.AppCompat).
Utilizando el estilo predefinido, y agregando propiedades, es bastante fácil modificar rápidamente el aspecto gráfico
de la aplicación LocDVD.
Edite el archivo styles.xml.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El color por defecto del texto debe cambiarse para toda la aplicación; de modo que hay que agregar el elemento
correspondiente en la declaración del estilo AppTheme.
Agregue un elemento llamado android:textColor. El valor del color puede definirse, como en HTML, en
valor hexadecimal. Por ejemplo, el color #71728f (color que se aproxima al morado). El código
correspondiente es el siguiente:
<style name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:textColor">#71728f</item>
</style>
El formulario de introducción de datos de los DVD debe modificarse: las etiquetas del formulario deben ser de tamaño
18 sp y estar en «negrita». El texto debe, también, estirarse en anchura, para destacar.
Los estilos de Android permiten, como en HTML, definir «subestilos». Así, si se define un estilo AppTheme, es posible
definir un estilo llamado AppTheme.Label: este último heredará de todas las propiedades del estilo AppTheme,
como si el estilo Label se hubiera definido con AppTheme como estilo padre. Esta flexibilidad permite crear una
jerarquía en la nomenclatura de los estilos que resulta muy práctica.
En el archivo de definición de estilos, agregue un nuevo estilo, llamado AppTheme.Label:
<style name="AppTheme.label">
</style>
El texto debe ser de tamaño 18 sp. Agregue un elemento y especifique el tamaño del texto para el estilo
AppTheme.Label:
<style name="AppTheme.label">
<item name="android:textSize">18sp</item>
</style>
También hay que especificar que el texto debe estar ensanchado. Para ello, el componente TextView
expone la propiedad android:textScaleX, que recibe como parámetro un valor de tipo float. Para
que el resultado se visualice claramente por pantalla, seleccione un valor igual a 1.5:
<style name="AppTheme.label">
<item name="android:textSize">18sp</item>
<item name="android:textScaleX">1.5</item>
</style>
Este estilo debe aplicarse a los componentes TextView del archivo de layout correspondiente a la pantalla de
introducción de datos de un DVD, es decir, el archivo activity_adddvd.xml.
Edite el archivo activity_adddvd.xml.
El estilo debe aplicarse únicamente sobre las etiquetas del formulario, para lo que hay que utilizar la
propiedad style, en vez de las propiedades android:textSize y android:textStyle aplicadas
actualmente. Por ejemplo:
<TextView
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Titulo_de_la_pelicula"
style="@style/AppTheme.label"/>
Esta modificación debe realizarse sobre todos los campos TextView del formulario.
La visualización de la pantalla de introducción de datos muestra que el estilo AppTheme.Label se ha aplicado
correctamente: el texto está conforme a lo definido, y su color hereda directamente del color asignado por el tema
AppTheme.
Cabe destacar que, si bien es sencillo cambiar el color del texto, su apariencia (negrita, cursiva, etc.) en el archivo de
estilo, o bien directamente en la etiqueta de definición del componente en un archivo de layout, no es posible
especificar directamente una tipografía diferente: esta característica solo es compatible con el código Java (la versión
8 de Android retira esta restricción, como veremos en el capítulo Android 8 Oreo). El archivo de tipografía debe
integrarse con la aplicación en la carpeta /assets/ (carpeta que ya contiene el archivo de datos data.txt).
Así, si se incorpora el archivo de tipografía Roboto-Thin.ttf (Roboto es la tipografía oficial de Android) a la
carpeta /assets/, el código que permite asignar esta tipografía al título del DVD, en la ficha de visualización del
DVD, es el siguiente:
Typeface typeface =
Typeface.createFromAsset(getActivity().getAssets(),"Roboto-Thin.ttf");
txtTituloDVD.setTypeface(typeface);
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Crear imágenes redimensionables
Además de modificar la tipografía, se desea agregar un efecto de enmarcado redondeado en las etiquetas del
formulario. Para ello, hay que agregar un recurso de tipo drawable (una imagen, llamada fond_label.png) y
definir esta imagen como imagen de fondo para la propiedad android:background del estilo AppTheme.Label:
<style name="AppTheme.label">
<item name="android:textSize">18sp</item>
<item name="android:textScaleX">1.5</item>
<item name="android:background">@drawable/fond_label</item>
</style>
La imagen utilizada está disponible en el código fuente para descargar correspondiente a este capítulo.
Sin embargo, aparecen dos problemas con la visualización cuando se define como imagen de fondo para las
etiquetas:
l El texto no se posiciona correctamente de manera automática en el centro del marco redondeado.
l El redondeado se deforma cuando el texto que se ha de mostrar es largo.
La siguiente captura de pantalla ilustra ambos problemas:
Para resolver estos problemas, en parte vinculados a la variedad de dimensiones de las pantallas de los terminales
Android, Google ha introducido un nuevo formato de archivo para las imágenes. Este formato, llamado 9patch,
permite indicar las zonas donde se sitúa el texto, así como las partes de la imagen que pueden extenderse o
reducirse sin generar deformaciones. Este formato deriva del formato PNG (Portable Network Graphic) y posee la
extensión .9.png.
La información de deformación y de posicionamiento se integra en el archivo gráfico de la siguiente manera:
l Una línea de un píxel de ancho se agrega en todas las dimensiones de la imagen.
l La zona que puede extenderse o reducirse se define situando píxeles negros sobre el lado izquierdo y la parte superior
de la imagen.
l La zona reservada para el texto se define situando píxeles negros sobre el lado derecho y la parte inferior de la imagen.
Las zonas se ilustran en los dos esquemas siguientes:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Observe que la zonas sombreadas coinciden con los píxeles negros de cada borde de la imagen.
Por otro lado, Android Studio se proporciona con una herramienta que permite producir imágenes en formato 9patch
a partir de imágenes PNG: esta herramienta, llamada Draw 9patch, estaba inicialmente disponible como una
aplicación independiente, distribuida a través del SDK de Android; ahora se encuentra completamente integrada en
Android Studio.
A priori, cualquier aplicación capaz de editar imágenes en formato PNG puede servir para producir imágenes en
formato 9patch: basta con situar píxeles negros que sirvan como guía manualmente. Sin embargo, hay que procurar
que esta aplicación no optimice la paleta de colores (y reemplace los píxeles negros) y no utilice filtro antialiasing sobre
estos píxeles.
Para transformar una imagen png en una imagen 9patch, hay que hacer clic con el botón derecho en el archivo,
almacenado en la carpeta drawable, y seleccionar la opción Create 9patch file, situada en la parte inferior del
menú contextual. Una vez abierto, Draw 9patch presenta en la ventana principal de Android Studio dos ventanas
yuxtapuestas: la ventana de la izquierda contiene la imagen, la ventana de la derecha provee una visualización
previa de la ampliación de la imagen según varias direcciones.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Los píxeles que indican la zona pueden dibujarse con ayuda del ratón, situando el cursor en la zona de un píxel
alrededor de la imagen, o bien utilizando las líneas que bordean la zona rosada, que representa la zona de texto.
Las reglas situadas debajo de la imagen permiten hacer zoom sobre la imagen y aumentar y reducir la deformación
ilustrada en la zona de la derecha.
Como con todos los recursos, no hay que mencionar la extensión del archivo cuando se hace referencia a las
imágenes 9patch.
Así, aplicando como imagen de fondo una imagen 9patch, cuyas zonas se han creado definidas tal y como presenta
la captura de pantalla anterior, las etiquetas se enmarcan correctamente en el formulario, y el marco no sufre
deformaciones.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Dibujar en XML
Cuando la forma que se ha de mostrar es simple, como la que hemos utilizado en la sección anterior, Android propone
una solución alternativa a las imágenes 9patch: las formas definidas en XML.
Las formas diseñadas en XML se consideran, por parte del sistema, como imágenes clásicas y en calidad de esto se
almacenan en la carpeta drawable del proyecto (o en alguna de las carpetas drawable cualificadas).
Vamos a ver cómo reemplazar la forma definida en 9patch por una imagen XML para el fondo de las etiquetas del
formulario:
Sitúe el cursor del ratón en la carpeta /res/drawable/ del proyecto.
Haga clic con el botón derecho y seleccione la opción New y, a continuación, Drawable resource file.
Se abre una ventana emergente que permite introducir el nombre del archivo, así como la etiqueta raíz.
Introduzca fond_label_xml como nombre del archivo, e introduzca shape para la opción Root element.
El archivo se edita en modo texto.
La etiqueta <shape> permite definir la forma principal del elemento visual. Las etiquetas hijas permiten, a
continuación, modificar algunas propiedades de esta forma.
Aquí, la forma básica es un rectangle. Las posibles formas son line (una línea), oval (óvalo), rectangle
(rectángulo) y ring (anillo).
Introduzca el atributo android:shape en la etiqueta shape, y asígnele el valor rectangle:
</shape>
Por defecto, las formas están vacías y no poseen borde, de modo que no son visibles.
Para especificar un color de relleno hay que agregar una de las etiquetas hijas siguientes a la forma:
l Etiqueta <solid>: permite especificar el color de fondo de la forma, introduciendo el atributo android:color.
Por ejemplo, para agregar un fondo de color gris suave a la forma rectangular anterior:
l Etiqueta <gradient>: permite especificar un color de fondo en degradado. Es posible indicar un color de partida
(atributo android:startColor), un color central (android:centerColor) y un color de fin
(android:endColor). El atributo android:type permite indicar la forma del degradado: los posibles valores
son linear (degradado lineal), radial (del centro hacia los bordes) o sweep (barrido circular). También es
posible modificar el ángulo de degradado (en el caso de un degradado lineal) agregando el atributo
android:angle, así como el centro (para los degradados de tipo radial y sweep) utilizando las etiquetas
android:centerX y android:centerY. Observe que los ángulos deben expresarse en grados y que solo se
aceptan valores de ángulos múltiplos de 45 grados.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Para indicar que los bordes de la forma deben mostrarse, hay que agregar como etiqueta hija de la etiqueta
<shape> una etiqueta <stroke>.
La etiqueta <stroke> acepta los siguientes atributos:
l android:width: permite precisar el ancho del borde dibujado.
l android:color: permite indicar el color del borde.
l android:dashGap: permite dibujar un borde punteado, expresando el espacio entre dos puntos.
l android:dashWidth: precisa la anchura del punteado.
Llegados a este punto, si se reemplaza el elemento visual 9patch por el elemento visual XML en la definición del
estilo AppTheme.label, la visualización será la siguiente:
Cuando la forma es un rectángulo, es posible agregar una etiqueta <corners> para modificar el aspecto de los
ángulos del rectángulo.
La etiqueta <corners> acepta el atributo android:radius que permite especificar, en dp, la dimensión del
redondeo. Alternativamente, es posible especificar un redondeo diferente para cada ángulo del rectángulo, con los
atributos android:topLeftRadius, android:topRightRadius,
android:bottomLeftRadius, android:bottomRightRadius. El valor 0 dp representa un ángulo recto.
La forma debe tener las esquinas redondeadas, siendo la medida del redondeo 16 dp. Agregue la etiqueta
correspondiente a la forma:
<stroke
android:width="1dp"
android:color="#7e7e7e"/>
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
<corners
android:radius="16dp" />
</shape>
La etiqueta <padding> permite, en caso de que la forma se utilice como fondo de un componente que muestra
contenido, especificar las dimensiones del espaciado interior. Los atributos de <padding> son android:bottom,
android:top, android:left, android:right, para precisar los espaciados debajo del contenido, por encima,
a la izquierda y a la derecha.
La forma, utilizada aquí como fondo, debe dejar un espacio de 16 dp a la izquierda y a la derecha del
texto, y de 8 dp por encima y por debajo. El código correspondiente es el siguiente:
<padding
android:bottom="8dp"
android:top="8dp"
android:left="16dp"
android:right="16dp"/>
Una vez definida completamente la forma, puede reemplazar al elemento visual 9patch en la definición del estilo
AppTheme.Label.
El archivo fond_label_xml.xml contiene, al final, el siguiente código:
<stroke
android:width="1dp"
android:color="#7e7e7e"/>
<corners
android:radius="16dp" />
<padding
android:bottom="8dp"
android:top="8dp"
android:left="16dp"
android:right="16dp"/>
</shape>
El estilo modificado utiliza el elemento visual XML como fondo:
<style name="AppTheme.label">
<item name="android:textSize">18sp</item>
<item name="android:textScaleX">1.5</item>
<item
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
name="android:background">@drawable/fond_label_xml</item>
</style>
Y la visualización del formulario es conforme a lo deseado:
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Animar las transiciones entre pantallas
Las animaciones de las pantallas, además de aportar una cierta sofisticación a una aplicación, permiten ayudar al
usuario a darse cuenta de los cambios que se producen en la pantalla.
Así, en la aplicación LocDVD, es interesante mejorar la percepción del usuario cuando hace clic en un DVD de la lista
presentada en la página de inicio: actualmente, no se produce ninguna transición entre la lista y la visualización de la
ficha del DVD seleccionado por el usuario.
La plataforma Android soporta varios tipos de animación para, por ejemplo, presentar en escena la aparición de un
componente, la transición entre pantallas, etc. Las animaciones pueden definirse en XML o directamente en el código
Java.
Para animar la transición entre la página de inicio (la lista de DVD) y una ficha de DVD, se decide implementar una
animación de translación: la página de inicio debe desplazarse hacia la izquierda y la ficha del DVD debe aparecer
desde la parte derecha de la pantalla.
Sitúese en el explorador del proyecto y haga clic con el botón derecho en la carpeta /res/.
Seleccione la opción New y, a continuación, Android resource directory.
En la ventana emergente que se abre, seleccione el valor anim en la lista desplegable Resource type y, a
continuación, haga clic en OK: Android Studio crea una nueva carpeta /res/anim/.
Hay que crear dos animaciones: la primera permite aplicar una translación hacia la izquierda de la pantalla de inicio y
la segunda permite la aparición de la ficha del DVD.
Cree el archivo de la primera animación: haga clic con el botón derecho en la carpeta /res/anim y
seleccione la opción New, y a continuación Animation Resource File.
Se abre una ventana emergente que permite introducir el nombre del archivo, así como el elemento raíz.
Introduzca el nombre anim_out.xml y deje el elemento set sugerido para la etiqueta raíz del archivo.
El archivo se muestra automáticamente en el editor XML de Android Studio.
Como etiqueta hija de la etiqueta <set>, agregue una nueva etiqueta, <translate>:
Los atributos de la etiqueta <translate> permiten precisar los parámetros de la animación de translación. El editor
de Android Studio muestra la lista de atributos disponibles para la animación.
Agregue un primer atributo android:duration, que permite especificar la duración de la animación. La
duración se expresa en milisegundos. Aquí, se configura una duración de 500 milisegundos:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
La animación debe desplazar la pantalla de inicio de su posición de origen hacia la izquierda: la translación debe ser
del ancho total de la pantalla.
Para salir por completo hacia la izquierda de la pantalla, el desplazamiento debe ser negativo, y de una cantidad igual
al tamaño de la pantalla, es decir el 100 %.
También existen los atributos fromYDelta y toYDelta, en caso de que la translación deba ser vertical. No los
utilizaremos aquí.
Agregue el atributo android:fromXDelta, con el valor 0, y el atributo android:toXDelta con el
valor -100%:
También es posible modificar la manera en la que se interpola la animación: por defecto, el movimiento de translación
es lineal (constante). La plataforma proporciona varios modos de interpolación.
Agregue el atributo android:interpolator. El editor de Android Studio presenta una lista de posibles
valores. Seleccione la opción @android, a continuación @android:anim/ y, por último, seleccione la
opción @android:anim/accelerate_decelerate_interpolator: la translación de la pantalla se
acelerará al principio del movimiento y será más lenta hacia el final del movimiento.
Una vez definidos los parámetros de la primera animación, ahora hay que crear la animación que desplaza la ficha del
DVD desde la parte derecha de la pantalla.
Agregue un segundo archivo XML de animación. El archivo debe llamarse anim_in.xml.
La animación es, también en este caso, una animación de translación. Solo difieren los parámetros fromXDelta y
toXDelta respecto a la animación anim_out.
Aquí, la ficha del DVD debe partir de una posición inicial en la que esté totalmente oculta, a la derecha. Su posición
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
inicial será, por tanto, un delta de 100 %. La posición final no cambia respecto a la posición normal: el valor
toXDelta es, entonces, igual a 0.
Agregue una animación de translación informando una duración y un interpolador idénticos a la animación
anim_out.
Modifique los valores de los parámetros fromXDelta y toXDelta:
Una vez definidas ambas animaciones, hay que aplicarlas a la transición entre la pantalla de inicio y la ficha del DVD.
La carga de la ficha del DVD se gestiona en la actividad principal, MainActivity, se realiza mediante un fragmento:
el método openDetailFragment se encarga de esta acción.
Edite el archivo MainActivity.java y sitúese en el cuerpo del método openDetailFragment.
El objeto FragmentManager, utilizado para la carga de los fragmentos de la página, presenta el método
setCustomAnimations, que permite modificar la animación por defecto que se utiliza para la transición entre
ambos fragmentos.
La primera versión de setCustomAnimations permite especificar la animación utilizada para el fragmento que se
carga (parámetro enter), así como el que se utiliza para el fragmento que se reemplaza (parámetro exit).
La segunda versión presenta dos parámetros suplementarios que permiten especificar un par de animaciones que se
utilizarán cuando el fragmento se elimine del layout (cuando el usuario presione en el botón de retorno del terminal,
por ejemplo).
Invoque el método setCustomAnimations del objeto FragmentManager. La animación de entrada
es anim_in y la animación de salida es anim_out.
transaction.setCustomAnimations(R.anim.anim_in, R.anim.anim_out);
setCustomAnimations debe invocarse una vez se ha iniciado la transacción y antes de invocar al
método replace. Por otro lado, esta animación solo debe aparecer para las versiones smartphone: en las
tabletas, la lista de DVD debe seguir viéndose. El código correspondiente es el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
FragmentTransaction transaction =
fragmentManager.beginTransaction();
if (findViewById(R.id.detail_placeHolder) != null)
transaction.replace(R.id.detail_placeHolder, fragment);
else {
transaction.setCustomAnimations(R.anim.anim_in,
R.anim.anim_out);
transaction.replace(R.id.main_placeHolder, fragment);
}
transaction.addToBackStack(null);
transaction.commit();
}
Durante la ejecución, cuando el usuario hace clic en un elemento de la lista, la animación es claramente visible y
mejora la comprensión del mecanismo.
En el caso de que la transición deba hacerse entre dos actividades, hay que utilizar, en vez del método
setCustomAnimations de la clase FragmentManager, el método overridePendingTransition de la
clase Activity.
[...]
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(R.anim.anim_in,R.anim.anim_out);
setContentView(R.layout.activity_layout);
}
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
[...]
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Tomar una fotografía
La mayoría de los smartphones y tabletas integran, además de los sensores que ya hemos estudiado en el capítulo
Explotar el teléfono, una o varias cámaras para tomar fotografías.
La aplicación LocDVD utiliza esta capacidad para ofrecer la posibilidad a los usuarios de tomar una fotografía, que se
muestra después en la ficha del DVD.
1. Preparación
La ficha del DVD debe presentar un botón que permita tomar una fotografía e integrar un componente ImageView
para visualizarla. A su vez, hay que agregar un campo en la tabla DVD para almacenar la ruta de acceso a la foto.
Edite el archivo de layout de la ficha del DVD, es decir, el archivo activity_viewdvd.xml.
El botón que permitirá tomar la fotografía está situado debajo de los botones Modificar la fecha de
última visualización y Enviar una notificación. El botón se explota en el código del fragmento, de modo
que hay que asignarle un identificador único, por ejemplo, takePhoto.
El código correspondiente es el siguiente:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tomar_una_foto"
android:id="@+id/takePhoto"/>
Se ha agregado la opción tomar_una_foto en el archivo strings.xml.
El componente ImageView se encarga de mostrar la foto. Debe situarse entre el componente encargado
de la visualización del resumen de la película y la serie de botones. Ocupa todo el ancho de la pantalla y
su altura se adapta a su contenido. Referenciado por el código, su identificador es, por ejemplo,
fotoDVD:
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fotoDVD"/>
La altura total del layout supera, a priori, la altura de los smartphones, de modo que hay que agregar un
contenedor de vistas de tipo ScrollView para que todos los componentes estén accesibles al usuario.
El archivo de layout completo es el siguiente:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
android:layout_margin="8dp"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/titulo_de_la_pelicula"
android:id="@+id/tituloDVD"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="16dp"
android:textSize="22sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/anyo_de_aparicion"
android:textSize="15sp"
android:id="@+id/anyoDVD" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/fecha_ultima_visualizacion"
android:textSize="15sp"
android:id="@+id/fechaVisualizacion" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/layoutActores"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:text="@string/resumen"
android:minLines="5"
android:maxLines="15"
android:id="@+id/resumenPelicula" />
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fotoDVD"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/modificar_fecha_ultima_visualizacion"
android:id="@+id/setFechaVisualizacion"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enviar_una_notificacion"
android:id="@+id/setNotification"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:text="@string/tomar_una_foto"
android:id="@+id/takePhoto"/>
</LinearLayout>
</ScrollView>
Una vez modificado el archivo de layout, hay que tener en cuenta los componentes agregados al código del
fragmento ViewDVDFragment.
Edite el archivo ViewDVDFragment.java.
Hay que agregar una referencia al componente ImageView así como al botón que acabamos de agregar.
Ambos componentes se declaran como variables globales privadas.
Las referencias se obtienen en el método onCreateView del fragmento.
[...]
takePhoto = (Button)view.findViewById(R.id.takePhoto);
fotoDVD=(ImageView)view.findViewById(R.id.fotoDVD);
[...]
Por último, hay que gestionar el clic en el botón invocando un nuevo método initTakePhoto, método
que no recibe ningún parámetro y que no devuelve ningún dato.
[...]
takePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initTakePhoto();
}
});
[...]
El código del fragmento ViewDVDFragment es, de momento, el siguiente (solo se muestra el código modificado,
para una mayor claridad):
TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
TextView txtFechaUltimaVisualizacion;
Button setFechaVisualizacion;
Button setNotification;
Button takePhoto;
ImageView fotoDVD;
DVD dvd;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setFechaVisualizacion.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
setNotification.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
sendNotification();
}
});
takePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initTakePhoto();
}
});
return view;
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
}
@Override
public void onResume() {
super.onResume();
[...]
}
}
La clase
DVD debe presentar una propiedad para almacenar la ruta hacia la foto. Esta propiedad, llamada
rutaFoto, es de tipo String. También hay que agregar una columna a la tabla DVD, columna llamada
rutaFoto de tipo TEXT.
Edite el archivo DVD.java para agregar la propiedad rutaFoto.
La propiedad debe tenerse en cuenta en el constructor privado de la clase:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
}
También hay que modificar los métodos getDVDList y getDVD: agregue el campo rutaFoto en la
llamada al método db.query para cada uno de estos métodos.
while (cursor.moveToNext()) {
listDVD.add(new DVD(cursor));
}
cursor.close();
db.close();
return listDVD;
}
if(cursor.moveToFirst())
dvd = new DVD(cursor);
cursor.close();
db.close();
return dvd;
}
El método getContentValues también debe tener en cuenta la propiedad rutaFoto:
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
for(int i =0;i<this.actores.length;i++) {
listActores+=this.actores[i];
if(i<this.actores.length-1)
listActores+=";";
}
values.put("actores",listActores);
}
values.put("resumen",this.resumen);
values.put("fechaVisualizacion",this.fechaVisualizacion);
values.put("rutaFoto", this.rutaFoto);
return values;
}
Por último, para terminar esta fase de preparación, queda por agregar el campo en la tabla DVD.
Edite la clase LocalSQLiteOpenHelper.
La base de datos debe pasar a la versión 3. Modifique, para ello, el valor de la constante DB_VERSION:
Para gestionar el caso en el que el usuario instala la aplicación directamente a partir de esta nueva
versión (sin actualizar una versión previa), hay que agregar el campo en el método onCreate:
@Override
public void onCreate(SQLiteDatabase db) {
String sqlFilTable ="CREATE TABLE DVD(id INTEGER PRIMARY KEY," +
"titulo TEXT, anyo NUMERIC, actores TEXT, resumen TEXT,"+
"fechaVisualizacion NUMERIC, rutaFoto TEXT);";
db.execSQL(sqlFilTable);
}
También hay que gestionar el caso de una actualización. Defina un nuevo método llamado
upgradeToVersion3 que agregue la columna rutaFoto, de tipo TEXT, a la tabla DVD:
Este método se invoca en el bucle for del método onUpgrade:
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
for(int i = oldVersion;i<newVersion;i++) {
int versionToUpdate = i+1;
if(versionToUpdate==2) {
upgradeToVersion2(db);
} else if(versionToUpdate==3) {
upgradeToVersion3(db);
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
}
}
Una vez terminada la preparación, hay que escribir el cuerpo del método initTakePhoto de la clase
ViewDVDFragment.
2. Implementar la captura
Sería largo y tedioso desarrollar completamente una función para capturar una imagen, dado que los smartphones
ya disponen de esta funcionalidad: lo más sencillo es utilizar la aplicación de capturas integrada en el smartphone,
invocando las intenciones implícitas, ya mencionadas a lo largo de este libro.
Para invocar la aplicación encargada de capturar las imágenes, la intención debe exponer la acción
MediaStore.ACTION_IMAGE_CAPTURE.
Edite la clase ViewDVDFragment y sitúese a nivel del método que se acaba de crear,
initTakePhoto.
Defina una intención implícita, con la acción MediaStore.ACTION_IMAGE_CAPTURE.
Hasta ahora, para invocar una intención, ya fuera explícita o implícita, había que utilizar el método
startActivity. Sin embago, aquí, este método no funciona: la aplicación de captura de imágenes debe devolver
datos a la aplicación LocDVD una vez tomada la foto. Para ello, la plataforma expone el método
startActivityForResult:
El primer parámetro es la intención que debe invocarse, el segundo parámetro es un valor entero que permitirá
informar un identificador para la llamada, identificador que se pasará a la aplicación tras la captura.
Cuando una actividad arranca utilizando el método startActivityForResult, el método
onActivityResult se invoca una vez terminan los procesamientos:
l int requestCode: corresponde al parámetro requestCode que se pasa durante la llamada a
startActivityForResult.
l int resultCode: indica si la operación solicitada se ha desarrollado correctamente. Los posibles valores son
Activity.RESULT_OK y Activity.RESULT_CANCELLED.
l Intent data: objeto que contiene eventualmente datos que envía la actividad invocada.
En resumen, la estructura de la gestión de la captura es la siguiente:
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
private void initTakePhoto() {
[...]
// Lanzamiento de la aplicación de capturas
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent data) {
// gestión del retorno de la captura
}
Invoque el método startActivityForResult en el cuerpo del método initTakePhoto.
Declare la sobrecarga del método onActivityResult e integre una comprobación para verificar que la
llamada corresponde a la hecha en initTakePhoto:
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent data) {
if(requestCode==TAKE_PHOTO ) {
// A completar
}
}
Si el resultado de la operación es Activity.RESULT_CANCELLED, hay que informar al usuario con un
mensaje Toast:
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent data) {
if(requestCode==TAKE_PHOTO ) {
if(resultCode!= Activity.RESULT_OK) {
Toast.makeText(getActivity(),
"No se ha tomado la foto",
Toast.LENGTH_LONG).show();
return;
}
}
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
3. Guardar el resultado
Cuando la llamada se produce con los parámetros por defecto, como ocurre aquí, la aplicación de captura devuelve,
a través del método onActivityResult, una miniatura de la foto que se ha tomado. Esta miniatura está
codificada en los datos Extras de la intención que se ha pasado como parámetro a onActivityResult.
En el marco de la aplicación LocDVD, obtener una miniatura no basta: la resolución de esta imagen es demasiado
baja para que la visualización sea satisfactoria.
Para que la foto tomada se guarde en el soporte de almacenamiento del terminal, hay que pasarle a la intención
que llama el nombre del archivo donde se quiere guardar la foto.
Para ello, hay que informar el dato MediaStore.EXTRA_OUTPUT en la intención:
Pueden seleccionarse varias ubicaciones para el almacenamiento de la foto, en función del objetivo buscado:
l Si la foto es pública, es decir, si está destinada a aparecer en la galería de fotos del terminal, el método estático
Environment.getExternalStoragePublicDirectory, invocado con el parámetro
Environment.DIRECTORY_PICTURES, devuelve un objeto de tipo File que define la ruta de la carpeta
correspondiente:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
l Si la foto está destinada únicamente a la aplicación, es preferible almacenarla en la carpeta devuelta por el método
getExternalFilesDir de la clase Context, utilizada con el parámetro
Environment.DIRECTORY_PICTURES:
getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
En ambos casos, el hecho de escribir en el soporte de almacenamiento implica declarar el permiso
android.permission.WRITE_EXTERNAL_STORAGE, para todos los terminales que funcionan con una versión
de Android inferior a la versión 4.4. En efecto, Android KitKat ha realizado una modificación de los permisos a este
nivel y considera que una aplicación no necesita exponer el permiso WRITE_EXTERNAL_STORAGE para escribir en
la carpeta apuntada por getExternalFilesDir: esta carpeta está dedicada a la aplicación y puede leer y
escribir sin necesidad de permisos.
En el primer caso, hay que agregar el permiso en el archivo AndroidManifest de la manera clásica:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
En el segundo caso, solo hace falta pedir el permiso para los terminales que trabajan con una API de nivel inferior o
igual a 18 (Android KitKat se corresponde con la API de nivel 19):
<uses-permission
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
En el marco de la aplicación, se decide utilizar la carpeta reservada para la aplicación.
Para que la foto se registre correctamente en el espacio de almacenamiento, hay que proporcionar el nombre
completo del archivo en el que debe almacenarse.
En la clase ViewDVDFragment, defina una variable global savedImage de tipo File:
import java.io.File;
[...]
File savedImage;
La variable savedImage debe instanciarse en initTakePhoto. El nombre del archivo estará
compuesto por el prefijo dvd_, seguido del identificador del DVD. La extensión será .jpeg. El código
correspondiente es el siguiente:
savedImage =
new File(
getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"dvd_" + dvd.id + ".jpg");
Agregue por último la URI del archivo definido en los datos Extras de la intención invocada, mediante la
etiqueta MediaStore.EXTRA_OUTPUT:
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(savedImage));
El código completo del método initTakePhoto es el siguiente:
getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"dvd_" + dvd.id + ".jpg");
startActivityForResult(intent, TAKE_PHOTO);
}
Como retorno de la llamada a la captura, es decir, al método onActivityResult, hay que procesar la foto y
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
modificar el DVD en curso.
Edite el método onActivityResult. Si el resultado devuelto por la aplicación de captura corresponde
a Activity.RESULT_OK, hay que asignar valor a la propiedad rutaFoto del DVD y actualizar el
registro. rutaFoto recibe el valor devuelto por el método getAbsolutePath de la clase File:
If(resultCode !=Activity.RESULT_OK) {
[...]
} else {
dvd.rutaFoto = savedImage.getAbsolutePath();
dvd.update(getActivity());
}
El componente ImageView debe mostrar la foto del DVD. Para ello, hay que invocar el método
setImageUri expuesto por la clase ImageView. Este método recibe como parámetro la URI de la
imagen que se ha de mostrar.
if(savedImage!=null) {
dvd.rutaFoto = savedImage.getAbsolutePath();
dvd.update(getActivity());
fotoDVD.setImageURI(Uri.fromFile(savedImage));
}
También hay que tener en cuenta la propiedad rutaFoto cuando se completan los campos del formulario. El
formulario se completa en el método onResume del fragmento.
En el método sobrecargado onResume de ViewDVDFragment, invoque el método setImageURI,
para asociar la foto del DVD si rutaFoto es distinta de null.
A partir de la ruta absoluta de la foto, la URI se obtiene invocando el método estático Uri.parse:
if(dvd.rutaFoto!=null)
fotoDVD.setImageURI(Uri.parse(dvd.rutaFoto));
El código completo del fragmento ViewDVDFragment es el siguiente:
package com.ejemplo.locdvd;
import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
File savedImage;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
setNotification =
(Button)view.findViewById(R.id.setNotification);
takePhoto = (Button)view.findViewById(R.id.takePhoto);
fotoDVD=(ImageView)view.findViewById(R.id.fotoDVD);
setFechaVisualizacion.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
setNotification.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendNotification();
}
});
takePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initTakePhoto();
}
});
return view;
}
getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"dvd_" + dvd.id + ".jpg");
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent data) {
if(requestCode==TAKE_PHOTO ) {
if(resultCode!= Activity.RESULT_OK) {
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Toast.makeText(getActivity(),
"No se ha tomado la foto",
Toast.LENGTH_LONG).show();
return;
}
if(savedImage!=null) {
dvd.rutaFoto = savedImage.getAbsolutePath();
dvd.update(getActivity());
fotoDVD.setImageURI(Uri.fromFile(savedImage));
}
}
}
@Override
public void onResume() {
super.onResume();
txtTituloDVD.setText(dvd.getTitulo());
txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion),
dvd.getAnyo()));
if(dvd.rutaFoto!=null)
fotoDVD.setImageURI(Uri.parse(dvd.rutaFoto));
}
}
Solo queda por resolver un problema que aparece si se prueba a capturar: una vez tomada la foto, cuando el
usuario valida la captura, la aplicación presenta, en lugar de la ficha del DVD abierta anteriormente, la lista de DVD.
Este problema está vinculado al ciclo de vida de las actividades: en efecto, cuando la aplicación de captura pasa al
primer plano, para permitir al usuario tomar la foto, la aplicación LocDVD se pone en pausa. Al volver de la captura,
se invoca el método onResume de la actividad MainActivity (actividad activa cuando se pone en pausa la
aplicación): este método, actualmente, carga el fragmento de visualización de la lista de DVD.
El ciclo de vida de las actividades (abordado en el capítulo Consulta e introducción de datos) precisa que, si bien se
invoca el método onResume cuando la actividad se «despierta» de una pausa, no es el caso del método
onCreate, que solo se invoca durante la creación de la actividad: basta, por tanto, con copiar el código de la
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 15 -
apertura del fragmento ListDVDFragment en el método onCreate. Así, el método onResume no modificará el
fragmento activo, y el usuario verá inmediatamente el resultado de la captura de la foto.
El código que se provee en el espacio de descargas tiene en cuenta esta modificación.
- 16 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Reproducir un sonido
Verdaderas estaciones multimedia, los terminales Android pueden reproducir sin problema cualquier archivo de
sonido, siempre y cuando sea compatible con la plataforma.
La lista completa de formatos soportados está disponible en la siguiente dirección, en el apartado Core Media
Format: http://developer.android.com/guide/appendix/mediaformats.html
1. Leer un archivo de sonido local
La lectura de un archivo sonoro distribuido con la aplicación es muy sencilla.
El archivo de sonido es, para el sistema, un recurso, al igual que los son las imágenes integradas o los archivos de
layout. Los sonidos deben, a este respecto, almacenarse en la carpeta /res/raw/ del proyecto Android.
El framework Android presenta la clase MediaPlayer, clase que se utiliza para la lectura de los archivos
multimedia (sonidos y vídeos).
Además del constructor por defecto, que se utilizará en la siguiente sección, MediaPlayer expone el método
estático create, que devuelve una instancia de MediaPlayer lista para su uso:
El primer parámetro es el contexto de ejecución de la aplicación, el segundo es el identificador del recurso sonoro
que debe recuperarse. Por ejemplo, en el contexto de una actividad:
Una vez agregado el archivo sonido_bip.mp3 a la carpeta /res/raw/ del proyecto, basta con lanzar su
lectura, invocando el método start de la clase MediaPlayer:
mediaPlayer.start();
Hay que tener la precaución, cuando la actividad se pone en pausa, de liberar los recursos invocando el método
release de la clase MediaPlayer, y poniendo a null la instancia utilizada:
mediaPlayer.release();
mediaPlayer = null;
2. Leer un flujo sonoro
La lectura de un flujo sonoro (stream en inglés) es un poco más compleja, en el sentido de que hace falta que
MediaPlayer prepare la lectura del flujo remoto (la preparación integra, por ejemplo, el llenado de un buffer
interno para evitar cortes).
En este caso, hay que seguir las etapas que se presentan a continuación:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Instancie la clase MediaPlayer, utilizando el constructor por defecto.
A continuación, para configurar el objeto MediaPlayer, es preciso especificar la naturaleza del medio que se va a
leer, invocando el método setAudioStreamType. Este método recibe como parámetro un valor entero que
representa el tipo de medio. Los posibles valores están contenidos en la clase AudioManager: por ejemplo
AudioManager.STREAM_ALARM, AudioManager.STREAM_MUSIC, AudioManager.STREAM_SYSTEM, etc.
El flujo sonoro está representado por la constante AudioManager.STREAM_MUSIC. Hay que invocar el
método setAudioStreamType con este parámetro:
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
El método setDataSource permite introducir la dirección del flujo sonoro que debe leerse. El flujo
puede no existir, de modo que este método debe encapsularse en un bloque try/catch:
try {
mediaPlayer.setDataSource(url);
} catch (IOException e) {
e.printStackTrace();
}
Cuando se configura el MediaPlayer, hay que pedirle al sistema que prepare la lectura del flujo. Para ello, la
clase MediaPlayer presenta el método prepare, que lanza la preparación.
Sin embargo, esta operación puede llevar cierto tiempo: por ello, se recomienda encarecidamente no realizar la
llamada a prepare en el thread principal de la aplicación.
Para evitar que el desarrollador tenga que implementar un mecanismo de AsyncTask, el framework propone
también el método prepareAsync que se encarga de preparar el flujo en un thread separado. A continuación, hay
que proveer una instancia de OnPreparedListener, cuyo método onPrepared se invocará cuando el flujo
esté listo para reproducirse.
Antes de interrogar a prepareAsync, hay que proveer una instancia de OnPreparedListener al
objeto MediaPlayer:
mediaPlayer.setOnPreparedListener(new
MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
}
});
Cuando el flujo esté listo, la lectura se lanza invocando el método start:
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
}
Una vez especificada la instancia de OnPreparedListener, la llamada a prepareAsync lanza la
preparación del MediaPlayer. El código completo de la lectura de un flujo sonoro es, por tanto, el
siguiente:
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mediaPlayer.setDataSource(url);
mediaPlayer.setOnPreparedListener(new
MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
Cabe destacar que, en el contexto de lectura de un flujo sonoro, si se lee a través de la red, hay que agregar el
permiso INTERNET al archivo AndroidManifest.
Para gestionar los eventuales errores que pueden producirse durante la lectura del flujo, MediaPlayer expone el
método setOnErrorListener, que permit especificar una instancia de MediaPlayer.OnErrorListener.
De este modo, se invocará el método onError si se produce algún error durante la lectura:
l MediaPlayer mp presenta una referencia al objeto MediaPlayer que ha producido el error.
l intwhat es un indicador que precisa la naturaleza del error. Los posibles valores son MEDIA_ERROR_UNKNOWN
y MEDIA_ERROR_SERVER_DIED.
l int extra precisa el error. Los posibles valores son MEDIA_ERROR_IO (error vinculado a la red o al sistema
de archivos),
MEDIA_ERROR_MALFORMED (error que se produce cuando el flujo no se corresponde con lo
esperado), MEDIA_ERROR_UNSUPPORTED (el tipo de flujo no es compatible con el sistema),
MEDIA_ERROR_TIMED_OUT (una operación de lectura toma demasiado tiempo, generalmente más de 5
segundos).
El método onError debe devolver verdadero si se gestiona el error o falso en caso contrario.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Reproducir un vídeo
Si bien es posible utilizar la clase MediaPlayer para reproducir une vídeo, la plataforma Android provee un
componente más sencillo de utilizar, VideoView.
Esta sección presenta una integración estándar de un componente VideoView, que permitirá leer un vídeo alojado
en un servidor remoto.
La primera etapa consiste en definir un archivo de layout para visualizar el vídeo. El componente
VideoView, como los componentes clásicos que ya hemos abordado, se integra directamente en un
archivo de layout, utilizando la etiqueta <VideoView>. Sin embargo, debe definirse en un contenedor de
vistas de tipo FrameLayout: esto permite al sistema agregar automáticamente los controles de lectura
del vídeo.
Un archivo de layout canónico sería, por ejemplo, el siguiente:
A continuación hay que obtener una referencia al objeto VideoView en el cuerpo de la actividad para
configurar el vídeo.
VideoView videoView =
(VideoView)findViewById(R.id.playVideo_player);
Para integrar los controles que permiten controlar el vídeo (lectura/pausa, barra de progreso, etc.), basta
con instanciar un objeto
MediaController y asignarlo al objeto VideoView. La clase
MediaController provee un constructor que recibe como parámetro el contexto de ejecución de la
aplicación:
El método setMediaController de la clase VideoView permite asignar el controlador al objeto
VideoView:
videoView.setMediaController(media_Controller);
A continuación hay que informar la dirección del vídeo invocando el método setVideoURI de la clase
VideoView:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
videoView.setVideoURI(
Uri.parse("http://techslides.com/demos/sample-videos/small.mp4"));
Incluso aunque es posible invocar directamente el método start del objeto VideoView para lanzar la
lectura del vídeo, es preferible asignar un objeto de tipo OnPreparedListener para realizar la carga del
requestFocus del
vídeo en un thread separado. Por otro lado, se recomienda invocar el método
componente VideoView (requestFocus es un método que expone la clase madre View), para
asegurar que el vídeo se muestre correctamente:
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
videoView.requestFocus();
videoView.start();
}
});
El código completo canónico para la lectura es, así, el siguiente:
videoView.setMediaController(media_Controller);
videoView.setVideoURI(
Uri.parse("http://techslides.com/demos/sample-videos/small.mp4"));
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
videoView.requestFocus();
videoView.start();
}
});
}
Si bien este código funciona correctamente, si se agrega en el cuerpo de una actividad, por ejemplo, no presenta una
experiencia de usuario óptima: el tiempo de carga del vídeo puede ser largo y no aparece ningún mensaje que
informe al usuario de la acción en curso.
Lo más fácil es, ahora, utilizar un componente ProgressDialog, que ya hemos visto en el capítulo Tareas
asíncronas y servicios.
El objeto ProgressDialog debe asignarse antes de la configuración de VideoView y debe cerrarse cuando el
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
vídeo esté listo para su reproducción.
El código correspondiente es, ahora, el siguiente:
videoView.setMediaController(media_Controller);
videoView.setVideoURI(
Uri.parse("http://techslides.com/demos/sample-videos/small.mp4"));
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener()
{
@Override
public void onPrepared(MediaPlayer mp) {
progressDialog.dismiss();
videoView.requestFocus();
videoView.start();
}
});
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Presentación de Bluetooth Low Energy
Los objetos conectados empiezan a invadir nuestro espacio cotidiano: desde el simple sensor de temperatura hasta
el brazalete capaz de medir la actividad física de su usuario, pasando por los altavoces portátiles, la mayoría de estos
nuevos dispositivos requieren un smartphone (o una tableta) para poder funcionar. En la gran mayoría de casos, se
utiliza Bluetooth Low Energy (BLE, en el resto de este capítulo) para realizar estas comunicaciones de corta distancia.
Esta norma, establecida por la compañía Nokia en 2006 a partir de la norma Bluetooth, posee en efecto muchas
ventajas: un consumo de energía controlado, una distancia de comunicación relativamente importante (casi 100 m en
un espacio abierto) y una implementación (relativamente) sencilla.
Este capítulo abandona temporalmente la aplicación LocDVD para abordar una pequeña aplicación que va a detectar
un objeto BLE, conectarse a él e intercambiar información.
Como ocurre a menudo en informática, varios fabricantes han decidido implementar protocolos específicos para la
conexión con objetos BLE: ya sea el formato iBeacons de Apple o EddyStone de Google, estos protocolos se basan en
la norma Bluetooth Low Energy.
En vez de decidirse por uno de estos formatos (EddyStone es el formato preferido para la plataforma Android), este
capítulo presenta un proceso de conexión que funciona con ambos formatos, sin utilizar sus especificidades. Si bien
esto aumenta ligeramente la complejidad del libro, se le deja al lector la libertad de escoger el objeto conectado que
se va a utilizar para los desarrollos.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Detectar un dispositivo BLE
Para intercambiar información con un objeto BLE, se requieren varias etapas:
l Que el terminal Android descubra el objeto.
l El establecimiento de la conexión entre el objeto BLE y el terminal.
l Por último, la lectura y la escritura de información; estas etapas están compuestas, por su parte, de varias subetapas
que detallaremos más adelante.
Esta sección presenta la primera etapa, que va a lanzar un análisis Bluetooth para detectar un objeto BLE.
1. Preparación del proyecto
Lo primero que hay que hacer es crear una nueva aplicación con Android Studio; esta aplicación, muy simple,
contendrá una única pantalla y estará destinada a terminales equipados con Android 4.3 o superior: la norma BLE
no es compatible con las versiones anteriores de Android.
Cree un nuevo proyecto Android Studio, llamado BLE, y con el dominio ejemplo.com.
Este proyecto debe ser compatible con smartphones y tabletas a partir de la versión Android 4.3 (Jelly
bean, API 18).
La plantilla seleccionada es «Empty Activity» que utiliza, como LocDVD, la biblioteca de soporte V7: la
actividad principal, MainActivity, hereda de AppCompatActivity.
Una vez terminado el asistente de creación del proyecto, Android Studio presenta una clase MainActivity y su
archivo de layout asociado, activity_main.xml.
La interfaz de la aplicación es muy simple: en primer lugar, una zona de texto presenta el nombre y la dirección del
objeto BLE detectado. Esta zona de texto debe permitir mostrar varias líneas.
El layout correspondiente es, por ejemplo, el siguiente:
package com.ejemplo.ble;
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
TextView text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text =(TextView)findViewById(R.id.main_text);
}
}
2. Gestión de permisos
La manipulación del Bluetooth Low Energy requiere registrar en el Manifest los permisos correspondientees:
BLUETOOTH y BLUETOOTH_ADMIN:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
Paradójicamente, si la aplicación se dirige a una versión del SDK superior a la versión 22 (como ocurre aquí, tal y
como hemos indicado por defecto en el asistente de creación del proyecto), hay que pedir también el permiso
ACCESS_FINE_LOCATION, ¡permiso correspondiente a la geolocalización fina!
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Por otro lado, como el permiso ACCES_FINE_LOCATION está definido como un permiso sensible a partir de
Android 6 (Marshmallow), también hay que implementar una petición de permisos al usuario, como vimos en el
capítulo Explotar el teléfono. El procedimiento se recuerda a continuación:
En primer lugar hay que comprobar si todavía no se ha concedido el permiso:
if(ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) !=
PackageManager.PERMISSION_GRANTED) {
// Permiso no concedido
A continuación, hay que comprobar si es necesario ofrecer una explicación al usuario (en caso de que no
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
haya rechazado previamente este permiso):
if (shouldShowRequestPermissionRationale(
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Presentar una ventana emergente para explicar la petición...
}
@Override
public void onRequestPermissionsResult(int requestCode, String[]
permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode==REQUEST_PERMISSION) {
if(permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Tras la petición del permiso, se invoca un nuevo método startBLEScan. El código completo de gestión
del permiso ACCESS_FINE_LOCATION es, por ejemplo, el siguiente:
@Override
protected void onResume() {
super.onResume();
ensurePermission();
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this,
R.string.permiso_obligatorio,
Toast.LENGTH_LONG).show();
finish(); // Se sale de la aplicación
}
});
builder.setPositiveButton("Sí", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
askPermission();
}
});
builder.show();
} else {
askPermission();
}
} else {
startBLEScan();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[]
permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if(requestCode==REQUEST_PERMISSION) {
if(permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) &&
grantResults[0] ==
PackageManager.PERMISSION_GRANTED)
{
startBLEScan();
}
}
3. Inicialización de BluetoothManager
El método startBLEScan debe inicializar los objetos necesarios para lanzar el escáner BLE e iniciar la búsqueda
de dispositivos Bluetooth Low Energy.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
En primer lugar hay que obtener una instancia de BluetoothManager, invocando el método
getSystemService. El servcio correspondiente es Context.BLUETOOTH_SERVICE. Todos los objetos deben
estar accesibles en el código de la clase MainActivity y se declaran como variables de clase:
BluetoothManager bluetoothManager;
[...]
bluetoothManager=
(BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
La instancia de BluetoothManager expone el método getAdapter, que permite obtener un objeto de tipo
BluetoothAdapter. Este objeto se encargará, entre otros, de lanzar la búsqueda de dispositivos BLE.
BluetoothAdapter bluetoothAdapter;
[...]
bluetoothAdapter = bluetoothManager.getAdapter();
Antes de lanzar el análisis BLE, es necesario garantizar que está activado el Bluetooth en el terminal del usuario.
Para ello, BluetoothAdapter presenta el método isEnabled, que devuelve false si el Bluetooth no está
activo.
Idealmente, si el Bluetooth no está activo, hay que presentar al usuario la pantalla correspondiente que permita
activar esta funcionalidad. Esto se hace invocando una intención implícita, con la acción
BluetoothAdapter.ACTION_REQUEST_ENABLE.
if(!bluetoothAdapter.isEnabled()) {
Intent askBLE = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(askBLE, REQUEST_ENABLE_BLE);
}
La respuesta del usuario se trata, como toda llamada a startActivityForResult, en el método sobrecargado
onActivityResult.
A continuación se muestra una propuesta de código que integra todos los puntos vistos hasta el momento:
BluetoothManager bluetoothManager=null;
BluetoothAdapter bluetoothAdapter=null;
[...]
private void startBLEScan() {
if(bluetoothManager==null)
bluetoothManager=
(BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
if(bluetoothAdapter==null)
bluetoothAdapter = bluetoothManager.getAdapter();
if(!bluetoothAdapter.isEnabled()) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
Intent askBLE = new
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(askBLE, REQUEST_ENABLE_BLE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_ENABLE_BLE && requestCode==RESULT_OK)
startBLEScan();
}
4. Búsqueda de objetos Bluetooth Low Energy
Una vez terminada la fase de inicialización, ahora es posible lanzar el análisis y la búsqueda del dispositivo (llamada
comúnmente escaneo) Bluetooth Low Energy.
Según la versión de Android, el lanzamiento del escaneo se hace de una manera diferente.
a. Lanzar el escaneo antes de Android 21 (Lollipop)
El lanzamiento del escaneo, con Android JellyBean o Kitkat, se hace mediante el objeto BluetoothAdapter
obtenido anteriormente. Para ello, hay que invocar el método startLeScan.
Este método recibe como único parámetro un objeto de tipo BluetoothAdapter.LeScanCallback, objeto
que se invocará para cada objeto BLE detectado: startLeScan es, como la mayoría de los métodos de
interacción con el BLE, asíncrono.
BluetoothAdapter.LeScanCallback leScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi,
byte[] scanRecord) {
}
};
En el método startBLEScan, hay que comprobar si la versión de Android ejecutada es inferior a la
versión 21 y lanzar el escaneo:
if(android.os.Build.VERSION.SDK_INT<21) {
bluetoothAdapter.startLeScan(leScanCallback);
} else {
[...]
}
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Veremos a continuación cómo tratar los dispositivos BLE detectados.
b. Lanzar el escaneo a partir de Android 21
Con Lollipop, una nueva clase se encarga de lanzar el análisis BLE: la clase BluetoothLeScanner, que expone
el método startScan.
Para obtener una instancia de BluetoothLeScanner, hay que invocar el método getBluetoothLeScanner
del objeto BluetoothAdapter.
BluetoothLeScanner bluetoothLeScanner;
[...}
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
El método
startScan, del objeto bluetoothLeScanner, recibe como parámetro un objeto de tipo
ScanCallback. Se invocará el método onScanResult, que debe implementarse, cuando el sistema detecte un
objeto BLE.
Declare, en la clase MainActivity, una instancia de ScanCallback. El procesamiento se verá más adelante.
Una vez definido el objeto ScanCallback, ahora es posible lanzar el escaneo para aquellos terminales
equipados con Lollipop.
if(android.os.Build.VERSION.SDK_INT<21) {
bluetoothAdapter.startLeScan(leScanCallback);
} else { // versión >= Lollipop
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(scanCallback);
5. Detener el análisis
La fase de búsqueda de dispositivo Bluetooth Low Energy consume, relativamente, bastante energía, de modo que
es importante detener la búsqueda cuando se encuentre el objeto buscado, o bien pasado cierto periodo de
tiempo. Por otro lado, algunos fabricantes de terminales Android no permiten conectarse a un objeto BLE si el
escaneo sigue activo.
Lo más sencillo para detener el escaneo es utilizar un objeto Handler, que expone el método postDelayed.
Este método permite, en efecto, definir el código que se ejecutará pasado cierto intervalo.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Handler handler = new Handler();
handler.postDelayed([...]);
Runnable es una interfaz que presenta el método run.
Pasado el intervalo definido en el método postDelayed, debe invocarse el método (por definir) stopBLEScan.
Como ocurre a menudo, los desarrolladores de Android prefieren una versión más compacta, versión que se
encuentra generalmente en todos los ejemplos de código:
Ahora hay que definir el método stopBLEScan, que debe detener la búsqueda BLE.
Como ocurre con el lanzamiento del escaneo, el código difiere en función de si el terminal del usuario ejecuta
Android Lollipop o una versión inferior.
Para las versiones Android 18 a 20, se invoca al método stopLeScan. Este método recibe como parámetro el
objeto de tipo BluetoothAdapter.LeScanCallback invocado durante la ejecución del escaneo.
Para las versiones iguales o superiores a Android 21, se utiliza el método stopScan del objeto
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
BluetoothLeScanner. Este método recibe como parámetro el objeto de tipo ScanCallback utilizado en la
llamada a startScan.
El código del método stopBLEScan es, por tanto, el siguiente:
6. Explotar el resultado del escaneo
Para terminar con la fase de búsqueda del objeto BLE, queda por procesar el resultado del escaneo, bien con el
método onLeScan del objeto BluetoothAdapter.LeScanCallback para Android 18, o bien con el método
onScanResult del objeto ScanCallback para las versiones iguales o superiores a Lollipop.
Las firmas de estos métodos son las siguientes:
Para el método onLeScan (Android 18):
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)
Y para onScanResult:
@Override
public void onScanResult(int callbackType, ScanResult result)
Si bien estos métodos presentan firmas diferentes, encontramos la misma información.
El método onLeScan presenta los tres parámetros siguientes:
l int rssi: representa la fuerza de la señal Bluetooth Low Energy recibida por el terminal. RSSI es el acrónimo de
Received Signal Strength Indicator.
l byte[] scanRecord: array de bytes que contienen la información expuesta eventualmente por el objeto
conectado.
El método onScanResult presenta, por su parte, los siguientes parámetros:
l int callbackType: este valor entero precisa el resultado del análisis Bluetooth en caso de que se hayan
aplicado uno o varios filtros a la búsqueda: no abordaremos la creación de filtros de búsqueda en este libro, pero
debe saber que es posible aplicar varios filtros para completar un análisis BLE más preciso (en este caso, se utiliza
una clase complementaria ScanFilter.Builder, y se invoca una versión de startScan que reciba como
parámetro un array de ScanFilter). El parámetro callbackType precisa, así, cómo se invoca el método
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
onScanResult respecto a los filtros definidos.
l ScanResult result: este objeto representa el resultado del escaneo. Expone, en particular, los métodos
getDevice que devuelven un objeto de tipo BluetoothDevice (como el primer parámetro de onLeScan),
y getRssi, que devuelve un valor entero que representa la fuerza de la señal: encontramos, así, los mismos datos
que con el método onLeScan.
Para minimizar el procesamiento, basta con definir un método onDeviceDetected, que se invocará por los
métodos onLeScan y onScanResult:
// ¡A completar!
El procesamiento de la detección de un objeto BLE es, de momento, el siguiente:
// API 18
BluetoothAdapter.LeScanCallback leScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[]
scanRecord) {
onDeviceDetected(device,rssi);
}
};
// API 21
ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
onDeviceDetected(result.getDevice(), result.getRssi());
}
};
// ¡A completar!
En primer lugar, el método onDeviceDetected debe mostrar, en el componente TextView, información acerca
del objeto Bluetooth Low Energy detectado: su nombre, su dirección, así como la fuerza de la señal.
Implemente el contenido de onDeviceDetected. El objeto BluetoothDevice expone los métodos getName y
getAddress, que permiten obtener el nombre y la dirección del objeto.
A continuación se muestra un ejemplo de implementación:
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
private void onDeviceDetected(BluetoothDevice device, int rssi) {
String info = "Objeto detectado:";
info+="\nNombre:" + device.getName();
info+="\nDirección:" + device.getAddress();
info+="\nRSSI: " + String.valueOf(rssi);
text.setText(info);
}
Una vez efectiva la detección del objeto BLE, puede realizarse la conexión entre el BLE y el terminal Android.
El código completo de MainActivity es, de momento, el siguiente:
TextView text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text =(TextView)findViewById(R.id.main_text);
ensurePermission();
}
Manifest.permission.ACCESS_FINE_LOCATION)!=
PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale
(Manifest.permission.ACCESS_FINE_LOCATION)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.peticion_permiso_titulo);
builder.setMessage(R.string.explicacion_permiso);
builder.setNegativeButton("No", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this,
R.string.permiso_obligatorio,
Toast.LENGTH_LONG).show();
finish();
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 11 -
}
});
builder.setPositiveButton("Sí", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
askPermission();
}
});
builder.show();
} else {
askPermission();
}
} else {
startBLEScan();
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if(requestCode==REQUEST_PERMISSION) {
if(permissions[0].equals
(Manifest.permission.ACCESS_FINE_LOCATION) &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startBLEScan();
}
}
}
if(!bluetoothAdapter.isEnabled()) {
Intent askBLE = new
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(askBLE, REQUEST_ENABLE_BLE);
return;
}
if(android.os.Build.VERSION.SDK_INT<21) {
bluetoothAdapter.startLeScan(leScanCallback);
Log.d(TAG,"Escaneo lanzado en versión 18");
} else {
- 12 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(scanCallback);
Log.d(TAG,"Escaneo lanzado en versión 21");
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_ENABLE_BLE && requestCode==RESULT_OK)
startBLEScan();
}
// API 18
BluetoothAdapter.LeScanCallback leScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi,
byte[] scanRecord) {
onDeviceDetected(device,rssi);
}
};
// API 21
ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
onDeviceDetected(result.getDevice(), result.getRssi());
}
};
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 13 -
}
- 14 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Conectar un objeto
Para poder interactuar con un objeto BLE, el terminal Android y el objeto deben estar emparejados. Un objeto BLE
solo puede emparejarse a un único terminal a la vez: esta fase es, hablando con propiedad, la fase de conexión.
En el contexto de la aplicación, la conexión se lanza automáticamente cuando se detecta un objeto BLE.
La conexión se realiza mediante la instancia de BluetoothDevice obtenida en la sección anterior tras la fase de
descubrimiento. Para ello, BluetoothDevice expone el método connectGatt, que recibe los siguientes
parámetros:
l Context context: el contexto de ejecución. Típicamente, la actividad en curso.
l boolean autoConnect: indicador que permite especificar si la conexión debe hacerse inmediatamente (falso) o
automáticamente cuando se ha detectado el objeto.
l BluetoothGattCallback bluetoothGattCallback: este objeto se utiliza durante el intercambio de
información entre el terminal y el objeto BLE. Expone varios métodos, cada uno correspondiente a una fase (o un
estado) del proceso de comunicación.
El método connectGatt devuelve un objeto BluetoothGatt, del que se conserva una referencia en la clase
MainActivity.
El acrónimo Gatt aparece en todos los nombres de las clases que permiten la comunicación Bluetooth Low Energy.
Significa Generic Attribute Profil;, este término representa el conjunto de normas que rigen la comunicación entre
dispositivos BLE.
Seleccione, de momento, únicamente el primer método de la lista, es decir onConnectionStateChange.
Si no aparece la ventana emergente, puede obtener la lista de métodos para sobrecargar utilizando la
combinación de teclas [Ctrl] [Insert] y seleccionando la opción Override Methods.
Una vez definido este objeto, es posible invocar connectGatt en el método onDeviceDetected. El
parámetro autoConnect vale false, para que la conexión se haga inmediatamente:
BluetoothGatt bluetoothGatt;
[...]
private void onDeviceDetected(BluetoothDevice device, int rssi) {
String info = "Objeto detectado:";
info+="\nNombre:" + device.getName();
info+="\nDirección:" + device.getAddress();
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
info+="\nRSSI: " + String.valueOf(rssi);
text.setText(info);
Antes de ir más lejos, resulta esencial prever qué ocurrirá cuando se cierre la aplicación: si se ha establecido una
conexión Bluetooth, es imprescindible, en efecto, cerrarla antes de salir de la aplicación, pues en caso contrario el
objeto BLE podría quedar en un estado conectado.
La instancia de BluetoothGatt se encarga de cerrar la conexión. Lo más sencillo, en el marco de la aplicación, es
cerrar automáticamente la conexión cuando se destruye la actividad:
En el cuerpo de la clase MainActivity, haga [Ctrl][Insert], seleccione Override Methods y a
continuación seleccione la opción onDestroy. El código se genera automáticamente.
En el método onDestroy, invoque, si el objeto bluetoothGatt es distinto de null, el método
close, que cierra la conexión Bluetooth, y asigne bluetoothGatt a null.
@Override
protected void onDestroy() {
super.onDestroy();
if(bluetoothGatt!=null) {
bluetoothGatt.close();
bluetoothGatt = null;
}
}
Como hemos indicado, es el objeto de tipo BluetoothGattCallback el encargado de realizar lo esencial de las
operaciones de comunicación entre el terminal y el objeto BLE. De momento, solo se implementa el método
onConnectionStateChange (parcialmente). Este método se invoca, como su nombre indica, cuando se produce
algún cambio en el estado de la conexión entre ambos dispositivos BLE. Los parámetros del método indican cuál es la
naturaleza del cambio.
La firma de este método es la siguiente:
BluetoothGatt gatt: es una referencia al objeto BluetoothGatt obtenido en la llamada a connectGatt.
int status: indicador que especifica si la operación se desarrolla con éxito, status recibe entonces el valor
BluetoothGatt.GATT_SUCCESS. Si se produce algún error, status recibe el valor del código devuelto: la lista de
códigos de error está disponible en la dirección
https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_CONNECTION_CONGESTED.
int newState: indica si el objeto está ahora conectado (valor BluetoothProfile.STATE_CONNECTED) o
desconectado ( BluetoothProfile.STATE_DISCONNECTED).
Por ahora, el método onConnectionStateChange indica solo si el objeto está conectado o
desconectado. Esta información se da simplemente mediante un mensaje Toast. Sin embargo, el método
onConnectionStateChange no se invoca desde el thread principal de la aplicación, de modo que no es
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
posible mostrar un mensaje Toast: hay que utilizar el método runOnUIThread, que se ha explicado
brevemente en el capítulo Tareas asíncronas y servicios.
Ahora es fácil mostrar, cuando cambia el estado de la conexión, un mensaje para informar al usuario:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Leer una característica
Una vez conectado el objeto BLE, ahora es posible intercambiar datos con el terminal Android.
En la norma Bluetooth Low Energy, los datos de un objeto se llaman características (characteristics en inglés). Las
características se agrupan por servicio. Las características y los servicios se identifican por un UUID (Universally Unique
Identifier), el conjunto lo define el consorcio Bluetooth.
Los UUID de los servicios y de las características se construyen según el siguiente principio:
l El formato general de los UUID es fijo: 0000xxxx00001000800000805f9b34fb
l El consorcio Bluetooth define, para cada servicio y cada característica de la norma, un número de cuatro bytes, que
reemplaza a las xxxx en el formato general.
Así, por ejemplo, el servicio Battery Level, que contiene la característica del mismo nombre que permite conocer el nivel
de carga de la batería del objeto conectado, posee el número 0x180F: su UUID es, por tanto, 0000180f00001000
800000805f9b34fb.
El consorcio presenta la lista de servicios y de características de la norma en las siguientes direcciones:
l Para los servicios: https://www.bluetooth.com/specifications/gatt/services
l Para las características: https://www.bluetooth.com/specifications/gatt/characteristics
La siguiente tabla es un extracto de la lista de servicios presentados por el consorcio.
Conviene utilizar solo letras minúsculas para los UUID: si bien las mayúsculas se admiten a veces, la plataforma
Android aplica reglas estrictas relativas a los UUID y solo acepta minúsculas.
La aplicación debe, una vez conectada, mostrar el nivel de carga de la batería del objeto. De modo que, según la
norma, hay que leer la característica número 0x2A19 (el UUID es, entonces, 00002a19000010008000
00805f9b34fb), característica que pertenece al servicio 0x180F.
No es posible leer una característica directamente, ni direccionar un servicio: hay que pasar por un proceso de
descubrimiento de servicios y, para el servicio o los servicios correspondientes, recorrer la lista de características
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
expuestas para obtener una referencia a cada característica que se desee leer (o escribir).
El objeto BluetoothGatt, que se obtiene con la llamada al método connectGatt permite lanzar el
descubrimiento de los servicios, exponiendo el método discoverServices.
En el método onConnectionStateChange, si el nuevo estado del objeto es conectado, invoque el
método discoverServices.
if(newState== BluetoothProfile.STATE_CONNECTED) {
message = "Objeto conectado";
bluetoothGatt.discoverServices();
}
else {
message = "Objeto desconectado";
}
showToastFromBackground(message);
El método
discoverServices es asíncrono: es el método onServicesDiscovered del objeto
BluetoothGattCallback el que se invoca cuando se descubre un servicio.
En el cuerpo de la declaración del objeto BluetoothGattCallback, de la misma manera que para el
método onConnectionStateChanged, utilice la combinación de teclas [Ctrl] [Insert] y seleccione el
método onServicesDiscovered: se genera automáticamente el código presentado a continuación.
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status)
{
super.onServicesDiscovered(gatt, status);
}
};
onServicesDiscovered presenta dos parámetros: el objeto BluetoothGatt, ya estudiado, y un valor entero
que representa el estado del descubrimiento. El valor BluetoothGatt.GATT_SUCCESS indica que el proceso de
descubrimiento se ha desarrollado correctamente.
Una vez descubiertos los servicios, el objeto BluetoothGatt da acceso a la lista de servicios exponiendo el
método getServices. Para obtener una referencia sobre la característica Battery_Level, hay que iterar sobre la
lista de servicios, identificar el servicio correspondiente a la característica y mostrar las características de dicho
servicio.
El método getServices devuelve una lista de instancias de BluetoothGattService, objeto que representa un
servicio BLE.
En el método onServicesDiscovered, si el estado es GATT_SUCCESS, construya un bucle for each
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
para iterar sobre la lista de servicios:
Hay que identificar el servicio Battery Level mediante su UUID. Para obtener un UUID a partir de una
cadena de caracteres, hay que invocar el constructor estático UUID.fromString de la clase UUID.
UUID batteryLevelServiceUUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
En el bucle for, compruebe si el UUID de cada servicio corresponde al UUID definido. Para obtener el UUID
de un objeto BluetoothGattService, hay que invocar el método getUuid:
UUID batteryLevelServiceUUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
}
}
Una vez identificado el servicio buscado, hay que recorrer la lista de características que expone y
comprobar el UUID de cada servicio. La lista de características de un servicio está accesible a través del
método getCharacteristics del objeto BluetoothGattService. Este método devuelve una lista
de BluetoothGattCharacteristic. Previamente, hay que definir el UUID de la característica
buscada:
UUID batteryLevelServiceUUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
UUID batteryLevelCharacteristicUUID =
UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
}
}
}
Para obtener la referencia a la característica deseada, solo queda por comprobar el UUID de cada
característica expuesta. Aquí también, el UUID de una característica se obtiene invocando el método
getUuid de la clase BluetoothGattCharacteristic:
for(BluetoothGattCharacteristic c: service.getCharacteristics())
{
if(c.getUuid().equals(batteryLevelCharacteristicUUID)) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
}
}
Una vez obtenida la referencia a la característica Battery Level, para leer el valor de la característica hay
que invocar al método readCharacteristic del objeto BluetoothGatt. El método
readCharacteristic recibe como único parámetro el objeto BluetoothGattCharacteristic
seleccionado.
for(BluetoothGattCharacteristic c: service.getCharacteristics()) {
if(c.getUuid().equals(batteryLevelCharacteristicUUID)) {
bluetoothGatt.readCharacteristic(c);
}
}
Aquí también, el método readCharacteristic es ascríncono: se invocará el método onCharacteristicRead
del objeto BluetoothGattCallback con cada lectura de característica.
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
El método onCharacteristicRead expone, además del objeto BluetoothGatt, una instancia de
BluetoothGattCharacteristic: la característica leída y un valor entero que indica el estado de la operación
de lectura.
En el método onCharacteristicRead, hay que comprobar si la característica leída es la buscada
(Battery Level) comprobando su UUID. El valor se obtiene, a continuación, invocando el método getValue
de la clase BluetoothGattCharacteristic o alguna de sus variantes en función del formato del
dato que se ha de leer.
Para conocer el formato del dato, hay que referirse a la información proporcionada por la norma Bluetooth
para cada característica de la norma (consulte las direcciones del consorcio que se han indicado al comienzo
de esta sección). Para la característica Battery Level, el consorcio define la siguiente información (que se
obtiene haciendo clic en la característica de la lista):
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El nivel de batería es un valor entero, con formato uint8. De modo que es posible leer el valor utilizando el método
getIntValue de la clase BluetoothGattCharacteristic. El método getIntValue recibe como parámetro
el formato del dato que se va a utilizar y un valor entero que representa el offset del dato, si existe. Aquí, el offset es
0, pues no se indica nada en la norma. El formato se define mediante una de las constantes FORMAT_xxxx de la clase
BluetoothGattCharacteristic, aquí FORMAT_UINT8:
if (characteristic.equals(UUID.fromString("00002a19-0000-1000-
8000-00805f9b34fb"))) {
Log.d(TAG, "Nivel de batería:" + characteristic.getIntValue
(BluetoothGattCharacteristic.FORMAT_UINT8, 0));
}
Tan solo queda por mostrar el nivel de batería leído. En el contexto de la aplicación, basta con modificar
ligeramente el archivo de layout para agregar un TextView debajo del TextView ya existente
(modificando su tamaño) e invocando el método setText de este nuevo TextView. Preste atención,
también aquí el método onCharacteristicRead se invoca desde un thread como tarea de fondo, de
modo que hay que llamar al método runOnUIThread:
[...]
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
super.onCharacteristicRead(gatt, characteristic, status);
if(status==BluetoothGatt.GATT_SUCCESS) {
if (characteristic.getUuid().equals(UUID.fromString("00002a19-0000-1000-
8000-00805f9b34fb"))) {
int batteryLevelValue =
characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
setBatteryLevelFromBackground(batteryLevelValue);
}
}
}
Así, el código completo para la lectura del nivel de batería de un objeto conectado es el siguiente:
TextView text;
TextView batteryLevel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text =(TextView)findViewById(R.id.main_text);
batteryLevel =(TextView)findViewById(R.id.main_batteryLevel);
ensurePermission();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(bluetoothGatt!=null) {
bluetoothGatt.close();
bluetoothGatt = null;
}
}
Manifest.permission.ACCESS_FINE_LOCATION)!=
PackageManager.PERMISSION_GRANTED) {
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
if (shouldShowRequestPermissionRationale
(Manifest.permission.ACCESS_FINE_LOCATION)) {
AlertDialog.Builder builder = new
AlertDialog.Builder(this);
builder.setTitle(R.string.peticion_permiso_titulo);
builder.setMessage(R.string.explicacion_permiso);
builder.setNegativeButton("No", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int
which) {
Toast.makeText(MainActivity.this,
R.string.permiso_obligatorio, Toast.LENGTH_LONG).show();
finish();
}
});
builder.setPositiveButton("Sí", new
DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int
which) {
askPermission();
}
});
builder.show();
} else {
askPermission();
}
} else {
startBLEScan();
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults);
if(requestCode==REQUEST_PERMISSION) {
if(permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) &&
grantResults[0] ==
PackageManager.PERMISSION_GRANTED) {
startBLEScan();
}
}
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
bluetoothManager= (BluetoothManager)getSystemService
(Context.BLUETOOTH_SERVICE);
if(bluetoothAdapter==null)
bluetoothAdapter = bluetoothManager.getAdapter();
if(!bluetoothAdapter.isEnabled()) {
Intent askBLE = new
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(askBLE, REQUEST_ENABLE_BLE);
return;
}
if(android.os.Build.VERSION.SDK_INT<21) {
bluetoothAdapter.startLeScan(leScanCallback);
Log.d(TAG,"Escaneo lanzado en versión 18");
} else {
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(scanCallback);
Log.d(TAG,"Escaneo lanzado en versión 21");
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_ENABLE_BLE && requestCode==RESULT_OK)
startBLEScan();
}
// API 18
BluetoothAdapter.LeScanCallback leScanCallback = new
BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi,
byte[] scanRecord) {
onDeviceDetected(device,rssi);
}
};
// API 21
ScanCallback scanCallback = new ScanCallback() {
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
onDeviceDetected(result.getDevice(), result.getRssi());
}
};
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if(status==BluetoothGatt.GATT_SUCCESS) {
UUID batteryLevelServiceUUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
UUID batteryLevelCharacteristicUUID =
UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
for(BluetoothGattService service: gatt.getServices()) {
if(service.getUuid().equals(batteryLevelServiceUUID)) {
for(BluetoothGattCharacteristic c:
service.getCharacteristics()) {
if(c.getUuid().equals(batteryLevelCharacteristicUUID)) {
bluetoothGatt.readCharacteristic(c);
}
}
}
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.d(TAG,"Característica leída:" + characteristic.getUuid());
if(status==BluetoothGatt.GATT_SUCCESS) {
if
(characteristic.getUuid().equals(UUID.fromString("00002a19-0000-1000-8000-
00805f9b34fb"))) {
int batteryLevelValue =
characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
setBatteryLevelFromBackground(batteryLevelValue);
}
}
}
};
- 10 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Escribir una característica
De la misma manera que solo es posible leer las características de un objeto BLE si está conectado, es posible escribir
valores en las características únicamente cuando el objeto se encuentra conectado al terminal Android.
El proceso que debe implementarse es exactamente el mismo: en primer lugar, hay que lanzar un descubrimiento de
servicios y, a continuación, una vez encontrado el servicio, iterar sobre las características del servicio.
La única diferencia se produce cuando se encuentra la característica deseada: en lugar de invocar el método
readCharacteristic del objeto BluetoothGatt, hay que invocar el método writeCharacteristic.
Este método recibe un único parámetro, que es la característica en cuestión (es decir, el objeto
BluetoothGattCharacteristic visto anteriormente).
En efecto, hay que asignar el valor correspondiente a la característica. Esto se hace invocando alguna de las
variantes del método setValue, variantes que proponen distintos tipos de datos: array de bytes, entero, cadena
de caracteres.
Una vez escrito el dato, se utiliza, de nuevo, el objeto BluetoothGattCallback para conocer el resultado de la
escritura, mediante el método para sobrecargar onCharacteristicWrite, método cuya firma es idéntica a la del
método onCharacteristicRead.
Antes de terminar este capítulo de presentación del Bluetooth Low Energy, nos queda por mencionar la noción de
notificación: algunos objetos BLE ofrecen la posibilidad al terminal de definir notificaciones para las características que
se han de leer. El terminal que esté abonado a una característica será notificado, siempre a través del
BluetoothGattCallback, de cada cambio de valor de la característica correspondiente.
Esta posibilidad es propia de cada característica, que, en función del objeto BLE, puede estar disponible en lectura,
escritura, notificación.
Mencionaremos por último, para ayudar al desarrollador a descubrir la programación con Bluetooth Low Energy, que
en Play Store hay disponibles varias aplicaciones, desarrolladas mayoritariamente por los propios fabricantes de chips
Bluetooth. Estas aplicaciones permiten, por lo general, analizar los objetos BLE detectables, conectarse a un BLE y
leer y escribir las características expuestas.
Por mencionar una aplicación, recomendamos nRF Connect, que ofrece gratuitamente la compañía Nordic
SemiConductor.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Introducción
Durante el verano de 2017, Google ha publicado, tras varias versiones calificadas como «Developper Preview», la
versión final de la última actualización del sistema Android. Como cada nueva versión, Android 8 Oreo (API 26) aporta
su conjunto de nuevas funcionalidades, de modificaciones y de cambios de comportamiento.
Este periodo es importante para el desarrollador que se encarga de mantener una o varias aplicaciones: debe
asegurarse de que cada aplicación es compatible con los cambios en el comportamiento del sistema y, si es posible,
integrar las principales novedades.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Integrar una nueva versión de Android
Lo primero que hay que hacer para gestionar una nueva versión de Android es, evidentemente, comprobar el nuevo
sistema en un conjunto de dispositivos Android.
Si bien ciertos fabricantes aprovechan lo antes posible las actualizaciones del sistema (esta era, en particular, la
razón de ser de la gama Nexus editada por Google), pasan tan solo unos pocos meses entre la publicación de una
nueva versión de Android y las versiones efectivas para cada smartphone o tableta.
Si bien esto da, de hecho, cierto tiempo a los desarrolladores para actualizar sus aplicaciones, hay que disponer no
obstante de un dispositivo sobre el que instalar las actualizaciones.
El emulador de Android es, en esta sección, una preciada ayuda que permite probar todo en profundidad. Gracias a
él, se evita tener que monopolizar un terminal con una versión del sistema que puede ser inestable; esto ocurría,
particularmente, con las versiones «Developper Preview», que tienen una estabilidad relativa.
La creación de una emulación del sistema se ha abordado en el capítulo Principios básicos de Android. He aquí las
etapas, resumidas:
Abra Android Studio y, a continuación, el administrador AVD Manager.
Haga clic en el botón Create Virtual Device para abrir el asistente de creación de un terminal virtual.
Seleccione un modelo de smartphone o tableta y haga clic en Siguiente.
Seleccione la última versión de Android, aquí la versión 8, API 26.
Dele un nombre explícito al terminal virtual y haga clic en Finish.
Una vez creada la emulación, arranque el terminal virtual para una primera ejecución de Android 8.
Si bien Oreo modifica profundamente ciertos aspectos del sistema, los cambios visuales son, a primera vista,
inexistentes: el lanzador de aplicaciones presenta el mismo aspecto.
Google provee, para cada terminal virtual, una aplicación llamada API Demos, que permite comprobar, con ayuda de
ejemplos sencillos, las nuevas funcionalidades.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
En paralelo, el código fuente de cada uno de los ejemplos está disponible en el repositorio Git de Google, en la
siguiente dirección: https://github.com/android/platform_development/tree/master/samples
Para conocer la lista completa de todas las modificaciones aportadas al sistema, hay que consultar el sitio web
dedicado por Google a Android en la siguiente dirección: https://developer.android.com/about/versions/oreo/index.html
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
El sitio presenta (solo en inglés, desafortunadamente) la lista exhaustiva de cambios aportados a la plataforma.
Entre el conjunto de cambios, algunos parecen más importantes que otros: la modificación del mecanismo de
notificaciones, por ejemplo, es un aspecto esencial de esta versión de Android.
Con esta lista delante, el desarrollador debe identificar aquellas evoluciones que requieren una verificación pausada
según las aplicaciones que mantiene y centrar sus primeras pruebas sobre estos aspectos.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Migrar la aplicación LocDVD
1. Detectar las modificaciones que se han de realizar
En el marco de la aplicación LocDVD, parece que la gestión de las notificaciones forma parte de las modificaciones
que habría que comprobar en un terminal que funcione con Android Oreo.
La primera etapa consiste, por tanto, en lanzar la ejecución de la aplicación en un terminal (aquí virtual) equipado
con Android 8, sin tener que cambiar nada al principio.
Si no lo hubiera hecho aún, arranque el terminal virtual con Android 8 definido anteriormente.
Abra la aplicación LocDVD y lance su ejecución en el terminal virtual.
Compruebe el comportamiento global de la aplicación y, en la pantalla de visualización de un DVD, haga
clic en el botón que permite crear una notificación.
La notificación aparece inmediatamente en la barra de notificaciones del terminal emulado: ¡las notificaciones, sin
haber realizado ninguna modificación, funcionan correctamente en Android 8!
Esta manipulación ilustra el principio general de compatibilidad ascendente, principio que prevalece, generalmente,
en el entorno Android. Cabe destacar, no obstante, que no siempre ocurre así, en particular si se debe a alguna
evolución de la plataforma relacionada con la seguridad. El mecanismo de permisos introducido con
Android Marshmallow, por ejemplo, requiere una intervención inmediata; el antiguo sistema de permisos no era
suficiente para los dispositivos equipados con Marshmallow.
Una vez hecha esta primera verificación, ahora hay que comprobar el comportamiento de la aplicación cuando se
ejecuta explícitamente en Android 8.
Edite el archivo build.gradle de la aplicación LocDVD en Android Studio.
Indique, en el archivo build.gradle, que la versión de destino es Android 8, es decir la API de nivel
26. Hay que indicar este número de API para las siguientes propiedades: compileSdkVersion y
targetSdkVersion.
También hay que cambiar el valor de buildToolsVersion por 26.0.0, así como la versión de la
biblioteca de soporte v7 utilizada; aquí se usa la versión 26.0.0.
Una vez sincronizada la aplicación con las modificaciones del archivo build.gradle, Android Studio
muestra un error: no es capaz de encontrar la versión 26.0.0 de
com.android.support:appcompat-v7.
Google indica que, para esta versión, hay que agregar la URL del repositorio de referencia. Agregue, en el
archivo build.gradle, la siguiente referencia:
repositories {
maven {
url ’https://maven.google.com’
// Alternative URL is ’https://dl.google.com/dl/android/maven2/’
}
}
Relance una sincronización del archivo de gradle: se corrige el error. La aplicación está lista para
ejecutarse en el terminal con Android 8.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Una vez realizadas estas modificaciones, el archivo build.gradle es el siguiente:
android {
compileSdkVersion 26
buildToolsVersion ’26.0.0’
defaultConfig {
applicationId "com.ejemplo.locdvd2017"
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner
"android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(’proguard-android.txt’),
’proguard-rules.pro’
}
}
productFlavors {
versionGratuita {
applicationId ’com.ejemplo.locdvd2017_gratuita’
}
versionDePago {
applicationId ’com.ejemplo.locdvd2017_de_pago’
}
}
}
repositories {
maven {
url ’https://maven.google.com’
// Alternative URL is ’https://dl.google.com/dl/android/maven2/’
}
}
dependencies {
compile fileTree(include: [’*.jar’], dir: ’libs’)
androidTestCompile(’com.android.support.test.espresso:
espresso-core:2.2.2’, {
exclude group: ’com.android.support’, module: ’support-annotations’
})
compile ’com.android.support:appcompat-v7:26.0.0’
compile ’com.android.support.constraint:constraint-layout:1.0.2’
testCompile ’junit:junit:4.12’
compile project(’:volley-master 2017’)
}
Una vez realizadas estas modificaciones, relance la aplicación LocDVD en el terminal Android 8 y pruebe el
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
envío de notificaciones: no aparece ninguna notificación, el mecanismo de notificación debe revisarse para
la API 26...
La ejecución de esta última versión de la aplicación en un dispositivo equipado con una versión anterior a Android 8
muestra que las notificaciones funcionan correctamente en las versiones antiguas. Son los cambios vinculados a
Oreo los que invalidan el envío de notificaciones.
2. Las notificaciones en Android 8
Android Oreo introduce, para las notificaciones, una noción de canal de notificación (Notification Channel). Cada
notificación, para mostrarla al usuario, debe estar vinculada a un canal de notificación. El canal es propio de cada
aplicación y los parámetros del canal pueden modificarse libremente por parte del usuario.
Si no se establece ningún canal, la notificación simplemente se ignora por parte del sistema. Este es el problema de
las notificaciones en la aplicación LocDVD.
Las opciones del desarrollador del terminal Android (que aparecen cuando un desarrollador configura un terminal
Android en modo «desarrollador») permiten, en Android 8, visualizar este problema. Para ello hay que habilitar la
característica Ver advertencias canal notificaciones (sic):
En la configuración, seleccione el apartado Sistema y, a continuación, Opciones para desarrolladores.
Desplace la pantalla hasta el apartado Aplicaciones.
En este apartado, habilite la opción Ver advertencias canal notificaciones.
Relance la aplicación LocDVD y trate de generar una notificación: aparece un mensaje toast que le indica
el fallo en el envío de la notificación.
De modo que, para hacer funcionar el mecanismo de notificaciones, hay que configurar los canales de notificación en
la aplicación LocDVD.
Recordaremos, en primer lugar, las principales etapas del envío de una notificación, tal y como vimos en el capítulo
Salir de la aplicación, en la sección Utilizar la barra de notificaciones:
NotificationCompat.Builder builder =
new NotificationCompat.Builder(getActivity());
Utilizando este objeto, se introduce la información en la notificación:
builder.setContentTitle(dvd.getTitulo());
builder.setContentText(dvd.getResumen());
builder.setSmallIcon(R.drawable.ic_notification);
A continuación, se utiliza el objeto de tipo Builder para definir la notificación propiamente dicha:
Notification notification;
if(Build.VERSION.SDK_INT<16)
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
notification = builder.getNotification();
else
notification = builder.build();
Por último, utilizando un objeto NotificationManager, se envía la notificación al sistema:
NotificationManager notificationManager =
(NotificationManager)getActivity().
getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify((int)dvd.id, notification);
La versión 8 de Android agrega un método setChannelId a las clases Notification.Builder y
NotificationCompat.Builder. Este método recibe, como único parámetro, una cadena de caracteres que
representa el identificador de la notificación.
String CHANNEL_ID="CHANNEL_LOCDVD";
[...]
builder.setChannelId(CHANNEL_ID);
Para que esta acción sea válida, el canal de notificación, con el identificador CHANNEL_ID debe estar definido en el
sistema.
Para crear un canal de notificación hay que utilizar la clase NotificationChannel, nueva clase de la API 26.
El constructor de la clase NotificationChannel recibe como parámetro un identificador de canal, un nombre
para el canal y un valor entero que representa el nivel de importancia que se aplicará a las notificaciones vinculadas
a este canal. Los posibles valores están representados por las constantes definidas en la clase
NotificationManager.
String CHANNEL_ID="CHANNEL_LOCDVD";
NotificationChannel notificationChannel =
new NotificationChannel(CHANNEL_ID, "Notificaciones LocDVD",
NotificationManager.IMPORTANCE_HIGH);
l setDescription: permite asignar una descripción al canal, descripción que se presentará al usuario en el
administrador de notificaciones (que veremos después).
l enableLights: permite indicar si las notificaciones se señalan con una luz en el terminal Android.
l enableVibration: permite indicar si el terminal debe vibrar para señalar una notificación.
l setVibrationPattern: permite definir el formato de la vibración que señala las notificaciones.
l setSound: permite especificar el sonido utilizado por las notificaciones.
La definición completa del canal de notificación es, por ejemplo, la siguiente:
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
NotificationChannel notificationChannel =
new NotificationChannel(CHANNEL_ID, "Notificaciones LocDVD",
NotificationManager.IMPORTANCE_HIGH);
notificationChannel.setDescription("Canal de notificaciones
de la aplicación LocDVD.");
notificationChannel.enableLights(true);
notificationChannel.enableVibration(true);
notificationChannel.setVibrationPattern(new long[]{100, 200, 100,
200, 200, 100, 200, 100, 400});
Para crear el canal de notificación, a continuación hay que invocar el método createNotificationChannel del
objeto notificationManager:
notificationManager.createNotificationChannel(notificationChannel);
Una vez creado el canal de notificación, es posible utilizarlo pasándole el identificador del canal creado al método
setChannelId.
La noción de canal solo está disponible a partir de la API 26, de modo que hay que condicionar el conjunto del
código correspondiente a la implementación del objeto NotificationChannel al nivel de la versión de API
utilizada por el terminal.
NotificationCompat.Builder builder =
new NotificationCompat.Builder(getActivity());
NotificationManager notificationManager =
(NotificationManager)getActivity().getSystemService
(Context.NOTIFICATION_SERVICE);
builder.setContentTitle(dvd.getTitulo());
builder.setContentText(dvd.getResumen());
builder.setSmallIcon(R.drawable.ic_notification);
if(Build.VERSION.SDK_INT>25) {
NotificationChannel notificationChannel =
new NotificationChannel(CHANNEL_ID, "Notificaciones LocDVD",
NotificationManager.IMPORTANCE_HIGH);
notificationChannel.setDescription("Canal de notificaciones
de la aplicación LocDVD.");
notificationChannel.enableLights(true);
notificationChannel.enableVibration(true);
notificationChannel.setVibrationPattern(new long[]{100, 200,
100, 200, 200, 100, 200, 100, 400});
notificationManager.createNotificationChannel
(notificationChannel);
builder.setChannelId(CHANNEL_ID);
}
Con ánimos de optimizar el código, sería preferible, en lugar de crear el canal de notificación cada vez que se envía
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
una notificación, comprobar si ya existe antes de crearlo. Para ello, la clase NotificationManager expone el
método getNotificationChannel, que permite obtener una instancia de NotificationChannel en
función de su identificador:
NotificationChannel notificationChannel =
notificationManager.getNotificationChannel(CHANNEL_ID) ;
Integrando una prueba en el procedimiento de envío de notificación, podemos proponer una versión final optimizada
del envío de notificación. El código del método sendNotification queda, al final, así:
NotificationManager notificationManager =
(NotificationManager)getActivity()
.getSystemService(Context.NOTIFICATION_SERVICE);
builder.setContentTitle(dvd.getTitulo());
builder.setContentText(dvd.getResumen());
builder.setSmallIcon(R.drawable.ic_notification);
if(Build.VERSION.SDK_INT>25) {
if(notificationManager.getNotificationChannel
(CHANNEL_ID)==null) {
NotificationChannel notificationChannel =
new NotificationChannel(CHANNEL_ID,
"Notificaciones LocDVD", NotificationManager.IMPORTANCE_HIGH);
notificationChannel.setDescription
("Canal de notificaciones de la aplicación LocDVD.");
notificationChannel.enableLights(true);
notificationChannel.enableVibration(true);
notificationChannel.setVibrationPattern(new long[]{100, 200, 100, 200, 200, 100, 200, 100, 400});
notificationManager
.createNotificationChannel(notificationChannel);
}
}
builder.setChannelId(CHANNEL_ID);
}
Notification notification;
if(Build.VERSION.SDK_INT<16)
notification = builder.getNotification();
else
notification = builder.build();
- 6- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
notificationManager.notify((int)dvd.id, notification);
}
La ejecución de la aplicación en Android 8 muestra que las notificaciones están, de nuevo, activas. Por otro lado,
vemos las modificaciones aportadas por Oreo: cuando se produce una notificación, el icono de la aplicación
LocDVD lleva un disco verde. Haciendo un clic largo, el usuario verá una ventana contextual que le presentará la
notificatión o las notificaciones activas y que dan acceso a la pantalla de información de la aplicación.
El usuario también puede acceder a la configuración del canal de notificación que hemos definido y modificar los
parámetros de las notificaciones. Para abrir esta configuración, seleccione la opción Aplicaciones y notificaciones y,
a continuación, seleccione la opción Notificaciones, haga clic en Notificaciones para obtener la lista de aplicaciones
y seleccione la aplicación LocDVD. Encontramos, en la sección Categorías, el canal de notificación que hemos creado.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 7-
Haciendo clic en el canal Notificaciones LocDVD, el usuario abre la pantalla que le permite modificar los parámetros
de notificación.
- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 9-
Algunas novedades de Android 8
Además de los cambios que requieren obligatoriamente modificar el código para que sea compatible con la nueva
versión de Android, existen otras modificaciones y mejoras de la plataforma que permiten simplificar el desarrollo o
agregar funcionalidades a la aplicación.
1. Las fuentes en XML
Android 8 introduce una simplificación esperada desde hace bastante tiempo por parte de los desarrolladores: la
posibilidad de declarar una o varias fuentes directamente en el código XML de un layout, en lugar de tener que
gestionar de forma obligatoria esta funcionalidad en el código Java.
En la ficha de presentación de un DVD, el título del DVD aparece con una fuente específica, Roboto-Thin, que se
aplica, de momento, al componente TextView en el código del fragmento ViewDVDFragment.
Typeface typeface =
Typeface.createFromAsset(getActivity().getAssets(),"Roboto-Thin.ttf");
txtTituloDVD.setTypeface(typeface);
Ahora es posible especificar esta fuente en el código del layout del fragmento. Para ello, hay que situar el archivo de
la fuente en una carpeta específica: la carpeta /font, subcarpeta de la carpeta /res.
Preste atención, esta característica exige utilizar la versión 3 de Android Studio. Android Studio 2.3.3 (versión
estable en el momento de escribir este libro) no es capaz de gestionar los archivos de fuente.
En la vista de proyecto de Android Studio, haga clic con el botón derecho en la carpeta /res de la
aplicación.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Copie a continuación el archivo Roboto-Thin.ttf de en la carpeta /assets a la carpeta /font que
acaba de crear.
El archivo Roboto-Thin.ttf no respeta las reglas de los archivos de recursos de Android: el guion no se permite
(los nombres de archivo se utilizan directamente como recursos, y el guion no está permitido en el nombre de
variables y constantes en Java) y las mayúsculas están prohibidas. De modo que hay que renombrar este archivo
por roboto_thin.ttf.
Haga clic con el botón derecho en el archivo en la vista de proyecto y seleccione la opción Refactor; a
continuación, Rename, e introduzca el nuevo nombre.
Para aplicar la fuente al componente TextView, basta con darle valor a la nueva propiedad font en el código
XML:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/titulo_de_la_pelicula"
android:id="@+id/tituloDVD"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="16dp"
android:font="@font/roboto_thin"
android:textSize="22sp"/>
Antes de probar la aplicación, no hay que olvidarse de eliminar, en el código del fragmento ViewDVDFragment, el
antiguo código utilizado para la declaración de la fuente.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Typeface.createFromAsset(getActivity().getAssets(),
"font/Roboto_Thin.ttf");
txtTituloDVD.setTypeface(typeface);
*/
2. Autocompletado
La API 26 introduce también un nuevo tipo de componente que presenta un mecanismo de autocompletado, muy
fácil de implementar.
Como con todo elemento del layout, la declaración del componente de autocompletado, llamado
AutoCompleteTextView, se hace en el archivo XML de layout (o en el código Java para los componentes
definidos dinámicamente).
<AutoCompleteTextView
android:id="@+id/search_queryText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"/>
Para definir la lista de los términos de autocompletado, hay que declarar un ArrayAdapter y asignarlo al
componente AutoCompleteTextView, como con un componente ListView.
String[] TEST =
new String[] {"Lunes","Martes","Miércoles","Jueves",
"Viernes","Sábado","Domingo"};
AutoCompleteTextView searchText =
view.findViewById(R.id.search_queryText);
ArrayAdapter<String> completionAdapter =
new ArrayAdapter<String>(getActivity(),
android.R.layout.simple_dropdown_item_1line, TEST);
searchText.setAdapter(completionAdapter);
Durante la ejecución, aparecen las propuestas de autocompletado cuando el usuario introduce dos caracteres:
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Leyendo el código presentado aquí arriba, vemos que el componente AutoCompleteTextView se define sin
tener que convertir explícitamente la salida del método findViewById: esta es una novedad aportada por la API
26, ¡válida para todos los componentes del layout!
3. Otras modificaciones
La versión Oreo, como todas las versiones de Android, aporta un número bastante importante de modificaciones y
de mejoras en la plataforma.
Se recomienda encarecidamente estudiar con detalle los ejemplos que proporciona Google en el repositorio Git
indicado al principio de este capítulo.
Algunos cambios en el comportamiento pueden ser complejos de integrar y requieren grandes modificaciones en el
código de una aplicación.
Respecto a Android 8, los lectores más atrevidos habrán observado, sin duda, las importantes mejoras relativas a
los procesamientos como tarea de fondo. Android Oreo introduce, en efecto, limitaciones a la carga del procesador
por parte de estos procesamientos. Estas modificaciones son complejas y afectan únicamente a casos particulares,
que superan ampliamente el propósito de este libro.
El desarrollador debe tener en mente que, cuando se presenta una nueva plataforma al público, esta no está
completamente terminada: la documentación, en particular, llega a veces con retraso o es incompleta respecto a las
novedades. De modo que es importante probar bien una nueva funcionalidad antes de implementarla en un entorno
de producción.
El retraso respecto al tiempo entre la salida de una nueva versión de Android y su difusión en los dispositivos de los
usuarios permite planificar el desarrollo de estas nuevas funcionalidades y deja tiempo a cada uno para integrar
correctamente estos cambios.
- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Abrir una cuenta de desarrollador
Si bien existen varias soluciones para difundir una aplicación, las tiendas de aplicaciones siguen siendo el medio más
sencillo para llegar al mayor número de usuarios potenciales.
Play Store de Google es el almacén de aplicaciones más conocido, y sin duda el más frecuentado; es prácticamente
imprescindible.
Para poder publicar una aplicación en Play Store, basta con poseer una cuenta de desarrollador Google Play.
La inscripción como desarrollador en Google Play se hace a partir de una simple cuenta de Google; puede ser la
cuenta del usuario definida para el terminal del desarrollador, una cuenta de Gmail o Google+, etc. (todos los servicios
de Google utilizan la misma cuenta).
El coste de la inscripción es de 25 $ estadounidenses, que se pagan una única vez: la cuenta es válida de por vida y
no impone ninguna restricción al número de aplicaciones que se pueden publicar.
La inscripción se hace directamente en la siguiente dirección: https://support.google.com/googleplay/android
developer/answer/6112435?hl=es&ref_topic=3450769
La información que debe proveerse corresponde principalmente a la identidad del desarrollador (nombre, apellidos,
dirección, etc.).
Cabe destacar que, para publicar una aplicación de pago (o que integre funcionalidades suplementarias de pago),
también hay que poseer una cuenta de Google Wallet.
Una vez realizada la inscripción, el desarrollador puede gestionar sus aplicaciones desde la consola especifica para él,
disponible en la siguiente dirección: https://play.google.com/apps/publish/
La página de inicio de la consola del desarrollador presenta la lista de aplicaciones del desarrollador y provee
directamente, para cada aplicación, la siguiente información:
l El número de instalaciones actuales y totales.
l La puntuación media asignada por los usuarios y el número de puntuaciones.
l La fecha de la última actualización.
l El estado de la aplicación (publicada o no).
Haciendo clic en el nombre de una aplicación se accede a su ficha detallada, que presenta en diversos apartados la
siguiente información:
l Apartado Panel de control: sirve como página de inicio de la consola esta página presenta varios gráficos que indican
la tendencia en cuanto al número de instalaciones, de desinstalaciones, los ingresos de la aplicación, etc.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
l Apartado Estadísticas: conjunto de estadísticas acerca del número de instalaciones de la aplicación, distribuidas en
función de varios criterios (versión de Android, país, versión, tipo de dispositivo, etc.).
l Apartado Android vitals: bajo este apartado, la consola agrupa todo lo relativo a los errores encontrados en la
aplicación en los dispositivos de los usuarios: tasa de errores ANR (del inglés Application Not responding), tasa de
bloqueos, visualización demasiado lenta, etc. Este apartado es fundamental para el desarrollador y permite visualizar,
por plataforma y por dispositivo, todos los problemas que puede encontrar la aplicación.
l Apartado Herramientas de desarrollo: este apartado, reciente, presenta las API y los servicios de Google utilizados
por la aplicación.
l Apartado Gestion de versiones: aquí, el desarrollador encuentra toda la información correspondiente a las distintas
publicaciones de su aplicación: histórico de publicaciones, características técnicas de los terminales sobre los que se ha
instalado la aplicación, etc.
l Apartado Presencia en Google Play Store: este es el apartado principal para lanzar una aplicación. Aquí se reúne
toda la información necesaria para la publicación: ficha de presentación de la aplicación en Play Store, precio,
disponibilidad en los distintos países, clasificación del contenido, compras integradas, servicio de traducción, etc.
l Apartado Adquisición de usuarios: este apartado permite realizar un seguimiento preciso del éxito obtenido por la
aplicación entre los usuarios: un conjunto de gráficos permiten seguir el número de visitas a la ficha de la aplicación en
Play Store, la tasa de instalaciones, de desinstalaciones, de uso de la aplicación, etc. Esta información es imprescindible
para supervisar la eficacia de la ficha en Play Store y medir el interés suscitado por la aplicación.
l Apartado Informes financieros: correspondiente a todo lo relativo a la información financiera de la aplicación: los
ingresos generados por las instalaciones de pago, las compras integradas, los compradores, etc. Toda esta información
está disponible como vista previa en la consola, y se proveen versiones detalladas de todos estos datos que es posible
descargar.
l Apartado Comentarios de los usuarios: detalla las notas asignadas a la aplicación y muestra los comentarios
publicados por los usuarios.
La pantalla de inicio de la consola del desarrollador es el punto de partida para publicar una nueva aplicación. La
publicación conlleva varias etapas: la información de la ficha de la aplicación en Play Store, la publicación del archivo o
de los archivos APK de la aplicación, la información relativa a la clasificación de la aplicación, así como la tarificación. La
aplicación solo puede publicarse en el repositorio de aplicaciones de Google una vez se ha completado toda esta
información.
Para publicar una nueva aplicación, hay que hacer clic en el botón correspondiente, en la parte superior derecha de la
consola del desarrollador.
Se abre una ventana emergente, que pregunta el idioma por defecto de la aplicación, así como su título. A
continuación, se redirige al usuario a la ficha de la aplicación en Play Store.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Preparar la ficha
La ficha Play Store de la aplicación es un elemento muy importante: más allá de las eventuales acciones de
comunicación, este es el único elemento que va a motivar la instalación de su aplicación antes que ninguna otra.
Aquí debe introducirse cierta información, información que idealmente debe prepararse con antelación.
La ficha se redacta según el idioma seleccionado en la creación de la nueva aplicación; es posible agregar
traducciones para esta ficha (la propia aplicación no se ve afectada aquí), haciendo clic en la lista desplegable
Gestionar traducciones y, a continuación, Añadir el texto de su propia traducción.
Cabe destacar que Google proporciona desde hace poco un servicio de traducción: las traducciones se realizan por
compañías externas, y son de pago. En función de la información presentada, la tarifa depende del idioma y del
número de palabras que se han de traducir. También es posible traducir los archivos de recursos String.xml (pero
habrá que reintegrarlos, a continuación, en la aplicación).
Para cada idioma seleccionado por el desarrollador, es obligatorio introducir la siguiente información:
l Título de la aplicación.
l Descripción breve, que debe tener como máximo 80 caracteres, incluyendo los espacios. La descripción breve se
corresponde con el primer texto que se presenta al usuario, antes de hacer clic en «Más información».
l Descripción completa, que no puede superar los 4000 caracteres. Este texto se utiliza para presentar con detalle la
aplicación, una vez se hace clic en el vínculo «Más información». Se corresponde con la ficha detallada de la aplicación.
A continuación hay que proveer los elementos gráficos: capturas de pantalla, elementos visuales, etc. Si bien basta,
para publicar en un smartphone, con un icono de alta resolución (512 x 512), una captura de pantalla y una imagen
(1024 x 500), se recomienda proveer un elemento visual por cada tipo de soporte: smartphone, tableta de
7 pulgadas, tableta de 10 pulgadas, Android TV, Android Wear.
Estos elementos visuales se presentan en una lista horizontal en la ficha de Play Store cuando el usuario accede a la
ficha detallada, salvo el icono en alta resolución, que se muestra en la lista de aplicaciones cuando el usuario recorre
Play Store.
Opcionalmente, es posible proveer también una imagen que se utilizará en eventuales operaciones promocionales
realizadas por Google. Debe tener una dimensión de 180 x 120 píxeles y formato JPG o PNG (sin transparencia).
A su vez, para presentar con más detalle la aplicación, es posible integrar un enlace hacia un vídeo alojado en
YouTube: este vídeo estará accesible a los usuarios de la misma manera que los elementos visuales.
Play Store presenta las aplicaciones clasificándolas por tipo (Aplicación o Juego) y por categoría: Actualidad,
Meteorología, Herramientas, Productividad, etc.; para las aplicaciones y, por ejemplo, Acción, Arcade, Estrategia, etc.;
para los juegos.
Esta información debe introducirse en Play Store a través de la ficha de la aplicación.
También es obligatorio definir una clasificación para el contenido (desde Nivel 1 Estricto, es decir, reservada a un
público hasta Para todos).
Se está implementando una nueva clasificación de tipo PEGI: la determina el sistema basándose en las respuestas de
un cuestionario, accesible desde el formulario de la ficha en Play Store.
Por último, es obligatorio comunicar a los usuarios una dirección de correo electrónico, que se presentará como la
dirección del desarrollador de la aplicación. También es posible proveer la URL de un sitio web para la aplicación y un
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
número de teléfono.
Las reglas de confidencialidad deben definirse, normalmente, para cada aplicación (hay que indicar la URL de la
página que las presenta), aunque Play Store permite omitirlas (en cuyo caso hay que marcar una opción): preste
atención, no obstante, a que, si la aplicación solicita determinada información sensible, las reglas de confidencialidad
se vuelven obligatorias para la aplicación (es el caso, por ejemplo del permiso que otorga acceso a los contactos del
usuario).
Una vez introducida la información acerca de la aplicación, hay que informar los datos relativos al precio de esta, así
como su disponibilidad en los distintos países donde esté accesible Play Store.
La primera información que hay que introducir es relativa al precio y, en primer lugar, si la aplicación es gratuita o de
pago.
Cabe destacar que, si una aplicación se define como gratuita durante su creación, no podrá pasar a ser de pago más
adelante. Lo contrario, sin embargo, sí es posible: una aplicación de pago puede pasar a ser gratuita en cualquier
momento.
Para una aplicación de pago, hay que definir su precio. El precio puede especificarse en cada divisa y cada país, o ser
el mismo para todos los países donde se distribuya la aplicación. Un asistente permite convertir automáticamente el
precio para todas las divisas partiendo del precio indicado en euros. A la hora de fijar el precio, no olvide que Google
carga una comisión correspondiente al 30 % del precio de la aplicación por cada compra.
En este apartado el desarrollador debe indicar que respeta las reglas de Play Store, relativas al contenido, así como
las leyes de los Estados Unidos acerca de la exportación. Para cada punto, debe marcarse una casilla de verificación.
Hasta que no se rellene completamente la ficha, la aplicación no podrá publicarse en Play Store. Es posible introducir
esta información en distintas sesiones, guardando los datos con el botón Guardar borrador situado en la parte
superior derecha del formulario de la ficha.
Una vez completada la ficha, y si se ha proporcionado el archivo APK, se habilita el botón Publicar la aplicación.
Basta, entonces, con hacer clic en este botón para lanzar el proceso de publicación.
El tiempo para la publicación en Play Store, a partir del momento en el que se proporcionan todos los elementos, es
bastante corto respecto a otros repositorios de aplicaciones y otras plataformas móviles: hay que contar con entre
dos y cuatro horas solamente.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Publicar un APK
La publicación de un APK, archivo que contiene el conjunto de elementos de la aplicación Android, es una etapa
importante para el desarrollador: culmina varias semanas o incluso varios meses de desarrollo, pruebas y
depuración. Es también una de las etapas más sencillas.
El archivo APK que se provee debe estar firmado digitalmente por el editor. La firma permite garantizar la identidad
del editor para cada actualización de la aplicación: las firmas de la versión en producción y de la nueva versión deben
ser idénticas para que se acepte la actualización.
Durante el desarrollo, los despliegues de la aplicación mediante Android Studio se firman automáticamente con una
clave temporal de depuración. Esta clave no permite publicar la aplicación en Play Store.
La clave se almacena en un repositorio de claves (Key Store), que es un archivo. Al mismo tiempo, la clave y el Key
Store se protegen con una contraseña: se recomienda introducir dos contraseñas diferentes y, sobre todo, no perder
jamás esta información, pues en caso contrario será imposible actualizar las aplicaciones.
Android Studio presenta la opción Generate Signed APK, en el menú Build, que simplifica la firma de la aplicación
para la versión de producción.
Al hacer clic en Generate Signed APK, se abre una ventana emergente, que permite seleccionar la ubicación del
almacén de la clave: haciendo clic en el botón Create new, Android Studio abre un asistente para crear un nuevo Key
Store y una nueva clave.
Hay que indicar la ruta del archivo que se creará, así como una contraseña para el Key Store. Para la clave, se pide la
siguiente información:
l Alias: nombre que se da a la clave.
l Password: contraseña para proteger la clave.
l Validity: tiempo de validez de la clave. La duración recomenda es de 25 años.
También debe proveerse la siguiente información de identidad: nombre del dueño de la clave, nombre de la
organización, nombre del departamento en la organización, etc. Debe introducir al menos uno de los datos.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Una vez definidos el almacén de claves y la clave, hay que hacer clic en OK. Android Studio le propone definir una
contraseña para securizar su base de datos interna de claves. Para no crear una contraseña a este nivel, basta con
dejar el formulario vacío y hacer clic en OK.
El asistente pide, a continuación, introducir la ruta de la carpeta donde se creará el archivo APK de producción.
Tras hacer clic en el botón Finish, Android Studio lanza la creación del archivo firmado del APK. Cuando termina esta
tarea, se abre una ventana emergente que le presenta el resultado: ¡la creación de la aplicación ha terminado!
Este archivo APK debe subirse, a continuación, a Play Store. Para ello, vaya a la ficha de la aplicación y seleccione el
apartado Archivos APK.
Play Store permite importar o bien un archivo de producción o bien un archivo para realizar pruebas beta de la
aplicación o bien un archivo para las pruebas alfa.
En el caso de las pruebas beta y alfa, la aplicación se publicará únicamente para los usuarios que se hayan declarado
como miembros del grupo beta o alfa. En este caso, no es necesario firmar el archivo APK, las firmas de las versiones
de desarrollo se aceptan.
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
Para subir el archivo APK, hay que hacer clic en el botón Importar su primer archivo APK en versión producción.
Se abre un asistente que le permite seleccionar el archivo APK del sistema de archivos. Una vez subido, el formulario
se actualiza y muestra el número de versión de la aplicación subida, la fecha, y presenta el número de plantillas de
modelos de terminales compatibles con la publicación: esta información está disponible en Play Store gracias a la
información <uses-feature> que se ha abordado, por ejemplo, en el capítulo Explotar el teléfono.
Cabe destacar, tras una subida para realizar la actualización de una aplicación, que el número de la versión que se
sube debe ser superior al número de versión correspondiente a la aplicación en producción. El número de versión se
informa en el atributo android:versionCode de la etiqueta manifest del archivo AndroidManifest.xml.
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ejemplo.locdvd" android:versionCode="1">
En lugar de introducir directamente esta información en el archivo Manifest, hay que utilizar la herramienta «build» de
Gradle e introducir el número y el nombre de la versión en el archivo build.gradle de la aplicación.
Cuando se ha introducido toda la información, se habilita el botón Publicar la aplicación. Si no fuera el caso, es
posible saber qué información falta haciendo clic en el vínculo ¿Por qué no se puede publicar la aplicación? de la
ficha Play Store.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Producir varias versiones
Si bien existen varios enfoques para promocionar una aplicación de pago, proveer una versión gratuita, aunque
limitada (en el tiempo, en sus funcionalidades), es sin duda uno de los métodos preferidos de los desarrolladores (¡y
de los usuarios!). Esto supone, no obstante, crear dos aplicaciones diferentes en Play Store, una gratuita y otra de
pago, por ejemplo, y publicar dos APK diferentes.
Android Studio, a través de las capacidades de Gradle, ofrece una solución muy simple y potente para facilitar la
producción de variantes de una misma aplicación: los sabores (flavors, en inglés).
Un flavor debe verse como una variante de la aplicación: a este respecto, cada flavor define un identificador de
aplicación diferente y produce por tanto una versión diferente de la aplicación.
En esta sección, vamos a definir dos versiones de la aplicación LocDVD y veremos cómo determinar, en el código Java,
qué versión se está ejecutando.
Si bien aquí los flavors permiten producir versiones gratuitas y de pago de la aplicación, también es posible
implementar flavors para distribuir la aplicación en distintos terminales: una versión puede reservarse para ciertas
versiones de Android, ciertos tipos de terminales, etc.
Abra la solución LocDVD en Android Studio, seleccione en el menú principal la opción Build y, a continuación,
Edit flavors: se abre la ventana Project Structure, vaya a la pestaña Flavors.
La pantalla muestra una lista (de momento compuesta de una única entrada) de flavors ya definidos: de momento,
existe un flavor defaultConfig.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Vamos a crear dos flavors: el primero, llamado versionGratuita, corresponderá a una versión más
ligera de la aplicación; el segundo, versionDePago, será la versión completa.
Haga clic en el botón + (de color verde), situado en la parte superior derecha de la lista de flavors. Se crea
una nueva entrada automáticamente: se llama flavor.
En el formulario de la derecha, cambie el nombre flavor por «versionGratuita».
Es posible informar todos los campos del formulario: los valores que se indican aquí reemplazarán a los
valores definidos en la versión defaultConfig. Como aquí el objetivo es únicamente crear varias
versiones, solo se modificará la propiedad Application Id.
Introduzca el valor com.ejemplo.locdvd2017_gratuita para la entrada Application Id.
Defina, a continuación, otro flavor (haciendo clic en el mismo botón + verde), e introduzca la información
para «versionDePago»: nombre y Application Id (que puede ser, por ejemplo,
com.ejemplo.locdvd2017_de_pago).
Haga clic, a continuación, en el botón OK. Se cierra la ventana y se lanza una compilación. ¡Hemos creado
dos flavors!
Para visualizar los distintos flavors, abra la ventana Build Variants de Android Studio: el botón está situado
a la izquierda del explorador del proyecto, sobre la barra lateral de Android Studio. Alternativamente,
puede seleccionar la opción de menú View, a continuación Tool Windows y, por último, Build Variants.
Aparece una ventana (normalmente en la parte inferior izquierda, debajo del explorador del proyecto), que
muestra una tabla que presenta los módulos del proyecto así como la variante correspondiente al módulo
app (que representa la aplicación), y se muestra una lista desplegable que presenta todas las
variantes posibles para la compilación: vemos las
variantes
versionGratuitaDebug, versionGratuitaRelease, versionDePagoDebug y
- 2- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
versionDePagoRelease. Basta con seleccionar la versión deseada para lanzar la compilación
correspondiente.
Para comprobar que los flavors se procesan correctamente, basta con agregar, por ejemplo, un mensaje Toast
durante la apertura de la aplicación.
Abra el archivo MainActivity.java y sitúe el cursor al final del método onCreate.
En el cuerpo de una actividad, el nombre del flavor en curso está disponible a través de la propiedad
BuildConfig.FLAVOR, propiedad de tipo String. De modo que es muy fácil mostrar un mensaje Toast
que presente este flavor, por ejemplo:
Para probar, ¡basta con seleccionar un flavor en la ventana Build Variants y lanzar la aplicación!
Siéntase libre para agregar, para cada funcionalidad que desee reservar a la versión de pago, una comprobación del
nombre del flavor en curso...
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 3-
Y después…
Algunos días después de la publicación, las estadísticas de la aplicación empiezan a estar disponibles: permiten
conocer el número de instalaciones, el ratio instalaciones/desinstalaciones, el reparto de versiones de Android, etc.
El desarrollador debe, llegados a este punto, preocuparse de los informes de fallo de la aplicación y de los ANR. Una
aplicación con un gran número de ANR corre el riesgo de ser ignorada rápidamente por los usuarios y no tendrá el
éxito esperado…
También hay que estar atento a los comentarios de los usuarios. Si bien un buen comentario anima a los usuarios a
probar la aplicación, los comentarios negativos pueden hacerles huir. En el caso de que algún comentario sea
manifiestamente malintencionado o tenga mala fe, es posible señalarlo como spam: no se mostrará más en Play
Store.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
Para ir más lejos
Habría sido ilusorio tratar de cubrir todos los aspectos de la plataforma Android en este libro; el framework es muy
extenso y algunas nociones son complejas.
Entre los temas que se han dejado de lado deliberadamente, debemos citar la tecnología NFC, que hace un uso
masivo de los filtros de intención. Esta tecnología, que permite comunicar rápidamente en la corta distancia, es
compatible con un gran número de dispositivos y puede, potencialmente, convertirse en algo muy importante en los
próximos años.
Se ha decidido no abordar la problemática de las compras integradas. Esta nueva moda de comercialización da
preferencia a las compras directamente en el seno de la aplicación. Existe una solución alternativa para el lector que
quiera integrar rápidamente las compras en una aplicación, que consiste en inspirarse en el ejemplo que pone a
nuestra disposición Google en la siguiente dirección: https://developer.android.com/training/inappbilling/preparingiab
app.html#GetSample
Cabe destacar que la implementación es bastante compleja y requiere un esfuerzo consecuente para realizar
pruebas eficaces. La ausencia de Play Store en los terminales emulados, por donde pasan todas las peticiones de
compra y de validación de las compras integradas, es, a este respecto, una dificultad suplementaria.
Para ir más lejos, conviene mencionar los siguientes sitios de Internet, que potencialmente pueden aportar una
ayuda preciosa:
l El sitio oficial de los desarrolladores para Android: https://developer.android.com
l El sitio de la compañía Vogella, que presenta tutoriales completos disponibles en la siguiente dirección:
http://www.vogella.com/tutorials/android.html
l El conocido foro https://stackoverflow.com , que permite al desarrollador pedir ayuda a la comunidad de desarrolladores.
La parte dedicada a Android es muy activa.
l Por último, el único sitio en castellano de esta lista, la célebre https://www.lawebdelprogramador.com , que presenta
tutoriales y foros para ayudar a los desarrolladores.
© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-