Vistas de página en total

lunes, 14 de agosto de 2017

SQLite (versión javascript)

Guardando datos

Algo habitual y necesario en las aplicaciones móviles es almacenar datos de forma persistente. De esta forma, podemos guardar datos para tenerlos disponibles en posteriores ejecuciones (por ejemplo, después de arrancar el dispositivo).
Existe un módulo Nativescript que permite utilizar los "application settings" para guardar datos en la forma clave-valor. Este módulo es adecuado cuando los datos que se quieren guardar son muy sencillos. Pero otras veces tendremos que guardar datos más complejos, y en este caso necesitaremos una base de datos.
SQLite es una base de datos que funciona tanto en Android como en iOS de forma nativa. Nativescript tiene un plugin para poder utilizar SQLite directamente.
En este artículo haremos una pequeña aplicación para demostrar el uso de este plugin.

Creación del proyecto

En primer lugar creamos el proyecto:

    tns create DatabaseApp
    cd DatabaseApp
    tns platform add android
    tns plugin add nativescript-sqlite

Después de crear el proyecto, le añadimos la plataforma Android (podríamos haber añadido la plataforma iOS en un Mac), y por último, añadimos un plugin al proyecto: nativescript-sqlite.
Al añadir el plugin, se crea un nuevo directorio node_modules/nativescript-sqlite con los ficheros javascript que dan acceso a la base de datos Sqlite.

Código

Realizaremos una sencilla aplicación con una única pantalla: main
Utilizaremos javascript.

app/app.js

var application = require("application");
application.start({ 
    moduleName: "main-page" 
});

Como siempre, importamos el módulo application y lanzamos la página main-page para arrancar la aplicación.

app/main-page.xml

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
    <StackLayout>
        <TextField id="login" hint="write login" text="{{ login }}" />
        <TextField id="mail" hint="write mail" text="{{ mail }}" />
        <StackLayout orientation="horizontal">
            <Button text="Insert" tap="{{ insert }}" />
            <Button text="Select" tap="{{ select }}" />
        </StackLayout>
    </StackLayout>
</Page>

La página entrará en el callback onNavigatingTo cuando se cargue.
El primer StackLayout es vertical (por defecto) y contiene dos TextField (para introducir login y mail) y otro StackLayout horizontal con dos botones.
Los dos TextField rellenan el atributo texto con valores del modelo: login y mail.
Los dos Button llaman a funciones del modelo cuando se pulsan: insert y select.

app/main-page.js

var createViewModel = require("./main-view-model").createViewModel;
var Sqlite = require("nativescript-sqlite");

function onNavigatingTo(args) {

    console.log("onNavigatingTo...");
    // store page variable for bind model
    var page = args.object;

    // open database
    var open = new Sqlite("my.db");
    open.then(
        db => {
            console.log("OK opening database");
            // create table if not exists
            var create = db.execSQL("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT, email TEXT)");
            create.then(
                id => {
                    console.log("OK creating database");
                    page.bindingContext = createViewModel(db);
                },
                error => {
                    console.log("Error opening database", error);
                }
            )
        },
        error => {
            console.log("Error opening database", error);
        }
    );
}

exports.onNavigatingTo = onNavigatingTo;

Primero importamos lo que necesitamos: la función createViewModel que crearemos en el fichero main-view-model.js y el objeto Sqlite del plugin que hemos añadido al proyecto.
Después implementamos la funcion onNavigatingTo que será invocada cuando se cargue la página.
Además de sacar la variable page (que representa a la página) del argumento args, la primera cosa interesante de esta función es la apertura de la base de datos.
Antes de entrar en detalles, hay que decir que el plugin nativescript-sqlite trabaja con promesas javascript, así que es necesario estar familiarizado con las promesas para entender el código. En resumen, una promesa es similar a un callback, pero con una sintaxis más cómoda. Cuando una función ejecuta código asíncrono (o sea, que el resultado llegará posteriormente), puede retornar un objeto promesa. Este objeto tiene una función then() con dos parámetros. Estos dos parámetros son a su vez funciones, una para tratar el caso de ejecución correcta y otro para tratar el caso de error.
Quizás la sintaxis x => { } sea nueva para el lector, pero realmente es una simplificación de function(x) { }, es lo mismo.
En nuestro caso, new Sqlite("my.db") devuelve una promesa (que nosotros asignamos a la variable open). Después llamamos a open.then() pasando dos argumentos función. El primero es la función que se ejecutará cuando se abra correctamente la base de datos (esta función recibe como argumento un objeto db). Y el segundo es una función que se ejecutará cuando ocurra un error abriendo la base de datos (esta función recibe como argumento un objeto error, realmente un mensaje de error).
En el caso de que el fichero de base de datos se abra correctamente, la promesa invocará a la función que recibe el argumento db, y dentro de esa función podremos ejecutar sentencias contra la base de datos.
La primera cosa que hacemos con db es llamar a db.execSQL() para crear una tabla (si no existiera). Esta función, execSQL(), también devuelve una promesa, que guardamos en la variable create. Después llamamos a la función then() de la promesa pasando otras dos funciones: una para tratar el caso correcto y otra para tratar posibles errores.
En el caso de que la tabla se cree correctamente (o bien ya existía), entrará la función que recibe como argumento un id (el cual no vamos a utilizar). Lo que hacemos aquí es enlazar el modelo asociado a la página (page.bindingContext) a createViewModel(db), o sea, llamamos a la función createViewModel pasando la base de datos como argumento.

Documentación del plugin nativescript-sqlite.

app/main-view-model.js

    var Observable = require("data/observable").Observable;
    var Sqlite = require("nativescript-sqlite");


    function createViewModel(database) {
        var viewModel = new Observable();

        viewModel.login = "";
        viewModel.mail = "";

        viewModel.insert = function() {
            console.log("Inserting...");
            var promise = database.execSQL("INSERT INTO users (login, email) VALUES (?, ?)", [ this.login, this.mail ]);
            promise.then(
                id => {
                    console.log("INSERT: ", id);
                }, 
                error => {
                    console.log("INSERT error", error);
                }
            );  
        }

        viewModel.select = function() {
            console.log("Selecting...");
            var promise = database.all("SELECT * FROM users");
            promise.then(
                rows => {
                    for (var r in rows) {
                        console.log(r, "->", rows[r]);
                    }
                },
                error => {
                    console.log("SELECT error", error);
                }
            );
        }

        return viewModel;
    }

    exports.createViewModel = createViewModel;

En el fichero main-view-model.js definimos el modelo que vamos a utilizar en esta página.
En primer lugar, importamos Observable y Sqlite.
La función createViewModel() crea los objetos y funciones que constituyen el modelo (como hemos visto, esta función es llamada desde main-view.js para asociar el modelo a la página). Los atributos del objeto modelo son:

  • login: atributo que guarda el login
  • mail: atributo que guarda el mail
  • insert(): función para insertar un nuevo usuario
  • select(): función para consultar la tabla de usuarios

Las funciones insert() y select() llaman a la base de datos para insertar y consultar respectivamente.
Estas funciones se activan en el evento tap de los Button que vimos en el fichero main-view.xml
Las llamadas a base de datos funcionan con promesas, de forma similar a como vimos anteriormente.



jueves, 3 de agosto de 2017

Layouts en Nativescript

En las aplicaciones para dispositivos móviles, la experiencia del usuario usando la aplicación es una parte fundamental del éxito de ésta. La funcionalidad es muy importante, pero hoy día existen muchas aplicaciones que hacen casi lo mismo, y lo único que distingue a unas de otras (y que es lo que hace decidir a los usuarios) es la interfaz de usuario.
Cada pantalla de la aplicación tendrá una estructura, dependiendo de la funcionalidad de esa pantalla y de la información que presenta al usuario. Un formulario sitúa un elemento encima de otro (en filas), una lista de elementos tiene un scroll si el número de elementos sobrepasa el alto de la pantalla, una gráfica ocupará el máximo sitio disponible para que se pueda visualizar sin problemas, datos tabulares se muestran en filas y columnas, etc, etc.
Nativescript permite organizar los elementos en la pantalla mediante los denominados Layouts. Un Layout es un elemento de interfaz de usuario que se pone en el fichero XML de la pantalla y que no se muestra en el dispositivo. Sin embargo, aunque no se muestre en pantalla, es capaz de organizar a todos sus elementos hijos (otros elementos de UI, incluidos otros layouts) de una forma determinada; cada tipo de layout organiza a sus hijos de una forma.
Existen cinco tipos de layouts en Nativescript:

  • StackLayout
  • GridLayout
  • WrapLayout
  • DockLayout
  • AbsoluteLayout

Resolución, pixels, dp, dpi, ...

Antes de entrar en los distintos layouts, es conveniente explicar (o recordar) cómo se miden los objetos en la pantalla de un dispositivo.

Resolución: es la medida del número de pixels en la pantalla. Normalmente se define como width x height, o sea, el número de pixels en el ancho de la pantalla y el número de pixels en el alto de la pantalla.
dpi, pixels por pulgada: es el número de pixels que hay en cada pulgada de la pantalla. Todos los dispositivos tienen el mismo valor de dpi en horizontal y en vertical (es decir, los pixels son cuadrados), por tanto, sólo se da un número de dpi para un dispositivo (en lugar de uno para horizontal y otro para vertical).
dp, pixels independientes del dispositivo: tal y como se explica aquí, los dp permiten definir las dimensiones de un dispositivo sin mencionar su tamaño físico. 160 dp corresponden a una pulgada en cualquier dispositivo.

AbsoluteLayout

Este layout es el más sencillo de entender, pero también el que menos se utiliza. ¿Por qué? Porque una aplicación se diseña para adaptarse a diversos tamaños de pantalla, y este layout, al situar los elementos en posiciones fijas, no tiene mucha versatilidad para adaptarse a los distintos tamaños.
AbsoluteLayout utiliza las coordenadas left,top de sus elementos hijos para situarlos. Cuando el layout cambiar de tamaño, no cambiará la posición ni el tamaño de sus hijos.
AbsoluteLayout no tiene ningún atributo.
Los hijos de este layout tienen que especificar los siguientes atributos:

  • left: distancia en pixels desde el lado izquierdo del AbsoluteLayout hasta el lado izquierdo del elemento hijo.
  • top: distancia en pixels desde el lado superior del AbsoluteLayout hasta el lado superior del elemento hijo.

Los elementos hijos pueden definir además los atributos width y height.
Los elementos hijos se podrían solapar unos encima de otros (en función de left, top, width y height).
Ejemplo:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <AbsoluteLayout width="210" height="210" style.backgroundColor="lightgray"> <Label text="10, 10" left="10" top="10" width="90" height="90" backgroundColor="red"/> <Label text="110, 10" left="110" top="10" width="90" height="90" backgroundColor="green"/> <Label text="110, 110" left="110" top="110" width="90" height="90" backgroundColor="blue"/> <Label text="10, 110" left="10" top="110" width="90" height="90" backgroundColor="yellow"/> </AbsoluteLayout> </Page>




DockLayout

Este layout permite situar a sus hijos en cinco posiciones:

  • left
  • top
  • right
  • bottom
  • center

No es necesario utilizar los cinco hijos. También se pueden situar varios hijos en una misma posición.
Para definir la posición de un hijo, se utiliza el atributo dock del elemento hijo, el cual puede valer "left", "top", "right" o "bottom". Si no se pone este atributo, se utilizará la posición central.
El elemento en la posición central se debe definir en último lugar, y el atributo stretchLastChild de AbsoluteLayout debe valer "true".
Propiedades de absoluteLayout:

  • stretchLastChild: indica si el último elemento hijo (sin atributo dock) debe ocupar el espacio restante o no. El valor por defecto es "true".

Propiedades de los elementos hijos:

  • dock: especifica la posición dentro del DockLayout (left, top, right, bottom).

Ejemplo:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <DockLayout width="210" height="210" style.backgroundColor="lightgray" stretchLastChild="true"> <Label text="left" dock="left" backgroundColor="red"/> <Label text="top" dock="top" backgroundColor="green"/> <Label text="right" dock="right" backgroundColor="blue"/> <Label text="bottom" dock="bottom" backgroundColor="yellow"/> </DockLayout> </Page>



GridLayout

Este layout organiza a sus elementos hijos en filas y columnas: celdas. Una celda puede contener varios hijos, un hijo puede expandirse a varias filas y/o columnas. Por defecto, GridLayout tiene una sola fila y una sola columna (o sea, una celda), y para definir más filas y columnas tenemos que utilizar los atributos rows y columns de GridLayout. El ancho de una columna se puede definir como un tamaño absoluto en pixels, como un porcentaje del ancho total, o bien automático. Igual para definir el alto de una fila:

  • absolute: tamaño fijo en pixels (es un número)
  • auto: toma el espacio que necesiten los elementos hijos
  • *: toma el espacio restante después de haber utilizado absolute y auto. Permite especificar proporciones, por ejemplo: 2*, 3*

En resumen, y para el caso de las columnas. Disponemos de un ancho total (que es el ancho del GridLayout). En primer lugar se toman las definiciones de columnas de tipo absolute, luego se usan las de tipo Auto (y se utiliza el mayor ancho utilizado por un elemento hijo). Con el espacio que sobra, se divide proporcionalmente entre las columnas *, dando un ancho proporcional a cada columna en función del peso de *.
Propiedades de GridLayout:

  • columns: especificación de las columnas (separadas por coma).
  • rows: especificación de las filas (separadas por coma).

Propiedades de los elementos hijos:

  • row: indica la fila del GridLayout en que se sitúa el elemento
  • col: indica la columna del GridLayout en que se sitúa el elemento
  • rowSpan: número de filas que abarca el elemento hijo
  • colSpan: número de columnas que abarca el elemento hijo

Ejemplo:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <GridLayout columns="50, auto, *" rows="50, auto, *" width="210" height="210" style.backgroundColor="lightgray" > <Label text="Label 1" row="0" col="0" backgroundColor="red"/> <Label text="Label 2" row="0" col="1" colSpan="2" backgroundColor="green"/> <Label text="Label 3" row="1" col="0" rowSpan="2" backgroundColor="blue"/> <Label text="Label 4" row="1" col="1" backgroundColor="yellow"/> <Label text="Label 5" row="1" col="2" backgroundColor="orange"/> <Label text="Label 6" row="2" col="1" backgroundColor="pink"/> <Label text="Label 7" row="2" col="2" backgroundColor="purple"/> </GridLayout> </Page>





StackLayout

Este layout organiza sus elementos hijos unos encima de otro, o bien, uno al lado de otro, dependiendo del atributo orientation, que puede valer:
  • vertical (por defecto)
  • horizontal
Propiedades de StackLayout:
  • orientation: puede valer "vertical" o bien "horizontal"
Los elementos hijos de StackLayout no tienen propiedades específicas para este layout.
Ejemplo vertical:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <StackLayout orientation="vertical" width="210" height="210" style.backgroundColor="lightgray"> <Label text="Label 1" width="50" height="50" backgroundColor="red"/> <Label text="Label 2" width="50" height="50" backgroundColor="green"/> <Label text="Label 3" width="50" height="50" backgroundColor="blue"/> <Label text="Label 4" width="50" height="50" backgroundColor="yellow"/> </StackLayout> </Page>



Ejemplo horizontal:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <StackLayout orientation="horizontal" width="210" height="210" style.backgroundColor="lightgray"> <Label text="Label 1" width="50" height="50" backgroundColor="red"/> <Label text="Label 2" width="50" height="50" backgroundColor="green"/> <Label text="Label 3" width="50" height="50" backgroundColor="blue"/> <Label text="Label 4" width="50" height="50" backgroundColor="yellow"/> </StackLayout> </Page>





WrapLayout

Este layout organiza sus elementos hijos de forma similar a StackLayout, pero con la diferencia de que si los elementos no caben en una fila, pasa a la siguiente fila (orientación horizontal). En el caso de orientación vertical, si los elementos no caben en una columna, pasa a la siguiente columna.
Propiedades de WrapLayout:
  • orientation: horizontal o vertical
  • itemWidth: ancho de cada elemento hijo
  • itemHeight: alto de cada elemento hijo
Los elementos hijos de WrapLayout no tienen propiedades específicas para este layout.
Ejemplo:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"> <WrapLayout orientation="horizontal" width="210" height="210" style.backgroundColor="lightgray"> <Label text="Label 1" width="70" height="70" backgroundColor="red"/> <Label text="Label 2" width="70" height="70" backgroundColor="green"/> <Label text="Label 3" width="70" height="70" backgroundColor="blue"/> <Label text="Label 4" width="70" height="70" backgroundColor="yellow"/> </WrapLayout> </Page>