Piano Android con Kotlin, MVVM y LiveData

0
369

Índice de contenidos

1. Introducción

Con este tutorial podrás construir una aplicación para tablets o móviles que utilicen Android, consistente en un piano de doce teclas que reaccionen al ser pulsadas cambiando visualmente y reproduciendo su sonido. Como lenguaje de desarrollo utilizaremos Kotlin y emplearemos una arquitectura MVVM con LiveData.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 17’ (2,66 GHz Intel Core i7, 8GB DDR3)
  • Sistema operativo: macOS Sierra 10.13.6
  • Entorno de desarrollo: Android Studio 3.2
  • Versión mínima Android SDK: 16

3. Arquitectura VMMV y LiveData

La arquitectura que utilizaremos para este tutorial será MVVM. Como MVC y MVP, consta de tres capas, pero guarda diferencias respecto a éstas, por lo que vamos a definirlas:

  • Modelo: representa la capa de datos y la lógica de negocio sin haber diferencias importantes con los otros patrones mencionados.
  • Vista: se encarga de mostrar la información directamente al usuario y es la capa con la que éste interacciona directamente a través de eventos, permitiendo la manipulación de la aplicación y sus datos.
  • View Model o Modelo de Vista: sirve de intermediario entre el modelo y la vista, proporcionando datos a esta última permitiéndole modificarlos. Es ajena a la vista, es decir, no contiene referencias a ésta y, por lo tanto, puede ser reutilizable por varias de ellas. Por tanto, la comunicación con ella se realiza por medio de enlaces de datos o bindings, que, en nuestro caso, será LiveData. Además, una vista puede necesitar de la información proporcionada por más de un View Model.

 

Como vemos, la Vista y el View Model tienen un bajo acoplamiento, lo cual ayuda mucho en la testabilidad al trasladar parte de la lógica al View Model, pero preservando la independencia de esta capa de los elementos de la interfaz de Android.

LiveData es un contenedor de datos asociado a un ciclo de vida de la aplicación. Permite ser observado, por lo que tendremos la seguridad de que al actualizarse las vistas serán notificadas y podrán tratar los cambios como necesiten. Además, al estar asociado al ciclo de vida de un fragmento o actividad, cuando éstos se destruyen, el objeto LiveData también lo hará, previniendo pérdidas de memoria y no siendo necesario el tratamiento manual de su ciclo de vida.

4. Instalación de Android Studio y creación del proyecto

Para desarrollar en Android recomiendo utilizar Android Studio que, como su nombre indica, está pensado para esta tarea. Es gratuito, tiene versiones para macOS, Windows y Linux, y está desarrollado por JetBrains, que son los mismos que desarrollan IntellIJ, así que si has utilizado este entorno te será fácil la adaptación.

Si nunca lo has utilizado, puedes descargarlo de su página oficial e instalarlo como otra aplicación para tu sistema operativo.

Una vez abierto, para crear un proyecto tan sólo tenemos que pulsar en la primera opción de la lista que se nos muestra y configurar nuestro proyecto, incluyendo datos como el nombre de la aplicación. Este tutorial está pensado para Kotlin, por lo que es importante marcar la opción para darle soporte.

Las últimas pantallas nos dejan seleccionar la versión de Android y la plantilla que vamos a utilizar. En nuestro caso indicaremos que nuestra aplicación será para teléfono y tablet, con un SDK mínimo de 16 (API 16: Android 4.1) y utilizaremos la plantilla de actividad vacía.

5. Desarrollando las capas

5.1. El Modelo

Una vez tenemos el proyecto creado, podemos comenzar a desarrollar. Para esto, vamos a crear la capa del modelo, que consiste en una clase que represente las teclas del piano (Key) y otra el piano en general (Keyboard).

Las teclas del piano tendrán dos atributos: pulsed, que indica si la tecla está pulsada o no, y pitch el cual indica los posibles valores de las notas en notación de enteros. Además, la clase Key contará con un método isWhite() que, en función del pitch, indicará si la tecla es blanca o negra.

La notación utilizada asigna a las teclas un valor del 0 al 11, siendo el do un 0, el do sostenido un 1 y el si un 11. Esto elimina la ambigüedad que existe con la denominación más corriente de las notas, donde la tecla del do sostenido también es re bemol. Para el tutorial esto es suficiente, ya que nuestro piano corresponde a una octava, pero si quisiéramos crear uno más grande, deberíamos añadir un atributo que se refiera a la octava de las teclas para poder diferenciarlas.

Por otro lado, la clase Keyboard es bastante sencilla, ya que contará con un único atributo: un array de teclas.

Por último, vamos a crear un objeto Factory para la creación del teclado por defecto, definiendo una clase que se encargue de esta responsabilidad.

5.2 El View Model

La responsabilidad de nuestro ViewModel será decidir cuándo hay que actualizar el modelo al pulsar o soltar teclas. Además, será quien contenga el objeto LiveData, el cual en Kotlin deberemos utilizar instanciando la clase MutableLiveData, y el encargado de actualizarlo para proporcionar el teclado a la vista.

Pero antes de implementar el ViewModel, necesitamos importar dos dependencias de livecycle al fichero de build.gradle, el relativo al módulo.

La sección de dependencias deberá contener las dos líneas que se marcan a continuación y después deberemos sincronizar nuestro proyecto:

Es posible que se encargue al ViewModel pulsar una tecla ya pulsada o soltar una no pulsada. Para evitar actualizar el LiveData cuando no sea necesario, comprobaremos si realmente hay que cambiar algo. Además, vamos a añadir un método estático para crear una instancia asociada al fragmento que se le pase por parámetro.

5.3. La Vista

Nuestra vista consistirá, en un primer nivel, en un fragmento, ya que la idea es que este piano pueda ser incorporado a proyectos más grandes en caso de ser necesario. Un fragmento de Android representa una parte de una actividad, ya sea un comportamiento o un trozo de la interfaz.

5.3.1. Las vistas de las teclas

Antes de programar nuestro fragmento, vamos a crear las clases necesarias para convertir los objetos de tipo Key en sus representaciones gráficas. Para ello implementaremos una clase denominada KeyView y una KeyViewFactory encargada de dicha conversión. Además, vamos a crear cuatro drawables para pintar los fondos de nuestras teclas y un BackgroundViewManager que se encargue de administrarlos.

La clase KeyView debe heredar de View para que Android pueda tratarla como tal, pero también deberá tener los atributos que tiene Key. Como Kotlin no soporta la herencia múltiple, añadiremos directamente Key como un atributo suyo.

Los drawables los tenemos que definir en unos archivos xml guardados en la carpeta drawable, dentro de res. Para su creación podemos hacer click derecho en esta carpeta y dar a New>Drawable resource file.

Los códigos de los cuatro fondos son los siguientes, teniendo que poner el nombre al crearlo y pegar el código en la pestaña Text (por defecto se puede abrir Design):

key_white_background.xml

key_white_pulsed_background.xml

key_black_background.xml

key_black_pulsed_background.xml

Una vez ya tenemos los drawables, podemos implementar su manager y el KeyViewFactory:

5.3.2. El layout del piano

El siguiente paso es crear un layout intermedio que sirva para agrupar estas KeyView y controlar sus dimensiones. Es importante que a la hora de añadir las teclas en él añadamos primero las blancas para que las teclas negras queden superpuestas, lo cual hacemos en el método addKeyboard.

5.3.3. Creación del fragmento

Y ahora vamos a crear el fragmento, sobrescribiendo los métodos necesarios, entre ellos onActivityCreated para observar el LiveData del ViewModel, estableciendo que, cada vez que éste se modifique, se deberá llamar al KeyboardLayout para redibujar las teclas.

Por último, vamos a añadir el fragmento a la actividad, modificando el fichero activity_main.xml que está en res>layout. Pero para hacerlo vamos a necesitar un id único que identifique este fragmento. ¿Y cómo lo conseguimos? Tan sólo necesitamos definirlo en un fichero ids.xml, que crearemos dentro de la carpeta res>values, de forma similar a cuando creamos los drawables.

Al editar el código de activity_main.xml, que es el código de a continuación, revisa el paquete del fragmento, ya que igual no coincide con el de tu proyecto.

Si ejecutamos nuestra aplicación, veremos cómo se muestran las teclas, pero que no se fuerzan a mostrarse en horizontal. Para solucionar esto, tan sólo tenemos que añadir una línea en el método onCreate de la clase MainActivity para establecer requestedOrientation como landscape:

En este punto ya tenemos la representación gráfica del piano, pero todavía nos queda controlar los eventos necesarios para que exista una interacción real con el usuario y que se reproduzcan los sonidos.

6. Creación del Event Handler

Para el tratamiento de los eventos del piano, vamos a crear una nueva clase que guarde las referencias a las KeyView para poder buscar cual está recibiendo un evento y notificárselo al ViewModel.

Los eventos que trataremos serán:

  • Eventos para pulsar las teclas:
    • ACTION_MOVE
    • ACTION_DOWN
    • ACTION_POINTER_DOWN
  • Eventos para soltar las teclas:
    • ACTION_CANCEL
    • ACTION_UP
    • ACTION_POINTER_UP

Para acoplar esta clase, debemos cambiar el KeyboardLayout para crearla, sobrescribir el método onTouchEvent y añadir las llamadas al KeyboardEventHandler para que actualice sus KeyView. Además, tenemos que modificar el KeyboardFragment para pasar la referencia del ViewModel al Layout y que éste pueda crear el EventHandler con ella. Ten en cuenta que las clases que se muestran a continuación no se muestran enteras para evitar alargar más el tutorial, por lo que hay que sustituir o añadir los métodos.

En este punto, si probamos la aplicación podremos ver cómo responde a los eventos correspondientes, actualizándose el teclado.

7. Creación del Sound Manager

El último paso de este tutorial es la creación de las clases necesarias para el tratamiento de los sonidos. Para ello haremos uso de la clase SoundPool que utiliza archivos de sonidos de pequeño tamaño para reproducirlos. Por lo tanto, necesitaremos los doce sonidos del teclado para poder implementar esta parte.

7.1. Los archivos de sonido

Hay varios sitios en internet donde puedes descargarte ficheros de música de forma gratuita, por ejemplo en la página de la Universidad de Iowa. Si eliges esta opción, los ficheros están en formato aiff, por lo que tendrás que convertirlos a wav. Para ello existen herramientas como la proporcionada por esta página.

Cuando tengamos los ficheros, tenemos que agregarlos a nuestro proyecto. Para ello tenemos que crear un directorio de recursos dentro de res, tal y como se indica en las imágenes a continuación. Como nombre y como tipo de recurso selecciona raw y da a OK.

Esto creará una carpeta a la que tendrás que añadir los ficheros wav, por ejemplo utilizando el gestor de archivos de tu sistema. Después, es posible que tengas que actualizar los recursos del proyecto, para lo que tan sólo haz click derecho en res y sincronízala (puede que incluso sea necesario cerrar y volver a abrir el proyecto para que el SoundResManager que vamos a hacer a continuación reconozca el acceso a los archivos).

Respecto al SoundResManager que hemos mencionado a continuación, será la clase que controlará todos estos ficheros, devolviendo un mapa donde las claves son los pitch y el valor el id de los ficheros. A la hora de copiar el código, ten en cuenta que tus ficheros puede que tengan un nombre diferente al que está indicado y que de ser así tendrás que cambiarlo en el fichero o en el código.

7.2. Administrar los sonidos

Nuestra clase KeyboardSoundPool, contará con los doce sonidos indicados que cargaremos al inicializar la clase y será la encargada de reproducirlos o pararlos. Al cargarlos, se crea un id que guardaremos en una clase que llamaremos Sound junto con el pitch correspondiente. Además, cuando mandemos al SoundPool que reproduzca el sonido, éste devolverá otro id del stream que se crea, devolviendo un 0 si no se logra iniciar. Dicho valor también lo guardaremos en Sound para poder pararlo, junto con un método que comprobará si el sonido está sonando.

Como SoundPool se debe crear utilizando un builder a partir de la API 21, pero nuestra versión mínima es anterior, guardaremos una instancia de esta como atributo del KeyboardSoundPool en vez de aplicar una herencia, inicializando el SoundPool dependiendo de la versión que se esté utilizando.

Cada vez que se actualice el piano en el layout, llamaremos a esta clase para que identifique qué sonidos hay que parar y cuáles iniciar, comparándolos con el array de Sound que tenemos.

Por último, tan sólo nos queda modificar el KeyboardLayout para inicializar el KeyboardSoundPool y para llamar a updateSounds cuando es necesario:

Y, ahora sí, ya tenemos un piano para Android utilizando una arquitectura MVVM que reacciona a los eventos de pulsar y soltar las teclas, actualizando tanto su representación gráfica como los sonidos que se reproducen.

¡Muchas gracias por haber seguido este tutorial!

8. Referencias

 

Dejar respuesta

Please enter your comment!
Please enter your name here