Después de nuestra breve intro a Phonegap, vamos a ver como montar una aplicación Phonegap completa, utilizando Ionic framework para el frontend, potenciado con AngularJS para aplicar el paradigma MVC.

El tutorial que proponemos a continuación está basado en la guía de la documentación de ionic.

¿Quieres aprender Ionic Framework? ¡¡Mira mi curso de ionic completamente gratuito!!

En las páginas de ambos frameworks, hay detalladas instrucciones de instalación, por lo que daremos por sentado este paso. Solo a modo de resumen:

Instalar ionic

sudo npm install -g ionic

Crear un proyecto con Ionic

Ionic dispone de una herramienta por línea de comandos que nos facilita la creación y empaquetado de aplicaciones, al estilo de la herramienta de comandos de cordova.

Lo primero que hacemos es crear un proyecto vacío con la instrucción:

ionic start ionic-blank-project blank

Lo que nos creará una copia del proyecto blank disponible en el respositorio de Ionic, en la carpeta ionic-blank-project.

Este proyecto es un buen punto de partida para copiarlo y crear nuestros proyectos a partir de éste.

Añadir plataformas

De forma similar a la herramienta de cordova, hacemos:

ionic platform android

(si quisieramos trabajar también con iPhone, también haríamos ionic platform ios).

Compilar y ejecutar

En función de la plataforma, compilamos con

ionic build android
ionic build ios

Para probar en el simulador:

ionic emulate ios
ionic emulate android

DETALLE: El equipo de Ionic no recomiendo probar las aplicaciones en el emulador de Android, por lo lenta de su ejecucción. Siempre que tengamos la oportunidad, es mejor probar en el dispositivo real

Para probar en dispositivo real:

ionic run ios
ionic run android

Abrir el proyecto en Eclipse

De forma similar al artículo anterior de Phonegap y Eclipse, podemos importar el proyecto a Eclipse con:
File > New > Project… > Android Project from Existing Code

Seleccionando el directorio ionic-blank-project donde tenemos el proyecto.

DETALLE: Para que nuestro proyecto se actualice con los cambios y podamos ejecutarlo correctamente desde Eclipse, antes de ejecutarlo deberemos ejecutar por consola: cordova prepare android, y posteriormente hacer un refresh en Eclipse.

Creando una Todo list:

Una vez hemos visto los pasos básicos para tener un proyecto con Ionic, vamos a entrar en materia, creando una aplicación tipo “todo list”.

Primeros pasos:

Creamos la base de nuestro www/index.html con el siguiente contenido:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Todo</title>
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">

    <script src="lib/ionic/js/ionic.bundle.js"></script>

    <!-- Needed for Cordova/PhoneGap (will be a 404 during development) -->
    <script src="cordova.js"></script>
  </head>
  <body>
  </body>
</html>

Cabe destacar que en el fragmento anterior estamos incluyendo los CSS de Ionic, así como el JS de AngularJS e Ionic a través de ionic.bundle.js

DETALLE: Ionic incluye (de AngularJS) ngAnimate y ngSanitize, pero si queremos usar otros módulos, deberemos incluirlos desde el directorio lib/js/angular

Estructura:

Nuestra aplicación tendrá el siguiente aspecto:

  • Una barra superior, con el título de la vista y acceso al menu (tipo lateral)
  • Un menu lateral, con los proyectos en los que tendremos tareas
  • Un listado de tareas, en función del proyecto seleccionado

Añadiendo menu lateral a nuestro index.html

Aquí empieza la gracia de Ionic. Para crear menus laterales, nos proporciona ionic-side-menus
Cambiamos el < body> de index.html para incluir:

<body>
  <ion-side-menus>
    <ion-side-menu-content>
    </ion-side-menu-content>
    <ion-side-menu side="left">
    </ion-side-menu>
  </ion-side-menus>
</body>

Explicando el código:

  • < ion-side-menus>: Es un controlador (AngularJS) que gestiona las acciones de esconder, mostrar y arrastrar el menú.
  • < ion-side-menu-content>: Es el contenido central de la aplicación
  • < ion-side-menu side=”left”>: Es el menu lateral, a la izquierda, que de entrada estará oculto.

Inicializando la app (AngularJS):

Si lanzamos ahora la app, no veremos nada. Eso se debe a que aún tenemos que crear la app Angular que comprenderá las etiquetas personalizadas anteriores, y además, tampoco hemos proporcionado ningún contenido.

Creamos un módulo AngularJS

Creamos un nuevo archivo en www/js/app.js, y le damos el siguiente contenido:

angular.module('todo', ['ionic'])

Esto es: una aplicación denominada todo, que depende del módulo ionic.

Incluimos la app en nuestro index.html

Para ello, incorporamos el nuevo archivo JS al final del < head>, y añadimos la etiqueta ng-app:”todo” a nuestro < body>.

    <!-- other stuff -->
    <script src="js/app.js"></script>
    </head>

    <body ng-app="todo">
        <!-- body content -->

Añadimos contenido a nuestro menu:

<body ng-app="todo">
  <ion-side-menus>

    <!-- Center content -->
    <ion-side-menu-content>
      <ion-header-bar class="bar-dark">
        <h1 class="title">Todo</h1>
      </ion-header-bar>
      <ion-content>
        <p> Hello world! </p>
      </ion-content>
    </ion-side-menu-content>

    <!-- Left menu -->
    <ion-side-menu side="left">
      <ion-header-bar class="bar-dark">
        <h1 class="title">Projects</h1>
      </ion-header-bar>
    </ion-side-menu>

  </ion-side-menus>
</body>

 

Probando la app en dispositivo real

Hecho esto, y tras ejecutar por linea de comandos ionic run android, deberíamos obtener una vista principal con la cabecera Todo:

demo app todo view

Y si desplazamos arrastrando con el dedo hacia la derecha, aparece el menu lateral:

demo app side menu

Probando la app en web

Una interesante utilidad que nos proporciona Ionic es la de probar nuestra aplicación en un navegador web. Esto lo podemos hacer a través de:

ionic serve

DETALLE: Probar la app en web tiene la ventaja de que los cambios que realicemos en el código se reflejan instantáneamente en la web, por lo que es una manera rápida de mirar que determinados cambios tienen el efecto esperado.

A continuación podemos ver el resultado. El contenido de la vista principal se ha modificado tras ejecutar el servidor. Los cambios se han reflejado satisfactoriamente.

app view using ionic serve

 

Mejorando la app

Editamos nuestro index.html y metemos el siguiente contenido en la parte central:

<!-- Center content -->
<ion-side-menu-content>
  <ion-header-bar class="bar-dark">
    <h1 class="title">Todo</h1>
  </ion-header-bar>
  <ion-content>
    <!-- our list and list items -->
    <ion-list>
      <ion-item ng-repeat="task in tasks">
        {{task.title}}
      </ion-item>
    </ion-list>
  </ion-content>
</ion-side-menu-content>

Es importante fijarse en que estamos utilizando la directiva ng-repeat de Angular para crear un item en la lista, para cada elemento que haya en tasks. Pero ojo, aun no tenemos ningun tasks que leer.

Proporcionando un controlador para la aplicación

Lo primero que haremos es crear un controlador (lo llamaremos TodoCtrl) para poder trabajar en la aplicación. Lo añadimos a la etiqueta < body> en index.html, que quedará:

<body ng-app="todo" ng-controller="TodoCtrl">

Acto seguido, lo definimos en nuestro app.js:

angular.module('todo', ['ionic'])

.controller('TodoCtrl', function($scope) {
  $scope.tasks = [
    { title: 'Collect coins' },
    { title: 'Eat mushrooms' },
    { title: 'Get high enough to grab the flag' },
    { title: 'Find the Princess' }
  ];
});

Podemos ver rápidamente los cambios:

todo list app demo

Pero no solo es interesante el aspecto. Podemos ver como si arrastramos hacia abajo o hacia arriba la lista se mueve como esperaríamos en una aplicación nativa.

Añadiendo tareas

En el mismo < body>, tras el cierre < /ion-side-menu>, añadiremos el siguiente script.

Se trata de una ventana modal que aparecerá desplazándose hacia arriba, y nos permitirá añadir una nueva tarea:

<script id="new-task.html" type="text/ng-template">
  <div class="modal">
    <!-- Modal header bar -->
    <ion-header-bar class="bar-secondary">
      <h1 class="title">New Task</h1>
      <button class="button button-clear button-positive" ng-click="closeNewTask()">Cancel</button>
    </ion-header-bar>

    <!-- Modal content area -->
    <ion-content>

      <form ng-submit="createTask(task)">
        <div class="list">
          <label class="item item-input">
            <input type="text" placeholder="What do you need to do?" ng-model="task.title">
          </label>
        </div>
        <div class="padding">
          <button type="submit" class="button button-block button-positive">Create Task</button>
        </div>
      </form>

    </ion-content>

  </div>

</script>

Expliquemos el código:

  • < sript id=”new-task.html” type=”text/ng-template”>: Hemos definido un template de Angular. Los templates nos permiten separar layouts de UIs, por lo que nos serán de gran utilidad.
  • En nuestra ventana modal, creamos una cabecera con botón de cierre, así como nuestro contenido.
  • El contenido es un formulario que llama al método createTask(task) al hacer submit. El objeto task que pasamos como argumento se corresponde con los datos introducidos en el formulario.
  • Dado que nuestro text-input utiliza ng-model=”task.title”, dicho campo de texto quedará vinculado con la propiedad title del objeto task.

Para poder abrir la ventana modal, necesitaremos un botón en el header bar principal, y los métodos necesarios en el controlador (newTask(), closeNewTask(), createTask(task) así como la creación y carga del modal view).

Nuestro contenido central queda:

 <!-- Center content -->
  <ion-side-menu-content>
    <ion-header-bar class="bar-dark">
      <h1 class="title">Todo</h1>
      <!-- New Task button-->
      <button class="button button-icon" ng-click="newTask()">
        <i class="icon ion-compose"></i>
      </button>
    </ion-header-bar>
    <ion-content>
      <!-- our list and list items -->
      <ion-list>
        <ion-item ng-repeat="task in tasks">
            {{task.title}}
        </ion-item>
      </ion-list>
    </ion-content>
  </ion-side-menu-content>

Y nuestro controlador:

angular.module('todo', ['ionic'])

.controller('TodoCtrl', function($scope, $ionicModal) {
  // No need for testing data anymore
  $scope.tasks = [];

  // Create and load the Modal
  $ionicModal.fromTemplateUrl('new-task.html', function(modal) {
    $scope.taskModal = modal;
  }, {
    scope: $scope,
    animation: 'slide-in-up'
  });

  // Called when the form is submitted
  $scope.createTask = function(task) {
    $scope.tasks.push({
      title: task.title
    });
    $scope.taskModal.hide();
    task.title = "";
  };

  // Open our new task modal
  $scope.newTask = function() {
    $scope.taskModal.show();
  };

  // Close the new task modal
  $scope.closeNewTask = function() {
    $scope.taskModal.hide();
  };
});

 

DETALLE I:
El $scope es un objeto que hace referencia al modelo de la aplicación y, en palabras de la documentación de Angular, es el pegamento entre vista y controlador. Tanto los controladores como las directivas tienen acceso al $scope, pero no entre ellos, así conseguimos aislar el controlador de las directivas y del DOM, lo cual es muy importante para diseñar controladores independientes de la vista, lo que nos facilitará el diseño de tests para la aplicación.

En este caso, gracias al uso de $scope, desde la vista podemos hacer referencia a newTask() o al array tasks (obsérvese que no son datos miembro del controlador), a los que paralelamente les puedo asignar un valor, o una función desde el controlador.

 

DETALLE II:
Es importante destacar como utilizamos el service $ionicModal para, mediante su método fromTemplateUrl vincular la ventana modal que hemos creado como un elemento del $scope (gracias a la template URL), asignarle el tipo de animación y su propio scope.

A continuación podemos ver como efectivamente, podemos añadir elementos a la lista, y ésta se actualiza con los mismos.

tasks list todo demo app

modal view new task todo demo app

Añadiendo proyectos al menu

Vamos a incluir soporte para añadir y seleccionar proyectos en nuestro menu. De modo similar a los pasos anteriores vamos a:

  • Crear una lista para mostrar los proyectos
  • Seleccionar un proyecto
  • Crear un botón para añadir proyectos
  • Cerrar el menu al crear un proyecto o seleccionar uno existente
  • Abstraer el modelo Project a un servicio de Angular
  • Gestionar la guarda y carga de proyectos y tareas en localStorage

Cambios en index.html

El contenido central quedará del siguiente modo:

<!-- Center content -->
<ion-side-menu-content>
  <ion-header-bar class="bar-dark">
    <button class="button button-icon" ng-click="toggleProjects()">
      <i class="icon ion-navicon"></i>
    </button>
    <h1 class="title">{{activeProject.title}}</h1>
    <!-- New Task button-->
    <button class="button button-icon" ng-click="newTask()">
      <i class="icon ion-compose"></i>
    </button>
  </ion-header-bar>
  <ion-content scroll=true has-bouncing=true>
    <ion-list>
      <ion-item ng-repeat="task in activeProject.tasks">
        {{task.title}}
      </ion-item>
    </ion-list>
  </ion-content>
</ion-side-menu-content>

Observemos como hemos añadido un botón para ir a los proyectos que llama al método toogleProjects(), donde es de esperar que implementemos la aparición/ocultación del menu lateral de forma equivalente a cuando arrastramos con el dedo hacia un lado u otro de la pantalla.

Además, ahora la idea es disponer de un array de proyectos, cada uno con sus tareas, de los cuales, únicamente uno puede ser el activeProject.

Por otro lado, el contenido del menu lateral, será:

 <!-- Left menu -->
  <ion-side-menu side="left">
    <ion-header-bar class="bar-dark">
      <h1 class="title">Projects</h1>
      <button class="button button-icon ion-plus" ng-click="newProject()">
      </button>
    </ion-header-bar>
    <ion-content scroll=true has-bouncing=false>
      <ion-list>
        <ion-item ng-repeat="project in projects" ng-click="selectProject(project, $index)" ng-class="{active: activeProject == project}">
          {{project.title}}
        </ion-item>
      </ion-list>
    </ion-content>
  </ion-side-menu>
  • Hemos creado el boton de añadir proyecto, que llama a newProject()
  • Hemos creado una lista de proyectos que son:
    • Seleccionables (a través de ng-click=”selectProject(project, $index)”)
    • Denotan si son activos con ng-class

Así queda nuestro javascript

angular.module('todo', ['ionic'])
/**
 * The Projects factory handles saving and loading projects
 * from local storage, and also lets us save and load the
 * last active project index.
 */
.factory('Projects', function() {
  return {
    all: function() {
      var projectString = window.localStorage['projects'];
      if(projectString) {
        return angular.fromJson(projectString);
      }
      return [];
    },
    save: function(projects) {
      window.localStorage['projects'] = angular.toJson(projects);
    },
    newProject: function(projectTitle) {
      // Add a new project
      return {
        title: projectTitle,
        tasks: []
      };
    },
    getLastActiveIndex: function() {
      return parseInt(window.localStorage['lastActiveProject']) || 0;
    },
    setLastActiveIndex: function(index) {
      window.localStorage['lastActiveProject'] = index;
    }
  }
})

.controller('TodoCtrl', function($scope, $timeout, $ionicModal, Projects, $ionicSideMenuDelegate) {

  // A utility function for creating a new project
  // with the given projectTitle
  var createProject = function(projectTitle) {
    var newProject = Projects.newProject(projectTitle);
    $scope.projects.push(newProject);
    Projects.save($scope.projects);
    $scope.selectProject(newProject, $scope.projects.length-1);
  }


  // Load or initialize projects
  $scope.projects = Projects.all();

  // Grab the last active, or the first project
  $scope.activeProject = $scope.projects[Projects.getLastActiveIndex()];

  // Called to create a new project
  $scope.newProject = function() {
    var projectTitle = prompt('Project name');
    if(projectTitle) {
      createProject(projectTitle);
    }
  };

  // Called to select the given project
  $scope.selectProject = function(project, index) {
    $scope.activeProject = project;
    Projects.setLastActiveIndex(index);
    $ionicSideMenuDelegate.toggleLeft(false);
  };

  // Create our modal
  $ionicModal.fromTemplateUrl('new-task.html', function(modal) {
    $scope.taskModal = modal;
  }, {
    scope: $scope
  });

  $scope.createTask = function(task) {
    if(!$scope.activeProject || !task) {
      return;
    }
    $scope.activeProject.tasks.push({
      title: task.title
    });
    $scope.taskModal.hide();

    // Inefficient, but save all the projects
    Projects.save($scope.projects);

    task.title = "";
  };

  $scope.newTask = function() {
    $scope.taskModal.show();
  };

  $scope.closeNewTask = function() {
    $scope.taskModal.hide();
  }

  $scope.toggleProjects = function() {
    $ionicSideMenuDelegate.toggleLeft();
  };


  // Try to create the first project, make sure to defer
  // this by using $timeout so everything is initialized
  // properly
  $timeout(function() {
    if($scope.projects.length == 0) {
      while(true) {
        var projectTitle = prompt('Your first project title:');
        if(projectTitle) {
          createProject(projectTitle);
          break;
        }
      }
    }
  });

});

Expliquemos el código punto por punto:

  • .factory(‘Projects’, function() { /* some methods * / }): Esta es la manera de registrar un servicio. De hecho, lo que hacemos exáctamente es registrar una función factory, que creará (construirá y devolverá) la instancia única (singleton) de nuestro servicio “Projects” cuando se la llame.

DETALLE:
Veamos el contenido de la función que declaramos con el metodo factory:
return {
all: function() {…},
save: function(projects) {…},

}
Lo que estamos haciendo es crear un diccionario con varios métodos, que es lo que se devolverá. De este modo, para utilizar el servicio que hemos registrado (Projects), solo será necesario llamar dichos métodos en nuestro singleton, es decir, utilizar Projects.all(), Projects.save(), etc.

  • window.localStorage[‘projects’]: LocalStorage es el mecanismo nativo de HTML5 para guardar información de forma persistente en formato clave-valor. Es un objeto Javascript que pertenece a la variable global window, por lo que
    • El setter sería window.localStorage[‘clave’] = valor;
    • El getter sería valor = window.localStorage[‘clave’];
    • Cabe destacar que todo el contenido se guarda en formato String, por lo que si queremos interpretar un dato como int, necesitaremos utilizar parseInt()
  • $ionicSideMenuDelegate.toggleLeft(false): toggleLeft es un metodo de $ionicSideMenuDelegate que sirve para mostrar/ocultar el menu. De forma opcional le podemos pasar la posición en que queremos que quede el menu (abierto=true, cerrado=false).
  • prompt(‘Your first project title:’): Prompt es un diálogo nativo que nos muestra un input de texto. Su aspecto, por tanto variará considerablemente entre el caso web y el caso Android.

A continuación vemos el aspecto que quedaría en nuestro menú lateral.

side menu with add project option todo demo app

Publicando nuestra aplicación

Bien, se supone que llegado este punto tenemos una aplicación que funciona completamente. Vamos a repasar que proceso deberíamos realizaar para publicarla en el Google Play Store.

En primer lugar, necesitaremos crear una release build de la aplicación, para nuestra plataforma (en este caso android). Antes, no obstante, deberemos ajustar los plugins para quitar aquellos necesarios en desarrollo que no queremos para producción, como por ejemplo el plugin para debugar por consola, así que antes del build, haremos (en este caso):

$ cordova plugin rm org.apache.cordova.console

Publicar en Android

Lo primero que deberemos hacer es eliminar el modo debuf en el AndroidManifest.xml.
Donde tenemos:

<application android:debuggable="true" android:hardwareAccelerated="true" android:icon="@drawable/icon" android:label="@string/app_name">

Cambiamos debuggable a false.

<application android:debuggable="false" android:hardwareAccelerated="true" android:icon="@drawable/icon" android:label="@string/app_name">

Ahora, a través de la linea de comandos de cordova generamos la versión release

$ cordova build --release android

Esto en teoría nos guardará un APK sin firmar en platforms/android/bin.
En la práctica -al menos en mi caso-, el APK sin firmar va a parar a platforms/android/ant-build/.

Necesitamos firmarlo y ejecutar una herramienta de alineación para optimizarlo y prepararlo para la app store.

Si no tenemos una clave privada:

  • Generamos una clave privada con el comando keytool de la JDK:
$ keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
  • Nos pedirá un password para la keystore, completamos las preguntas de la herramienta y habremos acabado: Tendremos un archivo my-release-key.keystore creado en nuestro directorio actual.

DETALLE: Este archivo es muy importante, deberemos guardarlo con cariño por que lo necesitaremos para publicar actualizaciones sobre nuestra app

Una vez tenemos la clave, deberemos firmar el APK con la herramienta jarsigner del JDK:

$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore HelloWorld-release-unsigned.apk alias_name

Nuestro APK estará ahora firmado. Solo necesitamos ejecutar la herramienta zipalign desde las build-tools de la SDK de Android para optimizar el APK:

$ cd ~/MiDirectorio/adt-bundle-linux-x86_64-20140702/sdk/build-tools/19.1.0
$ zipalign -v 4 path-to-apk/HelloWorld-release-unsigned.apk path-to-apk/HelloWorld.apk

Finalmente ya tenemos la versión release de nuestro binario (HelloWorld.apk), lista para publicar en Google Play.

De momento es todo, en próximos posts seguiremos profundizando en el tema. ¡Saludos!