Posts Tagged ‘javascript’


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

En este punto debemos hacer un repaso y considerar algunos puntos que potencian la aplicación.

Primero, comencemos que el código está disponible en Github en la siguiente dirección:

https://github.com/victorpease/SuperDatos

Para que comiencen a verlo funcionar tendrán que seguir los siguientes pasos:

  1. Crear una cuenta en cloudant.com
  2. Crear una base de datos
  3. Crear una key en la sección de permisos y asignarle permisos de Writer
  4. Actualizar la key y la clave en el archivo services.js
  5. Actualizar la url de la base de datos también en el archivo services.js

Con esto ya podrán tener funcionando su propio lector de noticias y verán que cada vez que alguien lea una noticia, un registro será añadido a la base de datos y podrán con esa información hacer reportes sobre las noticias más populares, que luego podremos incluir como un reporte en la misma aplicación.

Bien, ahora los consejos:

  • Hasta ahora hemos considerado la sincronización Live, pero tengan en cuenta que genera mucho tráfico y por lo tanto costo, además, el cobro es por transacción, así que no importa mucho el tamaño de la base de datos. En Cloudant dan gratis un saldo de 50 dólares y eso es bastante, pero para una aplicación seria esto se puede salir de control. Lo mejor es que desactiven Live y que sincronicen manualmente cuando ocurra algún evento relevante. Para que vean la diferencia, Live sincroniza datos cada 25 segundos, así que si cambian que la sincronización sea cada 1 minuto, ya estarían bajando su consumo a la casi la mitad.
  • Si pensamos en este modelo “Offline-First”, es porque no confiamos en el acceso a la red. El mayor impacto es la primera sincronización. Piensen en usar el plugin PouchDB Load, que permite que hagan una carga inicial de datos en el móvil en lugar de esperar los datos de la red. El archivo de texto tiene que ser generado mediante el plugin PouchDB Dump Cli.
  • En el código actual tenemos un estado llamado Splash donde podemos ejecutar todas estas acciones  de inicialización antes de entrar a la aplicación.

Sobre las vistas:

  • Las vistas en PouchDB se generan la primera vez que las llamamos, por lo que pueden demorar un poco mas de lo normal durante la primera vez. Ante esto, pueden controlar que la vista se genere de forma automática sobre todo durante la primera sincronización mediante la siguiente línea:
    pouch.query('vista', {stale: 'update_after'});
  • Hasta ahora hemos usado Map/Reduce para las vistas, y puede ser algo confuso. La alternativa principal es utilizar los índices, no hay nada mas simple y más rápido. La otra alternativa es utilizar PouchDB Find, que es el futuro de las consultas en todo el ecosistema CouchDB. Personalmente, aun me gusta más Map/Reduce aunque sea algo confuso y limitado por una razón: PouchDB Find es simplemente una máscara que hace las cosas fáciles, pero no agrega velocidad.
  • Finalmente, ya les había mencionado que las vistas ocupan sitio

Sobre los límites de espacio:

  • PouchDB maneja varios métodos para almacenar los datos siendo WebSQL (sqlite) y IndexedDB los disponibles en los móviles, estando igualmente sujetos a las restricciones de espacio del navegador del sistema. Aquí pueden revisar los límites . Un truco es utilizar el plugin SQLite que les permite saltarse esos límites y tener un almacenamiento casi ilimitado. Entiendan que esto es solamente recomendado cuando su base de datos esté por los 50 mb. Además, aún tiene algunos problemitas con las vistas.
  • IndexedDB es la alternativa estándar, de hecho WebSQL ya no es mantenido. De hecho es bastante rápido, además que IndexedDB no es tan fácil de abrir que WebSQL, así que les da un nivel de seguridad a los datos.

Sobre la seguridad:

  • Los que utilizan SQLite en Android están acostumbrados a ponerle una clave a la base de datos para protegerla. Esto no está disponible en el modelo Híbrido pero pueden usar el plugin Crypto Pouch que nos permite encriptar los datos.
  • CouchDB tiene usuarios y Cloudant también puede manejar algo parecido. Pueden acceder a esta lista de usuarios mediante el plugin PouchDB Authentication. Recuerden que esto significa que la red es necesaria. Personalmente, prefiero hacer un servicio externo que autentique y que distribuya las key y pass de Cloudant.

Modelamiento:

  • Modelar en NoSQL no es igual que en Relacional, pero hay trucos. Personalmente, prefiero el modelo minimalista de NoSQL pero existe un plugin Relational Pouch que permite utilizar la misma estructura relacional

Finalmente, como verán en algún comentario que hice, pueden tener toda su lógica en la aplicación cliente, siempre y cuando se quieran pegar al modelo “Offline-First”, pero si es que necesitan operaciones en línea, necesitaran de una capa más de lógica.

NOTA Principal: El problema principal con CouchDB y obviamente con Cloudant, es como integrarlo con nuestras bases en SQL. Es un trabajo en progreso en realidad, y no hay una herramienta porque en CouchDB el modelamiento está en función de la velocidad y de la aplicación que va a utilizar los datos, y eso puede ser muy diferente a lo que tenemos en SQL donde se programa para tener consistencia. Hay tantos modelos y tantas transformaciones posibles que una herramienta no puede hasta ahora hacer el trabajo. Felizmente, no es tan complicado porque ya en la base de datos hemos usando db.changes() , Así que el flujo sería:

  • Lenguaje recomendado: Python ( tiene una gran librería de acceso a Cloudant). También pueden usar NodeJS
  • Un script accede a la base de datos SQL y crea los documentos en Cloudant uno por uno según el modelo que hayamos fijado. Podemos cruzar dos tablas para crear documentos Maestro-Detalle siempre que no sean muchos detalles. No es tan rápido y lo ideal es usar BulkDocs para insertar por lotes.
  • Luego se invoca el comando db.changes()  donde cada documento cambiado es actualizado a la base de datos. Otra vez, puede ser como un insert, delete o update, todo depende de como sea su modelo.

Hasta ahora, todo esto es a medida por lo que mejor es que practiquen sus habilidades en NodeJS o Python.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

Con la parte 10, podríamos decir que comienza una nueva etapa del código así que debemos empezar con las cosas interesantes. Primero planifiquemos las siguientes funciones:

  • Alertas ante nuevas noticias. Esto ya me lo habían pedido, y recién ahora tiene sentido incluirlo pues ya tenemos todas las herramientas. Según lo que hemos visto, al activar la replicación estamos controlando el evento ‘paused’ para indicar que la sincronización terminó y no hay nada nuevo por el momento, pero no nos dice nada si hubieron noticias nuevas. Para eso debemos controlar otro evento llamado ‘change’ que se genera por cada cambio en la base de datos local, aquí simplemente ubicamos cuando se trate de una nueva noticia y podríamos ya lanzar la notificación, teniendo cuidado pues durante la primera ejecución del programa todas serán noticias nuevas y no queremos una avalancha de alertas.
  • Agregar un mecanismo para registrar las noticias más leídas. Esto supondrá que grabemos en algún lado que hemos leído la noticia. Recordemos que este documento será sincronizado hacia el servidor y a todos los usuarios con la versión actual, lo cual no queremos así que veremos como hacer para que la sincronización sea controlada: que los registros generados por el usuario sean sincronizados con el servidor pero que a los demás usuarios solamente lleguen los documentos con las estadísticas. Igual funcionaría para un mecanismo para registrar “Likes” tipo Facebook.

Comencemos con las alertas para nuevas noticias, para eso necesitamos un plugin para Ionic que nos ayude a manejar las notificaciones locales. Esto se refiere a las mismas alertas que reciben cuando tienen un correo o algún otro cambio relevante. Hay que recalcar que son locales, puesto que los datos ya están en la base de datos; en las apps normalmente se da todo lo contrario, primero llega la notificación y luego se descarga la información, pero como nuestro modelo es “off-line first” no podemos confiarnos en la red.

Para hacer la cosa aún más fácil, instalemos el componente Ng-Cordova que encapsula el código necesario para manejar plugins, mediante el siguiente comando:


bower install ng-Cordova --save

El parámetro –save es para agregar este componente a la lista de dependencias.

Seguidamente, instalemos el plugin:

ionic plugin add https://github.com/katzer/cordova-plugin-local-notifications.git

Y listo, ahora hay que ver donde es el mejor lugar para detectar que se ha insertado una nueva noticia y ese lugar puede ser al detectar un cambio. Recordemos que al momento de replicar ya se genera un evento al detectar el fin de la replicación. Veamos el archivo services.js


db.replicate.from(remote,
{live:true,
retry:true})
.on('paused',changed);

Es bueno recordar aquí como es esto de los eventos de replicación:

  • Paused: indica solamente que la replicación ha parado. Como hemos establecido el parámetro live en true, significa que ya no hay mas cambios que sincronizar. Esto también significa que la replicación no ha recibido mas registros en un rato largo, lo que sucede a cada rato cuando la velocidad del internet es bastante lenta.
  • Complete: Si es que establecemos live en false, significa que la replicación se hará una sola vez y al terminar se generará el evento Complete.

Noten que si queremos asegurar que la base local está al día, quizá haya que hacer un mecanismo de control como establecer un registro con la fecha de ultima actualización. Además, la replicación manual puede servir si sabemos que los cambios son poco frecuentes o si queremos que sea el usuario final quien controle la actualización.

Bien, entonces regresando al código, cuando la replicación se completa o se para, se ejecuta la función “changed” que está definida así:


function changed(){
console.log('cambios en la base de datos');
if (!initiated)
{
initiated=true;
db.changes({live: true, since: 'now', include_docs: true})
.on('change', newNews);
};

};

Aquí lo que hacemos es verificar que la base de datos está inicializada y usamos una variable “initiated” para controlar que solamente una vez llamemos  a la función db.changes. Con db.changes lo que hacemos es verificar un evento mas: cuando se ha modificado algún registro en la base de datos. Cuando se detecte este cambio se ejecuta la función “newNews”.

NOTA: fijense bien la secuencia, primero se replica y luego se monitorea el evento cambios. Por lo tanto, la secuencia de datos será así: inicias la aplicación, sincronizas, cuando se completa, cada registro nuevo o modificado generará una alerta. De esta manera, las noticias antiguas no generarán alertas.

Ahora veamos la funcion newNews:


function newNews(change){
if (!change.deleted){
if (change.doc.tipo=="news")
$rootScope.$broadcast('db:newNews',{newsId:change.id,
newsTitle:change.doc.titular});
}
}; function newNews(change){
if (!change.deleted){
if (change.doc.tipo=="news")
$rootScope.$broadcast('db:newNews',{newsId:change.id,
newsTitle:change.doc.titular});
}
};

Bien, notarán que esta función toma un parámetro llamado change que es el documento que ha sido afectado. El documento puede haber sido borrado, creado o modificado. Si es borrado la propiedad change.deleted será verdadera. Si el documento es creado o modificado por ahora los trataremos como iguales pues tendríamos que revisar la fecha de creación en alguna propiedad.

En la línea 10 verificamos que no se trate de un documento borrado y seguidamente verificamos que el documento sea del tipo news, porque también hay documentos del tipo cat y esos no queremos que manden notificaciones. Si todo se cumple, mandamos un broadcast para que se genere el evento de generación de la notificacion. Para ver eso vamos al archivo app.js.


.run(function($rootScope,$state,$cordovaLocalNotification){
$rootScope.$on('db:newNews',function(event,args){
var id = new Date().getMilliseconds();
$cordovaLocalNotification.schedule({
id: id,
title: 'Super Datos',
text: args.newsTitle,
data: {
newsId: args.newsId,
newsTitle: args.newstitle
}
}).then(function (result) {
console.log('Notificación registrada');
});

$rootScope.$on('$cordovaLocalNotification:click', function (
event,
notification,
state) {
var datos = JSON.parse(notification.data);
$state.go('app.detail',{id:datos.newsId});
});
});
})

Aquí notarán que hemos agregado una sección .run() para manejar dos eventos en la aplicación: el primero el evento que generamos anteriormente para avisar que hay una nueva noticia y otro mas para el evento $cordovaLocalNotification:click, este último evento es el que se genera cuando un usuario hace click encima de la notificación, es decir, que cuando llegue la notificación, el usuario vera el aviso en su teléfono y al hacer click se abrirá la aplicación y se abrirá en el estado detalle para que pueda leer el contenido. Creo que estamos de acuerdo que mas complicado ha sido encontrar y fijar el evento que lanza la notificación que programarla, pero lo hago así por una razón: trato que todos los eventos sean manejados en el app.js para no tener problema luego para mantenerlos y gestionarlos, incluso trato que los broadcast estén aquí en la medida de lo posible, o en su defecto en el archivo services.js, nunca en un controlador.

Y listo, ya pueden probar su nuevo lector de noticias, y cada vez que tengan una nueva noticia o una noticia antigua ha sido actualizada, recibirán una notificación que les permitirá enterarse e ir directamente al nuevo contenido.

Recalco que esto no es una notificación tradicional, pues el contenido ya está en el teléfono y es la misma aplicación la que genera la notificación. Las apps conectadas que son las mas comunes, mandan la notificación por la red y recién hacen la recuperación de los datos, todo depende de como quieren que su app se comporte.

Finalmente, pueden jugar con más opciones de las notificaciones locales, como agregar un botón para que el app te recuerde leer una noticia dentro de 15 minutos, o poner una notificación cuando el app esté sincronizando y demás cosas. Todo queda en ustedes.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

Bien, con lo desarrollado en la parte 07 ya tenemos una aplicación funcional que filtra los datos por categoría, pero no está completa pues faltaría una forma de poder elegir la categoría mediante un menú. En Ionic Framework, tenemos una librería de controles interesantes, y para nuestro caso, vamos a utilizar el side menú. Para eso tenemos que hacer la introducción de un concepto nuevo: estado abstracto.

Un estado abstracto es un estado utilizado para cuando lo que se quiere hacer es mostrar una parte del UI que es común para otros estados. Pensemos en que un estado abstracto es un marco donde podremos poner logos y cabeceras, e incluso poner cierta lógica y que tiene una zona donde otros estados podrán mostrar información.

Primero, comencemos cambiando nuestra configuración de estados en el archivo app.js:

.config(function($stateProvider, $urlRouterProvider) {
    $stateProvider
        .state('app',{
            abstract: true,
            views:{
                "home":{
                    templateUrl:"templates/menu.html",
                    controller:"menuController as menu"
                }
            }
        })
        .state('app.news', {
            url: "/",
            views: {
                "menuContent":{
                    templateUrl: "templates/news.html",
                    controller: "newsController as news"
                }
            }
        })
        .state('app.detail', {
            url: "/detail/:id",
            views: {
                "menuContent":{
                    templateUrl: "templates/detail.html",
                    controller: "detailController as detail"
                }
            },
            resolve:{
                detail: function($stateParams){
                    return $stateParams.id;
                }
            }
        });
        $urlRouterProvider.otherwise('/');
    });

Como pueden ver, hemos agregado un estado en la línea 17 donde el primer atributo es uno nuevo: abstract:true, lo que significa que éste será nuestro estado abstracto que servirá para colocar el side menu.
Una vez que hemos agregado el estado abstracto, ahora hay que indicar a los otros estados que deben tener como estado “padre” al estado “app”. Para eso, simplemente le añadimos “app.” al nombre de cada estado, con eso basta para indicar la dependencia. Fijense como ha cambiado el nombre en la línea 26 y en la línea 35.
Otro punto importante que deben notar es que hemos cambiado el valor del atributo “views” en la definición de cada estado en las líneas 29 y 38. Inicialmente, habíamos indicado que el estado sea mostrado en la vista “home” ahora le estamos diciendo que utilice la vista “menuContent”, puesto que estos estados serán mostrados dentro del estado abstracto, tendremos que indicar una vista definida dentro del estado abstracto. Veremos esto más claro al definir la vista para el menú.

En la configuración de estados hemos indicado que hay un controlador para el estado abstracto, así que agreguemos el controlador al archivo controllers.js:

angular.module('controllers',['services'])
	.controller('menuController',function(){
	})

Por ahora lo mantendremos así sin código.
Ahora agreguemos un template para el menú que hemos definido como el archivo menu.html

<ion-side-menus enable-menu-with-back-views="true">
    <ion-side-menu-content>
        <ion-nav-bar class="bar-stable">
            <ion-nav-buttons side="right">
                <button class="button button-icon icon ion-android-exit " ng-click="salir()"> Salir
                </button>
            </ion-nav-buttons>
            <ion-nav-buttons side="left">
                <button class="button button-icon button-clear ion-navicon" menu-toggle="left">
                </button>
            </ion-nav-buttons>
        </ion-nav-bar>
        <ion-nav-view name="menuContent"></ion-nav-view>
    </ion-side-menu-content>

    <ion-side-menu expose-aside-when="large" side="left">
        <ion-header-bar class="bar-stable">

<h1 class="title">Noticias</h1>

        </ion-header-bar>
        <ion-content>
<div align="center">
                <img width="50" src="img/ionic.png">Categorias
            </div>
            <ion-list>
                <ion-item nav-clear menu-close>
                    Nacional
                </ion-item>
                <ion-item nav-clear menu-close>
                    Internacional
                </ion-item>
                <ion-item nav-clear menu-close>
                    Espectáculos
                </ion-item>
                <ion-item nav-clear menu-close>
                    Opinión
                </ion-item>
            </ion-list>
        </ion-content>
    </ion-side-menu>
</ion-side-menus>

El componente sidemenu tiene dos partes: el side-menu y el side-menu-content, en nuestro ejemplo, hemos definido primero el side-menu-content donde podrán ver en la línea 13 que hemos definido la vista “menuContent” donde se mostrarán los demás estados dependientes.
En la línea 16 hemos definido el side-menu que es el menú propiamente que aparecerá a un costado. En este caso hemos definido que el estado se muestre a la izquierda “side=left” y que si la pantalla es muy ancha entonces el menú se muestre desplegado por defecto, si la pantalla es angosta, el menú se esconderá a la espera que hagamos un “swipe” o hagamos click en el botón de tres líneas que aparecerá.

Para resumir, tenemos ahora varios niveles de estado:
– Index.html define la vista “home”
– En la configuración de estados, pintamos el estado abstracto “app” definido en el archivo “menu.html” en la vista “home”
– Menu.html define la vista “menuContent”
– Todos los demás estados se hacen dependientes del estado app y serán mostrados en la vista “menuContent”.

El resultado es algo parecido a esto:
Con la pantalla ancha tipo tablet.

menuA

Con la pantalla angosta

menuB01 menuB02

Si hacemos click a una noticia podemos ver como se sigue mostrando dentro del mismo marco. También notarán que la lista de categorías está puesta manualmente y además no hacen mucho. Para no extender mucho el post, primero hacemos que el menú funcione.

Primero revisemos el estado original de nuestro servicio que controla los datos (services.js)

angular.module('services',[])
.factory('db',function($rootScope){
    var key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
   var pass = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
   var remote = 'https://'+key+':'+pass+'@server.cloudant.com/news';
   var db;
   var mostrar = function(){
        db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
    };
    var mostrarCat = function(catId){
        db.query('news/topic',{key:[catId],include_docs:true,descending:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
    };
    return {
        init: function(){
            if (!db) {
                db = new PouchDB('news');
            }
            mostrarCat("cat_01");
            this.replicate();
        },
        replicate: function(){
            db.replicate.from(remote,{live:true,retry:true})
                .on('paused',function(info){
                mostrarCat("cat_01");
            });
        },
        get: function(id){
            return db.get(id);
        }
    }
});

NOTA: En la línea 14 estoy incluyendo una corrección : he cambiado el parámetro “startkey” por “key”. La diferencia es que “startkey” se usa para cuando se quiere establecer un rango, por lo que “cat_01” con el parámetro descending true devolverá sólo “cat_01”, mientras que “cat_04” mostrará también “cat_03”, “cat_02” y “cat_01” lo que no buscamos. Con “key” solamente se devolverán los valores iguales al id de categoría.

Podemos ver que estamos pasando “en duro” la categoría 1 como parámetro para mostrar, por lo tanto tenemos que crear una variable miembro para almacenar este valor, cosa que al seleccionar un elemento del menú su valor se cambie para mostrar las noticias correspondientes. Por defecto mostraremos todas las noticias y tendremos que agregar un método para actualizar la variable de categoría y refrescar la vista. En el archivo services.js, empecemos agregando la variable cat:

angular.module('services',[])
.factory('db',function($rootScope){
        var key = 'bentareadyessharyinessee';
        var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
        var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
        var db;
        var cat;

Luego agregamos el método que llamaremos “setCat”

get: function(id){
    return db.get(id);
},
setCat: function(id){
    cat = id;
    mostrarCat(cat);
}

Ahora, tenemos que eliminar las llamadas de mostrarCat con la categoría “en duro”

return {
    init: function(){
        if (!db) {
            db = new PouchDB('news');
        }
        mostrarCat(cat);
        this.replicate();
    },
    replicate: function(){
        db.replicate.from(remote,{live:true,retry:true})
            .on('paused',function(info){
            mostrarCat(cat);
        });
    },

Finalmente, si mostrarCat recibe un valor nulo o vacio debemos mostrar todas las noticias.

var mostrarCat = function(catId){
    if (catId)        
    db.query('news/topic',{key:[catId],include_docs:true,descending:true})
                .then(function(result){$rootScope.$broadcast('refrescar',result.rows);});
    else mostrar();
};

Con eso ya arreglamos el modelo, ahora vamos al controlador en el archivo controllers.js.

angular.module('controllers',['services'])
	.controller('menuController',function($state,$scope,db){
		$scope.setCat = function(id){
			$state.go('app.news');
			db.setCat(id);
		};
	})

Aquí estamos usando una nueva entidad $state que nos va a permitir llamar a un estado.
Ahora, en la vista tenemos que llamar a la función del controlador al momento de hacer click. Como tenemos las categorías “en duro”, seguiremos indicando las claves “en duro”, así que modificamos el archivo menu.html para incluir los links que llamen a la función $scope.setCat(id):

 <ion-list>
     <ion-item nav-clear menu-close ng-click='setCat("cat_01")'>
       Nacional
     </ion-item>
     <ion-item nav-clear menu-close ng-click='setCat("cat_02")'>
       Internacional
     </ion-item>
     <ion-item nav-clear menu-close ng-click='setCat("cat_03")'>
       Espectáculos
     </ion-item>
     <ion-item nav-clear menu-close ng-click='setCat("cat_04")'>
       Opinión
     </ion-item>
</ion-list>

Lo que hemos hecho es agregar la propiedad ng-click para que se ejecute la funcion setCat(id) definida en el controlador tal como lo dijimos.

Y listo. Por ahora tendremos el siguiente comportamiento:

  • La aplicación mostrará todas las noticias al cargar
  • Al hacer click en una categoría, se mostrarán solamente las tareas relacionadas

En el siguiente post veremos como cargar las categorías directamente de la base de datos y ya no “en duro”. Ya saben si encuentran algún error, me avisan.


Ya hace un par de meses que empecé esta serie de código sobre Ionic Framework poniendo especial atención en la integración con bases de datos NoSQL, principalmente por la capacidad de sincronización. La intención es la de hacer fácil lo que me llevó a mí una gran cantidad de tiempo, principalmente porque eso de cambiar el tipo de base de datos es realmente algo confuso por naturaleza.

En este post voy a hacer el primer intento por ordenar los posts indicando el tema al que se refiere cada uno.

Recordarles que no hay código para descarga, pues para entender una sección deben entender la anterior, por lo que es realmente importante que escriban el código desde la parte 1. Antes de publicar el post, verifico que el código funciona correctamente, pero hay veces en los que se me pasa algún detalle. Pueden colaborar con sus comentarios.

Simplemente por utilizar Ionic Framework, todo el código que obtienen es funcional en al menos Android, iOS y Windows Phone, por lo que la recomendación general para todo es que comiencen desde lo más básico.

Por ahora ahi tienen los post de la serie Ionic, PouchDB y Cloudant

Post Tema
Part 1 Basics
Part 2 Ionic: Environment and Binary generation
Part 3 Organizing the code
Part 4 Master-Detail and UI-Router
Part 5 Improving the off-line experience of the app
Part 6 MVC review
Part 7 Filtering data with Cloudant Views.
Part 8 Abstract states and Views
Part 9 Abstract states and Views and Dynamic Menu
Part 10 AngularJS Style guide
Part 11 New features: Adding local notifications
Part 12 New features: Writing docs and sync with the Server
Part 13 General Review and tips for PouchDB

Como siempre sus comentarios son bienvenidos,así como su ayuda para encontrar bugs


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

El mayor desafio que hay al trabajar con NOSQL es consultar los datos pues no hay algo como SQL del mundo relacional que venga y nos simplifique la vida, bueno, si hay pero en cuestión de rendimiento no es lo mismo. CouchDB originalmente viene con funciones Map/Reduce para la creación de índices o vistas, es decir, si queremos recuperar los datos en cierto orden o hacer consultas tipo select * from tabla where campo3=XXX debemos crear un índice para poder buscar por el campo3, el problema es que como no hay tablas estos índices se aplican a todos los documentos en la base de datos y un detalle importante: estos índices se graban en disco lo que los hace muy eficientes. Suena bonito. Veamos como se hace.

Digamos que en nuestra aplicación de noticias queremos agregar secciones tal como lo tienen todos los sitios de noticias: política, espectáculos, deportes y así. Bien, entonces agreguemos los documentos a la base en Cloudant. Empecemos con 3 categorías.


{
 "_id": "cat_01",
 "nombre": "Nacional",
 "descripcion": "Noticias del país",
 "tipo": "topic"
}

{
"_id": "cat_02",
"nombre": "Internacional",
"descripcion": "Noticias del mundo",
"tipo": "topic"
}

{
"_id": "cat_03",
"nombre": "Espectáculos",
"descripcion": "Cine, TV y música",
"tipo": "topic"
}

{
"_id": "cat_04",
"nombre": "Opinión",
"descripcion": "Punto de vista de los editores",
"tipo": "topic"
}

Listo, 4 categorías para comenzar. Recuerden ingresar estos 4 documentos en su página de Cloudant. Seleccionen la base de datos y luego busquen la opción agregar doc.

addNewDoc

Ingresen uno a uno las 4 categorías.

Importante es el campo “tipo” que nos indicará que estos 4 documentos pertenecen a un tipo “topic” que es como identificaremos a esta entidad. Ahora tendremos en la base de datos documentos tipo “topic” y tipo “news”. Ahora lo que tenemos que hacer es vincular los documentos, así que iremos revisando los documentos tipo “news” uno por uno asignándole un topic. Por ejemplo:

{
 "_id": "news_20150724_vpease_001",
 "_rev": "2-6713e069e048d0eeb4d8d826f52454cd",
 "tipo": "news",
 "titular": "Retomando el demo!",
 "resumen": "Regresando a la lector de noticias esta vez con estados",
 "fecha": "2015/07/24",
 "autor": "vpease",
 "topic": "cat_01"
}

En la línea 9 podrán ver que se ha agregado el campo “topic” y el valor que tiene es el id de la categoría 1: “Nacional”.

Ahora necesitaremos una forma de recuperar la lista de categorías para mostrarlas en el app y luego al seleccionar cada una, mostremos las noticias asociadas.Las opciones que tenemos son :

  • Crear una vista: En el servidor buscaremos todos los documentos tipo “topic” y los devolveremos. Como todo índice, va a crear un archivo físico.
  • Usar el índice por defecto: podemos consultar el índice por defecto utilizando el Id. Si vemos el id que hemos fijado, todos los documentos tipo “topic” tienen un Id del tipo “cat_XXX” así que podemos usar esto para recuperarlos. Se reutiliza el índice principal.
  • Cloudant Query. Es el nuevo motor de búsqueda que viene con CouchDB 2.0 que es más fácil de usar que Map/Reduce. Igualmente creará un índice en un archivo físico.

Tengan en cuenta que:

  • Si crean un índice, se crea un archivo. Esto es tanto en el servidor como en el lado del cliente.
  • Ya existe un índice por defecto que nos permite consultar por el id.

Además, gracias al buen artículo publicado por Nolan Lawson en http://pouchdb.com/2014/05/01/secondary-indexes-have-landed-in-pouchdb.html, lo más conveniente es explotar al máximo el Id principal del documento.

Cloudant Query lo guardaremos para el siguiente post, así que por ahora vamos a utilizar la siguiente estrategia:

  • Crear una consulta al Id para recuperar la lista de categorías
  • Crear una vista para recuperar todas las noticias dentro de una categoría.

Empecemos creando la vista. Esto debemos hacerlo en Cloudant y luego será sincronizada automáticamente al cliente en PouchDB. Recuerden que para PouchDB esto se llama índices secundarios. En la pantalla de Cloudant vamos a la opción de crear vistas.

createview

Ahora le damos un nombre a nuestro documento de diseño, para hacerlo simple llamaremos ‘news’ a este documento. Un documento de diseño es donde se definen los indices y otras reglas en CouchDB. Le pondremos el nombre topics a la vista. CUIDADO:  el nombre del documento de diseño es la raíz para cuando quieran llamar a su vista. En nuestro caso, el nombre completo de nuestra vista será ‘news/topics’.

Lo importante es que queremos poder buscar las noticias por la categoría a la que pertenecen, para eso escribimos lo siguiente en la sección Map function:

function (doc) {
if (doc.tipo =="news") {
emit([doc.topic], {_id:doc._id});
}
}

Tremenda ensalada para tan pocos comandos. Lo que hace esto es recuperar todos los documentos tipo news y publicar el índice doc.topic que como ya saben es el campo que tiene la categoría de la noticia, luego le decimos que devuelva todo el documento con la información de la noticia.

La función Reduce es opcional y se usa para cuando queremos realizar una operación sobre los datos obtenidos, en otras palabras, para los nativos SQL es cuando usamos funciones tipo Sum, Count y esas cosas. Para este caso, no nos hace falta.

Y listo, ahora el documento de diseño será sincronizado con PouchDB en el cliente y podremos hacer las consultas, pero antes debemos cambiar el diseño de nuestra aplicación, Para no alargar este post, vamos a hacer un cambio simple, mostraremos solamente las noticias que pertenezcan a la categoría ‘cat_01’ que corresponde a las noticias nacionales.

Primero, comencemos agregando al servicio db un método para consultar la vista que acabamos de crear.


var mostrarCat = function(catId){
db.query('news/topics',{startkey:[catId],include_docs:true,descending:true})
.then(function(result){
$rootScope.$broadcast('refrescar',result.rows);
});
}

El nombre de la vista es ‘news/topics’, noten que comenzamos por incluir el documento de diseño, porque simplemente puede haber más de uno, pero no compliquemos el asunto por ahora. luego están las opciones que hemos incluido:

  • startkey: simple, es el valor que queremos comparar. Como nuestra clave fue definida simplemente por el campo doc.topic entonces es una cadena simple. Es posible definir mas campos en el índice y en ese caso tendríamos que pasar un array. En este parámetro pasamos el id de la categoría que queremos ver.
  • include_docs: Por defecto, CouchDB devuelve solamente el Id del documento asociado, pero como necesitamos mostrar la noticia completa, tenemos que fijar este parámetro en true.
  • descending: indica que el resultado será ordenado por la clave original en modo descendente, recuerden que la clave original tiene la fecha en el formato YYYYMMDD de tal forma que si la ordenamos en forma descendente tendremos las noticias nuevas al comienzo.

Y listo, ahora solo falta indicarle al código del servicio que en lugar de utilizar el método “mostrar()” , donde se recuperan todas las noticias, utilice “mostrarCat(“cat_01″)” donde le decimos que nos muestre solamente los de la categoría “cat_01” que corresponde a las noticias nacionales. Algo tosco pero efectivo. El código del servicio ‘db’ quedará así:


angular.module('services',[])
.factory('db',function($rootScope){
var key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
var pass = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
var remote = 'https://'+key+':'+pass+'@server.cloudant.com/news';
var db;
var mostrar = function(){
db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
.then(function(result){
$rootScope.$broadcast('refrescar',result.rows);
});
};
var mostrarCat = function(catId){
db.query('news/topic',{startkey:[catId],include_docs:true,descending:true})
.then(function(result){
$rootScope.$broadcast('refrescar',result.rows);
});
}
return {
init: function(){
if (!db) {
db = new PouchDB('news');
};
mostrarCat("cat_01");
this.replicate();
},
replicate: function(){
db.replicate.from(remote,{live:true,retry:true})
.on('paused',function(info){
mostrarCat("cat_01");
});
},
get: function(id){
return db.get(id);
}
}
})

Podrán ver aquí que hemos agregado el método mostrarCat en la línea 13 y luego cambiamos la función para mostrar los datos en la línea 24 y en la 30. Fijense que no hemos tenido que mostrar el Controller para nada.

Al ejecutar este nuevo código notarán que ya no tendrán todas las noticias que solían tener, ahora verán una o dos. Prueben insertando nuevos documentos y cambiando la categoría.

Para la próxima trataremos de insertar el concepto de estados abstractos y sobre todo, pasar datos a un estado, y para arreglar la parte gráfica, agregaremos un bonito menú que nos permita elegir las categorias como cualquier sitio de noticias decente.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

El día de hoy un lector me hizo saber un error en mi código y pude darme cuenta que hay algo que no he explicado y es muy importante para cuando nuestra aplicación se hace cada vez mas grande. Más que con javascript, lo que hay que aclarar es con el modelo MVC.

MVC-web-application-development

Este es el gráfico mas conocido del modelo MVC, pero creo que podríamos fijarnos también en este otro:

ocEWx

La idea principal es que la Vista es manipulada solamente por el Controlador, mientras que el Controlador es el único que manipula el Modelo.

En el código que hemos hecho hasta ahora, el Controlador tiene referencias a los Servicios que representan al Modelo. Revisemos el archivo controllers.js

angular.module('controllers',['services'])
.controller('newsController',function($scope,db){
    db.init();
    $scope.notas=[];
    $scope.$on('refrescar',function(event,news){
       $scope.$apply(function(){
            $scope.notas = news;
       }) 
    })
})
.controller('detailController',function($scope,db,detail){
 db.get(detail).then(function(doc){
 $scope.event=doc;
 })
})

Podemos ver que en la definición de cada controlador hemos ingresado unas referencias a cada controlador. Para el controlador “newsController” se tiene $scope y db, donde $scope es un objeto Angular para intercambiar datos con la vista, y db es el servicio que hemos escrito en el archivo services.js. Por lo tanto si tenemos algún problema con el objeto db, donde debemos ir a revisar es en la definición del objeto db, así que si revisamos el archivo services.js.

angular.module('services',[])
.factory('db',function($rootScope){
        var key = 'XXXXXXXXXXXXXXX';
	var pass = 'XXXXXXXXXXXXXXX';
	var remote = 'https://'+key+':'+pass+'@servidor.cloudant.com/news';
	var db;
	var mostrar = function(){
        db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
    };
    return {
        init: function(){
            if (!db) {
                db = new PouchDB('news');
            };
            mostrar();
            this.replicate();
        },
        replicate: function(){
            db.replicate.from(remote,{live:true,retry:true})
                .on('paused',function(info){
                mostrar();
            });
        },
        get: function(id){
            return db.get(id);
        }
    }
})

Vemos que el factory ‘db’ en la sección return publica tres métodos: init(), replicate() y get()
Por lo tanto, si el objeto db nos da algún error en alguna llamada en el controlador, debemos revisar esta parte del código para encontrar el error.

Lo que puede crear algo de confusión es que el factory se llama ‘db’ y luego internamente existe la variable ‘db’ que en realidad es un objeto de la clase PouchDB. En el controlador siempre debemos usar el objeto indicando el nombre del Factory.

Finalmente, regresando al concepto MVC de nuestro código, vemos que cada vez que encontramos un cambio en los datos, generamos un mensaje broadcast que es recibido en el controlador, como vemos en el controlador ‘newsController’ en el archivo controllers.js

.controller('newsController',function($scope,db){
    db.init();
	$scope.notas=[];
	$scope.$on('refrescar',function(event,news){
		$scope.$apply(function(){
			$scope.notas = news;
		})		
	})
})

Pueden ver que en la línea 5, definimos la lógica que se ejecutará al recibir el evento generado por el Modelo mediante el Factory db en el archivo services.js. Podrán notar que el evento es generado en el método mostrar:

        var mostrar = function(){
        db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
    };

En la línea 10 generamos el mensaje incluyendo los registros que queremos mostrar como parámetro.

Este envío de mensajes es muy importante porque de lo contrario estaremos declarando y verificando el valor de variables de control a cada rato, lo cual no es bueno ni para respetar el modelo MVC ni para el mantenimiento de su código. Incluso, ya cuando tengan aplicaciones grandes, notarán que necesitarán pasar mensajes entre objetos que no necesariamente alteran la vista, por ejemplo si quieren registrar la posición del móvil y guardarla cada 15 minutos, tendrán que invocar a un servicio enviando un broadcast desde otro servicio que controla el tiempo. El mejor lugar para indicar la lógica de estos eventos que no alteran es en el archivo app.js que podríamos considerar como parte de la capa Controller.

En un próximo post veremos un poco de este manejo mediante la creación de servicios lo más parecido a las clases que utilizamos en programación orientada a objetos donde podremos identificar los datos por un lado y los métodos por otro diferenciando cuáles son públicos y cuáles privados.

Espero que con esto me puedan ayudar a corregir errores ya sean por no incluir el código, o porque de verás se metió un bug, aunque les aseguro que primero me aseguro que el código funciona antes de publicar el post.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

Veamos, en la parte 4 vimos como mostrar los datos aún cuando no se tenga conexión a la red. Según algún comentario, la conexión era necesaria para comenzar la aplicación. A fin de asegurar que la aplicación pueda mostrar los datos existentes incluso sin red desde el inicio, hay que verificar lo siguiente:

  • Hay que separar la lógica de mostrar datos de la lógica de la replicación
  • La lógica de la replicación debe refrescar los datos.

Para cumplir con estos dos criterios hay que modificar nuestro archivo services.js. Primero agregamos la lógica de mostrar datos como una función privada:

angular.module('services',[])
.factory('db',function($rootScope){
    var key = 'bentareadyessharyinessee';
	var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
	var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
	var db;
	var mostrar = function(){
        db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
    };

Hemos agregado la funcion “mostrar” y ademas en la línea 6 podrán ver que hemos dejado la variable db sin inicializar. La función “mostrar” lo que hace es consultar todos los registros y generar el evento refrescar pasando el resultado de la consulta al controlador.

Ahora hacemos la separación de la replicación y la lógica de mostrar en el método “init” de esta manera.

init: function(){
            if (!db) {
                db = new PouchDB('news');
            };
            mostrar();
            this.replicate();
        },

La versión modificada del método “init” lo que hace ahora es inicializar la variable db y seguidamente muestra los datos y en un proceso aparte inicia la replicación. Recuerden que en Javascript estos métodos son asíncronos, así que un método no bloquea al otro.

Finalmente, el método de replicación también debe considerar la lógica de mostrar los datos.

replicate: function(){
            db.replicate.from(remote,{live:true,retry:true})
                .on('paused',function(info){
                mostrar();
            });
        },

Listo, ahora ya cumplimos con los criterios y nuestra aplicación mostrará los datos que tenga localmente incluso trabajando sin red.

Un punto importante es que la lógica de refrescar datos es una función privada llamada mostrar. Se hace en una función privada porque según el modelo MVC solamente el controlador puede actualizar la vista, pero no debe manipular los datos. Todo siempre por separado.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

En esta parte, haremos una introducción a lo que creo que es lo más potente que se ha incluido en Ionic, UI-Router. Hay otros componentes que hacen lo mismo, pero creo que lo más importante aquí es el concepto de estados.

Un estado es un conjunto de vista, controlador y modelo que hace una labor específica en nuestro programa. Tenemos que pensar en una labor muy simple para ver la potencia de este concepto. Por ejemplo, listar todos los correos que tenemos, es un estado, o ver un email en particular es otro estado. Mientras más simple que sea, más reutilizable será el estado.

Ahora, quizá me adelante, pero es posible que un conjunto de estados compartan una base común, por ejemplo, la lista de correos tienen que mostrarse dentro de una ventana con un menú de controles. Para eso, existe el concepto de estados abstractos.

Primero vamos por lo más simple, agregemos los estados. En nuestro caso, es simple pues tenemos solamente un estado que lista todas las noticias. Así que modificamos el archivo app.js para definir el estado.

angular.module('starter', ['ionic','controllers'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})
.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('news', {
      url: "/",
      views: {
          "home":{
              templateUrl: "templates/news.html",
              controller: "newsController as news"
          }
      }
  })
  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/');
});

Desde la línea 15 se puede ver la definición del estado. En nuestro caso, hemos llamado al estado con el nombre de “news” y le hemos asignado el url “/”, también se le ha asignado una vista llamada “home” que tiene un template llamado news.html en la carpeta templates y tiene asignado el controlador newsController al que le hemos puesto un alias news. ¿Qué significa todo esto?

Traducción: Al invocarse el estado “news”, AngularJS va a tomar el archivo news.html y le va a asignar el controlador newsController para ejecutarlo. Simple.

En la línea 27 se pone que la aplicación tomará la ruta “/” como ruta por defecto, lo que llevará a la aplicación al estado “news”.

Veamos el archivo news.html.

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Ionic Blank Starter>/h1>
 </ion-header-bar>
 <ion-content>
 <p> Hay {{notas.length}} noticias</p>
 <div class="list"
 ng-repeat="noticia in notas">
 <div class="card">
 <div class="item item-divider">
 {{noticia.doc.fecha}}</br>{{noticia.doc.titular}}
 </div>
 <div class="item item-text-wrap">
 <b>{{noticia.doc.resumen}}</b>
 </div>
 <div class="item item-divider">
 Autor: {{noticia.doc.autor}}
 </div>
 </div>
 </div>
 </ion-content>
</ion-view>

El contenido es casi todo lo que antes había en el archivo index.html pero con las tags iniciales en lugar de las <ion-content< que habían antes. Esto le indica a AngularJS que estamos definiendo una vista.

Lo que hemos hecho es separar la vista y asignarle un controlador de forma programática lo cual es muy conveniente para poder reutilizar el código.

Ahora, para que todo esto funcione falta indicarle a AngularJS donde vamos a mostrar el resultado del estado, y para eso veamos como queda el archivo “index.html”

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 <title></title>
 <!-- compiled css output -->
 <link href="css/ionic.app.css" rel="stylesheet">
 <!-- ionic/angularjs js -->
 <script src="lib/ionic/js/ionic.bundle.js"></script>
 <!-- cordova script (this will be a 404 during development) -->
 <script src="cordova.js"></script>
 <!-- your app's js -->
 <script src="js/services.js"></script>
 <script src="js/controllers.js"></script>
 <script src="js/app.js"></script>
 <script src="lib/pouchdb/dist/pouchdb.js"></script>
 </head>
 <body ng-app="starter">
 <ion-nav-view name="home"</ion-view>
 </body>
</html>

En la línea 20 notarán que hemos reemplazado toda la parte que mostraba las noticias por el tag . Aquí le estamos diciendo a AngularJS que queremos que se muestre la vista llamada “home”, que la hemos definido antes en el archivo app.js.

Traducción, AngularJS cargará news.html, le asignará el controlador newsController y la mostrará en el tag denominado “home”.

¿Cuál es la ventaja de todo eso? la respuesta es separación de responsabilidades. El diseñador Web podrá trabajar en la parte de la presentación con el html y el CSS y colores y demás, mientras que el programador podrá trabajar en el controlador, y si por ahí hay nuevas versiones de la vista o el controlador, pues simplemente se cambia en la definición de la vista y nuestra aplicación no sufrirá el cambio.

Ahora bien, regresemos a la pregunta inicial que recibí y que generó todos estos posts. La pregunta fue que si tenemos una lista de objetos de la base de datos, como hacemos para mostrar los detalles de estos objetos. Pues bien, en nuestro ejemplo, ya estamos recuperando todos los datos así que podríamos tener los detalles ocultos mostrando sólo las cabeceras. Para eso podemos usar la ayuda de Angular-bootstrap, así que instalemos rápidamente.

Paso 1: agregar el CSS en el index.html

<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

Paso 2: instalar el componente angular-bootstrap con bower

bower install angular-bootstrap --save

Paso 3: agregar la referencia en el archivo index.html

<script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>

Con el angular-bootstrap listo, sólo nos queda modificar el template news.html para utilizar el componente accordion:

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Ionic Blank Starter</h1>
 </ion-header-bar>
 <ion-content>
 <p> Hay {{notas.length}} noticias</p>
 <accordion close-others="oneAtATime">
 <accordion-group ng-repeat="noticia in notas"
 heading='{{noticia.doc.fecha}}-{{noticia.doc.titular}}'>
 <b>{{noticia.doc.resumen}}>/b>;
 </br>
 Autor: {{noticia.doc.autor}}
 </accordion-group>
 </accordion>
 </ion-content>
</ion-view>

Y obtendremos algo como esto, donde haciendo click en los títulos se despliega el contenido de la noticia:
gen21

Todo bien, pero hay otra alternativa, sobre todo si es que queremos ver los detalles de la noticia de forma mas flexible sobre todo si hay mucho texto, pues en ese caso deberíamos tener una lista de noticias y al seleccionar un titular, ir a otra pantalla para ver solamente esa noticia en toda la pantalla.

Para esto debemos crear otro estado a nuestra app, así que hagámoslo. En el archivo app.js agregamos ese nuevo estado y nuestro nuevo app.js lucirá así:

angular.module('starter', ['ionic','controllers','ui.bootstrap'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})
.config(function($stateProvider, $urlRouterProvider) {
    $stateProvider
        .state('news', {
        url: "/",
        views: {
            "home":{
                templateUrl: "templates/news.html",
                controller: "newsController as news"
            }
        }
    })
        .state('detail', {
        url: "/detail/:id",
        views: {
            "home":{
                templateUrl: "templates/detail.html",
                controller: "detailController as detail"
            }
        },
        resolve:{
            detail: function($stateParams){
                return $stateParams.id;
            }
        }
    })
    $urlRouterProvider.otherwise('/');
});

Los puntos de interés son la línea 27 donde fijamos el URL y también indicamos que el url vendrá con el parámetro ‘:id’; la línea 34, donde estamos fijando que la variable “detail” será pasada al controlador y tendrá el valor del parámetro ‘id’.
Ahora veamos como ha quedado el controlador nuevo en el archivo controller.js:

.controller('detailController',function($scope,db,detail){
	db.get(detail).then(function(doc){
        $scope.event=doc;
    })
})

Prestemos atención en la línea 11 que estamos pasando la variable ‘detail’ que definimos en la definición del estado y usamos ese valor para recuperar el objeto desde la base de datos. El valor del parámetro ‘id’ debe ser el id del documento que hemos elegido. La función db.get la estamos invocando desde el servicio que hemos definido en el archivo services.js así que tenemos que agregar este a la definición:

return {
  init: function(){
        db.replicate.from(remote,{live:true,retry:true})
        .on('paused',function(info){
            db.allDocs({startkey:'news_\uffff',endkey:'news_',descending:    true,include_docs:true})
            .then(function(result){
                  $rootScope.$broadcast('refrescar',result.rows);
            });
          });
        },
   get: function(id){
        return db.get(id);
   }
}

El documento resultante lo grabaremos en la variable de escope event para usarla en el template para mostrar.

Para verificar esto, veamos como quedan los templates.
Primero, index.html:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 <title></title>
 <!-- compiled css output -->
 <link href="css/ionic.app.css" rel="stylesheet">
 <link href="lib/angular-bootstrap/ui-bootstrap-csp.css" rel="stylesheet">
 <link href="css/bootstrap.css" rel="stylesheet">
 <!-- ionic/angularjs js -->
 <script src="lib/ionic/js/ionic.bundle.js"></script>
 <script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>
 <!-- cordova script (this will be a 404 during development) -->
 <script src="cordova.js"></script>
 <!-- your app's js -->
 <script src="js/services.js"></script>
 <script src="js/controllers.js"></script>
 <script src="js/app.js"></script>
 <script src="lib/pouchdb/dist/pouchdb.js"></script>
 </head>
 <body ng-app="starter">
 <ion-nav-bar class="bar-positive">
 <ion-nav-back-button class="button-clear">
 <i class="ion-arrow-left-c"></i> Regresar
 </ion-nav-back-button>
 </ion-nav-bar> 
 <ion-nav-view name="home"></ion-view>
 </body>
</html>

Aquí solamente hemos añadido una cabecera para facilitar la navegación en las líneas 23 a la 27.
Ahora veamos el archivo donde mostramos la lista de noticias: ‘news.html’

<ion-view view-title="Noticias">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Noticias</h1>
 </ion-header-bar>
 <ion-content>
 <ion-list>
 <ion-item ng-repeat="noticia in notas" 
 ui-sref='detail({id:"{{noticia.id}}"})'>
 {{noticia.doc.fecha}}-{{noticia.doc.resumen}}</ion-item>
 </ion-list>
 </ion-content>
</ion-view>

Notaremos en las líneas 6 a la 10 que hemos incluido una lista y cada item tiene una directiva nueva: ui-sref que se encarga de llevarnos al estado que se indica cuando se hace click en el item; además le estamos pasando el parámetro id con el valor del id del documento.
Y ahora, para mostrar la noticia usamos el template detail.html que lucirá así:

<ion-view view-title="Noticia">
 <ion-header-bar class="bar-stable">
 <h1 class="title">Detalle</h1>
 </ion-header-bar>
 <ion-content>
 <div class="card">
 <div class="item-divider">
 <h2>{{event.fecha}} {{event.titular}}</h2>
 </div>
 <div class="item-body">
 {{event.resumen}}
 </div>
 <div class="item-divider">
 <h2>{{event.autor}}</h2>
 </div>
 </div>
 </ion-content>
</ion-view>

Como ven, los detalles del documento recuperado los leemos usando la variable ‘event’.

Y el resultado es una app con dos estados: news que muestra la lista de noticias disponibles y al hacer click en alguna llegamos al segundo estado llamado detail donde se muestra la noticia seleccionada completa, para lo cual recibe un parámetro que nos sirve para recuperar los datos desde la base de datos.

gen22

gen23

Seguramente notaremos que para el ejemplo que he desarrollado, hacer otra llamada a la base de datos puede parecer un desperdicio, pero lo que he tratado de hacer es mostrar como pasar parámetros y como utilizar los estados.Lo mas importante es que tenemos dos estados que podemos reutilizar en el resto de nuestra app.

NOTA: Parece que tengo un lector que ha sido responsable y ha seguido todos los pasos. Muchas gracias a Tercio Santos. Al parecer tiene un problema con el app y luego de pruebas en mi equipo, puedo decir que no hay problema con el código. Sólo por si acaso, recomiendo la instalación del plugin Cordova Whitelist:

ionic plugin add cordova-plugin-whitelist

Sucede que en las últimas versiones de Android, es necesario especificar los lugares donde el app puede ingresar y para eso se puede usar whitelist. Si es que no está, Android puede bloquear el acceso a internet de su app, por eso es mejor que esté aunque no lo utilicen todavía. Para una implementación en producción si es necesario configurar bien esos permisos. Ya llegaremos a eso.
Y para que vean que el código camina, aquí va un pantallazo. Recuerden, corre el app la primera vez con red, se muestran los datos, y listo, ya pueden ponerlo en modo avión si quieren y el app tendrá que mostrarle los datos.


Esta es una continuación de la serie sobre desarrollo en móviles que comencé aquí. Les recomiendo comenzar a escribir el código fuente desde la parte 1 ya que no se publica el código para descarga.

Ayer mencioné que iba a publicar un nuevo post para contestar una pregunta que recibí en la semana. Notarán el post sobre tips para rendimiento donde la moraleja resumida es que preocuparse por la velocidad de PouchDB contra Sqlite es detenerse en el arbol en lugar de apreciar todo el bosque.

La primera pregunta que recibí fue sobre como hacer para recuperar una lista de objectos y luego recuperar los detalles de un objeto en particular. Como en cualquier correo, tienen la lista de correos y cuando hacen click en uno en particular, verán el contenido en otra ventana. Este mecanismo es la perfecta introducción al concepto de estados que Ionic soporta mediante la inclusión de UI-Router.

Primero hagamos un poco de orden en el código que dejamos en la parte 2 de esta serie. Para los que recién llegan, no hay un download del código, así que vayan a la Parte 1 para que hagan el código paso a paso con el post.

Como podrán ver, tenemos el código todo regado en el archivo app.js lo cual esta mal. Lo que yo suelo hacer es tener un archivo para los controladores (controller), otro para los servicios (service, factory) y dejamos el app.js lo más limpio posible. Esperen!!, services y factories es algo que no había mencionado hasta ahora. Así que toca un poco de introducción.

Los que venimos del mundo de cliente/servidor y objetos y todo eso recordaremos que solíamos tener un super objeto casi global para ciertas tareas que estaba corriendo todo el tiempo esperando que le pidamos cosas. Si es que necesitábamos mantener la consistencia de sus operaciones, entonces declarábamos este objeto como un Singleton y listo, estabamos seguros que todas las operaciones las podíamos verificar antes de mandarlas. Pues eso en el mundo de AngularJS lo encontramos en las estructuras Service y Factory. ¿Cuál usar? Personalmente yo utilizo Factory la mayor parte del tiempo, pero Service también debería hacer el trabajo. La diferencia principal es que Factory es un Singleton y Service no, nada mas.

Comencemos por el mas fácil, pongamos todos los controladores en un módulo:

angular.module('controllers',[])
.controller('newsController',function($scope){
	$scope.notas=[];
	$scope.$on('refrescar',function(event,news){
		$scope.$apply(function(){
			$scope.notas = news;
		})
	})
})

Llamemos a este archivo controllers.js y pondremos aquí todos los demás controladores que nos hagan falta.

Ahora, para que el cambio sea completo, hay que cambiar tanto el app.js y el index.html.

angular.module('starter', ['ionic','controllers'])

Esta es la única línea que cambia del archivo app.js. Simplemente le estamos diciendo a AngularJS que el módulo App.js llamado ‘starter’, tiene una dependencia adicional llamada ‘controllers’.

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 <title></title>
 <!-- compiled css output -->
 <link href="css/ionic.app.css" rel="stylesheet">
 <!-- ionic/angularjs js -->
 <script src="lib/ionic/js/ionic.bundle.js"></script>
 <!-- cordova script (this will be a 404 during development) -->
 <script src="cordova.js"></script>
 <!-- your app's js -->
 <script src="js/controllers.js"></script>
 <script src="js/app.js"></script>
 <script src="lib/pouchdb/dist/pouchdb.js"></script>
 </head>

En el caso de index.html, hemos agregado la línea 14 donde le indicamos que tiene que cargar el archivo controllers.js con nuestros controladores. Y listo, ya tenemos algo mas de orden.

Ahora nos toca ordenar la base de datos. Lo ideal sería tener un objeto que esté siempre disponible gestionando las llamadas a la base de datos, por lo que aplica a ser un Service o un Factory. Prefiero un factory como ya les había dicho así que esto quedaría en un módulo de la siguiente manera.

angular.module('services',[])
.factory('db',function($rootScope){
    var key = 'bentareadyessharyinessee';
	var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
	var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
	var db = new PouchDB('news');

    return {
        init: function(){
            db.replicate.from(remote,{live:true,retry:true})
                .on('paused',function(info){
                db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
                    .then(function(result){
                    $rootScope.$broadcast('refrescar',result.rows);
                });
            });
        }
    }
})

Llamaremos services.js a este archivo y como podrán ver es un Factory que publica solamente un método llamado init. Este método tenemos que invocar para que se inicie el manejo de la base de datos. Si queremos simplificar nuestro archivo de controladores quedará así:

angular.module('controllers',['services'])
.controller('newsController',function($scope,db){
 db.init();
 $scope.notas=[];
 $scope.$on('refrescar',function(event,news){
 $scope.$apply(function(){
 $scope.notas = news;
 })
 })
})

Notarán que en la línea 1 se ha agregado el módulo services como dependencia y en la línea 3 estamos llamando al método init del Factory db.

Y el app.js quedará así:

angular.module('starter', ['ionic','controllers'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})

Comparado con el desorden que teníamos al principio ahora todo se ve más ordenado. Pero lo mas importante es que podemos centrarnos en escribir servicios o factories de forma general y reutilizarlos en otros proyectos simplemente copiando los archivos, lo mismo con controladores.

Una sugerencia final es que según estuve leyendo, esto de separar el código en controladores y servicios es bueno, pero no sirve si nuestro proyecto crece demasiado. La solución es la de hacer la separación por funciones de nuestro app y dentro de cada función poner los controladores y servicios separados en archivos. Esto lo veremos mejor cuando incluyamos el concepto de estados.

Continuaremos en otro Post porque el tema de estados es muy importante entender el concepto antes de programar.

NOTA: Soy muy flojo programando, así que hacer todo esto paso a paso lo hago para combatir este defecto. Así que para notar que este esfuerzo vale la pena, les pido que sigan el tutorial desde la parte 1, si les paso el código para que lo copien simplemente no vale ni mi esfuerzo ni ustedes tampoco van a entender.


Ya publiqué hace unas semanas mi primera aplicación multiplataforma y con el tiempo y las pruebas que vengo realizando, confirmo cada día que esta combinación es realmente de mucho potencial, por su facilidad y su potencia. Ahora creo que es tiempo de hacer un ejemplo “End to End” que muestre el proceso desde la teoría hasta la ejecución de un app. Así que nada de código para que te bajes, sigue paso a paso y tendrás al final de este post un app en javascript que sincroniza con una base remota y que de yapa puede funcionar en cualquier móvil. “Fasten your seatbelt” y nos vemos al final del Post.

Empecemos definiendo el problema: Tienes tu smartphone super ultra plus y quieres abrir algún App de los miles que tienes instalado y te das cuenta de que se te acabó el saldo y no estas ni siquiera cerca de un Starbucks, o estas en una zona donde la cobertura es recontra mala. Lo que normalmente sucederá es que el App te dirá que no hay red y que no se puede usar. Este escenario parece trivial pero para algunas aplicaciones, es un escenario que tiene que evitarse a toda costa. Llevando el tema más allá, creo que todas las aplicaciones deberían considerar que en algún momento la red se puede caer y deben poder seguir funcionando, al menos en un conjunto básico de funciones. Desde el punto de vista de programación estamos hablando de aplicaciones que soporten trabajar “Off-Line”.

Para hacer todo esto posible hay miles de recetas disponibles y la que presento ahora combina simpleza y potencia. Para ver como debemos comenzar veamos el gráfico:

gen01

Si vamos componente por componente:

  • Servidor: Cloudant es parte de la propuesta Bluemix de la plataforma de servicios cloud de IBM. Está basado en Apache CouchDB con algunas mejoras en el campo de autenticación, manejo de información geográfica y búsquedas. Digamos que es la implementación mas segura y confiable de CouchDB con la gran ventaja de que podemos acceder al nivel gratis del servicio muy fácilmente. De hecho, si generas un tráfico por un total de 50 USD o menos, no pagas y gracias al esquema que les voy a explicar, eso es un montón.
  • Cliente: Ionic Framework es una herramienta para el desarrollo de aplicaciones basadas en HTML 5 con javascript. Está basado en Apache Cordova y permite desarrollar para Android, iOS, Windows Phone 8, Windows 8, y otras mas aunque por ahora solamente Android y iOS son oficialmente soportados. Lo mas importante puede ser que Ionic está basado en AngularJS que es un framework para el desarrollo de aplicaciones JavaScript que facilita el desarrollo de aplicaciones basados en el modelo MVC.
  • Base de datos cliente: PouchDB es un proyecto que trata de implementar CouchDB en javascript. Debido a que el soporte de formatos de base de datos en los móviles es muy variado, PouchDB funciona además como una capa de abstracción de almacenamiento aunque su mayor fortaleza es en la implementación del protocolo de sincronización.

Hay algo que no he dicho aún y es que para los que estamos acostumbrados al mundo SQL significa días difíciles. Personalmente, me parece que el cambio ya se dió al enterarme que el estándar WebSQL, (SQLite para los amigos) ya no es el estándar en muchas de las plataformas móviles. Esto significa que tenemos que empezar a lidiar con NoSQL.

NoSQL significa que ya no hay esquemas o “tablas”, simplemente hay documentos que lo único seguro es que tienen un id único ( aunque por ahí ya vi un caso donde incluso esto no es cierto ). No se engañen, No Tablas no significa No Diseño, al contrario, el diseño tiene que ser mayor con la ventaja de que no hay esquemas que ir impactando. De nuestro diagrama, Cloudant y PouchDB son NoSQL y serán a partir de ahora la columna vertebral de nuestras aplicaciones móviles.

Bien, creo que las explicaciones ya están, ahora lo que queremos ver es una aplicación que nos muestre lo fácil que es entrar al mundo de móviles. Preparemos entonces el entorno.

Organizando las herramientas

Empecemos instalando las herramientas. Primero se debe instalar NodeJS. Es una serie de librerías Javascript que son necesarias para Ionic. En el link podrán encontrar la versión para los sistemas operativos más populares.

Con el NodeJS listo, vamos a instalar ahora Ionic. En una ventana de consola:

c:\>npm install -g ionic

No es requisito pero también deberían instalar Cordova:

c:\>npm install -g cordova

Y listo, Ionic ya está y lo pueden comprobar así:

c:\>ionic info

Ahora, hay que instalar PouchDB, para lo cual necesitamos instalar Bower que es una herramienta para instalar librerías JavaScript. Para eso utilizamos el siguiente comando:

c:\>npm install -g bower

Ahora lo que tenemos que hacer es crear un proyecto Ionic y agregarle las librerías de PouchDB. Para crear un proyecto Ionic, procedemos con el siguiente comando:

c:\>ionic start superdatos

Mediante este comando estamos creando el proyecto llamado “superdatos”. Se ha creado una carpeta con todos los archivos necesarios. Una vez que el comando haya terminado debemos hacer la configuración de SASS que es algo super potente que explicaremos después, por ahora simplemente ejecuten el comando siguiente:

c:\>cd superdatos
c:\superdatos\> ionic setup sass

Para los que entienden CSS, SASS es un CSS con parámetros. Yo todavía no lo entiendo bien, pero lo poco que he hecho es suficiente para darme cuenta de su importancia, así que ahi está. Ahora instalemos PouchDB:

c:\superdatos\>bower install pouchdb

Y listo. Si quieren ver lo que han hecho hasta ahora entonces ejecuten el siguiente comando:

c:\superdatos>ionic serve

Ahora abran un navegador, ingresen el url http://localhost:8100 y listo:

gen07

Así es, sin darse cuenta han hecho una aplicación con una barra de título. Casí nada, pero al menos ya hemos comenzado

Organización de la aplicación

Ahora revisemos todo lo se que ha hecho. Veamos los archivos:

En la carpeta raíz no hay mucho que ver:

gen05

Nos enfocaremos inicialmente en la carpeta www

gen06

  • index.html: es el punto de entrada de nuestra aplicación y donde registramos todas las librerías que queremos usar
  • Carpeta js: es donde colocamos todos los archivos javascript. El principal archivo javascript es app.js que es donde configuramos nuestra aplicación.
  • Carpeta lib: Si ingresan es donde están grabadas todas las librerías que tenemos disponibles para la aplicación. Ionic y PouchDB estarán ahí, además de otras propias de Angular
  • Carpeta img: obvio, las imágenes van aquí.
  • Carpeta css: y aquí van los archivos CSS

Y listo. Ahora nos toca entender por donde comenzar, para lo cual debemos entender el concepto de MVC o Model – View – Controller.

MVC-web-application-development

Según esto, una aplicación se divide en estos tres elementos: modelo, vista y controlador, donde vista es toda la parte gráfica de la aplicación y como el usuario interactúa con ella. El controlador es quien maneja la lógica detrás de las pantallas, cuál es la lógica que se lanza al hacer click en un botón. El modelo es como la lógica del negocio, que también controla lo que grabamos en una base de datos, si al hacer click en un botón que dice grabar, el controlador le tendrá que decir al modelo que tiene grabar una entrada de producto, seguidamente, la vista se refrescará para mostrar todas las entradas de producto. Exacto, el producto es el modelo.

Ya dijimos que Ionic se basa en AngularJS donde es muy fácil identificar al modelo, la vista y el controlador, de hecho sería así:

  • Vista : carpeta templates, archivos HTML o simplemente index.html
  • Controlador: carpeta js, archivos js. Entidad controller en AngularJS
  • Modelo: carpeta js, archivos js. Entidad factory en AngularJS

Es mas simple de lo que parece. Y para eso revisemos nuestra simple aplicación tal cual está ahora. Primero veamos el archivo index.html que tiene definida la vista:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title></title>
<!-- compiled css output -->
<link href="css/ionic.app.css" rel="stylesheet">
<!-- ionic/angularjs js -->
<script src="lib/ionic/js/ionic.bundle.js"></script>
<!-- cordova script (this will be a 404 during development) -->
<script src="cordova.js"></script>
<!-- your app's js -->
<script src="js/app.js"></script>
</head>
<body ng-app="starter">
<ion-pane>
<ion-header-bar class="bar-stable">


<h1 class="title">Ionic Blank Starter</h1>


</ion-header-bar>
<ion-content>
</ion-content>
</ion-pane>
</body>
</html>

Las líneas que nos interesan por ahora son la 14 y la 16. Primero en la 14 veremos

<script src="js/app.js"></script>

Esta línea le dice a la aplicación que la lógica que necesita está en el archivo app.js que está escrito en javascript como podrán sospechar. Cualquier otra librería tiene que ser declarada aquí para poder ser utilizada. Recuerden esto pues tenemos que regresar aquí mismo para ver que hacemos con PouchDB.

<body ng-app="starter">

Y en la línea 16 le decimos que la aplicación que estamos haciendo es ng-app osea Angular con nombre “starter”.

Ahora revisemos el famoso app.js:

// Ionic Starter App
// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})

En la línea 5 está definido el módulo ‘starter’ que es el que encontramos en el archivo index.html. Cuando index.html quiera saber que hacer, vendrá a buscar la lógica a este archivo app.js.

Luego en la linea 7 vemos algo importante. Para los que ya tienen experiencia con aplicaciones en Cordova, entenderán la importancia del evento CordovaReady. Para ponerlo simple, Ionic es Cordova en esteroides, así que también hay que esperar por el evento CordovaReady, pero como Ionic habla el dialecto AngularJS entonces tenemos que usar el código tal cual está aquí.

Integrando PouchDB

Pues bien, ya estamos listos para integrar nuestra base de datos así que debemos preguntarnos ¿qué queremos hacer? Pongamos las cosas en el nivel mas simple posible. Digamos que queremos hacer un lector de noticias donde las noticias mas nuevas se muestren al comienzo. Nada complicado. Con esto en la mente, comencemos agregando activando PouchDB en nuestro proyecto, de la siguiente manera. Nuestro archivo index.html debería ser así ahora:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title></title>
    <!-- compiled css output -->
    <link href="css/ionic.app.css" rel="stylesheet">
    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>
    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>
    <!-- your app's js -->
    <script src="js/app.js"></script>
    <script src="lib/pouchdb/dist/pouchdb.js"></script>
  </head>
  <body ng-app="starter">
    <ion-pane>
      <ion-header-bar class="bar-stable">


<h1 class="title">Ionic Blank Starter</h1>


      </ion-header-bar>
      <ion-content>
      </ion-content>
    </ion-pane>
  </body>
</html>

Hemos agregado una línea indicando donde está el archivo con la librería PouchDB. Si se fijan bien, dentro de nuestra carpeta lib está la carpeta con toda la info requerida por PouchDB, aquí simplemente hemos apuntado al archivo de distribución.

NOTA: es una buena idea que tengan una consola abierta solamente para que el comando Ionic serve esté corriendo todo el tiempo ya que Ionic permite que todo lo que hacemos se compile en tiempo real.

En Google Chrome veremos que nuestra app no ha cambiado mucho. Nos toca entonces, crear la base de datos. Para eso, agregamos la siguiente línea en nuestro app.js:

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
    var db = new PouchDB('news');
  });
})

En la línea 18, creamos la variable que tendrá nuestra base de datos local y le ponemos de nombre ‘news’. Al grabar los cambios, notarán que el navegador se refresca solo y volvemos a ver nuestra app. Parece que todo sigue igual pero no. Para desengañarnos, tenemos que abrir las herramientas (Ctrl + Mayus + I) y vamos a la sección Resources y veremos:

gen08

En recursos, veremos que se ha agregado una base de datos con el nombre _pouch_news y no es coincidencia. Es nuestra base de datos ‘news’ creada por PouchDB en el formato IndexedDB. Todo bien hasta ahora. Ahora veamos como nos conectamos a IBM Bluemix para acceder a Cloudant.

Jugando con Cloudant

En el mundo CouchDB, encontré muchas alternativas “cuasi” gratuitas para comenzar a trabajar pero es lamentable como la falta de visión a largo plazo hace que la confiabilidad no sea algo característico. Todo esto se terminó con Cloudant, ya que IBM es quien está detrás y eso es ya bastante. ¿Porqué no instalar mi propia instancia de CouchDB en mi PC? Simple, Cloudant es gratis hasta 50 USD de tráfico, es super rápido, confiable y otros adjetivos todos muy buenos. Además, estamos en la onda de utilizar herramientas Cloud así que va a tono.

Como siempre, primero hay que crear una cuenta en https://cloudant.com/sign-up/ y ya podrán acceder a esta pantalla:

gen09

Para los fans de CouchDB, esta es una versión algo mejorada de Fauxton con las mejoras que les comenté al inicio. No perdamos mas tiempo y creemos nuestra primera base de datos. Un click en Add new Database, ponemos el nombre ‘news’ por facilidad y listo, veremos esta pantalla ahora:

gen10

Ahora es el momento de reflexionar sobre la estructura de datos que necesitamos. Una noticia tiene un titular, un resumen y el cuerpo, además, tiene una fecha y un autor. Para comenzar, creo que es suficiente, así que agregaremos nuestro primer documento. Hacemos click en el signo mas junto a All Documents  y seleccionamos nuevo documento. Y luego empecemos a crear nuestro documento en formato JSON:

gen12

Simple, ¿Queremos mas campos? no hay problema, le agregas nomas al JSON y listo. ¿Y los índices, llaves primarias, foráneas y otros? Pues no hay, al menos no aquí. Lo único que hay que respetar aquí es que el campo “_id” es obligatorio y debe ser único. ¿Y la clave no es autoincrement? no necesariamente, podemos dejar que se autogenere pero nos perdemos de mucho si hacemos eso. Si se dan cuenta la _id que estoy usando es “news_20150507_vpease_001” que sospechosamente parece ser hecha a propósito y la verdad es que si. Es altamente recomendable utilizar una convención para la creación de claves. En este caso:

  • “news” : indica el tipo de documento. Recuerden que no hay esquemas así que mejor si identificamos así a nuestras entidades.
  • “20150507” : así es, se refiere a la fecha, pues ya dijimos que queremos mostrar las últimas noticias primero.
  • “vpease” : correcto otra vez, es el autor de la noticia.
  • “001” : una pequeña cadena al final para asegurar que el id sea único.

¿Porque todo este lio con la clave? Si bien es cierto que tenemos un campo “tipo” que nos permite diferenciar las entidades entre todos los documentos de la base, CouchDB tiene un método super rápido llamado AllDocs que sólo puede ser ordenado por el campo “_id”. Podemos crear nuevos indices pero ocupan espacio adicional, así que para ahorrarnos algo de espacio y ganar algo de velocidad, NO DEBEMOS AUTOGENERAR LA CLAVE, SIEMPRE DEBEMOS COMPONER LA CLAVE CON CAMPOS DE ORDENACION.

Y finalmente, grabamos y listo.

gen13

Notarán que se ha agregado un campo “_rev”, esto es un mecanismo interno de CouchDB para controlar los cambios y hacer lo que mejor hace: sincronizar.

Podríamos agregar mas noticias, pero por ahora es suficiente. Finalmente tenemos que crear algún tipo de identidad o usuario para poder acceder a los datos. Esta es una de las mejoras de Cloudant, todas las llamadas deben ser autenticadas y el HTTPS viene de gratis. En Cloudant, volvamos a la lista de base de datos:

gen14

Entremos a la sección de permisos ahora:

gen15

Hay dos maneras de dar acceso:

  • Le damos permiso a todos a entrar a los datos
  • Crear un API key/clave

Hay una tercera opción que es la de usar nuestras credenciales administrativas para dar acceso, lo cual es a todas luces incorrecto, como también es incorrecta la opción 1, así que API Keys es la voz. También se puede compartir la base a otro usuario de Cloudant pero eso no es masivo, así que nos quedamos con las API keys. En simple, un API key es un usuario temporal que solamente sirve para acceder a los datos y le podemos asignar permisos. Para mantener todo simple, vamos a darle a esta API key los privilegios de Replicator y Reader.

gen16

Y listo, apunten bien esta combinación de Key y password porque ya no puede volver a generarse. Podrán generar una nueva Key pero ya no podrán recuperar la clave. Fijense que ya le asignamos los privilegios de Reader y Replicator. Ahora, toca finalizar nuestro app cliente.

NOTA CRITICA: Antes de salir hay que activar CORS, así que denle click en Account – CORS y activen All Domains. ¿Qué es CORS? es una protección de los navegadores para no ejecutar código remoto. Para nuestro caso, CORS debe estar siempre activado. La pantalla debe lucir así:

gen18

Agregando la sincronización 

Para esto, debemos entender un tema previamente. La sincronización tiene muchas variantes pero por ahora sólo nos va a importar tres cosas: Dirección, Frecuencia y Eventos.

La dirección se refiere al sentido que tendrán los datos, del Servidor al móvil o del Móvil al Servidor. Dado que solamente vamos a leer, consideraremos la sincronía de Servidor al Móvil.

La frecuencia se refiere a que tan seguido queremos actualizar los cambios. Convenientemente, PouchDB tiene una opción “live” que tratará  de actualizar los datos lo más rápido posible, suena bien pero es mas costosa del lado de Cloudant, así que debemos usarla con mucho cuidado. Igual siempre podemos sincronizar manualmente cuando lo creamos conveniente.

Finalmente, los eventos nos indican como va el proceso. En resumen:

  • change (info) – Generado cada vez que se detecta un nuevo documento escrito.
  • complete (info) – Se genera cuando una sincronización manual termina o cuando se interrumpe una sincronización live.
  • paused (err) – cuando una sincronización live ha terminado y espera por nuevos cambios o cuando se ha interrumpido por algún err.
  • active – cuando una sincronización comienza o cuando se recupera de algún error.
  • denied (err) – cuando las credenciales son rechazadas por el servidor.
  • error (err) – cuando se genera un error irrecuperable como cuando se corta la red.
  • uptodate (deprecated) – Este evento ya esta de salida así que no lo usen

Ahora, como por arte de magia, agregaremos las líneas para activar la sincronización. La magía será en el archivo app.js.

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
	var key = 'bentareadyessharyinessee';
	var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
	var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
	var db = new PouchDB('news');
	db.replicate.from(remote,{live:true,retry:true});	
  });
})

Veamos, simplemente hemos agregado unas variables con el API Key y la clave de acceso y el url de la base de datos en Cloudant. Verán que estoy agregando las credenciales al url, sería algo asi: https://user:pass@servidor/basededatos.

En la línea 22 está la línea que hace la verdadera llamada a la sincronización donde hemos indicado un par de parámetros: live y retry que como su nombre lo indica, activarán la replicación “live” y en el caso de algún error, la replicación se intentará nuevamente.

Veamos como se ve la app hasta ahora en Chrome, primero en la consola:

gen17

Si vemos líneas con XHR significa que ya estamos sincronizando. Y para que me crean vamos al tab Resources:

gen20

Y ahí vemos nuestro documento!! Es cierto!

Guardemos ánimo para hacer lo que de veras hace falta que es presentar los datos.

Finalmente, Presentando los datos

Antes de presentar los datos debemos tomar en cuenta los eventos pues recuerden que la base de datos en el móvil comienza vacía y luego irán llegando las noticias debemos poder refrescar la vista. Los eventos ya los vimos así que recordemos que estamos haciendo una replicación “Live” y cuando acabe se generará un evento “paused”, también podríamos usar “change” pero este evento sólo funcionaría para los nuevos cambios. Si por alguna razón, la base remota y la local están iguales, el evento “change” no se generaría, mientras que “paused” si. Primero veamos como va la cosa con “paused”. Para eso agregamos las siguientes líneas a app.js justo después del comando de replicación:

.on('paused',function(info){
		db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
		.then(function(result){
			$rootScope.$broadcast('refrescar',result.rows);			
		});		
	})

Veamos línea por línea:

  • Línea 23: agregamos un manejador para el evento “paused”. Fijense que el comando db.replicate no tiene el “;” al final, por lo que “.on” es una propiedad del comando “db.replicate”. Luego de la “,” tenemos la función que se ejecutará cuando ocurra el evento.
  • Línea 24: aquí viene el primer choque para los que venimos del mundo SQL. El comando “db.allDocs” es el comando más potente de PouchDB y de hecho de CouchDB también y permite recuperar documentos mediante el índice principal que si se habrán dado cuenta, incluye a todos los documentos en la base de datos. Inicialmente, allDocs recupera todos los documentos, pero toma parámetros para limitar un poco eso. Por ejemplo, aquí le hemos pasado los dos primeros “startkey” y “endkey” que se refieren a algo así como un like y como notarán la cadena que pasamos es “news_” y agregándole el caracter “\uffff” es como ponerle el asterisco, por lo que los dos primeros parámetros significan que debemos traer todos los documentos cuya clave comience con “news_” que gracias a la estructura de claves que fijamos es equivalente a decir que traiga todas las noticias en la base de datos. Seguidamente verán que hemos fijado “descending” a “true” y eso es por la fecha, si recuerdan la estructura de nuestra clave que era “news_fecha_autor_XXX” así que decirle que sea descendente significa que las noticias más recientes estarán al principio, lindo truco. Finalmente, con “include_docs” le decimos que nos traiga todos los datos en el documento, de lo contrario solamente tendremos la clave.
  • Línea 25: Aquí vemos una de las mayores características de aplicaciones NodeJS, Promesas. Dado que db.allDocs se ejecuta de forma asíncrona, no sabemos en que momento ejecutará la siguiente línea, así que con “.then” le decimos que al terminar, ejecute la función incluida.
  • Línea 26: La función que pasamos como promesa ejecuta una sola línea y esta es: “$rootScope.$broadcast” que en simple significa que se enviará un mensaje a todos los contextos indicando que un evento se ha realizado. El evento se llamará “refrescar” y tendrá como parámetros todos los “rows” incluidos en el objeto result, o más simple, todos los documentos recuperados desde la base de datos.

Y listo, hasta ahora hemos trabajado en lo que puede decirse que es el “Controller” de toda la aplicación y como no está asociada a ninguna vista, no podremos crear ninguna lógica que presente los datos. Entonces ahora tenemos que ligar la vista que tenemos ( osea “index.html”) con algún controller.

Primero en la vista “index.html” declaramos el controlador asi:

<body ng-app="starter">
    <ion-pane ng-controller="newsController">
      <ion-header-bar class="bar-stable">


<h1 class="title">Ionic Blank Starter</h1>


      </ion-header-bar>
      <ion-content>
      </ion-content>
    </ion-pane>
  </body>

En la línea 18 verán que hemos agregado la directiva “ng-controller” donde le indicamos a la vista que el controlador es “newsController”. Ahora definamos el controlador en el archivo “app.js”. Pongamos todo el código en el archivo pues los “;” y “.” se vuelven muy importantes:

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
	var key = 'bentareadyessharyinessee';
	var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
	var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
	var db = new PouchDB('news');
	db.replicate.from(remote,{live:true,retry:true})
	.on('paused',function(info){
		db.allDocs({startkey:'news_\uffff',endkey:'news_',descending: true,include_docs:true})
		.then(function(result){
			$rootScope.$broadcast('refrescar',result.rows);			
		});		
	})
  });
})
.controller('newsController',function($scope){
        $scope.notas='';
	$scope.$on('refrescar',function(event,news){
		$scope.$apply(function(){
                      $scope.notas = news;
                })
	})
})

Primero veamos todo el archivo en panorama y notaremos que el módulo ‘starter’ tiene propiedades “.run” y “.controller” que hemos definido. Esto se puede notar porque no hay “;” que los separe justo en la línea anterior. Ahora revisemos las líneas nuevas:

  • Línea 31: definimos el controlador ‘newsController’ y la funcion asociada que utiliza la entidad $scope. $scope es una entidad AngujarJS que identifica al ámbito definido en la vista que para nuestro caso es ‘index.html’ pero solamente en la sección donde está “ng-controller” definida. Para ponerla simple, nos permite declarar variables y funciones que serán visibles dentro de la vista.
  • Línea 32: declaramos una variable en el entorno llamada “notas”
  • Línea 33: Si la replicación envía eventos “paused”, pues es necesario que los recibamos en alguna parte así que aquí definimos que para cuando se reciba el evento ‘refrescar’, ejecutemos la función anexa la que toma dos parámetros: el evento y el objeto asociado donde están nuestros documentos.
  • Línea 34: En angular, si cambiamos el modelo tenemos que “aplicar” el cambio en el $scope
  • Línea 35: Finalmente, asignamos el array de documentos a una variable de entorno llamada “notas”

En la línea 34, sin notarlo, también estaremos usando una de las grandes potencias de AngularJS: “two-way data binding” que es el refresco automático de la vista cuando cambien las variables del entorno. Créanme que es mas fácil verlo que explicarlo.

Veamos, ya el controlador tiene los datos, así que vayamos a la vista a mostrar los datos. Para esto agregamos el siguiente código a “index.html”:

<ion-content>


 Hay {{notas.length}} noticias



<div class="list" ng-repeat="noticia in notas">


<div class="card">


<div class="item item-divider">{{noticia.doc.fecha}}</br>{{noticia.doc.titular}}</div>




<div class="item item-text-wrap"><b>{{noticia.doc.resumen}}</b></div>


			


<div class="item item-divider">Autor: {{noticia.doc.autor}}</div>


		</div>


	  </div>


      </ion-content>

Otra vez el línea por línea:

  • Línea 23: mostramos el tamaño de la variable notas y como es un Array de documentos acepta la propiedad length
  • Línea 24: Hacemos una lista para que se vean las noticias algo ordenadas mediante la clase CSS “list” y dejamos abierto el tag para agregarle otra directiva
  • Línea 25: al <div> de la lista le agregamos una directiva Angular para iterar en los elementos del array “notas” y a cada elemento lo conoceremos como “noticia”
  • Línea 26 a la 30: Cada elemento del array trae mucha información desde Cloudant, pero nos interesa uno en particular que es la propiedad “doc” que tiene todos los campos que hemos definido, así que lo incluimos. Además, incluimos el nombre de cada campo del documento noticia.

Graben y listo. Ya tienen su primera aplicación en Ionic con integración de base de datos local PouchDB y remota en Cloudant!!. Para ver más allá de lo evidente, pueden hacer la prueba agregando otro documento en Cloudant para que vean la potencia de la sincronización con PouchDB. Ya saben como agregar documentos a Cloudant así que tomen su reloj y tomen el tiempo que toma mostrarse la nueva noticia. Recuerden usar el formato para el campo “_id”: “news_fecha_autor_XXX”. En mi caso, el refresco fue casi instantáneo.

Y con esto podemos dar por terminado este largo Post con la satisfacción de tener una app completamente funcional. Para los que recién comienzan, los tags que vienen del mundo Ionic pueden confundir (<ion-pane>,<ion-content>, etc) pero no hay nada como leerse la documentación en la página de Ionic Framework pues está con ejemplos y todo. Para el caso de AngularJS, el peligro es que en javascript hay muchas maneras de hacer lo mismo y ya les adelanto que lo que les he explicado aquí es una forma muy simple de hacer las cosas. Una aplicación real hace la separación de la lógica en varios módulos, mientras que nosotros hemos usado uno solo para todo. Igual con las vistas, se suele tener muchas y no una sola. En lo que se refiere a Cloudant, hemos utilizado la instancia remota solamente cuando se sincroniza y luego la consulta a los datos ya es hecha localmente al PouchDB por lo que nuestro consumo será muy bajo. Además, si bien la sincronización “live” es muy potente, en una app real verán que a veces es mejor hacer una sincronización manual, sobre todo si sabemos que los cambios no se generan a cada rato, lo que nos permitirá ahorrar aún mas en Cloudant. Aún queda mas que aprovechar de Cloudant como las vistas (el famoso map/reduce de CouchDB), como manejar el almacenamiento, Sincronización filtrada  y algunas otras optimizaciones, todo con la seguridad de que sus datos están seguros y disponibles.

A ver si preparo el siguiente Post sobre vistas en Cloudant para poder combinar mas tipos de dato en el App y como generar sus apps para Android o iOS gracias a Ionic. Por ahora, jueguen con el app y modifíquenlo para que se vayan acostumbrando.

Mas información:

AngularJS: http://angularjs.org

Ionic Framework: http://ionicframework.com

PouchDB: http://pouchdb.com

IBM Cloudant: http://cloudant.com





%d bloggers like this: