Sei sulla pagina 1di 385

Desarrolle una aplicación Android

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 start­up. 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). 

Versión de la plataforma Android  Fecha  Nombre  Nivel de API 

1.0  09/2008  Apple Pie  1 

1.1  02/2009  Banana Split  2 

1.5  04/2009  Cupcake  3 

1.6  09/2009  Donut  4 

2.0  10/2009  Eclair  5 

2.2  05/2010  Froyo  8 

2.3  12/2010  Gingerbread  9 

3.0  02/2011  Honeycomb  11 

4.0.1  10/2011  Ice Cream Sandwich  14 

4.1  07/2012  Jelly Bean  16 

4.4  10/2013  KitKat  19 

5.0  11/2014  Lollipop  21 

6  10/2015  Marshmallow  23 

7  08/2016  Nougat  24 

7.1  12/2016  Nougat  25 

8  08/2017  Oreo  26 

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 GingerBread: compatible con NFC (Near  Field  Communication, comunicación de campo cercano), con varias cámaras 


en un mismo dispositivo y una mejora significativa del rendimiento. 

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 
(In­App 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 Android­Studio/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. 

l El  siguiente  campo, Package  name, es por defecto de solo lectura. Se corresponde con el nombre del paquete de su 


aplicación. Como se ha precisado en la sección anterior, este nombre debe ser único. 

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 Wi­Fi. 

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. 

static Toast makeText(Context context, CharSequence text, int duration)


void show()

El  primer  método,  makeText, permite construir un mensaje de tipo Toast; el segundo método,  show, muestra el 


mensaje Toast correspondiente por pantalla. 

- 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:  

public class MainActivity extends Activity {


[...]
Toast.makeText(this,"Un mensaje informativo",
Toast.LENGTH_LONG).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: 

Log.v(String tag, String message)


Log.d(String tag, String message)
Log.i(String tag, String message)
Log.w(String tag, String message)
Log.e(String tag, String message)

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. 

public class WebViewActivity extends Activity{


static final String TAG="WEBVIEW_ACTIVITY";

- 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. 

Android clasifica las pantallas según su densidad en varias categorías: de ldpi (low  dpi) a hdpi (high  dpi), e incluso 


xhdpi (extra high dpi) o más. La siguiente tabla muestra los valores medios de densidad para cada categoría. 

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. Density­independent 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  density­independent 
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  density­independent 
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: 

Densidad de pantalla  Dimensión en dip  Dimensión en píxeles 

ldpi  100  75 

mdpi  100  100 

hdpi  100  150 

xhdpi  100  200 

xxhdpi  100  300 

xxxhdpi  100  400 

Es  habitual  utilizar  el  sufijo  dp  en  lugar  de  dip  para  especificar  una  dimensión  en  density­independent  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 v7­appcompat: 

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 v7­appcompat:  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  v7­appcompat  (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. 

Basta entonces con proveer un archivo  strings.xml para el idioma por defecto y crear un archivo  strings.xml 


para  cada  idioma  soportado,  archivo  que  se  almacenará  en  la  carpeta  llamada  value­xx  correspondiente  (xx 
representa aquí el código de idioma). 

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/providing­resources.html (consultar la tabla «table 2»). 

Propiedad afectada  Ejemplo de calificador  Descripción 

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 
en­rUS  partir de las dos letras siguientes a la letra r (de región). 

Ancho  sw320dp   Dimensión más pequeña de la pantalla, sw significa smallest 


sw480dp  width  (ancho  menor).  Por  ejemplo,  el  calificador  sw320dp  se 
corresponde  con  los  terminales  que  poseen  un  ancho 
mínimo de 320 dp. 

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). 

Densidad de la pantalla  ldpi   Densidad de la pantalla, tal y como hemos visto antes. 


mdpi  
hdpi 

Versión de API  v7   Versión de Android ejecutada, tal y como hemos visto antes. 


v11  
v14 

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). 

Android­Studio, o con más exactitud Gradle ­la herramienta de construcción utilizada por Android­Studio­, 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. 

Sitúese  en  la  carpeta  app/src/main/java/com.ejemplo.locdvd:  debe  existir  un  archivo 


MainActivity.java; se trata del archivo generado por Android Studio durante la creación del 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  pop­up,  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. 

Las actividades deben heredar de la clase  android.app.Activity o de alguna clase derivada de  Activity: 


por lo tanto, la primera acción que debe llevar a cabo es declarar esta herencia. 

package com.ejemplo.locdvd;

import android.app.Activity;

public class ViewDVDActivity extends 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. 

onDestroy  Se invoca cuando la actividad se destruye, bien durante la llamada al método  finish


(), o bien directamente por parte del sistema para liberar recursos. 

Existe potencialmente otro estado entre  onCreate y  onStart, llamado  onRestart, que se invoca cuando una 


actividad que se ha detenido debe volver a visualizarse. 

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;

public class ViewDVDActivity extends Activity{

@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). 

El método  onCreate recibe como parámetro un objeto de tipo  Bundle cuyo rol es almacenar datos temporales. 


Su uso se abordará en el marco de la actividad de introducción de datos. 

protected void onCreate(Bundle savedInstanceState)

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  pop­up  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. 

Muestre el conjunto de propiedades y seleccione la propiedad layout:margin  de  la  lista.  Haga  clic  en  la 


flecha  para  ampliar  las  propiedades.  Se  muestran  los  elementos  layout_margin,  layout_marginLeft, 
layout_marginTop, layout_marginRight y layout_marginBottom. 

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. 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<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 

alpha  Opacidad  del  componente,  desde  0  (completamente  transparente)  hasta  1 


(completamente opaco). 

- 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. 

padding  Define el  padding del componente (el margen interior). 

paddingLeft Define el  padding del componente en la dirección indicada. 


paddingRight
paddingTop
paddingBottom 
tag  Cadena de caracteres de uso opcional para el desarrollador. 

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. 

Todos los contenedores definen las propiedades  layout_width y  layout_height, propiedades obligatorias 


que deben definirse para todos los componentes de la vista. 

Los posibles valores para estas propiedades son los siguientes: 

l Una dimensión, es decir, un nombre seguido de una unidad:  dp  o dip,  px  (píxeles),  mm  (milímetros), sp  (scale­


independent pixels). 

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  scaled­pixels  (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. 

textStyle  Estilo del texto (los posibles valores son  bold,  italic,  bolditalic). 

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: 

<?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="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: 

View findViewById(int id)

- 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: 

TextView txtTituloDVD = (TextView)findViewById(R.id.tituloDVD);

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);

// asignación del archivo de layout


setContentView(R.layout.activity_viewdvd);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)findViewById(R.id.anyoDVD);
txtActor1= (TextView)findViewById(R.id.actor1);
txtActor2= (TextView)findViewById(R.id.actor2);
txtResumenPelicula=(TextView)findViewById(R.id.resumenPelicula);
}

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: 

txtTituloDVD.setText("Título de mi DVD preferido");


txtTituloDVD.setText(R.string.un_recurso_texto);

Teniendo en cuenta estos elementos, el código del método onResume es el siguiente: 

@Override
protected void onResume() {
super.onResume();

txtTituloDVD.setText("Las Vacaciones del Pequeño Nicolás");


txtAnyoDVD.setText("Año de aparición: 2014");
txtActor1.setText("Valérie Lemercier");
txtActor2.setText("Kad Merad");
String resumen="Es el fin del año escolar. El momento tan " +
"esperado de las vacaciones ha llegado. El Pequeño Nicolás, " +
"sus padres y Mémé toman la ruta en dirección " +
"a mar, y se instalan por un tiempo " +
"en el Hotel Beau-Rivage. En la playa, " +
"Nicolás hace pronto nuevos amigos";
txtResumenPelicula.setText(resumen);
}

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. 

Por ejemplo: "Año de aparición: %d". 

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: 

<string name="anyo_de_aparicion">"Año de aparición: %d"</string>

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: 

// devuelve la cadena de caracteres definida en los recursos


getString(R.string.anyo_de_aparicion)

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;

public class ViewDVDActivity extends Activity{

TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtActor1;
TextView txtActor2;
TextView txtResumenPelicula;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// asignación del archivo de layout


setContentView(R.layout.activity_viewdvd);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)findViewById(R.id.anyoDVD);
txtActor1= (TextView)findViewById(R.id.actor1);
txtActor2= (TextView)findViewById(R.id.actor2);
txtResumenPelicula= (TextView)findViewById(R.id.resumenPelicula);
}

@Override
protected void onStart() {
super.onStart();
}

@Override
protected void onResume() {
super.onResume();

txtTituloDVD.setText("Las Vacaciones del Pequeño Nicolás");


txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion), 2014));
txtActor1.setText("Valérie Lemercier");
txtActor2.setText("Kad Merad");
String resume="Es el fin del año escolar. El momento tan" +
"esperado de las vacaciones ha llegado. El Pequeño Nicolás," +
"sus padres y Mémé toman la ruta en dirección" +
"al mar, y se instalan por un tiempo" +
"en el Hotel Beau-Rivage. En la playa," +
"Nicolás hace pronto nuevos amigos";
txtResumenPelicula.setText(resume);
}

@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: 

<?xml version="1.0" encoding="utf-8"?>

© É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. 

El componente de tipo botón tampoco difere de los componentes  TextView y  EditText (Button hereda, a su 


vez, de TextView). La dificultad aquí, si es que reviste alguna, es gestionar el posicionamiento de los botones: el 
botón + debe situarse a la derecha de la etiqueta Actores, el botón ok debe estar a la derecha de la pantalla. 

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. 

Por último, hay que prever que los componentes de tipo  EditText y de tipo  Button deben referenciarse en el 


código  Java  de  la  actividad:  de  modo  que  es  necesario  asignarles  un  identificador  único  explícito  (por  ejemplo, 
prefijado por el nombre del archivo de layout). Los componentes  TextView que no es necesario referenciar en el 
código Java, no requieren un identificador explícito. 

Teniendo en cuenta estas reglas, ahora es posible crear el nuevo archivo de layout, que se llamará, por ejemplo, 
activity_adddvd.xml: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_margin="8dp"
android:layout_height="match_parent">
<!-- Zona del título de la película -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/titulo_de_la_pelicula"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addDVD_titulo" />
<!-- Zona del año de aparición -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
<!-- Zona de los actores -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

© É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);

// asignación del archivo de layout


setContentView(R.layout.activity_adddvd);

// Obtención de las referencias sobre los componentes


editTituloPelicula = (EditText)findViewById(R.id.addDVD_titulo);
editAnyo= (EditText)findViewById(R.id.addDVD_anyo);
editResumen= (EditText)findViewById(R.id.addDVD_resumen);
btnAddActor = (Button)findViewById(R.id.addDVD_addActor);
btnOk = (Button)findViewById(R.id.addDVD_ok);
}

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() ;
}
});

[...]

private void 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. 

EditText editText = new EditText(this);

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. 

private void addActor() {


EditText editNewActor = new EditText(this);
addActoresLayout.addView(editNewActor);
}

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;

public class AddDVDActivity extends Activity{

EditText editTituloPelicula;
EditText editAnyo;
EditText editResumen;
Button btnAddActor;
Button btnOk;
LinearLayout addActoresLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// asignación del archivo de layout


setContentView(R.layout.activity_adddvd);

// Obtención de las referencias sobre los componentes


editTituloPelicula =
(EditText)findViewById(R.id.addDVD_titulo);
editAnyo= (EditText)findViewById(R.id.addDVD_anyo);
editResumen= (EditText)findViewById(R.id.addDVD_resumen);
btnAddActor =
(Button)findViewById(R.id.addDVD_addActor);
btnOk = (Button)findViewById(R.id.addDVD_ok);
addActoresLayout =
(LinearLayout)findViewById(R.id.addDVD_addActorLayout);

// Declaración del OnClickListener


btnAddActor.setOnClickListener(new

- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
View.OnClickListener() {
@Override
public void onClick(View v) {
addActor();
}
});
}

private void addActor() {


EditText editNewActor = new EditText(this);
addActoresLayout.addView(editNewActor);
}
}

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. 

Edite el archivo  AndroidManifest.xml para definir que la actividad  AddDVDActivity se lance tras 


el arranque de la aplicación. 

<?xml version="1.0" encoding="utf-8"?>


<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">
</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: 

<?xml version="1.0" encoding="utf-8"?>


<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_margin="8dp"
android:layout_height="match_parent">
<!-- Zona de título de la película -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/titulo_de_la_pelicula"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addDVD_titulo" />

<!-- Zona del año de aparición -->


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

© É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" />

<!-- 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:id="@+id/addDVD_actor1" />
</LinearLayout>
<!-- 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"

- 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: 
 

private void addActor() {


EditText editNewActor = new EditText(this);
editNewActor.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME
| InputType.TYPE_TEXT_FLAG_CAP_WORDS);
addActoresLayout.addView(editNewActor);
}

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. 

protected void onCreate(Bundle savedInstanceState)

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;

public class AddDVDActivity extends Activity{

EditText editTituloPelicula;
EditText editAnyo;
EditText editResumen;
Button btnAddActor;
Button btnOk;
LinearLayout addActoresLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// asignación del archivo de layout


setContentView(R.layout.activity_adddvd);

// Obtención de las referencias sobre los componentes


editTituloPelicula = (EditText)findViewById(R.id.addDVD_titulo);
editAnyo= (EditText)findViewById(R.id.addDVD_anyo);
editResumen= (EditText)findViewById(R.id.addDVD_resumen);
btnAddActor = (Button)findViewById(R.id.addDVD_addActor);
btnOk = (Button)findViewById(R.id.addDVD_ok);

addActoresLayout =
(LinearLayout)findViewById(R.id.addDVD_addActorLayout);

btnAddActor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addActor(null);
}
});

// ¿Es una recreación tras una rotación del pantalla ?


if(savedInstanceState!=null) {
String [] actores
=savedInstanceState.getStringArray("actores");
for(String s: actores) {
addActor(s);
}
}

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);
}

private void addActor(String content) {


EditText editNewActor = new EditText(this);
editNewActor.
setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME
| InputType.TYPE_TEXT_FLAG_CAP_WORDS);
if(content!=null)
editNewActor.setText(content);

addActoresLayout.addView(editNewActor);
}
}

El archivo de layout es el siguiente: 

<?xml version="1.0" encoding="utf-8"?>


<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_margin="8dp"
android:layout_height="match_parent">
<!-- Zona de título de la película -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/titulo_de_la_pelicula"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addDVD_titulo" />

<!-- Zona del año de aparición -->


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/anyo_de_aparicion_etiqueta"
android:textSize="18sp"
android:textStyle="bold"/>
<EditText
android:layout_width="100dp"
android:layout_height="wrap_content"

- 8- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
android:id="@+id/addDVD_anyo"
android:inputType="number"/>

<!-- 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">
</LinearLayout>
<!-- 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"
android:inputType="textCapSentences|textMultiLine"/>

<!-- 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"/>

© É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. 

Pulse  [Alt][Enter]  para  importar  automáticamente  el  paquete 


android.database.sqlite.SQLiteOpenHelper. 

La clase creada contiene ahora el siguiente código: 

package com.ejemplo.locdvd;

import android.database.sqlite.SQLiteOpenHelper;

public class LocalSQLiteOpenHelper extends 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 pop­up, 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;

public class LocalSQLiteOpenHelper extends 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: 

SQLiteOpenHelper(Context context, String name,


SQLiteDatabase.CursorFactory factory, int version)

SQLiteOpenHelper(Context context, String name,


SQLiteDatabase.CursorFactory factory, int version,
DatabaseErrorHandler errorHandler)

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:  

static String DB_NAME="locDVD.db";


static int DB_VERSION=1;

public LocalSQLiteOpenHelper(Context context) {


super(context, DB_NAME, null, DB_VERSION);
}

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: 

String sqlFilTable ="CREATE TABLE DVD(id INTEGER PRIMARY KEY," +

© É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: 

public class DVD {


long id;
String titulo;
int anyo;
String[]actores;
String resumen;

public long getId() {


return id;
}

public void setId(long id) {


this.id = id;
}

public String getTitulo() {

© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 1-
return titulo;
}

public void setTitulo(String titulo) {


this.titulo = titulo;
}

public int getAnyo() {


return anyo;
}

public void setAnyo(int anyo) {


this.anyo = anyo;
}

public String[] getActores() {


return actores;
}

public void setActores(String[] actores) {


this.actores = actores;
}

public String getResumen() {


return resumen;
}

public void setResumen(String resumen) {


this.resumen = resumen;
}
}

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. 

LocalSQLiteOpenHelper helper = new LocalSQLiteOpenHelper(this);


SQLiteDatabase dbReadable = helper.getReadableDatabase();
SQLiteDatabase dbWritable = helper.getWritableDatabase();

El  método  execSQL,  visto  durante  la  creación  de  la  base  de  datos,  permite  ejecutar  una  consulta  SQL  que  no 
devuelve datos.  

public void execSQL (String sql)

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.  

public Cursor query (boolean distinct, String table, String[]


columns, String selection, String[] selectionArgs, String
groupBy, String having, String orderBy, String limit)

public Cursor rawQuery (String sql, String[] selectionArgs)

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: 

public static ArrayList<DVD> getDVDList(Context context) {


LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);

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 

int getColumnIndex(String name)  Devuelve el índice de la columna llamada  name. 

int getColumnCount()  Devuelve el número de columnas. 

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. 

private DVD(Cursor cursor) {


id = cursor.getLong(cursor.getColumnIndex("id"));
titulo = cursor.getString(cursor.getColumnIndex("titulo"));
anyo = cursor.getInt(cursor.getColumnIndex("anyo"));
actores =
cursor.getString(cursor.getColumnIndex("actores")).split(";");
resumen = cursor.getString(cursor.getColumnIndex("resumen"));
}

Ahora es posible completar el método getDVDList: 

public static ArrayList<DVD> getDVDList(Context context) {


ArrayList<DVD> listDVD = new ArrayList<>();
LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = db.query(true, "DVD", new String[]{"id",
"titulo", "anyo", "actores", "resumer"}, null,
null,null,null,"titulo", null );

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: 
 

public static DVD getDVD(Context context, long id) {


DVD dvd = null;
LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getReadableDatabase();
String where ="id = " + String.valueOf(id);
Cursor cursor = db.query(true, "DVD", new String[]{"id",
"titulo", "anyo", "actores", "resumen"}, where,
null,null,null,"titulo", null );

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: 

public long insert (String table, String nullColumnHack,


ContentValues values)

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: 

public void insert(Context context) {


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);

LocalSQLiteOpenHelper helper = new


LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getWritableDatabase();
this.id=db.insert("DVD", null, values);
db.close();
}

SQLiteDatabase presenta también el método update, que permite actualizar un registro: 

public int update (String table, ContentValues values, String whereClause,


String[] whereArgs)

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: 

public void update(Context context) {

- 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: 

public int delete (String table, String whereClause, String[]


whereArgs)

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: 
 

public void delete(Context context) {


String whereClause = "id= ?" ;
String[] whereArgs = new String[1];
whereArgs[0] = String.valueOf(this.id);
LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getWritableDatabase();
db.delete("DVD", whereClause,whereArgs);
db.close();
}

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: 

l public void beginTransaction (): inicia la transacción. 


l public void endTransaction ():  termina  la  transacción.  Los  comandos se  ejecutan  si  la  transacción  se 
declara correcta. 

l public void setTransactionSuccessful (): indica que la transacción ha sido correcta. Típicamente, 


hay  que  invocar  a  endTransaction  inmediatamente  después  de  llamar  a  setTransactionSuccessful
(). 

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. 

public abstract SharedPreferences getSharedPreferences (String


name, int mode)

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: 

boolean getBoolean(String key, boolean defValue)


float getFloat(String key, float defValue)
int getInt(String key, int defValue)
long getLong(String key, long defValue)
String getString(String key, String defValue)

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. 

putBoolean(String key, boolean value)


putFloat(String key, float value)
putInt(String key, int value)
putLong(String key, long value)
putString(String key, String value)

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 pop­up 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

El  framework  Android  integra  lo  esencial  del  paquete  java.io, la lectura o escritura de los datos en Android no 


difiere de los métodos clásicos conocidos en Java. 

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. 

La clase  BufferedReader presenta el método  readLine, que lee una línea completa de un archivo de texto y 


devuelve el resultado en forma de dato String. 

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: 

InputStreamReader (InputStream stream)

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: 

InputStreamReader reader = null;


InputStream file=null;
BufferedReader bufferedReader=null;

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. 

String line= null;


while((line=bufferedReader.readLine())!=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: 

private void readEmbeddedData() {


InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
file = getAssets().open("data.txt");
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];

© É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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/listItemDVD_titulo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:textStyle="bold"/>
<TextView
android:id="@+id/listItemDVD_anyo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/listItemDVD_resumen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="3"
android:ellipsize="end"
android:textStyle="italic"/>
</LinearLayout>

© É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. 

BaseAdapter,  invoca  la  clase 


La  aplicación  LocDVD,  en  vez  de  utilizar  directamente  la  clase 
ArrayAdapter<T>,  que  implementa  BaseAdapter  utilizando  un  tipo  genérico.  Para  hacer  que  la 
representación gráfica se adapte específicamente a las necesidades de la aplicación, cree una clase que herede de 
ArrayAdapter<T> y sobrecargue el método getView. 

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: 

public ArrayAdapter (Context context, int resource, List<T> objects)

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: 

public class DVDAdapter extends ArrayAdapter<DVD> {

public DVDAdapter(Context context, List<DVD>) {


super(context, -1, listDVD);
}
}

- 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. 

View inflate(int resource, ViewGroup root)

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: 

DVD dvd = getItem(pos);

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;
}

DVD dvd = getItem(pos);

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;

public DVDAdapter(Context context, List<DVD> objects) {


super(context, -1, objects);
this.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: 

public void setAdapter (ListAdapter adapter)

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. 

ArrayList<DVD> dvdList = DVD.getDVDList(this);


DVDAdapter dvdAdapter = new DVDAdapter(this, dvdList);

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/listItemDVD_titulo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:textStyle="bold"/>
<TextView
android:id="@+id/listItemDVD_anyo"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/listItemDVD_resumen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="3"
android:ellipsize="end"
android:textStyle="italic"/>
</LinearLayout>

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;

public class DVDAdapter extends ArrayAdapter<DVD> {

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;
}

DVD dvd = getItem(pos);

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;

public class MainActivity extends Activity {

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);
}

private void readEmbeddedData() {


InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
file = getAssets().open("data.txt");
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(this);
}
}
} catch (IOException e) {
e.printStackTrace();

- 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. 

public void setOnItemClickListener


(AdapterView.OnItemClickListener listener)

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. 

onItemClick(AdapterView<?> parent, View view, int position, long id)

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;
}

DVD dvd = getItem(pos);

// la referencia al DVD en curso se almacena en la vista en curso.


view.setTag(dvd);

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;
}

En este caso, el método  onItemClick debe invocar el complementario del método  setTag: el método  getTag


(). 

@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. 

public void startActivity (Intent intent)

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: 

public Intent (Context paqueteContext, Class<?> cls)

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: 

public Intent putExtra (String name, boolean value)


public Intent putExtra (String name, boolean[] value)
public Intent putExtra (String name, int value)
public Intent putExtra (String name, int[] value)
public Intent putExtra (String name, String value)
public Intent putExtra (String name, String[] value)

© É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: 

private void startViewDVDActivity(long dvdId) {


Intent intent = new Intent(this, ViewDVDActivity.class);
intent.putExtra("dvdId",dvdId);
startActivity(intent);
}

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. 

El método  onItemClick invoca simplemente el método  startViewDVDActivity, pasándole como parámetro 


el identificador del DVD escogido por el usuario: 

@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;

public class MainActivity extends Activity {

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();
}

private void startViewDVDActivity(long dvdId) {


Intent intent = new Intent(this, ViewDVDActivity.class);
intent.putExtra("dvdId",dvdId);
startActivity(intent);
}

@Override
public void onResume() {
super.onResume();
ArrayList<DVD> dvdList = DVD.getDVDList(this);
DVDAdapter dvdAdapter = new DVDAdapter(this, dvdList);
list.setAdapter(dvdAdapter);
}

private void readEmbeddedData() {


InputStreamReader reader = null;
InputStream file=null;
BufferedReader bufferedReader=null;
try {
file = getAssets().open("data.txt");
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];

© É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. 

Intent intent = getIntent();

El objeto  Intent expone los métodos  getExtra, que varían según el tipo del dato que se ha de devolver. Por 


ejemplo: 

boolean getBooleanExtra(String name, boolean defaultValue)


boolean[] getBooleanArrayExtra(String name)
int getIntExtra(String name, int defaultValue)
int[] getIntArrayExtra(String name)
String getStringExtra(String name)
String[] getStringArrayExtra(String name)
<T extends Parcelable> T getParcelableExtra(String name)
Parcelable[] getParcelableArrayExtra(String name)

- 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. 

Intent intent = getIntent();


if (intent.hasExtra("una-clave")){
[...]
}

El identificador del DVD se recupera con el siguiente código: 

Intent intent = getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);

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);

Intent intent = getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);
[...]
}

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: 

DVD dvd = DVD.getDVD(this, dvdId);

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;

public class ViewDVDActivity extends Activity{

TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;

DVD dvd;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// asignación del archivo de layout


setContentView(R.layout.activity_viewdvd);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)findViewById(R.id.anyoDVD);
txtResumenPelicula= (TextView)findViewById(R.id.resumenPelicula);
layoutActores =
(LinearLayout)findViewById(R.id.layoutActores);

Intent intent = getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);

dvd = DVD.getDVD(this, dvdId);


}

@Override
protected void onResume() {
super.onResume();

txtTituloDVD.setText(dvd.getTitulo());
txtAnyoDVD.setText(

String.format(getString(R.string.anyo_de_aparicion),
dvd.getAnyo()));

for(String actor: dvd.getActores()) {


TextView textView = new TextView(this);
textView.setText(actor);
layoutActores.addView(textView);
}
txtResumenPelicula.setText(dvd.getResumen());
}

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. 

El componente  Spinner utiliza, como el componente  ListView, un adaptador para gestionar los datos. Además 


de  las  características  vistas  para  el  componente  ListView,  el  adaptador  que  se  utiliza  para  el  Spinner  debe 
implementar la interfaz SpinnerAdapter. 

Esta interfaz exige, además del método getView, la implementación del método getDropDownView: 

View getDropDownView(int position, View convertView, ViewGroup


parent)

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 pop­up 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: 

private void upgradeToVersion2(SQLiteDatabase db)

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:  

private void upgradeToVersion2(SQLiteDatabase db) {


String sqlCommand = "ALTER TABLE DVD ADD COLUMN
fechaVisionado NUMERIC";
db.execSQL(sqlCommand);
}

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);
}

Por último, hay que agregar la propiedad  fechaVisionado en la clase  DVD: también aquí, la fecha de visionado 


se almacena como entero long. 

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 pop­up 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);

// asignación del archivo de layout


setContentView(R.layout.activity_viewdvd);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)findViewById(R.id.anyoDVD);
txtResumenPelicula= (TextView)findViewById(R.id.resumenPelicula);
layoutActores = (LinearLayout)findViewById(R.id.layoutActores);
setFechaVisionado = (Button)findViewById(R.id.setFechaVisionado);
txtFechaUltimoVisionado =(TextView)findViewById(R.id.fechaVisionado);

Intent intent = getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);

dvd = DVD.getDVD(this, dvdId);

setFechaVisionado.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
}

private void 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. 

En  vez  de  utilizar  el  componente 


DatePicker  tal  cual,  es  más  habitual  instanciar  un  componente 
DatePickerDialog, que se encarga completamente de la gestión del pop­up. 

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");

String dateValue = String.format(


getString(R.string.fecha_ultimo_visionado_con_valor),
simpleDateFormat.format(calendar.getTime()));

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. 

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));

Por  último,  hay  que  lanzar  la  visualización  de  la  ventana  pop­up  invocando  al  método  show()  de  la  clase 
DatePickerDialog. 

datePickerDialog.show();

El código completo del método showDatePicker es el siguiente: 

private void showDatePicker() {


DatePickerDialog datePickerDialog;

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");

String dateValue = String.format(


getString(R.string.fecha_ultimo_visionado_con_valor),
simpleDateFormat.format(calendar.getTime()));

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. 

public class MiTextView extends 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: 

public class MiTextView extends TextView {


public MiTextView(Context context) {
super(context);
}

public MiTextView(Context context, AttributeSet attrs) {


super(context, attrs);
}
}

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: 

<?xml version="1.0" encoding="utf-8"?>


<resources>
<declare-styleable name="nombre_del_conjunto_de_atributos">
<attr name="nombre_del_atributo_1"
format="tipo_de_datos_esperado"/>
<attr name="nombre_del_atributo_2"
format="tipo_de_datos_esperado"/>
[...]
</declare-styleable>
</resources>

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: 

<?xml version="1.0" encoding="utf-8"?>


<resources>
<declare-styleable name="misAtributos">
<attr name="miAtributo1" format="boolean"/>
<attr name="miAtributo2" format="string"/>
</declare-styleable>
</resources>

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mis_atributos="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<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. 

public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs)

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: 

TypedArray typedArray = context.obtainStyledAttributes(attrs,


R.styleable.misAtributos);

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: 

boolean getBoolean(int index, boolean defValue)


float getFloat(int index, float defValue)
int getInteger(int index, int defValue)
String getString(int index)
[...]

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;

public class MiTextView extends TextView {


public MiTextView(Context context) {
super(context);
}

public MiTextView(Context context, AttributeSet attrs) {


super(context, attrs);
TypedArray typedArray =
context.obtainStyledAttributes(attrs, R.styleable.misAtributos);
boolean valorMiAtributo1 =
typedArray.getBoolean(R.styleable.misAtributos_miAtributo1, false);
String valorMiAtributo2 =
typedArray.getString(R.styleable.misAtributos_miAtributo2);

}
}

- 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. 

onActivityCreate  Se invoca cuando el método  onCreate de la actividad termina. 

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. 

onDestroyView  Se invoca para liberar los eventuales recursos de tipo  View. 

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. 

De manera cotidiana, el uso de esta biblioteca, llamada  android-support v4, no cambia en nada la forma de 


trabajar con los fragmentos. Por un lado, Android Studio incorpora automáticamente la referencia a la biblioteca de 
soporte cuando se crea un nuevo proyecto y, por otro lado, la jerarquía de clases se ha respetado: 

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: 

public class ListDVDFragment extends 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. 

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

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>

Una vez completado y registrado el archivo, hay que invocar el método  inflate en  onCreateView, 


pasándole como parámetro el identificador del archivo de layout creado. El código correspondiente es el 
siguiente: 

@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. 

Mueva el método  onResume de la clase  MainActivity a la clase  ListDVDFragment e importe las 


clases utilizadas en este método. 

Android Studio señala varios errores a nivel del uso de la palabra clave  this:  en  efecto,  a  diferencia  de  la  clase 


Activity, la clase Fragment no implementa la interfaz  Context. Sin embargo, es fácil recuperar una referencia 
a  la  actividad padre  de  un  fragmento  invocando  el  método  getActivity() de  la  clase  Fragment.  Esta 
referencia sustituye el contexto de ejecución. 

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: 

private void startViewDVDActivity(long dvdId) {


Intent intent = new Intent(getActivity(),
ViewDVDActivity.class);
intent.putExtra("dvdId",dvdId);
startActivity(intent);
}

Mueva, por último, la declaración del administrador de eventos onItemClick de la lista. 

El código es un simple copia­pega 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;

public class ListDVDFragment extends Fragment {

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);
}

private void startViewDVDActivity(long dvdId) {


Intent intent = new Intent(getActivity(),
ViewDVDActivity.class);
intent.putExtra("dvdId",dvdId);
startActivity(intent);

- 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: 

FragmentManager fragmentManager = getFragmentManager();

Sin  embargo,  el  objeto 


FragmentManager  solo  tiene  en  cuenta  la  clase  Fragment  nativa  (la  clase 
android.app.Fragment).  Y,  como  hemos  visto  antes,  la  opción  ha  sido  utilizar  clases  de  soporte  para  que  la 
aplicación  sea  compatible  con  los  terminales  más  antiguos,  lo  que  nos  prohíbe  utilizar  directamente  el  objeto 
FragmentManager en este caso concreto.  

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;
[...]

public class MainActivity extends 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. 

private void openFragment(Fragment 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. 

FragmentManager fragmentManager = getSupportFragmentManager();

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: 

replace (int containerViewId, Fragment fragment, String tag)

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. 

addToBackStack (String name)

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: 

private void openFragment(Fragment fragment) {

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  pop­up  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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/main_placeHolder"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/detail_placeHolder"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="match_parent"/>
</LinearLayout>

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. 

Observe  que  la  propiedad  layout_weight permite definir una dimensión relativa: aquí, el ancho de la pantalla 


se  divide  en  tres  secciones,  la  lista  ocupa  un  tercio  de  la  pantalla  y  la  zona  detail_placeHolder  ocupa  los 
dos tercios restantes. 

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;

public class ViewDVDFragment extends Fragment{

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);

// asignación del archivo de layout


View view = inflater.inflate(R.layout.activity_viewdvd,
null);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)view.findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)view.findViewById(R.id.anyoDVD);

© É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);

Intent intent = getActivity().getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);

setFechaVisionado.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});

return view;
}

private void showDatePicker() {


DatePickerDialog datePickerDialog;

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());

SimpleDateFormat simpleDateFormat = new


SimpleDateFormat("dd-MM-yyyy");
String dateValue =
String.format(getString(R.string.fecha_ultimo_visionado_con_valor),
simpleDateFormat.format(calendar.getTime()));
txtFechaUltimoVisionado.setText(dateValue);
}
};

Calendar calendar = Calendar.getInstance();


if(dvd.fechaVisionado>0) {
calendar.setTimeInMillis(dvd.fechaVisionado);
}

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()));

for(String actor: dvd.getActores()) {


TextView textView = new TextView(getActivity());
textView.setText(actor);
layoutActores.addView(textView);
}
txtResumenPelicula.setText(dvd.getResumen());
}

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. 

De momento, la gestión del clic se realiza directamente en el fragmento  ListDVDFragment: en el marco de uso 


de varios fragmentos, la carga del fragmento que muestra el detalle del DVD debe hacerse desde la actividad host. 
Es, en efecto, la actividad la que puede determinar cómo realizar esta carga. 

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: 

public class ListDVDFragment extends Fragment {

public interface OnDVDSelectedListener {


public void onDVDSelected(long dvdId);
}

[...]

© É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. 

public class MainActivity extends FragmentActivity implements


ListDVDFragment.OnDVDSelectedListener {

@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;

public class ListDVDFragment extends Fragment {

public interface OnDVDSelectedListener {


public void onDVDSelected(long dvdId);
}

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. 

private void openDetailFragment(Fragment 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. 

private void openDetailFragment(Fragment fragment) {


FragmentManager fragmentManager =
getSupportFragmentManager();

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. 

El código correspondiente a la comprobación utiliza el método  findViewById de la clase  Activity: el método 


devuelve  null  si  el  identificador  del  recurso  que  se  pasa  como  parámetro  no  se  corresponde  con  una  vista 
cargada. 

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: 

private void openDetailFragment(Fragment fragment) {


FragmentManager fragmentManager =
getSupportFragmentManager();

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. 

En el marco de uso del fragmento, no es posible utilizar este mismo objeto  Intent: la actividad es la misma, pero 


el objeto  Intent ya no está disponible. Como reemplazo, Android permite asignar un objeto de tipo Bundle a los 
fragmentos; se debe utilizar este objeto para pasar variables simples a un fragmento. 

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. 

private void startViewDVDActivity(long dvdId) {


ViewDVDFragment viewDVDFragment = new ViewDVDFragment();

// Tiene en cuenta el paso del parámetro

openDetailFragment(viewDVDFragment);
}

Tras el código de instanciación del fragmento, defina una instancia de Bundle. 

Bundle bundle = new Bundle();

Agregue en el objeto bundle una entrada para el identificador del DVD. 

bundle.putLong("dvdId",dvdId);

Invoque  el  método 


setArguments  de  la  clase  Fragment  para  asignar  el  bundle  a  la  instancia 
viewDVDFragment. 

viewDVDFragment.setArguments(bundle);

El código completo del método startViewDVDActivity es el siguiente: 

private void startViewDVDActivity(long dvdId) {


ViewDVDFragment viewDVDFragment = new ViewDVDFragment();

Bundle bundle = new Bundle();


bundle.putLong("dvdId",dvdId);
viewDVDFragment.setArguments(bundle);

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: 

Intent intent = getIntent();


long dvdId = intent.getLongExtra("dvdId",-1);

- 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. 

long dvdId = getArguments().getLong("dvdId",-1);

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);

// asignación del archivo de layout


View view = inflater.inflate(R.layout.activity_viewdvd,
null);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)view.findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)view.findViewById(R.id.anyoDVD);
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);

long dvdId = getArguments().getLong("dvdId",-1);

dvd = DVD.getDVD(getActivity(), dvdId);

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í: 

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_reinicializar"
android:title="@string/reinicialice"
app:showAsAction="never" />
<item android:id="@+id/menu_informacion"
android:title="@string/info"
app:showAsAction="never"/>
</menu>

Se han agregado las siguientes entradas al archivo strings.xml: 

<string name="reinicialice">"Reinicializar la base de datos"</string>


<string name="info">Información</string>

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. 

Google  provee,  a  través  de  la  biblioteca  de  soporte  Android­Support v7, una alternativa para mostrar la barra de 


acción sin tener que utilizar un tema específico: la clase AppCompatActivity. 

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.  

La clase AppCompatActivity, como la clase  Activity, expone el método  getMenuInflater, que devuelve 


una instancia de MenuInflater. 

- 4- © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
MenuInflater inflater = getMenuInflater();

El método  inflate de la clase  MenuInflater recibe como parámetro el identificador del recurso de tipo menú 


que  debe  utilizarse  para  el  contenido  del  menú  y  una  instancia  de  objeto  de  tipo  Menu,  que  representa  el  menú 
que debe «inflarse»: típicamente, el objeto Menu pasado como parámetro del método onCreateOptionsMenu. 

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. 

Reemplace,  en  este  archivo, 


etiqueta  raíz  la 
LinearLayout  por  la  etiqueta 
android.support.v4.widget.DrawerLayout (también hay que reemplazar la etiqueta de cierre 
</LinearLayout>). 

<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: 

<?xml version="1.0" encoding="utf-8"?>


<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ffffff"
android:textStyle="bold"
android:textSize="22sp"
android:layout_margin="32dp"/>

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. 

Tras la asignación del adaptador al objeto  ListView, asigne un nuevo  OnItemClickListener, que 


se define de tipo anónimo. 

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);

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

© É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. 

public void openDrawer(int gravity)


public void closeDrawer(int gravity)

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 onDrawerSlide(View drawerView, float slideOffset) 


Se invoca cuando se modifica la posición del panel. El parámetro slideOffset define el ratio de desplazamiento. 
El valor 0 indica que el panel está completamente cerrado, el valor 1 indica que está completamente abierto. 

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. 

private void ensureReInitializeApp() {

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. 

AlertDialog.Builder builder = new AlertDialog.Builder(this);

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) {

}
}
);

El método  onClick de la instancia  OnClickListener debe realizar la reinicialización de la base de datos. Esta 


reinicialización  se  lleva  a  cabo  en  dos  etapas:  en  primer  lugar  hay  que  recrear  la  base  de  datos  y,  a  continuación, 
alimentarla con los datos insertados por defecto. 

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. 

Defina un nuevo método público y estático, que reciba como parámetro una instancia de  Context y que 


no devuelva nada. Llame a este método deleteDatabase.  

public static void deleteDatabase(Context context)

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: 

public static void deleteDatabase(Context context) {


context.deleteDatabase(DB_NAME);
}

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();

Intent intent = new Intent(MainActivity.this,


MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}

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. 

AlertDialog dialog = builder.create();


dialog.show();

En definitiva, el método ensureReInitializeApp tiene el siguiente aspecto:  

private void ensureReInitializeApp() {


AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.confirmar_reinicializacion_title);
builder.setMessage(R.string.confirmar_reinicializacion_message);
builder.setNegativeButton(R.string.no, null);
builder.setPositiveButton(R.string.si,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

LocalSQLiteOpenHelper.deleteDatabase(MainActivity.this);
readEmbeddedData();
}
});

AlertDialog dialog = builder.create();


dialog.show();
}

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. 

Sitúese  en  la  carpeta 


/res/layout  y  cree  un  nuevo  archivo  de  layout.  Llame  a  este  archivo 
dialog_informacion.xml. 
Para el ejemplo, el layout estará compuesto únicamente de un controlador de vista  LinearLayout, que 
contendrá por su parte un componente TextView.  

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í: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialog_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:lines="10"
android:layout_margin="16dp"/>
</LinearLayout>

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. 

private void showInformation() {


[...]
}

Como con una ventana emergente clásica, instancie un objeto de tipo AlertDialog.Builder. 

AlertDialog.Builder builder = new AlertDialog.Builder(this);

© É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: 

LayoutInflater inflater = getLayoutInflater();


View view = inflater.inflate(R.layout.dialog_informacion, null);

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: 

private void showInformation() {


AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(R.string.info);
builder.setPositiveButton(R.string.cerrar, null);

LayoutInflater inflater = getLayoutInflater();


View view = inflater.inflate(R.layout.dialog_informacion, null);
TextView message =(TextView)view.findViewById(R.id.dialog_message);
message.setText(R.string.mensaje_informacion);
message.setMovementMethod(
new android.text.method.ScrollingMovementMethod());

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. 

AsyncTask<Params, Progress, Result>

Los tipos genéricos  Params,  Progress,  Result representan, como se detalla aquí arriba, los tipos de datos que 


se pasan como parámetro o devueltos por los métodos expuestos por la clase. 

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 Result doInBackground(Params... params):  este  método  es  abstracto,  debe  sobrecargarse 


obligatoriamente, y se ejecuta en un thread como tarea de fondo cuando termina el método onPreExecute. En este 
método  debe  realizarse  el  procesamiento,  y  no  permite  llevar  a  cabo  ninguna  operación  sobre  los  componentes  de  la 
interfaz. doInBackground recibe como parámetro un conjunto de datos de tipo genérico Params y debe devolver 
un objeto de tipo genérico Result. 

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. 

l void onProgressUpdate(Progress... values): este método se ejecuta en el thread principal cuando 


se  invoca  el  método  publishProgress  desdel  método  doInBackground.  Su  cometido  es,  principalmente, 
gestionar la visualización de una ventana emergente de progreso. 

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>. 

class AsyncReadEmbeddedData extends AsyncTask<String, Integer, Boolean> {


}

Hay  que  sobrecargar  los  métodos  onPreExecute,  doInBackground,  onProgressUpdate  y 


onPostExecute: 

class AsyncReadEmbeddedData extends 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 método  doInBackground parte del código del método  readEmbeddedData que realiza la lectura e 


inserción de los DVD de ejemplo. El nombre del archivo, en vez de indicarse directamente en el cuerpo del 
método, se pasa como parámetro de doInBackground. 

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  pop­up  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: 

class AsyncReadEmbeddedDataextends AsyncTask<String, Integer, Boolean> {

ProgressDialog progressDialog;
[...]
};

El método  onPreExecute instancia la variable  progressDialog, introduce el título de la ventana emergente de 


progreso, define el modo de visualización y abre la ventana emergente. 

@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: 

<string name="x_dvd_insertados_en_la_base_de_datos">"%d DVD insertados en


la base de datos"</string>

El código de definición de la clase AsyncReadEmbeddedData es, completo, el siguiente: 

class AsyncReadEmbeddedData extends AsyncTask<String, Integer, Boolean> {

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. 

private void readEmbeddedData() {


AsyncReadEmbeddedData asyncReadEmbeddedData =
new AsyncReadEmbeddedData();
asyncReadEmbeddedData.execute("data.txt");
}

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. 

public void updateDVDList() {


ArrayList<DVD> dvdList = DVD.getDVDList(getActivity());
DVDAdapter dvdAdapter = new DVDAdapter(getActivity(), dvdList);
list.setAdapter(dvdAdapter);
}

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: 

private void openFragment(Fragment fragment, String tag) {

Modifique la llamada al método replace, para integrar el tag: 

transaction.replace(R.id.main_placeHolder, fragment, tag);

El código del método openFragment es, por tanto, el siguiente:  

private void openFragment(Fragment fragment, String tag) {

FragmentManager fragmentManager =
getSupportFragmentManager();
FragmentTransaction transaction =
fragmentManager.beginTransaction();
transaction.replace(R.id.main_placeHolder, fragment, tag);
transaction.addToBackStack(null);
transaction.commit();
}

También hay que modificar la llamada a  openFragment, agregando el parámetro  tag. El tag debe definirse como 


una constante, disponible a nivel de la clase MainActivity: 

private static final String TAG_FRAGMENT_LISTDVD="FragementListDVD";

[...]

@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: 

private void ensureReInitializeApp() {


AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.confirmar_reinicializacion_titulo);
builder.setMessage(R.string.confirmar_reinicializacion_mensaje);
builder.setNegativeButton(R.string.no, null);
builder.setPositiveButton(R.string.si,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
LocalSQLiteOpenHelper.deleteDatabase(MainActivity.this);
readEmbeddedData();
}
});

AlertDialog dialog = builder.create();


dialog.show();
}

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. 

private AsyncTask<String, Integer, Boolean> asyncTask =


new AsyncTask<String, Integer, Boolean>() {

@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: 

public class LocDVDIntentService extends 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. 

<?xml version="1.0" encoding="utf-8"?>


<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" >
[...]
<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. 

Public class MiActivity extends Activity {


[...]

private void startService() {


Intent intent =
new Intent(MiActivity.this,
LocDVDIntentService.class);
intent.putExtra("waitDuration", 30000);
startService(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: 

private void startService() {


[...]
Handler handler = new android.os.Handler() {
@Override
public void handleMessage(Message msg) {
[...]
}
};
[...]
}

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. 

Messenger mensajero = new Messenger(handler);


intent.putExtra("mensajero",mensajero);

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: 

Message message = Message.obtain();

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 class LocDVDIntentService extends IntentService{

public LocDVDIntentService() {
super("LocDVDIntentService");
}

@Override
protected void onHandleIntent(Intent intent) {

int waitDuration = intent.getIntExtra("waitDuration", 1000);

- 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: 

private void startService() {

Handler handler = new android.os.Handler() {


@Override
public void handleMessage(Message msg) {
Bundle reply = msg.getData();
String message = reply.getString("reply");
}
};
Messenger mensajero = new Messenger(handler);

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. 

private BroadcastReceiver myBroadcastReceiver = new


BroadcastReceiver() {

@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. 

Intent registerReceiver (BroadcastReceiver receiver, IntentFilter


filter)

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: 

Intent resultIntent = new Intent("LocDVD.ServiceEnded");

La intención definida permite almacenar la información destinada al receptor de eventos: 

resultIntent.putExtra("replyMessage","El servicio ha terminado el


procesamiento");

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: 

IntentFilter intentFilter = new


IntentFilter("LocDVD.ServiceEnded");
registerReceiver(myBroadcastReceiver,intentFilter);

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 class LocDVDIntentService extends IntentService{

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();
}

Intent resultIntent = new Intent("LocDVD.ServiceEnded");


resultIntent.putExtra("replyMessage",
"El servicio ha enviado un broadcast");

sendBroadcast(resultIntent);
}
}

La gestión de la comunicación para la clase MainActivity se realiza así:  

private void startService() {

BroadcastReceiver myBroadcastReceiver = new


BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String receivedMessage =
intent.getStringExtra("replyMessage");
[...]
}
};

Intent intent = new Intent(MainActivity.this,


LocDVDIntentService.class);

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 pop­up 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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"/>

</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;
[...]

public class SearchFragment extends 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);

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);

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. 

Edite el archivo  AndroidManifest.xml del proyecto LocDVD. Este archivo contiene, de momento, la 


declaración de las actividades. 

Agregue,  antes  de  la  etiqueta  <application>,  una  etiqueta  <uses-permission>.  La  sintaxis 
general de la etiqueta es la siguiente: 

<uses-permission android:name="nombre del permiso"/>

Aquí, el permiso se llama  android.permission.INTERNET. El archivo AndroidManifest.xml debe ser el 
siguiente: 

<?xml version="1.0" encoding="utf-8"?>


<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ejemplo.locdvd" >

<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);

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);
searchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchSearch();
}
});
return view;
}

private void launchSearch() {


}

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: 

https://api.themoviedb.org/3/search/movie?query=[Título de la película]&api_key=[api_key privada]

El título lo introduce el usuario a través del componente  EditText searchText. Hay que procurar que el texto 


introducido  por  el  usuario  cumpla  las  reglas  correspondientes  a  las  URL.  Para  ello,  la  plataforma  provee  la  clase 

© Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo - 5-
URLEncoder, que presenta el método estático encode. 

static String encode(String s, String charsetName)

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: 

public JsonObjectRequest(int method, String url, JSONObject jsonRequest,


Listener<JSONObject> listener, ErrorListener errorListener)

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: 

private Response.ErrorListener errorListener =


new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {

}
};

Una vez definidas estas variables, es posible instanciar un objeto de tipo JsonObjectRequest:  

JsonObjectRequest request = new JsonObjectRequest(


Request.Method.GET,
url,
null,
jsonRequestListener ,
errorListener);

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: 

public static RequestQueue Volley.newRequestQueue(Context context)

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: 

public RequestQueue(Cache cache, Network network, int

- 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. 

l ResponseDelivery delivery:  instancia  de  la  interfaz 


com.android.volley.ResponseDelivery, que permite gestionar la respuesta del servicio web. 

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: 

private void launchSearch() {


try {
String api_key="cfa8a04a45xxxxxxxxxxxx14d159495";
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);
JsonObjectRequest request = new JsonObjectRequest(
Request.Method.GET,
url,
null,
jsonRequestListener ,
errorListener);
RequestQueue requestQueue = Volley.newRequestQueue(getActivity());
requestQueue.add(request);

} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

private Response.Listener<JSONObject> jsonRequestListener =


new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
// ¡Completar!
}
};

© É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;
}

private void launchSearch() {


try {
String api_key=" cfa8a04a45xxxxxxxxxxxx14d159495";
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);
JsonObjectRequest request = new JsonObjectRequest(
Request.Method.GET,
url,
null,
jsonRequestListener ,
errorListener);
getRequestQueue().add(request);

} 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. 

public void onResponse(JSONObject response) {


try {
JSONArray jsonArray = response.getJSONArray("Search");
} catch (JSONException e) {
Log.e("JSON",e.getLocalizedMessage());
}
}

A continuación hay que recorrer cada elemento del array recuperado con ayuda de un bucle for. 

for (int i =0;i<jsonArray.length();i++) {


}

Cada objeto JSON del array se obtiene invocando el método getJSONObject del objeto JSONArray. 

JSONObject jsonObject =jsonArray.getJSONObject(i);

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: 

for (int i =0;i<jsonArray.length();i++) {


JSONObject jsonObject =jsonArray.getJSONObject(i);
String title = jsonObject.getString("title");
String releaseDate = jsonObject.getString("release_date");

© É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: 

private Response.Listener<JSONObject> jsonRequestListener =


new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
JSONArray jsonArray =response.getJSONArray("results");
for (int i =0;i<jsonArray.length();i++) {
JSONObject jsonObject =jsonArray.getJSONObject(i);
String title = jsonObject.getString("title");
String releaseDate =
jsonObject.getString("release_date");
String movieId = jsonObject.getString("id");
String overview = jsonObject.getString("overview");
}
} catch (JSONException e) {
Log.e("JSON",e.getLocalizedMessage());
}
}
};

¡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. 

public class SearchFragment extends Fragment {

[...]

public static class Movie {


public String title;
public String releaseDate;
public String movieId;
public String 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>: 

ArrayList<Movie> listOfMovies = new ArrayList<>();


JSONArray jsonArray =response.getJSONArray("results");
for (int i =0;i<jsonArray.length();i++) {
JSONObject jsonObject =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);
}

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
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_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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<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: 

class SearchListAdapter extends ArrayAdapter<Movie> {

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;
}

Movie movie = getItem(pos);


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;
}

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;

public class SearchFragment extends Fragment {

public static class Movie {


public String title;
public String releaseDate;
public String movieId;
public String overview;
}

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;
}

private void launchSearch() {


try {
String api_key="cfa8a04a45e30fc04a822b914d159495";
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);
Log.d("Búsqueda", "url:" + url);
JsonObjectRequest request = new JsonObjectRequest(
Request.Method.GET,
url,
null,
jsonRequestListener ,
errorListener);
getRequestQueue().add(request);

} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

private Response.Listener<JSONObject> jsonRequestListener =


new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
ArrayList<Movie> listOfMovies = new ArrayList<>();
JSONArray jsonArray =
response.getJSONArray("results");
for (int i =0;i<jsonArray.length();i++) {
JSONObject jsonObject =

- 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());
}
}
};

private Response.ErrorListener errorListener =


new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d("Búsqueda","Error " + error.getMessage());
}
};

class SearchListAdapter extends ArrayAdapter<Movie> {

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;
}

Movie movie = getItem(pos);

© É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í: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"

- 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: 

final Button detailButton=


(Button)view.findViewById(R.id.movie_closeDetail);
final RelativeLayout detailLayout =
(RelativeLayout)view.findViewById(R.id.movie_detailLayout);

De  manera  inversa,  el  botón  closeDetail debe permitir ocultar la vista de detalle y hacer visible el botón Más 


detalles. Por tanto, hay que asociarle un OnClickListener y modificar la visibilidad de dichos componentes: 

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: 

String posterPath = response.getString("poster_path");


String plot = response.getString("overview");

El texto debe mostrarse en el componente  TextView detailPlot definido antes. La instancia detailPlot se 


manipula a este nivel y debe declararse como final. 

- 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: 

String url = "https://image.tmdb.org/t/p/w500/" + posterPath;

NetworkImageView  se  hace  invocando  el  método 


La  asignación  de  la  URL  así  obtenida  al  componente 
setImageUrl de la clase NetworkImageView. Este método presenta la siguiente firma: 

setImageUrl(String url, ImageLoader imageLoader)

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. 

Defina una variable en la clase  SearchFragment, de tipo  ImageLoader, e implemente un método de 


acceso getImageLoader: 

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: 

public ImageLoader(RequestQueue queue, ImageCache imageCache)

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: 

public interface ImageCache {


public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}

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: 

ImageLoader.ImageCache imageCache = new ImageLoader.ImageCache() {


public void putBitmap(String url, Bitmap bitmap) {
// Completar
}
public Bitmap getBitmap(String url) {
// Completar
}
};

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. 

ImageLoader.ImageCache imageCache = new ImageLoader.ImageCache() {

LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(10);

public void putBitmap(String url, Bitmap bitmap) {


// Completar
}
public Bitmap getBitmap(String url) {
// Completar
}
};

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: 

imageLoader = new ImageLoader(getRequestQueue(),imageCache);

La implementación completa del método getImageLoader es la siguiente: 

ImageLoader imageLoader;
ImageLoader getImageLoader() {
if(imageLoader==null) {
ImageLoader.ImageCache imageCache = new
ImageLoader.ImageCache() {

LruCache<String, Bitmap> cache =


new LruCache<String, Bitmap>(10);

public void putBitmap(String url, Bitmap bitmap) {


cache.put(url, bitmap);
}

public Bitmap getBitmap(String url) {


return cache.get(url);
}
};

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. 

final Button detailButton =

- 20 - © Éditions ENI – Todos los derechos reservados – Copia personal de Omar Deseusa Sampayo
(Button)view.findViewById(R.id.listItemOMdbFilm_detail);

final RelativeLayout detailLayout =


(RelativeLayout)view.findViewById(R.id.listItemOMdbFilm_detailLayout);
detailButton.setVisibility(View.VISIBLE);
detailLayout.setVisibility(View.GONE);

Al final, el código del adaptador de la lista que presenta los resultados de la búsqueda es el siguiente: 

class SearchListAdapter extends ArrayAdapter<Movie> {

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;
}

final Movie movie = getItem(pos);


view.setTag(movie);

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);

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);
}
});

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) {

new AsyncTask<Void, Void, ArrayList<OMdbFilm>>(){


JSONArray jsonArray;

@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: 

static Spanned fromHtml(String fuente)

El texto transformado se asigna, a continuación, al componente TextView invocando el método setText. 

Por ejemplo, las instrucciones que permiten asignar el texto  «Hola a todos» (el término «todos»  está en negrita, el 


resto del texto está en tipografía estándar) a un componente TextView son las siguientes: 

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: 

public class WebViewActivity extends Activity{

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

WebView webView = new WebView(this);

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: 

Uri uri = Uri.parse("http://www.ediciones-eni.com/");


Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

- 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: 

<uses-feature android:name="nombre de la funcionalidad"


android:required="true|false"/>

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: 

boolean hasSystemFeature (String name)

© É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: 

public class MainActivity extends Activity {


[...]
private void sendSMS() {
PackageManager packageManager = this.getPackageManager();
if( packageManager.
hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
// El terminal puede enviar SMS
}
else {
// El terminal no puede enviar SMS
}
}
}

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. 

public class MainActivity extends AppCompatActivity {

[...]
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. 

final static int REQUEST_SMS = 3;

[...]
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:  

public class MainActivity extends AppCompatActivity implements


ActivityCompat.OnRequestPermissionsResultCallback{

final static int REQUEST_SEND_SMS = 3;


boolean canSendSMS = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
[...]
ensurePermission();
}

private void 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();
}
}
}

private void askPermissions() {


String[] permissions =new String[] {Manifest.permission.SEND_SMS};
requestPermissions(permissions , REQUEST_SEND_SMS);
}

@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: 

public void sendTextMessage (String destinationAddress, String


scAddress, String text, PendingIntent sentIntent, PendingIntent
deliveryIntent)

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. 

La  instancia  de 


PendingIntent  se  obtiene  invocando  el  método  estático  getBroadcast  de  la  clase 
PendingIntent: 

static PendingIntent getBroadcast (Context context, int


requestCode, Intent intent, int flags)

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: 

String SMS_SEND = "SMS_SEND";

Intent sendIntent = new Intent(SMS_SEND);


PendingIntent sendPendingIntent =
PendingIntent.getBroadcast(this,0,sendIntent, 0);

registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
}, new IntentFilter(SMS_SEND));

El resultado del envío se devuelve al  BroadcastReceiver: indica si la acción se ha desarrollado correctamente 


o  bien  qué  tipo  de  error  se  ha  producido.  Para  obtener  este  resultado,  hay  que  invocar  el  método 
getResultCode en el onReceive del objeto BroadcastReceiver. 

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";

SmsManager smsManager = SmsManager.getDefault();

String SMS_SEND = "SMS_SEND";


Intent sendIntent = new Intent(SMS_SEND);

© É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));

smsManager.sendTextMessage(numero, null, mensaje,


sendPendingIntent, null);

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. 

public class MainActivity extends AppCompatActivity implements


ActivityCompat.OnRequestPermissionsResultCallback{

final static int REQUEST_ALL_PERMISSIONS = 5;


boolean canSendSMS = false;
boolean canReceiveSMS = false;

@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();
}

private void ensurePermission() {


canSendSMS =ContextCompat.checkSelfPermission(this,
Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED;
canReceiveSMS = ContextCompat.checkSelfPermission(this, Manifest.
permission.RECEIVE_SMS) == PackageManager.PERMISSION_GRANTED;

if(!(canSendSMS && canReceiveSMS)) {


if (shouldShowRequestPermissionRationale
(Manifest.permission.SEND_SMS ) ||
shouldShowRequestPermissionRationale
(Manifest.permission.RECEIVE_SMS )) {
// proveer una explicación al usuario
AlertDialog.Builder builder =
new AlertDialog.Builder(this);
builder.setTitle("Solicitar permiso");
builder.setMessage("Los permisos de envío y recepción
de SMS son necesarios 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 la aplicación se va a cerrar,
}
});
builder.setPositiveButton("Sí",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which){
askPermissions();
}
});
builder.show();
} else {
askPermissions();
}
}
}

private void askPermissions() {


String[] permissions =
new String[] {Manifest.permission.SEND_SMS,
Manifest.permission.RECEIVE_SMS};
requestPermissions(permissions , REQUEST_ALL_PERMISSIONS);
}

© É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;

public class SMSReceiver extends BroadcastReceiver {


public SMSReceiver() {
}

@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: 

Bundle bundle = intent.getExtras();

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. 

Object[] pdusObj = (Object[]) bundle.get("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: 

SmsMessage smsMessage = SmsMessage.createFromPdu((byte[])


pdusObj[i]);

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) {

Bundle bundle = intent.getExtras();

Object[] pdusObj = (Object[]) bundle.get("pdus");

for (int i = 0; i < pdusObj.length; i++) {


SmsMessage smsMessage =
SmsMessage.createFromPdu((byte[]) pdusObj[i]);
String number =
smsMessage.getDisplayOriginatingAddress();
String message = smsMessage.getDisplayMessageBody();
[...]
}
}

- 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/uses­feature­
element.html#hw­features. 

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. 

La clase  SensorManager ­del paquete  android.hardware­ es el punto de entrada para manipular los sensores 


del terminal. Para obtener una instancia de  SensorManager, hay que invocar el método getSystemService de 
la clase Context. 

SensorManager sensorManager =
(SensorManager)getSystemService(SENSOR_SERVICE);

SensorManager expone el método getSensorList, que devuelve la lista de sensores incorporados al terminal: 

public List<Sensor> getSensorList (int type)

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: 

public Sensor getDefaultSensor (int type)

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: 

onAccuracyChanged(Sensor sensor, int accuracy)


onSensorChanged(SensorEvent event)

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: 

boolean registerListener (SensorEventListener listener, Sensor


sensor, int samplingPeriodUs)

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. 

El  sensor  funciona ­consume  energía­ cuando se le asigna un objeto  SensorEventListener.  De  modo  que  hay 


que invocar, obligatoriamente, el método  unregisterListener de la clase  SensorManager cuando la medida 
ya no es necesaria (o cuando la aplicación se pone en pausa): 

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: 

public Location getLastKnownLocation (String provider)

El  parámetro  String


provider  permite  indicar  qué  fuente  de  datos  debe  utilizarse.  Los  posibles  valores  son 
GPS_PROVIDER (la medida debe provenir del GPS),  NETWORK_PROVIDER (la medida debe provenir de la red) o 

© É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: 

void requestSingleUpdate(String provider, LocationListener listener,


Looper looper)
void requestSingleUpdate(String provider, PendingIntent intent)
void requestSingleUpdate(Criteria criteria, LocationListener listener,
Looper looper)
void requestSingleUpdate(Criteria criteria, PendingIntent intent)

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 onLocationChanged(Location location): se invoca cuando la medida ha cambiado. 


l void onProviderDisabled(String provider):  se  invoca  cuando  el  servicio  de  geolocalización  se 
deshabilita (por el usuario). 

l void onProviderEnabled(String provider):  se  invoca  cuando  el  servicio  de  geolocalización  se 
activa (por el usuario). 

l void onStatusChanged(String provider, int status, Bundle extras):  se  invoca 


cuando  el  estado  del  proveedor  de  geolocalización  cambia.  El  parámetro 
status  indica  el  nuevo  estado  del 
proveedor,  y  puede  tomar  los  valores  OUT_OF_SERVICE  (el  proveedor  ya  no  está  disponible,  por  una  duración 
importante) TEMPORARILY_UNAVAILABLE (el proveedor está temporalmente no disponible) y  AVAILABLE (el 
proveedor  está  disponible).  El  parámetro  Bundle  puede,  eventualmente,  contener  datos  específicos  de  cada 
proveedor. 

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: 

void requestLocationUpdates(long minTime, float minDistance,


Criteria criteria, PendingIntent intent)

void requestLocationUpdates(long minTime, float minDistance,


Criteria criteria, LocationListener listener, Looper looper)

void requestLocationUpdates(String provider, long minTime,


float minDistance, LocationListener listener)

void requestLocationUpdates(String provider, long minTime,


float minDistance, LocationListener listener, Looper looper)

void requestLocationUpdates(String provider, long minTime,


float minDistance, PendingIntent intent)

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. 
 

void removeUpdates(PendingIntent intent)


void removeUpdates(LocationListener listener)

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 

double getLatitude()  Define la latitud medida, en grados. 

double getLongitude()  Define la longitud medida, en grados. 

String getProvider()  Devuelve el proveedor utilizado para realizar la medida. 

float getSpeed()  Devuelve  la  velocidad  de  desplazamiento  medida,  en  metros  por 
segundo. 

long getTime()  Define la hora de la medida, en milisegundos. 

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_material_dark">
<TextView
android:id="@+id/widget_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/accent_material_light"
android:text="Información sobre la película"
android:gravity="center"
android:textSize="22sp"/>
<TextView
android:id="@+id/widget_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/accent_material_light"
android:lines="3"
android:text="Resumen de la película"
android:gravity="center"
android:textSize="14sp"/>

</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;

public class LocDVDWidget extends AppWidgetProvider {

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: 

Void onUpdate(Context c, AppWidgetManager manager, int[]


appWidgetIds)

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. 

En  la  clase  LocDVDWidget, sobrecargue el método  onUpdate: sitúe el cursor en el cuerpo de la clase 


LocDVDWidget,  en  el  editor  de  código,  haga  clic  con  el  botón  derecho  y  seleccione  a  continuación  la 
opción Generate... Seleccione después la opción Override Methods, y por último seleccione onUpdate en la 
lista y haga clic en ok: el código se genera automáticamente. 

@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: 

for(int widgetId: appWidgetIds) {


}

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: 

public RemoteViews (String packageName, int layoutId)

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:  

void setTextViewText(int viewId, CharSequence text)

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: 

ArrayList<DVD> list = DVD.getDVDList(context);


[...]
Random random = new Random();
int randomIndex = random.nextInt(list.size());

DVD selected = list.get(randomIndex);

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);

RemoteViews remoteViews = new RemoteViews(context.getPackageName(),


R.layout.widget_layout);
ArrayList<DVD> list = DVD.getDVDList(context);

for(int widgetId: appWidgetIds) {

Random random = new Random();


int randomIndex = random.nextInt(list.size());

DVD selected = list.get(randomIndex);


remoteViews.setTextViewText(R.id.widget_title,
selected.getTitulo());
remoteViews.setTextViewText(R.id.widget_summary,
selected.getResumen());
}
}

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: 

void setOnClickPendingIntent(int viewId, PendingIntent


pendingIntent)

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: 

static PendingIntent getBroadcast(Context context, int


requestCode, Intent intent, int flags)

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);

Hay  que  agregar,  en  los  datos 


Extras,  la  lista  de  identificadores,  utilizando  la  clave 
AppWidgetManager.EXTRA_APPWIDGET_IDS. El código correspondiente es el siguiente: 

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: 

public void updateAppWidget (int appWidgetId, RemoteViews views)

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);

for(int widgetId: appWidgetIds) {

Random random = new Random();


int randomIndex = random.nextInt(list.size());

DVD selected = list.get(randomIndex);


remoteViews.setTextViewText(R.id.widget_title,
selected.getTitulo());
remoteViews.setTextViewText(R.id.widget_summary,
selected.getResumen());

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: 

<?xml version="1.0" encoding="utf-8"?>


<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
/>

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: 

Dimensiones_en_dp = (número_de_casillas * 74) - 2

Así, si el widget debe ocupar cuatro casillas de altura, su anchura mínima debe ser de 4*74­2 dp, es decir, 294 dp. 

Introduzca la anchura mínima del widget, utilizando la propiedad android:minWidth.  

<?xml version="1.0" encoding="utf-8"?>


<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
/>

La  altura  mínima,  que  corresponde  a  la  propiedad  android:minHeight,  debe  ser  de  72 dp  (es  decir, 
una casilla): 

<?xml version="1.0" encoding="utf-8"?>


<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
/>

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. 

<?xml version="1.0" encoding="utf-8"?>

- 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. 

<?xml version="1.0" encoding="utf-8"?>


<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/widget_layout"
/>

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: 

<?xml version="1.0" encoding="utf-8"?>


<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/widget_layout"
android:widgetCategory="home_screen|keyguard"
/>

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. 

Los widgets se registran como objetos  receiver en el archivo  AndroidManifest.xml (el widget es 


una especialización de BroadcastReceiver). La etiqueta a utilizar para declarar el widget es, por tanto, 
la etiqueta receiver: 

<receiver android:name=".LocDVDWidget" >

</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: 

<receiver android:name=".LocDVDWidget" >


<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<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: 

private void sendNotification() {


}

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. 

Notification.Builder builder = new


Notification.Builder(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: 

private void sendNotification() {


Notification.Builder builder = new
Notification.Builder(getActivity());

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). 

En  el  código  del  método  sendNotification, defina un objeto  Intent. Esta definición debe aparecer 


antes de la asignación del objeto Notification. La intención declarada debe lanzar la actividad principal 
de la aplicación: 

[...]
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: 

private void sendNotification() {


Notification.Builder builder =
new Notification.Builder(getActivity());
builder.setContentTitle(dvd.getTitulo());
builder.setContentText(dvd.getResumen());

builder.setSmallIcon(R.drawable.ic_notification);

Intent intent = new Intent(getActivity(),


MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(getActivity(), 0, intent, 0);
builder.setContentIntent(pendingIntent);

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):  

static ActionProvider getActionProvider(MenuItem item)

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. 

Defina una instancia de  MenuItem, llamada  shareItem. Esta instancia debe obtener valor invocando el 


método findItem del objeto Menu: 

MenuItem shareItem = menu.findItem(R.id.menu_compartir);

- 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 intent = new Intent(Intent.ACTION_SEND);

Hay que especificar el tipo de datos compartidos, invocando el método  setType del objeto  Intent. Aquí, 


debe compartirse texto, es decir, el tipo "text/plain": 

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: 

intent.putExtra(Intent.EXTRA_SUBJECT, "Mi Aplicación");


intent.putExtra(Intent.EXTRA_TEXT,"Me gusta mi aplicación LocDVD!!!");

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);

MenuItem shareItem = menu.findItem(R.id.menu_compartir);


ShareActionProvider shareActionProvider =
(ShareActionProvider)
MenuItemCompat.getActionProvider(shareItem);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, "Mi Aplicación");
intent.putExtra(Intent.EXTRA_TEXT,"Me gusta mi aplicación
LocDVD!!!");

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>

Los estilos soportan la noción de herencia: el atributo  parent de la etiqueta  <style> permite indicar el estilo del 


que va a heredar. Todas las propiedades definidas en el estilo padre se recuperan en el estilo hijo; las propiedades 
que  él  defina  sustituirán  a  las  propiedades  del  padre.  Un  estilo  puede  heredard  de  cualquier  estilo  previamente 
definido, ya sea un estilo integrado en la plataforma o definido por el usuario. 

© É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  9­patch, 
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 9­patch 
a  partir  de  imágenes  PNG:  esta  herramienta,  llamada  Draw  9­patch,  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  9­patch: 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  9­patch,  hay  que  hacer  clic  con  el  botón  derecho  en  el  archivo, 
almacenado  en  la  carpeta  drawable,  y  seleccionar  la  opción  Create  9­patch  file,  situada  en  la  parte  inferior  del 
menú  contextual.  Una  vez  abierto,  Draw  9­patch  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 9­patch. 

Así, aplicando como imagen de fondo una imagen 9­patch, 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 9­patch: 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 9­patch 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: 

<?xml version="1.0" encoding="utf-8"?>


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="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: 

<?xml version="1.0" encoding="utf-8"?>


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#c0c0c0"/>
</shape>

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. 

La forma  fond_label_xml debe mostrar un borde de 1 dp de ancho, y ser de color  #7e7e7e (que se 


corresponde con un color gris oscuro). Agregue una etiqueta <stroke> como etiqueta hija de <shape>: 

<?xml version="1.0" encoding="utf-8"?>


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="#7e7e7e"/>
</shape>

Llegados  a  este  punto,  si  se  reemplaza  el  elemento  visual  9­patch  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: 

<?xml version="1.0" encoding="utf-8"?>


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<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 9­patch  en  la  definición  del  estilo 
AppTheme.Label.  

El archivo fond_label_xml.xml contiene, al final, el siguiente código: 

<?xml version="1.0" encoding="utf-8"?>


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<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>: 

<?xml version="1.0" encoding="utf-8"?>


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate/>
</set>

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: 

<?xml version="1.0" encoding="utf-8"?>


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"/>
</set>

© É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. 

Las animaciones de translación presentan las propiedades  fromXDelta y  toXDelta, que permiten dar un valor al 


desplazamiento  como  entrada  de  la  animación  (propiedad  fromXDelta)  y  como  salida  de  la  animación 
(toXDelta).  Aquí, la posición inicial no cambia, la propiedad fromXDelta debe configurase a 0. 

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%: 

<?xml version="1.0" encoding="utf-8"?>


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0"
android:toXDelta="-100%"/>
</set>

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. 

<?xml version="1.0" encoding="utf-8"?>


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0"
android:toXDelta="-100%"
android:interpolator=
"@android:anim/accelerate_decelerate_interpolator"
/>
</set>

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: 

<?xml version="1.0" encoding="utf-8"?>


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100%"
android:toXDelta="0"
android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

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. 

setCustomAnimations (int enter, int exit)


setCustomAnimations (int enter, int exit, int popEnter, int
popExit)

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: 

private void openDetailFragment(Fragment fragment) {


FragmentManager fragmentManager =
getSoporteFragmentManager();

© É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. 

void overridePendingTransition (int enterAnim, int exitAnim)

El método debe invocarse en el  onCreate de la actividad que se inicia llamando a  startActivity, antes de la 


llamada al método setContentView. Por ejemplo: 

[...]
@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: 

<?xml version="1.0" encoding="utf-8"?>


<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_gravity="center_horizontal"
android:layout_width="match_parent"

© É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. 

public class ViewDVDFragment extends Fragment{


[...]
Button takePhoto;
ImageView fotoDVD;
[...]

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();
}
});

[...]

private void initTakePhoto() {


}

El código del fragmento  ViewDVDFragment es, de momento, el siguiente (solo se muestra el código modificado, 
para una mayor claridad): 

public class ViewDVDFragment extends Fragment{

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);

// asignación del archivo de layout


View view = inflater.inflate(R.layout.activity_viewdvd, null);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)view.findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)view.findViewById(R.id.anyoDVD);
txtResumenPelicula= (TextView)view.findViewById
(R.id.resumenPelicula);
layoutActores =
(LinearLayout)view.findViewById(R.id.layoutActores);
setFechaVisualizacion =
(Button)view.findViewById(R.id.setFechaVisualizacion);
txtFechaUltimaVisualizacion =
(TextView)view.findViewById(R.id.fechaVisualizacion);
setNotification =
(Button)view.findViewById(R.id.setNotification);
takePhoto = (Button)view.findViewById(R.id.takePhoto);
fotoDVD=(ImageView)view.findViewById(R.id.fotoDVD);

long dvdId = getArguments().getLong("dvdId",-1);


dvd = DVD.getDVD(getActivity(), dvdId);

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
}

private void initTakePhoto() {


}

private void showDatePicker() {


[...]
}

private void sendNotification() {


[...]
}

@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. 

public class DVD {


long id;
String titulo;
int anyo;
String[]actores;
String resumen;
long fechaVisualizacion;
String rutaFoto;
[...]
}

La propiedad debe tenerse en cuenta en el constructor privado de la clase: 

private DVD(Cursor cursor) {


// DVD dvd = new DVD();
id = cursor.getLong(cursor.getColumnIndex("id"));
titulo = cursor.getString(cursor.getColumnIndex("titulo"));
anyo = cursor.getInt(cursor.getColumnIndex("anyo"));
actores =
cursor.getString(cursor.getColumnIndex("actores")).split(";");
resumen = cursor.getString(cursor.getColumnIndex("resumen"));
fechaVisualizacion =
cursor.getLong(cursor.getColumnIndex("fechaVisualizacion"));
rutaFoto =
cursor.getString(cursor.getColumnIndex("rutaFoto"));

© É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. 

public static ArrayList<DVD> getDVDList(Context context) {


ArrayList<DVD> listDVD = new ArrayList<DVD>();
LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor =
db.query(true, "DVD", new String[]{"id", "titulo", "anyo",
"actores", "resumen", "fechaVisualizacion","rutaFoto"},null,
null,null,null,"titulo", null );

while (cursor.moveToNext()) {
listDVD.add(new DVD(cursor));
}

cursor.close();
db.close();

return listDVD;
}

public static DVD getDVD(Context context, long id) {


DVD dvd = null;
LocalSQLiteOpenHelper helper = new
LocalSQLiteOpenHelper(context);
SQLiteDatabase db = helper.getReadableDatabase();
String where ="id = " + String.valueOf(id);
Cursor cursor =
db.query(true, "DVD", new String[]{"id", "titulo", "anyo",
"actores", "resumen","fechaVisualizacion","rutaFoto"},where,
null,null,null,"titulo", null );

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: 

private ContentValues getContentValues() {


ContentValues values = new ContentValues();
values.put("titulo",this.titulo);
values.put("anyo",this.anyo);
if(this.actores!=null) {
String listActores = new String();

- 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: 

static int DB_VERSION=3;

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: 

private void upgradeToVersion3(SQLiteDatabase db) {


String sqlCommand = "ALTER TABLE DVD ADD COLUMN rutaFoto TEXT";
db.execSQL(sqlCommand);
}

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.   

Intent intent = new Intent(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: 

public void startActivityForResult (Intent intent, int requestCode)

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: 

public void onActivityResult (int requestCode, int resultCode, Intent data)

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: 

static final int TAKE_PHOTO=...;

- 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
}

Defina una constante global de tipo entero,  TAKE_PHOTO, y asígnele un valor (en el siguiente ejemplo, 


se escoge, arbitrariamente, el valor 2015): 

static final int TAKE_PHOTO = 2015;

Invoque el método startActivityForResult en el cuerpo del método initTakePhoto. 

private void initTakePhoto() {


Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, TAKE_PHOTO);
}

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:  

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);


intent.putExtra(MediaStore.EXTRA_OUTPUT, ...);

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. 

Edite  el  archivo  AndroidManifest.xml  y  agregue  el  permiso  WRITE_EXTERNAL_STORAGE  para 


todos los terminales con la API de nivel 18 o inferior. 

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: 

private void initTakePhoto() {


savedImage =
new File(

getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"dvd_" + dvd.id + ".jpg");

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);


intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(savedImage));

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;

public class ViewDVDFragment extends Fragment{

static final int TAKE_PHOTO = 2015;


TextView txtTituloDVD;
TextView txtAnyoDVD;
TextView txtResumenPelicula;
LinearLayout layoutActores;
TextView txtFechaUltimaVisualizacion;
Button setFechaVisualizacion;
Button setNotification;
Button takePhoto;
ImageView fotoDVD;
DVD dvd;

File savedImage;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View view = inflater.inflate(R.layout.activity_viewdvd,


null);

// Obtención de las referencias sobre los componentes


txtTituloDVD = (TextView)view.findViewById(R.id.tituloDVD);
txtAnyoDVD= (TextView)view.findViewById(R.id.anyoDVD);
txtResumenPelicula=
(TextView)view.findViewById(R.id.resumenPelicula);
layoutActores =
(LinearLayout)view.findViewById(R.id.layoutActores);
setFechaVisualizacion =
(Button)view.findViewById(R.id.setFechaVisualizacion);
txtFechaUltimaVisualizacion =
(TextView)view.findViewById(R.id.fechaVisualizacion);

© É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);

long dvdId = getArguments().getLong("dvdId",-1);

dvd = DVD.getDVD(getActivity(), dvdId);

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;
}

private void initTakePhoto() {


savedImage =
new File(

getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"dvd_" + dvd.id + ".jpg");

Intent intent = new


Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(savedImage));

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));
}
}
}

private void showDatePicker() {


[...]
}

private void sendNotification() {


[...]
}

@Override
public void onResume() {
super.onResume();

txtTituloDVD.setText(dvd.getTitulo());
txtAnyoDVD.setText(
String.format(getString(R.string.anyo_de_aparicion),
dvd.getAnyo()));

for(String actor: dvd.getActores()) {


TextView textView = new TextView(getActivity());
textView.setText(actor);
layoutActores.addView(textView);
}
txtResumenPelicula.setText(dvd.getResumen());

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/media­formats.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: 

static MediaPlayer create(Context context, int resid)

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: 

MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.sonido_bip);

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. 

MediaPlayer mediaPlayer = new MediaPlayer();

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: 

String url = ...


MediaPlayer mediaPlayer = new MediaPlayer();

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: 

boolean onError (MediaPlayer mp, int what, int extra)

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<VideoView
android:id="@+id/playVideo_player"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>

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: 

MediaController media_Controller = new MediaController(this);

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();
}
});

Cabe destacar que el objeto  VideoView que se utiliza en la clase  OnPreparedListener debe declararse como 


final. 

El código completo canónico para la lectura es, así, el siguiente:  

private void playWithVideoView() {

final VideoView videoView =


(VideoView)findViewById(R.id.playVideo_player);

MediaController media_Controller = new MediaController(this);

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: 

private void playWithVideoView() {


final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Carga en curso...");
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();

final VideoView videoView =


(VideoView)findViewById(R.id.playVideo_player);

MediaController media_Controller = new MediaController(this);

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: 

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="8dp">
<TextView
android:id="@+id/main_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

En  el  método  onCreate de la actividad  MainActivity, hay que declarar el componente  TextView.  El  código 


correspondiente es, como mínimo, 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;

public class MainActivity extends AppCompatActivity {

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...
}

La petición del permiso se hace invocando el método  requestPermissions. La respuesta se procesa 


en el método sobrecargado onRequestPermissionsResult: 

private void askPermission() {


requestPermissions(
new String[] {Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERMISSION);
}

@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) {

// ¡El permiso está concedido!


}
}
}

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();
}

private void ensurePermission() {


if(PackageManager.PERMISSION_GRANTED!=
ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
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

© É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();
}
}

private void askPermission() {


requestPermissions(new String[]
{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION);
}

@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();
}
}

private void startBLEScan() {


// A completar...
}

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;

final static int REQUEST_PERMISSON = 102;


final static int REQUEST_ENABLE_BLE = 201;

[...]
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. 

Declare, en la clase  MainActivity, un objeto de tipo  BluetoothAdapter.LeScanCallback. El 


método que de ha de implementar es onLeScan. 

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. 

ScanCallback scanCallback = new ScanCallback() {


@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
}
};

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([...]);

El método  postDelayed recibe dos parámetros: un objeto de tipo Runnable, que especifica el código que se ha 


de ejecutar, y un valor entero de tipo long que precisa el tiempo de espera (en milisegundos) antes de la ejecución 
del código. 

Runnable es una interfaz que presenta el método run. 

Runnable runnable = new Runnable() {


@Override
public void run() {
// código por definir
}
};

Pasado el intervalo definido en el método postDelayed, debe invocarse el método (por definir) stopBLEScan. 

Una primera versión del código es la siguiente (el escaneo se interrumpirá pasados 5 segundos): 

Handler handler = new Handler();


Runnable runnable = new Runnable() {
@Override
public void run() {
stopBLEScan();
}
};
handler.postDelayed(runnable, 5000);

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: 

new Handler().postDelayed(new Runnable() {


@Override
public void run() {
stopBLEScan();
}
}, 5000);

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: 
 

private void stopBLEScan() {


if(android.os.Build.VERSION.SDK_INT<21)
bluetoothAdapter.stopLeScan(leScanCallback);
else
bluetoothLeScanner.stopScan(scanCallback);
}

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 BluetoothDevice device: representa el objeto BLE detectado. Este objeto se utilizará para la conexión entre 


el objeto BLE físico y el terminal Android.  

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: 

private void onDeviceDetected(BluetoothDevice device, int rssi) {

// ¡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());
}
};

private void onDeviceDetected(BluetoothDevice device, int rssi) {

// ¡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: 

public class MainActivity extends AppCompatActivity {


final static String TAG="MainActivity";

TextView text;

BluetoothManager bluetoothManager = null;


BluetoothAdapter bluetoothAdapter=null;
BluetoothLeScanner bluetoothLeScanner;

final static int REQUEST_PERMISSION = 102;


final static int REQUEST_ENABLE_BLE = 201;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

text =(TextView)findViewById(R.id.main_text);

ensurePermission();
}

private void ensurePermission() {


if(ContextCompat.checkSelfPermission(this,

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();
}
}

private void askPermission() {


requestPermissions(new String[]
{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERMISSION);
}

@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();
}
}
}

private void startBLEScan() {


if(bluetoothManager==null)
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 {

- 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");
}

new Handler().postDelayed(new Runnable() {


@Override
public void run() {
stopBLEScan();
}
}, 5000);
}

@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_ENABLE_BLE && requestCode==RESULT_OK)
startBLEScan();
}

private void stopBLEScan() {


if(android.os.Build.VERSION.SDK_INT<21)
bluetoothAdapter.stopLeScan(leScanCallback);
else
bluetoothAdapter.getBluetoothLeScanner().stopScan(scanCallback);
}

// 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());
}
};

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);
}

© É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. 

La llamada al método  connectGatt se hace una vez se ha detectado el objeto BLE: por ejemplo, en el 


método onDeviceDetected. Previamente, hay que definir un objeto BluetoothGattCallback en la 
clase  MainActivity. Sitúese  tras  el  método  onDeviceDetected  y  defina  una  instancia  de 
BluetoothGattCallback:  Android  Studio  debe,  cuando  escribe new  BluetoothGattCallback, 
presentarle una ventana emergente que le permite indicar cuáles son los métodos que desea implementar. 

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. 

BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {


@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
super.onConnectionStateChange(gatt, status, newState);
}
};

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);

bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);


}

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:  

public void onConnectionStateChange(BluetoothGatt gatt, int status,


int newState)

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. 

runOnUIThread, método de la clase  Activity, recibe como parámetro un objeto de tipo  Runnable, 


como el método postDelayed que hemos visto antes. Lo más fácil aquí es definir en  MainActivity un 
método  showToastFromBackground,  que  recibe  como  parámetro  una  cadena  de  caracteres  y  que 
fuerza la visualización del mensaje Toast en el thread principal: 

private void showToastFromBackground(final String message) {


runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, message,
Toast.LENGTH_SHORT).show();
}
});
}

Ahora es fácil mostrar, cuando cambia el estado de la conexión, un mensaje para informar al usuario: 

private void showToastFromBackground(final String message) {


runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, message,
Toast.LENGTH_SHORT).show();
}
});
}

BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {


@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
super.onConnectionStateChange(gatt, status, newState);
if(status==BluetoothGatt.GATT_SUCCESS) {
String message ="";
if(newState== BluetoothProfile.STATE_CONNECTED)
message = "Objeto conectado";
else
message = "Objeto desconectado";
showToastFromBackground(message);
}
}
};

© É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: 0000xxxx­0000­1000­8000­00805f9b34fb 

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, 0000180f­0000­1000­
8000­00805f9b34fb. 

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,  00002a19­0000­1000­8000­
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. 

BluetoothGattCallback bluetoothGattCallback = new


BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
[...]
}

@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: 

for(BluetoothGattService service: gatt.getServices()) {

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");

for(BluetoothGattService service: gatt.getServices()) {


if(service.getUuid().equals(batteryLevelServiceUUID)) {

}
}

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");

for(BluetoothGattService service: gatt.getServices()) {


if(service.getUuid().equals(batteryLevelServiceUUID)) {
for(BluetoothGattCharacteristic c:
service.getCharacteristics()) {

}
}
}

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. 

Como  con  los  métodos  onConnectionStateChanged  y  onServicesDiscovered,  sobrecargue  el 


método onCharacteristicRead del objeto BluetoothGattCallback. 

@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:  

private void setBatteryLevelFromBackground(final int value) {


runOnUiThread(new Runnable() {
@Override
public void run() {
batteryLevel.setText(String.format("Nivel de batería:%d %%",value));
}
});
}

[...]

@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: 

public class MainActivity extends AppCompatActivity {


final static String TAG="MainActivity";

TextView text;
TextView batteryLevel;

BluetoothManager bluetoothManager = null;


BluetoothAdapter bluetoothAdapter=null;
BluetoothLeScanner bluetoothLeScanner;
BluetoothGatt bluetoothGatt;

final static int REQUEST_PERMISSION = 102;


final static int REQUEST_ENABLE_BLE = 201;

@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;
}
}

private void ensurePermission() {


if(ContextCompat.checkSelfPermission(this,

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();
}
}

private void askPermission() {


requestPermissions(new String[]
{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION);
}

@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();
}
}

private void startBLEScan() {


if(bluetoothManager==null)

© É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");
}

new Handler().postDelayed(new Runnable() {


@Override
public void run() {
stopBLEScan();
}
}, 5000);
}

@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==REQUEST_ENABLE_BLE && requestCode==RESULT_OK)
startBLEScan();
}

private void stopBLEScan() {


if(android.os.Build.VERSION.SDK_INT<21)
bluetoothAdapter.stopLeScan(leScanCallback);
else
bluetoothAdapter.getBluetoothLeScanner().stopScan(scanCallback);
}

// 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());
}
};

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);
bluetoothGatt = device.connectGatt(this, false,
bluetoothGattCallback);
}

private void showToastFromBackground(final String message) {


runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, message,
Toast.LENGTH_SHORT).show();
}
});
}

private void setBatteryLevelFromBackground(final int value) {


runOnUiThread(new Runnable() {
@Override
public void run() {
batteryLevel.setText(String.format("Nivel de batería
:%d %%",value));
}
});
}

BluetoothGattCallback bluetoothGattCallback = new


BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int
status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if(status==BluetoothGatt.GATT_SUCCESS) {
String message ="";
if(newState== BluetoothProfile.STATE_CONNECTED) {
message = "Objeto conectado";
bluetoothGatt.discoverServices();
}
else {
message = "Objeto desconectado";
}
showToastFromBackground(message);
}
}

© É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:  

apply plugin: ’com.android.application’

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: 

Se instancia un objeto de tipo  Notification.Builder (o  NotificationCompat.Builder, si se utiliza la 


biblioteca de soporte v7):  

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);

La clase  NotificationChannel expone varios métodos para indicar el canal de notificación. Podemos citar los 


siguientes: 

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í:  

private void sendNotification() {


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) {
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);
}

Intent intent = new Intent(getActivity(), MainActivity.class);


PendingIntent pendingIntent =
PendingIntent.getActivity(getActivity(), 0, intent, 0);
builder.setContentIntent(pendingIntent);

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.  

Seleccione, en el menú contextual, la opción New y, a continuación, Android resource directory. Se abre 


el asistente de creación de una carpeta. 

Seleccione, en la lista desplegable  Resource  type,  la  opción font y haga clic en OK (no se le da ningún 


calificador a esta carpeta). 

© É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. 

/* ¡Reemplazar por las fuentes en el código XML desde la API 26!


Typeface typeface =

- 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: 

Toast.makeText(this, "Ejecutando la versión: " + Build


Config.FLAVOR, Toast.LENGTH_LONG).show();

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/in­app­billing/preparing­iab­
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-

Potrebbero piacerti anche