Posts Tagged ‘Amazon’


En las noticias se puede leer hoy que un Operador ofrece “Internet gratis” usando el famoso “Internet.org” de Facebook y los comentarios no son nada halagadores, comenzando con “engaño” hasta amenaza hasta “atentado” a la libertad de Internet.

La cruda realidad es lo siguiente:

  • Internet.org es una opción comercial creada por Facebook para capturar aquellos mercados donde el acceso a Internet aún no es masivo con la intención de “capturar” usuarios nuevos. Teniendo en cuenta que estos usuarios tendrían una experiencia limitada, Facebook se volvería el “internet” para los pobres.
  • Internet.org suena bonito para el operador por que se pueden utilizar dos palabras: “gratis” y “facebook”. Esto no puede sonar mal, más aún cuando hay otras páginas disponibles además de facebook.
  • Internet.org se basa en que Facebook te da acceso, así que ellos podrán ver todo lo que haces y hacer con eso lo que quieran.
  • Hay otro rollo más: Los que no pueden pagar un plan de datos, o los que tienen un plan prepago suelen apagar el servicio de datos para evitar el consumo de saldo. Los Operadores no se benefician de esto así que empezaron a ofrecer Whatsapp gratis y ahora también Internet.org, porque de esa manera ya no apagamos el servicio de datos por lo que queda abierta la posibilidad de que consumamos el servicio de datos ya sea a propósito o por casualidad.

Ninguna de estas cosas me parece mal, al final las empresas tratan de ganarse el pan. El problema está en el usuario: TIENE QUE ESTAR 100% SEGURO DE LO QUE ESTÁ COMPRANDO. Esto quiere decir que la Operadora tiene la obligación de explicar lo que vende y el consumidor también se debe informar por su lado. Ambos por igual.

Es cierto que para muchos tener full Internet es algo normal y hasta necesario, pero no todos tenemos la misma suerte. Si tuviera que elegir, no iría por Internet.org ni loco, pero ese es mi caso en particular y la razón no es que crea que Facebook es diabólico o que me parezca una estafa, simplemente que no me gusta que vean mi tráfico ni que me impongan a que lugares puedo entrar.

¿El acceso a Internet es un lujo? Si, suena feo pero lo es. La razón para esto es que Internet no es una librería, es más como un centro comercial donde puedes encontrar buenas cosas gratis o no. Hay buenos libros como los hay también malos y recontra malos y lo más doloroso es que es la mayoría. Es cierto que puedes entrar a cosas “pseudo educativas” como Wikipedia (que no sigue un método transparente para la creación de sus contenidos), Wattpad (donde hay libros gratis, pero no están curados por edades) y otros contenidos interesantes, pero no hay garantía de que encuentren algo que sea realmente tan confiable como lo que se puede encontrar en un libro impreso.

Cierto que estar en Internet es cool, incluso si ese “Internet” fuera solamente Facebook, pero de ahí a que se vuelva un “servicio público” lo dudo mucho, incluso si hablamos de un servicio tan básico como el correo electrónico.

Hace algún tiempo surgió el proyecto “Huascarán” que ofrecía Internet en colegios con las famosas laptops XO de la fundación “one laptop per child” de Mr Negroponte. Si bien la intención era buena, se trató de una iniciativa que olvidaba la capacitación de los profesores, tal vez en la tecnología y en los métodos que venían con él (entiendo que se capacitaba a los profesores en el método constructivista de la educación), pero no en los temas básicos de fondo que al final son los que más importan.

Si los libros son tan buenos, hay forma que hacerlos llegar de forma eficiente, controlada y con menores costos, y no hace falta que me ponga a decir cuales. El punto es que la tecnología no puede estar por encima del objetivo principal.

En fin, la conclusión es que si puedes vivir sin Internet, o con un Internet recortado y si tienes suerte podrías tener un Internet Full, pero eso no te hace ni mejor ni peor persona y tampoco quiere decir que tus oportunidades en la sociedad se reduzcan. En el peor de los casos, siempre es posible conseguir Internet de algún lado a un precio módico.

Tener Internet full y libre es lo ideal, pero si no te alcanza la plata aún tienes chances, Internet.org es una opción entre muchas pero debes saber realmente que estás comprando un servicio recortado y aceptarlo de tal forma, y si no te gusta un Internet limitado, no te preocupes, aún se puede vivir sin Internet.

Advertisements

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.

En este post vamos a incluir algunos puntos propios de la plataforma Cordova que no han sido tan obvios hasta ahora y que deben tenerse en cuenta para aplicaciones reales, pero antes hay que planificar.

Planificación:

Quedó pendiente en el post anterior la implementación de un método para ver cuáles noticias eran las más leídas. Algo tan simple nos plantea los siguientes retos:

  1. Al leer una noticia se tendría que agregar un documento indicando que un usuario determinado la ha leído.
  2. El documento agregado debe ser sincronizado solamente con el servidor. No debe ser sincronizada con los demás usuarios
  3. En el servidor debe existir un mecanismo que concentre todos los documentos de lectura y haga la suma para poder ordenarlos y así presentarlos en el móvil.

El punto 1 requiere de insertar documentos en la base de datos. Para eso recordemos que no hay un esquema así que tendríamos que definir uno que tenga al menos la siguiente información: usuario, noticia leída, la fecha. Digamos que algo así:

{
_id:’id del documento’,
newsId: ‘id de la noticia leída’,
fecha: ‘fecha cuando fue leída la noticia’,
tipo: ‘read’,
usuario: ‘usuario’
}

Por ahora este documento cumple con lo que nos hace falta. Y para grabarlo a la base de datos el comando es super simple “db.put(doc)”, ya veremos como usarlo.

Para el punto 2 debemos considerar que al sincronizar, debe haber alguna información en el documento para definir si hace falta ser sincronizado o no. Además, también debemos considerar que hay información que tiene que ir a todos los usuarios. Entonces podríamos usar el campo “usuario” de la siguiente manera:

  • Consideraremos que la información común a todos, será asignada al usuario “all”
  • La información particular a un usuario, será asignada al usuario actual

De esta manera, podríamos definir una regla para que se sincronicen todos los registros donde el campo usuario sea igual a “all” o al “usuario actual”. Lo único que debemos asegurar es que tengamos un mecanismo para generar un id único. También podríamos considerar que no se pasen las noticias muy antiguas, para eso podríamos pasar la fecha, lo haremos al final si sobra tiempo. Todas estas reglas se entienden fácil, pero hace falta que le hagamos entender esto a CouchDB en Cloudant para lo que tendremos que usar una característica llamada “filtered replication” o replicación filtrada.

Para el punto 3, tendremos que usar una vista en el servidor que utilice una característica llamada map/reduce, que no es otra cosa que un proceso de dos pasos, que está optimizado para cuando trabajamos con un software distribuido en muchos servidores, tal como es nuestro caso. Para entenderlo fácil, MAP concentra todos los documentos que deben ser manipulados para que luego REDUCE realice cierta operación sobre los mismos. En nuestro caso, lo que haremos será buscar los documentos donde registramos que una noticia ha sido leída, luego las agrupamos por noticia y contamos las ocurrencias para hacer el ordenamiento.

Ejecución:

Empecemos por un punto 2, la replicación filtrada pues no queremos que los datos viajen por la red sin control para eso tenemos que crear un filtro así que vayamos a la sección de documentos de diseño:

alldesigndocs

Encontraremos el documento por diseño por defecto y para ver su contenido vamos a hacer click en el icono de lápiz:

designdoc

El documento de diseño tiene varias secciones, podrán reconocer la sección “views” donde he definido unas vistas de prueba. Lo que necesitamos hacer es crear una sección nueva para definir nuestro filtro, el cual debe ser una función que se evalúa por cada documento que se envía en una replicación. Tengamos en cuenta que para nuestro caso, la función debe ser verdadera para cuando el campo de usuario sea igual a “all” o al usuario actual. Digamos que el campo usuario lo llamamos “owner” entonces nuestra función de filtro sería:

"filters": {
"leidos": "function(doc,req){ if (doc._deleted){ return false; } if (doc.owner){ if (doc.owner==req.query.user){ return true; }; if (doc.owner==’all’){return true;};} else {if (doc._id==’_design/news’){return true;} else {return false;}}}"
}

Hemos agregado una condición al inicio y se refiere a los documentos que no hayan sido borrados. Por diseño, Couchdb guarda incluso los documentos que borras y los sincroniza también, así que es mejor filtrarlos para que la comunicación sea más fluida y de pasadita ahorramos algo de espacio.

La replicación filtrada nos permite asegurar que los datos que se replican son los necesarios y esto es super importante por dos razones: seguridad y uso de ancho de banda. Es muy común tener apps donde los usuarios tienen información particular que les es exclusiva, como por ejemplo, tu lista de compras, tu lista de reservas, tus preferencias y así. Con este mecanismo aseguramos que cada usuario tiene lo que realmente necesita y de pasadita al dejar de enviar un montón de documentos inútiles ahorramos espacio y ancho de banda.

Y bien, agregado el filtro a nuestro documento de diseño, éste quedaría así:

{
"_id": "_design/news",
"_rev": "41-667b5e312f96dfec37e4480d2c34479d",
"views": {
"topic": {
"map": "function (doc) {\n if (doc.tipo ==\"news\") {\n emit([doc.topic], {_id:doc._id});\n }\n}"
}
},
"filters": {
"leidos": "function(doc,req){ if (doc._deleted){ return false; } if (doc.owner){ if (doc.owner==req.query.user){ return true; }; if (doc.owner==’all’){return true;};} else {if (doc._id==’_design/news’){return true;} else {return false;}}}"
}

Tengan cuidado de poner todos los valores en una sola línea y de no utilizar la comila (“) dentro de cada valor.

Y listo, ahora vamos al código de replicación y pongamos la llamada a la replicación filtrada paso a paso:

Creamos una función para obtener un id único. Para eso, nuevamente usamos ng-Cordova para usar el plugin Device, esta vez en el archivo app.js ya que tenemos que agregar un concepto propio de la plataforma Cordova:

angular.module('starter', ['ionic','controller','service','ngCordova'])
.run(function($ionicPlatform,$cordovaDevice,$cordovaSplashscreen,$state,db) {
 $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 id='1231232132';
 if (ionic.Platform.platform()!="win32"){
 id = $cordovaDevice.getUUID();
 };
 db.setUser(id);
 $state.go('app.news',{status:'loading'});
 });
})

Como verán, hemos incluido el módulo ngCordova como una dependencia y llamamos a $cordovaDevice.getUUID() que genera un código único por equipo móvil. Para ser exactos no es tan único, o al menos no tenemos garantía de eso, pero para nuestro ejemplo es suficiente. Quizá lo mas interesante aquí es como y cuando llamamos a esta función.

Si se dan cuenta en la línea 3, llamamos a la función $ionicPlatform.ready. Como les dije en algún momento, ionic está basado en Apache Cordova y pues Apache Cordova se toma un tiempo en estar listo para contestarnos por lo que usamos la línea 3 para “esperar” a que cargue Cordova y en ese momento, ya podemos utilizar los plugins que nos dan acceso a funciones nativas, en nuestro caso $cordovaDevice. Ahora bien, el valor del id del equipo es crítico, al punto que tendremos que hacer que el app espere a que tengamos este valor disponible y eso es lo que se hace en las líneas siguientes:

  • En la línea 16, pasamos el valor del id del equipo al servicio porque nos va a servir para sincronizar la base de datos
  • En la línea 17, cuando ya tenemos todo listo, recién pasamos al estado que muestra las noticias.

Ahora bien, ¿Que hacemos mientras Cordova termina de cargar? pues lo mejor es mostrar un Splash Screen o el logo de nuestra aplicación y para eso tenemos que agregar un nuevo estado


$stateProvider
.state('splash',{
url:"/splash:status",
cache:false,
views:{
"home":{
templateUrl:"templates/splash.html",
controller:"splashController as splash"
}
},
resolve:{
status: function($stateParams){
return $stateParams.status;
}
}
})

Obviamente, tendremos que agregar una vista y un controlador. Aquí va la vista:

<ion-view view-title="">
<ion-content>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row row-center">
<div class="col">
</div>
<div class="col">
<img src="img/ionic.png">
</div>
<div class="col">
</div>
</div>
<div class="row-bottom">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
</ion-content>
</ion-view>

y aquí va el controlador:


.controller('splashController',
['$state',
'status',SplashController])
function SplashController($state,status){
if (status!='loading'){
$state.go('app.news');
}
};

Y verán que hay código de más. El tema es el siguiente: si el splash screen es un estado quiere decir que cuando se muestren las noticias, el usuario podría “regresar” al splash screen con el botón Back en Android o Windows Phone, cosa que nadie quiere. Para eso, en la línea 17 del archivo app.js al ir al estado de noticias, pasamos un parámetro para indicar que llamamos al estado desde el flujo de inicio del programa. Si vamos al estado de noticias mediante el botón Back, llegará sin éste parámetro y haremos que se vaya automáticamente al estado news nuevamente.

Lo bueno de todo esto es que con este mecanismo implementado, estamos 100% seguros que Cordova está listo y todas las llamadas a plugins funcionarán correctamente.

Ahora vamos hasta la método replicate y le tendremos que agregar el usuario como un parámetro, así que también tenemos que actualizar la línea donde llamamos a la replicación en método init. Mostremos todo el archivo services.js porque hay mas cosas que explicar

angular.module('service',[])
.factory('db',['$q','$rootScope',DbService]);
 function DbService($q,$rootScope){
 var key = 'xxxxxxxx';
 var pass = 'xxxxxxxxxx';
 var remote = 'https://'+key+':'+pass+'@server.cloudant.com/news';
 var db;
 var user;
 var initiated=false; 
 function newNews(change){
 if (!change.deleted){
 if (change.doc.tipo=="news")
 $rootScope.$broadcast('db:newNews',
 {newsId:change.id,
 newsTitle:change.doc.titular});
 }
 };
 function put(doc){
 db.put(doc);
 console.log('documento grabado');
 };
 function changed(change){
 console.log('cambios en la base de datos');
 if (!initiated)
 {
 initiated=true;
 db.changes({live: true, since: 'now', include_docs: true})
 .on('change', newNews);
 };
 $rootScope.$broadcast('db:changed',change);
 };
 return {
 setUser:function(userId){
 user = userId;
 },
 init: function(){
 if (!db) db = new PouchDB('news',{adapter:'websql'});
 this.replicate();
 return true;
 },
 replicate: function(){
 if (!db) this.init();
 db.replicate.to(remote,{live:true,
 retry:true,
 filter:'news/leidos',
 query_params:{'user':user}})
 .on('change',function(info){
 console.log('Evento Chage:'+JSON.stringify(info));
 })
 .on('paused',function(){
 console.log('Evento Paused:');
 })
 .on('complete',function(info){
 console.log('Evento Complete:'+JSON.stringify(info));
 })
 .on('active',function(){
 console.log('Evento Active:');
 })
 .on('error',function(err){
 console.log('Evento error:'+JSON.stringify(err));
 })
 .on('denied',function(info){
 console.log('Evento error:'+JSON.stringify(info));
 });
 db.replicate.from(remote,{live:true,
 retry:true,
 filter:'news/leidos',
 query_params:{'user':user}})
 .on('paused',changed);
 },
 get: function(id){
 if (!db) this.init();
 return db.get(id);
 },
 getCats: function(){
 if (!db) this.init();
 return db.allDocs(
 {startkey:'cat_',
 endkey:'cat_\uffff',
 include_docs:true});
 },
 getNews: function(catId){
 if (!db) this.init();
 if (catId)
 return db.query('news/topic',
 {key:[catId],
 include_docs:true,
 descending:true});
 else return db.allDocs({startkey:'news_\uffff',
 endkey:'news_',
 descending: true,
 include_docs:true});
 },
 putRead: function(pnewsId){
 var doc = {
 _id :'read_'+user+'_'+pnewsId,
 newsId: pnewsId,
 fecha:new Date().toString(),
 tipo:'read',
 owner:user 
 };
 put(doc);
 }
 }
 };

Veamos, vayamos en orden:

  • Primero, tenemos que agregar una función para recoger el dato de usuario, para eso hemos agregado una variable interna en la línea 8 y un método público en la línea 33.
  • Seguidamente, necesitamos grabar registros y para eso tenemos un método privado bastante genérico en la línea 18. Pero como necesitamos agregar un documento por lo que también agregamos un método público en la línea 94 donde verán que usamos una plantilla para crear el documento JSON que describimos al comienzo donde solamente cambiamos los valores de los campos para luego llamar al método interno genérico.
  • Finalmente, agregamos un comando de replicación. Los atentos se habrán dado cuenta que el comando de replicación que hemos venido usando es db.replicate.from lo cual significa que la dirección de los datos es del servidor al móvil, así que faltaría agregar la dirección del móvil al servidor. Para hacer un poco más gráfico el proceso de debug, hemos agregado todos los eventos del proceso. NOTA: Hay un comando db.sync que replica en ambos sentidos, pero prefiero separar las dos direcciones porque quiero capturar solamente los eventos solamente cuando “llegan” nuevas noticias del servidor.

Tanto lío y la pregunta que se cae de madura: ¿Ya podemos grabar y sincronizar? Aún no. Falta que la llave que usamos para conectarnos a la base de datos en Cloudant tenga permisos para escribir. Para eso vamos a Cloudant y hacemos click en el icono de candado al costado del nombre de nuestra base de datos:

Security

permissions

En el segundo gráfico podrán verificar que el key que están utilizando tenga el privilegio Writer.

Ahora, agreguemos la grabación cuando una noticia es leída en el controlador:


function DetailController(db,$scope,$state,noticia){
$scope.event= noticia;
db.putRead(noticia._id);
$scope.$on('db:changed',
function(event,changed){
$state.go('.', null, { reload: true });
console.log('Recargando Noticia');
});
};

En la línea 34, hemos agregado la dependencia al servicio db y en la línea 36 hemos agregado la bendita línea para grabar que hemos leído una noticia.

Y con eso ya podemos ver como nuestra aplicación hace lo siguiente:

  • Graba un documento del tipo ‘read’ cada vez que alguien lee una noticia. Como la clave del documento es única, si entramos a una noticia ya leída, la inserción simplemente fallará y eso esta bien porque asegura que sólo tendremos un documento tipo ‘read’ por noticia.
  • Los documentos serán sincronizados con la base de datos en Cloudant de forma automática.
  • Finalmente, manejamos el evento Ready para verificar que Cordova ya está listo y podemos usar plugins que nos den acceso a servicios nativos.

El Post ya se hizo largo, así que en uno próximo haremos un reporte de las noticias más leídas y ya que el código se ha hecho algo grande, vamos a ponerlo en un repositorio Github.

Por mientras, traten de actualizar su código para que tengan la funcionalidad desarrollada hasta el momento. Y sobre todo revisen los eventos que genera la replicación móvil – Servidor.


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.

Me he tomado algo de tiempo para escribir este post por una simple razón: es hora de limpiar el código. Hasta ahora el código que han ido desarrollando es funcional pero tiene cosas que les va a crear problemas cuando quieran hacer algo mas grande. Lo bueno en todo esto es que si han ido cambiado el código desde el post 1, entonces estos cambios les harán mucho sentido. La buena noticia en todo esto, es que puedo publicar todo el código sin problemas.

Comencemos con el archivo app.js:


angular.module('starter', ['ionic','controller','service'])
.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();
};
});
})
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app',{
cache:false,
abstract: true,
views:{
"home":{
templateUrl:"templates/menu.html",
controller:"menuController as menu"
}
},
resolve:{
cats: function(db){
return db.getCats();
}
}
})
.state('app.news', {
cache:false,
url: "/:catId",
views: {
"menuContent":{
templateUrl: "templates/news.html",
controller: "newsController as news"
}
},
resolve:{
noticias:function($stateParams,db){
return db.getNews($stateParams.catId);
}
}
})
.state('app.detail', {
cache:false,
url: "/detail/:id",
views: {
"menuContent":{
templateUrl: "templates/detail.html",
controller: "detailController as detail"
}
},
resolve:{
noticia: function($stateParams,db){
return db.get($stateParams.id);
}
}
});
$urlRouterProvider.otherwise('/');
})

Empecemos poniendo atención en la definición de los estados a partir de la línea 14.

  • Si tenemos un estado que muestra datos que cambian muy seguido en la base de datos, lo mejor es desactivar el cache. Eso lo hacemos mediante el atributo “cache:false” como pueden ver en la definición de cada estado.
  • Los datos es mejor cargarlos en la definición del estado. Esto se traduce en la utilización del atributo “resolve” donde verán que hacemos la llamada al servicio que recupera los datos y los pasa mediante una variable al controlador. Podrán ver que un estado puede recibir parámetros y estos nos sirven para recuperar los datos llamando a los servicios.
  • $ionicPlatform.ready() es muy importante. Conforme nuestro proyecto crezca, notarán que hará falta inicializar algunos componentes o plugins de Cordova. Cordova es el core de todo esto y la noticia es que se demora en cargar, por lo que si tenemos que inicializar algún componente o plugin propio de Cordova tienen que ponerlo dentro de esta función pues se ejecuta cuando ya es un hecho que Cordova ya está cargado.

Ahora nos toca ir a revisar el archivo controller.js


angular.module('controller',['service'])
.controller('menuController',
['$scope',
'$state',
'cats',MenuController])
.controller('newsController',
['$scope',
'$state',
'noticias',NewsController])
.controller('detailController',
['$scope',
'$state',
'noticia',DetailController]);
function MenuController($scope,$state,cats){
$scope.cats = cats.rows;
};
function NewsController($scope,$state,noticias){
$scope.notas=noticias.rows;
$scope.$on('db:changed',
function(event,changed){
$state.go('.', null, { reload: true });
console.log('Recargando noticias');
});
};
function DetailController($scope,$state,noticia){
$scope.event= noticia;
$scope.$on('db:changed',
function(event,changed){
$state.go('.', null, { reload: true });
console.log('Recargando Noticia');
});
};

Aquí si hay varias cosas para revisar. Primero, si bien no hay reglas a la hora de programar, si existen buenas prácticas y la mejor que he encontrado es esta: Angular Style Guide, así que apliquemos algunas reglas.

  • La primera es lo que se llama IIFE. Considerando que el app.js es el archivo principal, tanto controller.js como service.js deben tener todo el código contenido en una estructura como esta:
    • (function(){ … })(); En nuestro caso, hemos incumplido esto para simplificar la explicación, pero pueden agregarlo y verán que el código funciona sin problemas.
  • Verán que las definiciones de cada controlador ahora es algo diferente y es por dos razones:
    • Dependency injection: Esto es casi un formalismo, es decir, la podríamos saltar sin problema pero nos costará cuando usemos herramientas para comprimir nuestro código o para cuando querramos empaquetar nuestro código para evitar copiones. Si se fijan el primer controlador, verán que primero indicamos todos los componentes, entre comillas, que vamos a utilizar dentro del controlador, y luego, tenemos una función como parámetro final sin comillas. Finalmente la función es definida al final incluyendo como parámetros todos los componentes que vamos a usar ya sin comillas.
    • Siguiendo las sugerencias de estilo, el código de cada controlador es ahora puesta en una función aparte a fin de facilitar la lectura del código
  • La base de datos puede cambiar. Para esto tienen que pensar mucho en como diseñar sus estados. En cada controlador he incluido un $scope.$on para recibir el evento $broadcast con el nombre ‘db:changed’ que ya veremos exactamente cuando se genera pero básicamente es que cuando se detecte un cambio en la base de datos debemos refrescar los datos, en nuestro caso, hemos elegido volver a correr el estado mediante la línea $state.go() que vuelve a correr el código.
    • Algo es super importante: Cuando refrescan un estado, el estado padre no se refresca, pero si vemos bien en nuestro caso, el controlador del estado padre no tiene nada de código para refrescar. La clave aquí es que el estado padre se trata de un estado abstracto y estas si se refrescan junto con los estados hijos. Es por eso que basta con refrescar los estados hijos.

Nota final, hay casos en los que su vista no requiera refrescar datos a cada rato, así que evalúen activar el cache o que el refresco de los datos venga de algún evento como un click en algún botón, o un cambio de estado. Refrescar automágicamente cuesta.

Ahora veamos el service.js


angular.module('service',[])
.factory('db',['$q','$rootScope',DbService]);
function DbService($q,$rootScope){
var key = 'bentareadyessharyinessee';
var pass = 'OnEixgKgpt8LyEtl0S5DkAon';
var remote = 'https://'+key+':'+pass+'@supermio.cloudant.com/news';
var db;
function changed(change){
console.log('cambios en la base de datos');
$rootScope.$broadcast('db:changed',change);
};
return {
init: function(){
if (!db) db = new PouchDB('news');
this.replicate();
return true;
},
replicate: function(){
if (!db) this.init();
db.replicate.from(remote,
{live:true,
retry:true})
.on('paused',function(changes){
changed(changes);
});
},
get: function(id){
if (!db) this.init();
return db.get(id);
},
getCats: function(){
if (!db) this.init();
return db.allDocs(
{startkey:'cat_',
endkey:'cat_\uffff',
include_docs:true});
},
getNews: function(catId){
if (!db) this.init();
if (catId)
return db.query('news/topic',
{key:[catId],
include_docs:true,
descending:true});
else return db.allDocs({startkey:'news_\uffff',
endkey:'news_',
descending: true,
include_docs:true});
}
}
};

  • Cuiden la replicación.
    • En la línea 20 verán que iniciamos la replicación  y con el atributo “live” y esto nos garantiza que los cambios se pasan casi instantáneamente pero como todo en la vida, tiene costo, así que consideren tenerlo apagado y encenderlo cuando haga falta. El siguiente atributo “retry” se refiere a que la replicación continúe luego de una falla en la red, tenganlo siempre en true, pero no se confíen.
    • Al encender la replicación podemos especificar eventos. En nuestro caso en la línea 23 encendemos el soporte del evento “paused” que se refiere a que la replicación “live” ya no recibe nuevos cambios, así que podemos decir que la base de datos remota y local son iguales. En nuestro caso, este es un buen momento para refrescar las vistas. Hay otro evento que es “change” que se genera con cada cambio que se recibe, lo cual es bueno pero no tanto porque si refrescamos aquí, tendríamos ejecutando el refresco muchas veces, sobre todo para la corrida inicial y nuestra base de datos local está vacía. Luego utilizaremos estos eventos para crear notificaciones locales.
  • Cuídense de los procesos asíncronos. Todos los métodos en el servicio “db” son iguales en una cosa: todos son asíncronos. Tal como les recomendé, usen los estados en el archivo app.js para llamar a estos métodos y se librarán de la pesadilla de javascript llamada “Promesas”. Ya luego veremos como manejar promesas.

Y listo, todos los demás archivos van iguales y en lineas generales podrán ver que el código está más limpio y ordenado, además, estamos llamando todos los datos directamente desde la base de datos.

Nota final: en lo que se refiere a performance, el consejo básico es que diseñen su app para que no tenga que refrescar automáticamente nada de nada, a menos que sea absolutamente necesario por la simple razón que si bien PouchDB es una buena base de datos, puede volverse bastante pesada si tienen que lidiar con una gran cantidad de datos. Refrescar datos por eventos es la mejor estrategia.

Como podrán ver, no han habido nuevas funciones que hayamos incluido, pero al menos hemos mejorado el código y espero que los consejos y el estilo de programación mostrado aquí les sirva para organizar su código en algo que puedan mantener y entender con facilidad.


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 la parte 8 dejamos el app con menus que permiten mostrar las noticias por categorías, pero faltaba cargar las categorías directamente de la base de datos, de tal forma que se vayan mostrando automáticamente las categorías nuevas.

Las categorías serán extraídas de la base de datos en una consulta bastante simple y debemos considerar lo siguiente:

  • Debemos poder establecer el orden en que se muestran las categorías. Para eso deberíamos agregar un campo que nos permita establecer el orden.
  • Debemos poder desactivar las categorías. Para eso también agregaremos un campo que indique el estado

Ok, hagamos esto primero entonces, felizmente tenemos pocas categorías. Para ejemplo, veamos como quedaría “cat_01”:


{
"_id": "cat_01",
"_rev": "2-df694a2ed157b1403baa2ca9214d801b",
"nombre": "Nacional",
"descripcion": "Noticias del país",
"tipo": "topic",
"orden":1,
"estado":1
}

Lo mismo para todas las demás categorías. Con estos cambios, consultaremos las categorías, las ordenamos por el campo orden y las filtramos por el campo estado.

Ahora para hacer la consulta tenemos dos alternativas:

  • Usando la clave principal: Todas las categorías tienen un id que comienza con “cat_” entonces podemos usar la clave por defecto. El orden será el orden de las claves: cat_01 luego cat_02 y así, igual podemos ordenarlo al mostrarlo.
  • Creando una vista: estaremos creando un índice solamente para recuperar categorías, ¿valdrá la pena? pues son pocas categorías así que aún no vale la pena ir por este camino.

Listo, ahora empieza lo complicado. Necesitamos que la lista de categorías sea pasado al estado inicial para que pueda ser mostrado. Recordemos que hemos definido un estado abstracto para incluir el sidemenu. Como en el ejemplo anterior, pasar datos a un estado requiere el uso de “resolve”, eso es fácil, lo  complicado es que tenemos que hacer la consulta a la base de datos para obtener esas categorias. Si revisan el archivo app.js, por ninguna parte hago alguna llamada al factory “db” que contiene todas nuestras operaciones por lo que si llamamos alguna operación de datos en el “app.js” simplemente fallarán porque acuerdense que inicializamos la factory db con el método db.init()  en el controlador “newsController” en la línea 8 del archivo “controllers.js” como pueden ver aquí:


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

Esto nos funcionó en su momento, y como ya podrán darse cuenta, es una mala práctica inicializar las cosas en un controlador. Incluso llamar a un inicializador puede resultar malo. Así que comenzaremos modificando el archivo “services.js” para dos cosas:

  • Ejecutar la rutina init() cuando haga falta y no tener que llamarla en ningún lugar. De esta manera simplemente llamaremos a las consultas y siempre tendremos la db lista.
  • Agregar el método para consultar las categorias

El primer paso es fácil: simplemente agregamos estas líneas al comienzo de cada método:


if (!db) {
this.init();
}

Simple, preguntamos si la variable interna es nula o no, y ejecutamos el método init() cuando haga falta. Agregamos eso a todos los métodos del factory.

El segundo paso también es simple, al igual que el método “mostrar” usamos el método allDocs así:


getCats: function(){
if (!db) {
this.init();
}
return db.allDocs({startkey:'cat_',endkey:'cat_\uffff',include_docs:true})
}

Con tanto cambio, el archivo services.js. quedará así:


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;
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){
if (catId)
db.query('news/topic',{key:[catId],include_docs:true,descending:true}).then(function(result){
$rootScope.$broadcast('refrescar',result.rows);});
else mostrar();
};
return {
init: function(){
if (!db) {
db = new PouchDB('news');
}
mostrarCat(cat);
this.replicate();
},
replicate: function(){
if (!db) {
this.init();
}
db.replicate.from(remote,{live:true,retry:true})
.on('paused',function(info){
mostrarCat(cat);
});
},
get: function(id){
if (!db) {
this.init();
}
return db.get(id);
},
setCat: function(id){
if (!db) {
this.init();
}
cat = id;
mostrarCat(cat);
},
getCats: function(){
if (!db) {
this.init();
}
return db.allDocs({startkey:'cat_',endkey:'cat_\uffff',include_docs:true})
}
}
});

Y listo, ahora vamos al archivo “app.js” donde vamos a agregar el resolve al estado abstracto:


.state('app',{
abstract: true,
views:{
"home":{
templateUrl:"templates/menu.html",
controller:"menuController as menu"
}
},
resolve:{
cats: function(db){
return db.getCats();
}
}
})

Agregamos la llamada al nuevo método y la pasamos al controlador a través de la variable “cats”. No se olviden de que, ya que estamos llamando a un factory definido en el modulo “services”, tenemos que agregarlo como dependencia. Así que la primera línea de nuestro “app.js” será ahora así.


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

Y listo, terminamos con este archivo. Ya terminamos actualizando el modelo, ahora toca el controlador. Aquí simplemente tenemos que recoger la variable cats con las categorias y quitar cualquier llamada a “db.init()”. El controlador “menuController” quedaría así:


.controller('menuController',function($state,$scope,db,cats){
$scope.cats = cats.rows;
$scope.setCat = function(id){
$state.go('app.news');
db.setCat(id);
};
})

Acuérdense que cuando llamamos a la base de datos obtenemos un objeto y dentro de ese objeto el array “rows” nos trae los registros que queremos.

Ahora así quedaría el controlador “newsController”:


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

Como ven, sólo le hemos quitado la llamada a “db.init()”. Y con esto hemos acabado con el controlador y vamos a arreglar la vista en el archivo “menu.html”. Si se fijan en la línea 25 comienza una lista donde todos los valores son estáticos y va hasta la línea 38. Reemplazamos todo eso por un iterador y quedaría así:


<ion-list>
<ion-item collection-repeat="cat in cats | orderBy: 'doc.orden'| filter:{'doc':{'estado':1}}" nav-clear menu-close ng-click='setCat(cat.id)'>
{{cat.doc.nombre}}
</ion-item>
</ion-list>

Cosas interesantes aqui:

  • Collection-repeat es propio de Ionic y es más eficiente en listas largas
  • El array cats es el que metimos en el $scope desde el controlador y tiene todas nuestras categorías.
  • Mediante “orderBy” las ordenamos por el campo “orden” que agregamos.
  • Y mediante “filter” mostramos solamente los que tienen estado =1

Todo lo demás viene del template anterior, incluso para llamar al método “setCat” llamamos al cat.id.

Y listo!, Si prueban el código hasta aquí funcionará, pero hay algunas limitaciones:

  • Si agregan una categoría en el servidor o se cambia el orden de alguna, no se refresca en la lista del menú automáticamente
  • Al inicio se mostrarán todas las noticias, incluidas las que pertenezcan a categorías inactivas

Esas las veremos en el siguiente post. Lo que si es urgente es corregir el estilo del código, siendo el más urgente el que figura en el controlador “detailController” donde verán que tenemos una llamada a db.get y esperamos por la respuesta. Si se habrán dado cuenta, para las categorías es lo mismo pero no esperamos por respuesta en ningún lado, simplemente esperamos valores y ya, así que usemos ese patrón aquí y corrijamos el controlador para que se vea así:


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

y tendríamos que cambiar el resolve de la ruta en el archivo “app.js” para que se vea asi:


resolve:{
noticia: function($stateParams,db){
return db.get($stateParams.id);
}

Y listo. La explicación para este cambio es que como casi todo en JavaScript es asíncrono, tenemos que usar un modelo llamado “promesas” para esperar por la respuesta y hacer algo con eso. Es por esto que usamos objeto.then() y dentro del then() incluimos una función con una operación. Pues bien, al momento de utilizar estados, el componente detrás de toda esa magia (UI-Router!!!) hace muchas cosas bien, y una de esas cosas es manejar la resolución de las promesas y es tan inteligente que esperará hasta que la promesa sea resuelta y podamos recibir los documentos en el controlador. Con eso nuestro código queda mas limpio.

Promises es todo un tema y siempre es un motivo de confusión así que mejor dejar que sea UI-Router quien las maneje. Así que para que se diviertan notarán que para cargar las noticias para la categoría seleccionada podemos usar el mismo esquema anterior. A ver si pueden completarlo por su cuenta.

NOTA: Ya un lector notó que todo es sobre leer datos en el cliente y tiene una razón. Grabar datos es bien simple pero supone que tengamos mucho cuidado durante el diseño, porque si dejamos escribir a un usuario, debemos tener en cuenta que cualquier usuario podrá hacerlo a menos que pongamos un control. Establecer un control requiere de cierto manejo adicional que aún falta explicar. Para que lo vean en perspectiva: 

  • Cualquiera puede escribir:
    • No hace falta establecer usuario y pass al inicio
    • Se debe tener cuidado de no duplicar el id del documento que se vaya a grabar
    • La replicación debe modificarse, porque si hay muchos usuarios, estaremos recibiendo muchos cambios y datos en nuestra db local.
  • Todos usuarios registrados pueden escribir:
    • Hay que incluir usuario y pass
    • También la replicación se tiene que ajustar para controlar los datos entrantes.
  • Sólo algunos usuarios registrados pueden escribir:
    • Usuario y clave es obligatorio
    • Hay que determinar quien puede escribir y quien no. Podemos integrar la autentication propia de CouchDB para determinar esto, pero esto aplica a todos los documentos, lo cual no necesariamente es bueno.
    • La replicación también se modifica pero ya es más simple

Recuerden que más importante que empezar a construir es sentarse a diseñar.


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.


Tuve la oportunidad de comprar un HTC M8 y estuve muy contento con su acabado metálico y con su estuche “dot”, pues se trata de un equipo aceptablemente rápido y sobre todo muy fácil de usar, principalmente porque podías crear grupos de programas directamente en la lista total y no solo en la pantalla de inicio. Lo que más me preocupaba era malograr su acabado metálico. Con el lanzamiento del M9, por primera vez un fabricante puso atención al sentimiento de obsolescencia y sacó un equipo que era muy parecido al actual y donde los cambios eran casi todos internos. No me sentía mal por tener un equipo “viejito”, y a pesar de eso el M9 seguía siendo reconocido como una buena opción nueva.

Para los que se acuerdan, HTC siempre fue una marca que comenzaba sacando muy buenos equipos top, pero sus equipos intermedios y bajos no lo eran tanto, porque todos queríamos tener los modelos top. Sorprendentemente hoy me entero que tiene problemas financieros: http://www.engadget.com/2015/08/07/htc-cuts-android-smartphones/.

En estos días suena fuerte la entrada de Xiaomi al mercado latinoamericano, que si bien tiene equipos top, su fuerte ha sido la de colocar equipos de bajo costo con muy buenas funciones. Hace unos pocos días también Motorola acaba de presentar los nuevos modelos de su línea de Android que se basa en la línea MotoX pero también hay Moto G o E y que en su mayoría son equipos de bajo costo y donde los equipos top se diferencian por las características internas básicamente. OnePlus es un startup chino que se ha hecho famoso con su equipo OnePlus One y que acaba de lanzar el OnePlus Two cuya característica es la de tener características Top a un precio relativamente bajo,  entre estas características top resalta que tengan el bootloader abierto y que rootear el equipo ya que la compañía anima a sus usuarios a instalar sistemas operativos alternativos o “cocinados”. Huawei nunca fue muy popular en el mundo de los móviles, pero eran casi la única opción para los modems, pero el día de hoy sus equipos de bajo precio se ven muy bien siendo muy delgados y con buenas cámaras.

Estos son algunos ejemplos en el mundo de hoy donde se nota que la fortaleza de un fabricante está en el bajo precio, y es donde se puede ubicar un teléfono estrella que no es el top de su línea por lo general, y lo más importarte es que no escuchamos problemas financieros de estas empresas.

El caso de Samsung es particular, porque tienen una cartera muy fuerte de equipos de bajo costo, pero sus modelos top tampoco han disparado lo que se esperaba http://www.forbes.com/sites/ewanspence/2015/07/07/samsung-q2-2015-galaxy-s6-failure/.

Todo esto muestra que si bien el precio es muy importante, es también importante el sentimiento del consumidor al momento de adquirir un equipo, el sentimiento aspiracional: De repente no tenga el dinero para comprar el modelo top, así que compraré un modelo mas bajo que luzca muy parecido al modelo top por menos dinero, y espero no sacrificar muchas funciones.

Con este pensamiento, claramente destaca Sony que tiene una oferta de equipos en los que el acabado premium se nota en todos sus modelos, pero que ofrecen una gama amplia de precios. Si esto funciona para vender mas equipos pues no lo se, pero la apuesta de Sony sigue fuerte.

Lo único malo de todo esto es que el criterio técnico está quedando de lado, claro, tampoco es que necesitemos ser todos ingenieros para comprar un celular, pero debemos ser concientes de que estamos comprando una computadora que recibe y hace llamadas. Así que hay criterios mínimos que debemos verificar antes de decidir la compra de un teléfono:

  • RAM 2 GB
  • Almacenamiento: 32 gb
  • Cámara al gusto
  • Procesador al gusto
  • Pantalla al gusto

Esos que he marcado como “al gusto” significa que tienen que manipular el equipo y ver si se acomoda a sus necesidades. Para nuestro caso:

  • Pantalla: verifiquen que puedan leer texto claramente y que las imágenes se muestran bien tanto bajo techo como en el exterior. El mayor problema de las pantallas hoy en día es la de dejarse ver claramente en exteriores.
  • Camara: tomen algunas fotos con mucha y poca luz y comparen si es que la calidad final es de su gusto. No se fijen mucho en los megapixeles, fíjense en lo que ven sus ojos.
  • Procesador: manipulen el equipo y abran varios programas, cambien entre ellos y sientan si es la velocidad que quieren tener. Tengan en cuenta que conforme vayan instalando programas y por el uso diario, el rendimiento del teléfono tiende a reducirse, así que verifiquen que el equipo sea lo suficientemente rápido para ustedes.

Finalmente, hasta hace poco la batería era un problema por lo que tener una batería adicional era común, al día de hoy la mayoría de equipos tienen la batería integrada por lo que no tenemos opción a cambiarla además, las mejoras en rendimiento en los equipos nuevos se notan. Es muy difícil que prueben la calidad de la batería, así que tienen que exigir que el vendedor les indique claramente la duración de batería y ustedes tienen la obligación de verificar en foros de internet o en los sitios de evaluación.

En conclusión, un teléfono celular es una computadora y por lo tanto nuestra decisión de compra debe considerar también las características técnicas además del factor precio, y al día de hoy la oferta de los fabricantes se está centrando en hacer un buen equipo intermedio a un precio razonable.


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.





%d bloggers like this: