4.5. Desarrollo de aplicaciones MIDP

Vamos a estudiar ahora las APIs principales que encontramos en CLDC y MIDP para el desarrollo de aplicaciones. Comenzaremos viendo la parte básica del lenguaje contenida en CLDC, que está basada en la API de J2SE, para después estudiar la API de MIDP propia de los dispositivos móviles.

4.5.1. Programación en Java para MIDs

La parte básica del lenguaje Java para el desarrollo de aplicaciones J2ME está contenida en la configuración CLDC. Esta configuración está basada en la API básica de J2SE, por lo que para desarrollar aplicaciones J2ME utilizaremos una API que ya conocemos.

A continuación se muestran las características de J2SE que podemos utilizar en CLDC, y las diferencias más importantes existentes entre J2ME y J2SE:

Hasta ahora hemos visto la parte básica del lenguaje Java que podemos utilizar en los dispositivos móviles. Esta parte de la API está basada en la API básica de J2SE, reducida y optimizada para su utilización en dispositivos de baja capacidad. Esta es la base que necesitaremos para programar cualquier tipo de dispositivo, sin embargo con ella por si sola no podemos acceder a las características propias de los móviles, como su pantalla, su teclado, reproducir tonos, etc.

Vamos a ver ahora las APIs propias para el desarrollo de aplicaciones móviles. Estas APIs ya no están basadas en APIs existentes en J2SE, sino que se han desarrollado específicamente para la programación en estos dispositivos. Todas ellas pertenecen al paquete javax.microedition.

4.5.2. MIDlets

Los MIDlets son las aplicaciones para MIDs, realizadas con la API de MIDP. La clase principal de cualquier aplicación MIDP deberá ser un MIDlet. Ese MIDlet podrá utilizar cualquier otra clase Java y la API de MIDP para realizar sus funciones.

Para crear un MIDlet deberemos heredar de la clase MIDlet. Esta clase define una serie de métodos abstractos que deberemos definir en nuestros MIDlets, introduciendo en ellos el código propio de nuestra aplicación:

protected abstract void startApp();
protected abstract void pauseApp(); protected abstract void destroyApp(boolean incondicional);

A continuación veremos con más detalle qué deberemos introducir en cada uno de estos métodos.

Componentes y contenedores

Numerosas veces encontramos dentro de las tecnologías Java el concepto de componentes y contenedores. Los componentes son elementos que tienen una determinada interfaz, y los contenedores son la infraestructura que da soporte a estos componentes.

Por ejemplo, podemos ver los applets como un tipo de componente, que para poderse ejecutar necesita un navegador web que haga de contenedor y que lo soporte. De la misma forma, los servlets son componentes que encapsulan el mecanismo petición/respuesta de la web, y el servidor web tendrá un contenedor que de soporte a estos componentes, para ejecutarlos cuando se produzca una petición desde un cliente. De esta forma nosotros podemos deberemos definir sólo el componente, con su correspondiente interfaz, y será el contenedor quien se encargue de controlar su ciclo de vida (instanciarlo, ejecutarlo, destruirlo).

Cuando desarrollamos componentes, no deberemos crear el método main, ya que estos componentes no se ejecutan como una aplicación independiente (stand-alone), sino que son ejecutados dentro de una aplicación ya existente, que será el contenedor.

El contenedor que da soporte a los MIDlets recibe el nombre de Application Management Software (AMS). El AMS además de controlar el ciclo de vida de la ejecución MIDlets (inicio, pausa, destrucción), controlará el ciclo de vida de las aplicaciones que se instalen en el móvil (instalación, actualización, ejecución, desinstalación).

Ciclo de vida

Durante su ciclo de vida un MIDlet puede estar en los siguientes estados:

Será el AMS quién se encargue de controlar este ciclo de vida, es decir, quién realice las transiciones de un estado a otro. Nosotros podremos saber cuando hemos entrado en cada uno de estos estados porque el AMS invocará al método correspondiente dentro de la clase del MIDlet. Estos métodos son los que se muestran en el siguiente esqueleto de un MIDlet:

import javax.microedition.midlet.*;

public class MiMIDlet extends MIDlet {

protected void startApp()
throws MIDletStateChangeException { // Estado activo -> comenzar }

protected void pauseApp() { // Estado pausa -> detener hilos
}

protected void destroyApp(boolean incondicional)
throws MIDletStateChangeException { // Estado destruido -> liberar recursos
}
}

Deberemos definir los siguientes métodos para controlar el ciclo de vida del MIDlet:

Si ocurre un error que impida que el MIDlet empiece a ejecutarse deberemos notificarlo. Podemos distinguir entre errores pasajeros o errores permanentes. Los errores pasajeros impiden que el MIDlet se empiece a ejecutar ahora, pero podría hacerlo más tarde. Los permanentes se dan cuando el MIDlet no podrá ejecutarse nunca.

Pasajero: En el caso de que el error sea pasajero, lo notificaremos lanzando una excepción de tipo MIDletStateChangeException, de modo que el MIDlet pasará a estado pausado, y se volverá intentar activar más tarde.

Permanente: Si por el contrario el error es permanente, entonces deberemos destruir el MIDlet llamando a notifyDestroyed porque sabemos que nunca podrá ejecutarse correctamente. Si se lanza una excepción de tipo RuntimeException dentro del método startApp tendremos el mismo efecto, se destruirá el MIDlet.

Igual que en el caso anterior, si se produce una excepción de tipo RuntimeException durante la ejecución de este método, el MIDlet se destruirá.

Figura 1. Ciclo de vida de un MIDlet

Hemos visto que el AMS es quien realiza las transiciones entre distintos estados. Sin embargo, nosotros podremos forzar a que se produzcan transiciones a los estados pausado o destruido:

NOTA: La llamada a este método notifica que el MIDlet ha sido destruido, pero no invoca el método destroyApp para liberar los recursos, por lo que tendremos que invocarlo nosotros manualmente antes de llamar a notifyDestroyed.

Cerrar la aplicación

La aplicación puede ser cerrada por el AMS, por ejemplo si desde el sistema operativo del móvil hemos forzado a que se cierre. En ese caso, el AMS invocará el método destroyApp que nosotros habremos definido para liberar los recursos, y pasará a estado destruido.

Si queremos hacer que la aplicación termine de ejecutarse desde dentro del código, nunca utilizaremos el método System.exit (o Runtime.exit), ya que estos métodos se utilizan para salir de la máquina virtual. En este caso, como se trata de un componente, si ejecutásemos este método cerraríamos toda la aplicación, es decir, el AMS. Por esta razón esto no se permite, si intentásemos hacerlo obtendríamos una excepción de seguridad.

La única forma de salir de una aplicación MIDP es haciendo pasar el componente a estado destruido, como hemos visto en el punto anterior, para que el contenedor pueda eliminarlo. Esto lo haremos invocando notifyDestroyed para cambiar el estado a destruido. Sin embargo, si hacemos esto no se invocará automáticamente el método destroyApp para liberar los recursos, por lo que deberemos ejecutarlo nosotros manualmente antes de marcar la aplicación como destruida:

destroyApp(true);
notifyDestroyed();

Si queremos implementar una salida condicional, para que el método destroyApp pueda decidir si permitir que se cierre o no la aplicación, podemos hacerlo de la siguiente forma:

public void salir_cond() {
    try {

        destroyApp(false);

        notifyDestroyed();
    } catch(MIDletStateChangeException e) {
    }

}

Parametrización de los MIDlets

Podemos añadir una serie de propiedades en el fichero descriptor de la aplicación (JAD), que podrán ser leídas desde el MIDlet. De esta forma, podremos cambiar el valor de estas propiedades sin tener que rehacer el fichero JAR.

Cada propiedad consistirá en una clave (key) y en un valor. La clave será el nombre de la propiedad. De esta forma tendremos un conjunto de parámetros de configuración (claves) con un valor asignado a cada una. Podremos cambiar fácilmente estos valores editando el fichero JAD con cualquier editor de texto.

Para leer estas propiedades desde el MIDlet utilizaremos el método:

String valor = getAppProperty(String key)

Que nos devolverá el valor asignado a la clave con nombre key.

4.5.3. Interfaz gráfica

Vamos a ver ahora como crear la interfaz de las aplicaciones MIDP. En la reducida pantalla de los móviles no tendremos una consola en la que imprimir utilizando la salida estándar, por lo que toda la salida la tendremos que mostrar utilizando una API propia que nos permita crear componentes adecuados para ser mostrados en este tipo de pantallas.

Esta API propia para crear la interfaz gráfica de usuario de los MIDlets se denomina LCDUI (Limited Connected Devices User Interface), y se encuentra en el paquete javax.microedition.lcdui.

Acceso al visor

El visor del dispositivo está representado por un objeto Display. Este objeto nos permitirá acceder a este visor y a los dispositivos de entrada (normalmente el teclado) del móvil.

Tendremos asociado un único display a cada aplicación (MIDlet). Para obtener el display asociado a nuestro MIDlet deberemos utilizar el siguiente método estático:

Display mi_display = Display.getDisplay(mi_midlet);

Donde mi_midlet será una referencia al MIDlet del cual queremos obtener el Display. Podremos acceder a este display desde el momento en que startApp es invocado por primera vez (no podremos hacerlo en el constructor del MIDlet), y una vez se haya terminado de ejecutar destroyApp ya no podremos volver a acceder al display del MIDlet.

Cada MIDlet tiene un display y sólo uno. Si el MIDlet ha pasado a segundo plano (pausado), seguirá asociado al mismo display, pero en ese momento no se mostrará su contenido en la pantalla del dispositivo ni será capaz de leer las teclas que pulse el usuario.

Podemos utilizar este objeto para obtener propiedades del visor como el número de colores que soporta:

boolean color = mi_display.isColor();
int num_color = mi_display.numColors();

Componentes disponibles

Una vez hemos accedido al display, deberemos mostrar algo en él. Tenemos una serie de elementos que podemos mostrar en el display, estos son conocidos como elementos displayables.

En el display podremos mostrar a lo sumo un elemento displayable. Para obtener el elemento que se está mostrando actualmente en el visor utilizaremos el siguiente método:

Displayable elemento = mi_display.getCurrent();

Nos devolverá el objeto Displayable correspondiente al objeto que se está mostrando en la pantalla, o null en el caso de que no se esté mostrando ningún elemento. Esto ocurrirá al comienzo de la ejecución de la aplicación cuando todavía no se ha asignado ningún elemento al Display. Podemos establecer el elemento que queremos mostrar en pantalla con:

mi_display.setCurrent(nuevo_elemento);

Como sólo podemos mostrar simultáneamente un elemento displayable en el display, este elemento ocupará todo el visor. Además será este elemento el que recibirá la entrada del usuario.

Entre estos elementos displayables podemos distinguir una API de bajo nivel, y una API de alto nivel.

API de alto nivel

Consiste en una serie de elementos predefinidos: Form, List, Alert y TextBox que son extensiones de la clase abstracta Screen. Estos son elementos comunes que podemos encontrar en la interfaz de todos los dispositivos, por lo que el tenerlos predefinidos nos permitirá utilizarlos de forma sencilla sin tenerlos que crear nosotros a mano en nuestras aplicaciones. Se implementan de forma nativa por cada dispositivo concreto, por lo que pueden variar de unos dispositivos a otros. Estos componentes hacen que las aplicaciones sean más sencillas y portables, pero nos limita a una serie de controles predefinidos.

Este tipo de componentes serán adecuados para realizar front-ends de aplicaciones corporativas. De esta forma obtendremos aplicaciones totalmente portables, en las que la implementación nativa será la que se deberá encargar de dibujar estos componentes. Por lo tanto, en cada dispositivo podrán mostrarse de una forma distinta. Además no se permitirá acceder directamente a los eventos de entrada del teclado.

API de bajo nivel

Consiste en la clase Canvas, que nos permitirá dibujar lo que queramos en la pantalla. Tendremos que dibujarlo todo nosotros a mano. Esto nos permitirá tener un mayor control sobre lo que dibujamos, y podremos recibir eventos del teclado a bajo nivel. Esto provocará que las aplicaciones sean menos portables. Esta API será conveniente para las aplicaciones que necesitan tener control total sobre lo que se dibuja y sobre la entrada, como por ejemplo los juegos.

Figura 2. Jerarquía de elementos displayables

Vamos a centrarnos en el estudio de la API de alto nivel.

Componentes de alto nivel

Todos los componentes de alto nivel derivan de la clase Screen. Se llama así debido a que cada uno de estos componentes será una pantalla de nuestra aplicación, ya que no puede haber más de un componente en la pantalla al mismo tiempo. Esta clase contiene las propiedades comunes a todos los elementos de alto nivel:

Título: Es el título que se mostrará en la pantalla correspondiente al componente.

Ticker: Podemos mostrar un ticker en la pantalla. El ticker consiste en un texto que irá desplazándose de derecha a izquierda.

A continuación podemos ver cómo se muestra el título y el ticker en distintos modelos de móviles:

Figura 3. Título y ticker de las pantallas

Los componentes de alto nivel disponibles son cuadros de texto (TextBox), listas (List), formularios (Form) y alertas (Alert).

Cuadros de texto

Este componente muestra un cuadro donde el usuario puede introducir texto. La forma en la que se introduce el texto es dependiente del dispositivo. Por ejemplo, los teléfonos que soporten texto predictivo podrán introducir texto de esta forma. Esto se hace de forma totalmente nativa, por lo que desde Java no podremos modificar este método de introducción del texto. Para crear un campo de texto deberemos crear un objeto displayable de la clase TextBox.

El aspecto que mostrará esta pantalla en distintos modelos de móviles será el siguiente:

Figura 4. Cuadros de texto

Listas

Este componente muestra una lista de elementos en la pantalla. Las listas pueden ser de distintos tipos:

Las listas se definen mediante la clase List. A continuación se muestra el aspecto de los distintos tipos de listas existentes:


IMPLÍCITA

EXCLUSIVA

MÚLTIPLE

Figura 5. Tipos de listas

Formularios

Este componente es más complejo, permitiéndonos mostrar varios elementos en una misma pantalla. Los formularios se encapsulan en la clase Form, y los elementos que podemos incluir en ellos son todos derivados de la clase Item. Tenemos disponibles los siguientes elementos:

Figura 6. Jerarquía de los elementos de los formularios

A continuación se muestra el aspecto de un formulario:

Figura 7. Aspecto de los formularios

Alertas

Las alertas son un tipo especial de pantallas, que servirán normalmente de transición entre dos pantallas. En ellas normalmente se muestra un mensaje de información, error o advertencia y se pasa automáticamente a la siguiente pantalla. Las alertas se encapsulan en la clase Alert.

Para mostrar una alerta lo haremos de forma distinta a los componentes que hemos visto anteriormente. En este caso utilizaremos el siguiente método:

mi_display.setCurrent(alerta, siguiente_pantalla);

A continuación podemos ver dos alertas distintas, mostrando mensajes de error, de información y de alarma respectivamente:

Figura 8. Alertas de error, de información y de alarma

Puede ser interesante combinar las alertas con temporizadores para implementar agendas en las que el móvil nos recuerde diferentes eventos mostrando una alerta a una hora determinada, o hacer sonar una alarma, ya que estas alertas nos permiten incorporar sonido.

4.5.4. Comandos de entrada

Hemos visto como crear una serie de componentes de alto nivel para mostrar en nuestra aplicación. Sin embargo no hemos visto como interactuar con las acciones que realice el usuario, para poderles dar una respuesta desde nuestra aplicación.

En estos componentes de alto nivel el usuario podrá interactuar mediante una serie de comandos que podrá ejecutar. Para cada pantalla podremos definir una lista de comandos, de forma que el usuario pueda seleccionar y ejecutar uno de ellos. Esta es una forma de interacción de alto nivel, que se implementará a nivel nativo y que será totalmente portable.

En el móvil estos comandos se encontrarán normalmente en una o en las dos esquinas inferiores, y se podrán activar pulsando sobre el botón situado justo bajo dicha esquina:

Figura 9. Comandos de las pantallas

Según el dispositivo tendremos uno o dos botones de este tipo. Si tenemos varios comandos, al pulsar sobre el botón de la esquina correspondiente se abrirá un menú con todos los comandos disponibles para seleccionar uno de ellos.

Figura 10. Despliegue del menú de comandos

Creación de comandos

Estos comandos se definen mediante la clase Command, y pueden ser creados utilizando el siguiente constructor:

Command c = new Command(etiqueta, tipo, prioridad);

En etiqueta especificaremos el texto que se mostrará en el comando. Los otros dos parámetros se utilizarán para mejorar la portabilidad entre dispositivos. El utilizar un determinado tipo de comando no servirá para que el comando realice una de estas acciones. Las acciones que se realicen al ejecutar el comando las deberemos implementar siempre nosotros. El asignar estos tipos simplemente sirve para que la implementación nativa del dispositivo conozca qué función desempeña cada comando, de forma que los sitúe en el lugar adecuado para dicho dispositivo. Cada dispositivo podrá distribuir los distintos tipos de comandos utilizando diferentes criterios.

Por ejemplo, si en nuestro dispositivo la acción de volver atrás suele asignarse al botón de la esquina derecha, si añadimos un comando de este tipo intentará situarlo en este lugar.

Además les daremos una prioridad con la que establecemos la importancia de los comandos. Esta prioridad es un valor entero, que cuanto menor sea más importancia tendrá el comando. Un comando con prioridad 1 tiene importancia máxima. Primero situará los comandos utilizando el tipo como criterio, y para los comandos con el mismo tipo utilizará la prioridad para poner más accesibles aquellos con mayor prioridad.

Una vez hemos creado los comandos, podemos añadirlos a la pantalla actual utilizando el método:

pantalla.addCommand(c);

Esta pantalla podrá ser cualquier elemento displayable de los que hemos visto anteriormente excepto Alarm. ya que no esta permitido añadir comandos a las alarmas. De esta forma añadiremos todos los comandos necesarios.

Por ejemplo, podemos añadir una serie de comandos a la pantalla de login de nuestra aplicación de la siguiente forma:

TextBox tb = new TextBox("Login", "", 8, TextField.ANY);

Command cmdOK = new Command("OK", Command.OK, 1); Command cmdAyuda = new Command("Ayuda", Command.HELP, 1); Command cmdSalir = new Command("Salir", Command.EXIT, 1); Command cmdBorrar = new Command("Borrar", Command.SCREEN, 1); Command cmdCancelar = new Command("Cancelar", Command.CANCEL, 1);
tb.addCommand(cmdOK); tb.addCommand(cmdAyuda); tb.addCommand(cmdSalir); tb.addCommand(cmdBorrar); tb.addCommand(cmdCancelar);
Display d = Display.getDisplay(this); d.setCurrent(tb);

Listener de comandos

Una vez añadidos los comandos a la pantalla, deberemos definir el código para dar respuesta a cada uno de ellos. Para ello deberemos crear un listener, que es un objeto que escucha las acciones del usuario para darles una respuesta.

El listener será una clase en la que introduciremos el código que queremos que se ejecute cuando el usuario selecciona uno de los comandos. Cuando se pulse sobre uno de estos comandos, se invocará dicho código.

Para crear el listener debemos crear una clase que implemente la interfaz commandListener. El implementar esta interfaz nos obligará a definir el método commandAction, que será donde deberemos introducir el código que dé respuesta al evento de selección de un comando.

class MiListener implements CommandListener {

    public void commandAction(Command c, Displayable d) {

        // Código de respuesta al comando

    }
}

Cuando se produzca un evento de este tipo, conoceremos qué comando se ha seleccionado y en que displayable estaba, ya que esta información se proporciona como parámetros. Según el comando que se haya ejecutado, dentro de este método deberemos decidir qué acción realizar.

Por ejemplo, podemos crear un listener para los comandos añadidos a la pantalla de login del ejemplo anterior:

class ListenerLogin implements CommandListener {

    public void commandAction(Command c, Displayable d) {

        if(c == cmdOK) {
// Aceptar
} else if(c == cmdCancelar) {
// Cancelar
} else if(c == cmdSalir) {
// Salir
} else if(c == cmdAyuda) {
// Ayuda
} else if(c == cmdBorrar) {
// Borrar
}
} }

Una vez creado el listener tendremos registrarlo en el displayable que contiene los comandos para ser notificado de los comandos que ejecute el usuario. Para establecerlo como listener utilizaremos el método setCommandListener del displayable.

Por ejemplo, en el caso del campo de texto de la pantalla de login lo registraremos de la siguiente forma:

tb.setCommandListener(new ListenerLogin());

Una vez hecho esto, cada vez que el usuario ejecute un comando se invocará el método commandAction del listener que hemos definido, indicándonos el comando que se ha invocado.

Listas implícitas

En las listas implícitas dijimos que cuando se pulsa sobre un elemento de la lista se notifica inmediatamente a la aplicación para que se realice la acción correspondiente, de forma que se comporta como un menú.

La forma que tiene de notificarse la selección de un elemento de este tipo de listas es invocando un comando. En este caso se invocará un tipo especial de comando definido como constante en la clase List, se trata de List.SELECT_COMMAND.

Dentro de commandAction podemos comprobar si se ha ejecutado un comando de este tipo para saber si se ha seleccionado un elemento de la lista. En este caso, podremos saber el elemento del que se trata viendo el índice que se ha seleccionado:

class ListenerLogin implements CommandListener {

    public void commandAction(Command c, Displayable d) {

        if(c == List.SELECT_COMMAND) {

int indice = l.getSelectedIndex();
if(indice == 0) {
// Nuevo juego
} else if(indice == 1) {
// Continuar
} else if(indice == 2) {
// Instrucciones
} else if(indice == 3) {
// Hi-score
} else if(indice == 4) {
// Salir
}
}
} }

Listener de items

En el caso de los formularios, podremos tener constancia de cualquier cambio que el usuario haya introducido en alguno de sus campos antes de que se ejecute algún comando para realizar alguna acción.

Por ejemplo, esto nos puede servir para validar los datos introducidos. En el momento que el usuario cambie algún campo, se nos notificará dicho cambio pudiendo comprobar de esta forma si el valor introducido es correcto o no. Además, de esta forma sabremos si ha habido cambios, por lo que podremos volver a grabar los datos del formulario de forma persistente sólo en caso necesario.

Para recibir la notificación de cambio de algún item del formulario, utilizaremos un listener de tipo ItemStateListener, en el que deberemos definir el método itemStateChanged donde introduciremos el código a ejecutar en caso de que el usuario modifique alguno de los campos modificables (cuadros de opciones, campo de texto, campo de fecha o barra de nivel). El esqueleto de un listener de este tipo será el siguiente:

class MiListener implements ItemStateListener {
public void itemStateChanged(Item i) {
// Se ha modificado el item i
}
}

Transiciones entre pantallas

Hemos visto que cada uno de los componentes displayables que tenemos disponibles representa una pantalla, y podemos cambiar esta pantalla utilizando el método setCurrent del display.

De esta forma podremos pasar de una pantalla a otra de la aplicación cuando ocurra un determinado evento, como puede ser por ejemplo que el usuario ejecute un determinado comando o que se ejecute alguna tarea planificada por un temporizador.

Cuando tengamos una aplicación con un número elevado de pantallas, será recomendable hacer previamente un diseño de esta aplicación. Definiremos un diagrama de navegación, en el que cada bloque representará una pantalla, y las flechas que unen dichos bloques serán las transiciones entre pantallas.

Figura 11. Mapa de pantallas

Debemos asegurarnos en este mapa de pantallas que el usuario en todo momento puede volver atrás y que hemos definido todos los enlaces necesarios para acceder a todas las pantallas de la aplicación.

Diseño de pantallas

Es conveniente tomar algún determinado patrón de diseño para implementar las pantallas de nuestra aplicación. Podemos crear una clase por cada pantalla, donde encapsularemos todo el contenido que se debe mostrar en la pantalla, los comandos disponibles, y los listeners que den respuesta a estos comandos.

Las clases implementadas según este patrón de diseño cumplirán lo siguiente:

Por ejemplo, podemos implementar el menú principal de nuestra aplicación de la siguiente forma:

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class MenuPrincipal extends List implements CommandListener {

MiMIDlet owner;
Command selec;
int itemNuevo; int itemSalir;

public MenuPrincipal(MiMIDlet owner) { super("Menu", List.IMPLICIT);
this.owner = owner;

// Añade opciones al menu itemNuevo = this.append("Nuevo juego", null); itemSalir = this.append("Salir", null);

// Crea comandos selec = new Command("Seleccionar", Command.SCREEN, 1); this.addCommand(selec); this.setCommandListener(this); } public void commandAction(Command c, Displayable d) { if(c == selec || c == List.SELECT_COMMAND) {
if(getSelectedIndex() == itemNuevo) {
// Nuevo juego Display display = Display.getDisplay(owner); PantallaJuego pj = new PantallaJuego(owner, this); display.setCurrent(pj);
} else if(getSelectedIndex() == itemSalir) {
// Salir de la aplicación
try {
owner.destroyApp(false);
owner.notifyDestroyed();
} catch(MIDletStateChangeException e) {
// Evitamos salir de la aplicacion
}
}
} } }

Si esta es la pantalla principal de nuestra aplicación, la podremos mostrar desde nuestro MIDlet de la siguiente forma:

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class MiMIDlet extends MIDlet {
protected void startApp() throws MIDletStateChangeException { Display d = Display.getDisplay(this); MenuPrincipal mp = new MenuPrincipal(this); d.setCurrent(mp); }

protected void pauseApp() {
}

protected void destroyApp(boolean incondicional)
throws MIDletStateChangeException {
}
}

Este patrón de diseño encapsula el comportamiento de cada pantalla en clases independientes, lo cual hará más legible y reutilizable el código.

Con este diseño, si queremos permitir volver a una pantalla anterior podemos pasar como parámetro del constructor, además del MIDlet, el elemento displayable correspondiente a esta pantalla anterior. De esta forma cuando pulsemos Atrás sólo tendremos que mostrar este elemento en el display.

4.5.5. Almacenamiento persistente

Muchas veces las aplicaciones necesitan almacenar datos de forma persistente. Cuando realizamos aplicaciones para PCs de sobremesa o servidores podemos almacenar esta información en algún fichero en el disco o bien en una base de datos. Lo más sencillo será almacenarla en ficheros, pero en los dispositivos móviles no podemos contar ni tan solo con esta característica. Aunque los móviles normalmente tienen su propio sistema de ficheros, por cuestiones de seguridad MIDP no nos dejará acceder directamente a él. Es posible que en alguna implementación podamos acceder a ficheros en el dispositivo, pero esto no es requerido por la especificación, por lo que si queremos que nuestra aplicación sea portable no deberemos confiar en esta característica.

Para almacenar datos de forma persistente en el móvil utilizaremos RMS (Record Management System). Se trata de un sistema de almacenamiento que nos permitirá almacenar registros con información de forma persistente en los dispositivos móviles. No se especifica ninguna forma determinada en la que se deba almacenar esta información, cada implementación deberá guardar estos datos de la mejor forma posible para cada dispositivo concreto, utilizando memoria no volátil, de forma que no se pierda la información aunque reiniciemos el dispositivo o cambiemos las baterías. Por ejemplo, algunas implementaciones podrán utilizar el sistema de ficheros del dispositivo para almacenar la información de RMS, o bien cualquier otro dispositivo de memoria no volátil que contenga el móvil. La forma de almacenamiento real de la información en el dispositivo será transparente para los MIDlets, éstos sólo podrán acceder a la información utilizando la API de RMS. Esta API se encuentra en el paquete javax.microedition.rms.

Almacenes de registros

La información se almacena en almacenes de registros (Record Stores), que serán identificados con un nombre que deberemos asignar nosotros. Cada aplicación podrá crear y utilizar tantos almacenes de registros como quiera. Cada almacén de registros contendrá una serie de registros con la información que queramos almacenar en ellos.

Los almacenes de registros son propios de la suite. Es decir, los almacenes de registro creados por un MIDlet dentro de una suite, serán compartidos por todos los MIDlets de esa suite, pero no podrán acceder a ellos los MIDlets de suites distintas. Por seguridad, no se permite acceder a recursos ni a almacenes de registros de suites distintas a la nuestra.

Figura 12. Acceso a los almacenes de registros

Cada suite define su propio espacio de nombres. Es decir, los nombres de los almacenes de registros deben ser únicos para cada suite, pero pueden estar repetidos en diferentes suites. Como hemos dicho antes, nunca podremos acceder a un almacén de registros perteneciente a otra suite.

Abrir el almacén de registros

Lo primero que deberemos hacer es abrir o crear el almacén de registros. Para ello utilizaremos el siguiente método:

RecordStore rs = RecordStore.openRecordStore(nombre, true);

Con el segundo parámetro a true estamos diciendo que si el almacén de registros con nombre nombre no existiese en nuestra suite lo crearía. Si por el contrario estuviese a false, sólo intentaría abrir un almacén de registros existente, y si éste no existe se producirá una excepción RecordStoreNotFoundException.

El nombre que especificamos para el almacén de registros deberá se un nombre de como mucho 32 caracteres codificado en Unicode.

Una vez hayamos terminado de trabajar con el almacén de registros, podremos cerrarlo con:

rs.close();

Listar los almacenes de registros

Si queremos ver la lista completa de almacenes de registros creados dentro de nuestra suite, podemos utilizar el siguiente método:

String [] nombres = RecordStore.listRecordStores();

Esto nos devolverá una lista con los nombres de los almacenes de registros que hayan sido creados. Teniendo estos nombres podremos abrirlos como hemos visto anteriormente para consultarlos, o bien eliminarlos.

Eliminar un almacén de registros

Podemos eliminar un almacén de registros existente proporcionando su nombre, con:

RecordStore.deleteRecordStore(nombre); 

Propiedades de los almacenes de registros

Los almacenes de registros tienen una serie de propiedades que podemos obtener con información sobre ellos. Una vez hayamos abierto el almacén de registros para trabajar con él, podremos obtener los valores de las siguientes propiedades:

String nombre = rs.getName();
long timestamp = rs.getLastModified();
int version = rs.getVersion();
int tam = rs.getSize();
int libre = rs.getSizeAvailable();

Registros

El almacén de registros contendrá una serie de registros donde podemos almacenar la información. Podemos ver el almacén de registros como una tabla en la que cada fila corresponde a un registro. Los registros tienen un identificador y un array de datos.

Identificador Datos
1 array de datos ...
2 array de datos ...
3 array de datos ...
... ...

Estos datos de cada registro se almacenan como un array de bytes. Podremos acceder a estos registros mediante su identificador o bien recorriendo todos los registros de la tabla.

Cuando añadamos un nuevo registro al almacén se le asignará un identificador una unidad superior al identificador del último registro que tengamos. Es decir, si añadimos dos registros y al primero se le asigna un identificador n, el segundo tendrá un identificador n+1.

Las operaciones para acceder a los datos de los registros son atómicas, por lo que no tendremos problemas cuando se acceda concurrentemente al almacén de registros.

Almacenar información

Tenemos dos formas de almacenar información en el almacén de registros. Lo primero que deberemos hacer en ambos casos es construir un array de bytes con la información que queramos añadir. Para hacer esto podemos utilizar un flujo DataOutputStream, como se muestra en el siguiente ejemplo:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);

dos.writeUTF(nombre);
dos.writeInt(edad);

byte [] datos = baos.toByteArray();

Una vez tenemos el array de datos que queremos almacenar, podremos utilizar uno de los siguientes métodos del objeto almacén de datos:

int id = rs.addRecord(datos, 0, datos.length);
rs.setRecord(id, datos, 0, datos.length);

En el caso de addRecord, lo que se hace es añadir un nuevo registro al almacén con la información que hemos proporcionado, devolviéndonos el identificador id asignado al registro que acabamos de añadir.

Con setRecord lo que se hace es sobrescribir el registro correspondiente al identificador id indicado con los datos proporcionados. En este caso no se añade ningún registro nuevo, sólo se almacenan los datos en un registro ya existente.

Leer información

Si tenemos el identificador del registro que queremos leer, podemos obtener su contenido como array de bytes directamente utilizando el método:

byte [] datos = rs.getRecord(id);

Si hemos codificado la información dentro de este registro utilizando un flujo DataOutputStream, podemos descodificarlo realizando el proceso inverso con un flujo DataInputStream:

ByteArrayInputStream bais = new ByteArrayInputStream(datos);
DataInputStream dis = DataInputStream(bais);

String nombre = dis.readUTF();
String edad = dis.readInt();

dis.close();

Borrar registros

Podremos borrar un registro del almacén a partir de su identificador con el siguiente método:

rs.deleteRecord(id);

Almacenar y recuperar objetos

Si hemos definido una forma de serializar los objetos, podemos aprovechar esta serialización para almacenar los objetos de forma persistente en RMS y posteriormente poder recuperarlos.

Navegar en el almacén de registros

Si no conocemos el identificador del registro al que queremos acceder, podremos recorrer todos los registros del almacén utilizando un objeto RecordEnumeration. Para obtener la enumeración de registros del almacén podemos utilizar el siguiente método:

RecordEnumeration re = rs.enumerateRecords(null, null, false);

Con los dos primeros parámetros podremos establecer la ordenación y el filtrado de los registros que se enumeren. Podemos dejarlo a null para obtener la enumeración con todos los registros y en un orden arbitrario. Esta es la forma más eficiente de acceder a los registros.

El tercer parámetro nos dice si la enumeración debe mantenerse actualizada con los registros que hay realmente almacenados, o si por el contrario los cambios que se realicen en el almacén después de haber obtenido la enumeración no afectarán a dicha enumeración. Será más eficiente establecer el valor a false para evitar que se tenga que mantener actualizado, pero esto tendrá el inconveniente de que puede que alguno de los registros de la enumeración se haya borrado o que se hayan añadido nuevos registros que no constan en la enumeración. En al caso de que especifiquemos false para que no actualice automáticamente la enumeración, podremos forzar manualmente a que se actualice invocando el método rebuild de la misma, que la reconstruirá utilizando los nuevos datos.

Para recorrer la enumeración de registros tendremos un cursor que en cada momento estará en uno de los elementos de la enumeración. Podremos mover este cursor de forma bidireccional.

Para pasar al siguiente registro de la enumeración y obtener sus datos utilizaremos el método nextRecord. Podremos saber si existe un siguiente registro llamando a hasNextElement. Nada más crear la enumeración el cursor no se encontrará en ninguno de los registros. Cuando llamemos a nextRecord por primera vez se situará en el primer registro y nos devolverá su array de datos. De esta forma podremos seguir recorriendo la enumeración mientras haya más registros. Un bucle típico para hacer este recorrido es el siguiente:

while(re.hasNextElement()) {
byte [] datos = re.nextRecordId();
// Procesar datos obtenidos
...
}

Hemos dicho que el recorrido puede ser bidireccional. Por lo tanto, tenemos un método previousRecord que moverá el cursor al registro anterior devolviéndonos su contenido. De la misma forma, tenemos un método hasPreviousElement que nos dirá si existe un registro anterior. Si invocamos previousRecord nada más crear la enumeración, cuando el cursor todavía no se ha posicionado en ningún registro, moverá el cursor al último registro de la enumeración devolviéndonos su resultado. Podemos también volver al estado inicial de la enumeración en el que el cursor no apunta a ningún registro llamando a su método reset.

En lugar de obtener el contenido de los registros puede que nos interese obtener su identificador, de forma que podamos eliminarlos o hacer otras operaciones con ellos. Para ello tenemos los métodos nextRecordId y previousRecordId, que tendrán el mismo comportamiento que nextRecord y previousRecord respectivamente, salvo porque devuelven el identificador de los registros recorridos, y no su contenido.

4.5.6. Red y E/S

En J2SE tenemos una gran cantidad de clases en el paquete java.net para permitir establecer distintos tipos de conexiones en red. Sin embargo, el soportar esta gran API no es viable en la configuración CLDC dedicada a dispositivos muy limitados. Por lo tanto en CLDC se sustituye esta API por el marco de conexiones genéricas (GCF, Generic Connection Framework), con el que se pretenden cubrir todas las necesidades de conectividad de estos dispositivos a través de una API sencilla.

Marco de conexiones genéricas

Los distintos dispositivos móviles pueden utilizar distintos tipos de redes para conectarse. Algunos utilizan redes de conmutación de circuitos, orientadas a conexión, que necesitarán protocolos como TCP. Otros utilizan redes de transmisión de paquetes en las que no se establece una conexión permanente, y con las que deberemos trabajar con protocolos como por ejemplo UDP. Incluso otros dispositivos podrían utilizar otras redes distintas en las que debamos utilizar otro tipo de protocolos.

El marco de conexiones genéricas (GCF) hará que esta red móvil subyacente sea transparente para el usuario, proporcionando a éste protocolos estándar de comunicaciones. La API de GCF se encuentra en el paquete javax.microedition.io. Esta API utilizará un único método que nos servirá para establecer cualquier tipo de conexión que queramos, por esta razón recibe el nombre de marco de conexiones genéricas, lo cuál además lo hace extensible para incorporar nuevos tipos de conexiones. Para crear la conexión utilizaremos el siguiente método:

Connection con = Connector.open(url);

En el que deberemos especificar una URL como parámetro con el siguiente formato:

protocolo:direccion;parámetros

Cambiando el protocolo podremos especificar distintos tipos de conexiones. Por ejemplo, podríamos utilizar las siguientes URLs:

"http://j2ee.ua.es/pdm" Abre una conexión HTTP.
"datagram://192.168.0.4:6666" Abre una conexión por datagramas.
"socket://192.168.0.4:4444" Abre una conexión por sockets.
"comm:0;baudrate=9600" Abre una conexión a través de un puerto de comunicaciones.
"file:/fichero.txt" Abre un fichero.

Cuando especifiquemos uno de estos protocolos, la clase Connector buscará en tiempo de ejecución la clase que implemente dicho tipo de conexión, y si la encuentra nos devolverá un objeto que implemente la interfaz Connection que nos permitirá comunicarnos a través de dicha conexión.

CLDC nos proporciona interfaces para cada tipo genérico de conexión, pero las implementaciones reales de los protocolos pertenecen a los perfiles.

Figura 13. Componentes de GCF

El único protocolo que la especificación de MIDP exige que se implemente es el protocolo HTTP. Este protocolo pertenece a MIDP, y no a CLDC como era el caso de las clases genéricas anteriores. Distintos modelos de dispositivos pueden soportar otro tipo de conexiones, pero si queremos hacer aplicaciones portables deberemos utilizar HTTP.

Conexión HTTP

La conexión mediante el protocolo HTTP es el único tipo de conexión que sabemos que va a estar soportado por todos los dispositivos MIDP. Este protocolo podrá ser implementado en cada modelo de móvil bien utilizando protocolos IP como TCP/IP o bien protocolos no IP como WAP o i-Mode.

Figura 14. Gateway para protocolos no IP

De esta forma nosotros podremos utilizar directamente HTTP de una forma estándar sin importarnos el tipo de red que el móvil tenga por debajo.

Cuando establezcamos una conexión mediante protocolo HTTP, podemos hacer una conversión cast del objeto Connection devuelto a un subtipo HttpConnection especializado en conexiones HTTP:

HttpConnection con = 
    (HttpConnection)Connector.open("http://j2ee.ua.es/datos.txt");

Este objeto HttpConnection contiene gran cantidad de métodos dedicados a trabajar con el protocolo HTTP, lo cuál facilitará en gran medida el trabajo de los desarrolladores.

HTTP es un protocolo de petición/respuesta. El cliente crea un mensaje de petición y lo envía a una determinada URL. El servidor analizará esta petición y le devolverá una respuesta al cliente. Estos mensajes de petición y respuesta se compondrán de una serie de cabeceras y del bloque de contenido. Cada cabecera tendrá un nombre y un valor. El contenido podrá contener cualquier tipo de información (texto, HTML, imágenes, mensajes codificados en binario, etc). Tendremos una serie de cabeceras estándar con las que podremos intercambiar datos sobre el cliente o el servidor, o bien sobre la información que estamos transmitiendo. También podremos añadir nuestras propias cabeceras para intercambiar datos propios.

Trabajaremos con este objeto HttpConnection de forma muy parecida a como trabajabamos con URLConnection en J2SE, más concretamente con HttpURLConnection.

Al igual que URLConnection, HttpConnection pasará por las fases de configuración, conectado y cerrado. Nada más crear la conexión se encontrará en estado de configuración, y en el momento que solicitemos alguna información sobre la respuesta pasará automáticamente a estado conectado.

Durante el estado de configuración podremos establecer las propiedades del mensaje de petición. Abriendo flujos de entrada o salida para recibir o enviar información al servidor, o leyendo la información de las cabeceras de la respuesta provocaremos el paso a estado conectado. Todas estas operaciones se harán prácticamente de la misma forma que con el objeto URLConnection.

En este caso para enviar información a la URL podremos escribir en el flujo de salida directamente, sin necesidad de activar la salida previamente con setDoOutput.

Lectura de la respuesta

Vamos a comenzar viendo cómo leer el contenido de una URL. En este caso no vamos a añadir ninguna información al mensaje de petición, ya que no es necesario. Sólo queremos obtener el contenido del recurso solicitado en la URL.

Una vez hayamos creado la conexión con la URL como hemos visto anteriormente, abriremos un flujo de entrada para leer su contenido de la siguiente forma:

InputStream in = con.openInputStream();

Una vez hecho esto, la conexión pasará a estado conectada, ya que estamos solicitando leer su contenido. Por lo tanto en este momento será cuando envíe el mensaje de petición al servidor, y se quede esperando a recibir la respuesta. Con el flujo de datos obtenido podremos leer el contenido de la misma, al igual que leemos cualquier otro flujo de datos en Java.

Dado que en este momento ya se ha enviado el mensaje de petición, ya no tendrá sentido realizar modificaciones en la petición. Es por esta razón por lo que la creación del mensaje de petición debe hacerse en el estado de configuración.

Una vez hayamos terminado de leer la respuesta, deberemos cerrar el flujo y la conexión:

in.close();
con.close();

Con esto la conexión pasará a estado cerrada, liberando todos los recursos.

Método de petición

En el caso del protocolo HTTP, tenemos disponibles diferentes métodos para realizar la petición. Desde MIDP podremos utilizar los siguientes tipos:

HttpConnection.GET

Los datos que se envían al servidor se incluyen en la misma URL. Por ejemplo, podemos mandar un parámetro login en la petición de la siguiente forma:
http://j2ee.ua.es/pdm?login=miguel

HttpConnection.POST Los datos que se envían al servidor se incluyen como contenido del mensaje. Tiene la ventaja de que se puede enviar la cantidad de datos que queramos, a diferencia del método GET en el que esta cantidad puede estar limitada. Además los datos no serán visibles en la misma URL, ya que se incluyen como contenido del mensaje.
HttpConnection.HEAD No se solicita el contenido del recurso al servidor, sólo información sobre éste, es decir, las cabeceras HTTP.

Podemos establecer uno de estos tipos utilizando el método setRequestMethod, por ejemplo para utilizar una petición POST haremos lo siguiente:

con.setRequestMethod(HttpConnection.POST);

Mensajes POST

Si utilizamos una petición POST, deberemos incluir la información que enviemos al servidor como contenido del mensaje.

Deberemos especificar el tipo MIME del contenido que vamos a añadir. Esto lo haremos mediante una cabecera estándar de HTTP. Por ejemplo, si añadimos texto ASCII, podemos establecer la siguiente cabecera:

con.setRequestProperty("Content-Type", "text/plain");

Para escribir en el contenido del mensaje de petición deberemos abrir un flujo de salida de la siguiente forma:

OutputStream out = con.openOutputStream();

Podremos escribir en este flujo de salida igual que lo hacemos en cualquier otro flujo de salida, con lo que de esta forma podremos escribir cualquier contenido en el mensaje de petición.

Al abrir el flujo para escribir en la petición provocaremos que se pase a estado conectado. Por lo tanto deberemos haber establecido el tipo de petición y todas las cabeceras previamente a la apertura de este flujo, cuando todavía estábamos en estado de configuración.

Estado de la respuesta

En estado conectado, además del contenido del mensaje de la respuesta, podemos obtener el estado de la respuesta y sus cabeceras. Los estados de respuesta se componen de un código y un mensaje y nos permitirán saber si la petición ha podido atenderse correctamente o si por el contrario ha habido algún tipo de error. Por ejemplo, posibles estados son:

HttpConnection.HTTP_OK 200 OK
HttpConnection.HTTP_BAD_REQUEST 400 Bad Request
HttpConnection.HTTP_INTERNAL_ERROR 500 Internal Server Error

Este mensaje de estado encabeza el mensaje de respuesta. Si el servidor nos devuelve un mensaje con código 200 como el siguiente:

HTTP/1.1 200 OK

Es que se ha procesado correctamente la petición y nos devuelve su respuesta. Si ha ocurrido un error, nos mandará el código y mensaje de error correspondiente. Por ejemplo, el error 400 indica que el servidor no ha entendido la petición que hemos hecho, posiblemente porque la hemos escrito incorrectamente. El error 500 nos dice que se trata de un error interno del servidor, no de la petición realizada.

Podemos obtener tanto el código como el mensaje de estado con los siguientes métodos:

int cod = con.getResponseCode(); 
String msg = con.getResponseMessage();

Los códigos de estado podemos encontrarlos como constantes de la clase HttpConnection como hemos visto para los tres códigos anteriores.

Ejemplo

Por ejemplo, si queremos leer un recurso datos.txt que localizamos mediante la URL http://j2ee.ua.es/datos.txt, podremos hacerlo de la siguiente forma:

// Creamos la conexión
HttpConnection con = 
(HttpConnection)Connector.open("http://j2ee.ua.es/datos.txt");
// Establecemos el método de la petición
con.setRequestMethod(HttpConnection.GET);

// Establecemos las cabeceras de la petición con.setRequestProperty("User-Agent", "Profile/MIDP-1.0 Configuration/CLDC-1.0"); // Comprobamos estado (pasa a estado conectado) if (con.getResponseCode() == HttpConnection.HTTP_OK) {

// Leemos cabeceras de la respuesta
String tipo = con.getHeaderField("content-type"));
// Lee el contenido InputStream in = con.openInputStream();
leerDatos(in);

} else {
mostrarError();
}

Si necesitamos enviar datos al servidor, podemos incorporar estos datos como parámetros en la URL o enviarlos como contenido del mensaje de petición, escribiendo este contenido en el flujo de salida de la conexión:

// Creamos la conexión
HttpConnection con = 
(HttpConnection)Connector.open("http://j2ee.ua.es/publica");
// Establecemos el método de la petición
con.setRequestMethod(HttpConnection.POST);

// Establecemos las cabeceras de la petición con.setRequestProperty("User-Agent", "Profile/MIDP-1.0 Configuration/CLDC-1.0"); // Escribimos el contenido de la petición
OutputStream out = con.openOutputStream();
escribirMensaje(out);

// Comprobamos estado (pasa a estado conectado) if (con.getResponseCode() == HttpConnection.HTTP_OK) {
// Leemos la respuesta
...
} else {
mostrarError();
}

Front-ends de aplicaciones corporativas

El poder establecer conexiones en red nos permitirá acceder a aplicaciones web corporativas desde el móvil. De esta forma, podremos hacer que estos dispositivos móviles se comporten como front-end de estas aplicaciones corporativas.

Desde los PCs de sobremesa normalmente accedemos a estas aplicaciones utilizando un navegador web. La aplicación web genera de forma dinámica la presentación en el servidor en forma de un documento HTML que será mostrado en los navegadores de los clientes. Podemos aplicar este mismo sistema al caso de los móviles, generando nuestra aplicación web la respuesta en forma de algún tipo de documento que pueda ser interpretado y mostrado en un navegador de teléfono móvil. Por ejemplo estos documentos pueden estar en formato WML, cHTML o XHTML. Esto puede ser suficiente para acceder a algunas aplicaciones desde los móviles.

Utilizar J2ME para realizar este front-end aporta una serie de ventajas sobre el paradigma anterior, como por ejemplo las siguientes:

Normalmente será preferible utilizar HTTP a sockets o datagramas porque esto nos aportará una serie de ventajas. Por un lado, HTTP está soportado por todos los dispositivos MIDP. Al utilizar HTTP tampoco tendremos problema con firewalls intermedios, cosa que puede ocurrir si conectamos por sockets mediante un puerto que esté cerrado. Además las APIs de Java incluyen facilidades para trabajar con HTTP, por lo que será sencillo realizar la comunicación tanto en el cliente como en el servidor.

La conexión de red en los móviles normalmente tiene una alta latencia, un reducido ancho de banda y es posible que se produzcan interrupciones cuando la cobertura es baja. Deberemos tener en cuenta todos estos factores cuando diseñemos nuestra aplicación. Por esta razón deberemos minimizar la cantidad de datos que se intercambian a través de la red, y permitir que la aplicación pueda continuar trabajando correctamente sin conexión.

Optimizaciones en el cliente

Para reducir el número de datos que se envían por la red, evitando que se hagan conexiones innecesarias, es conveniente realizar una validación de los datos introducidos por el usuario en el cliente. Normalmente no podremos validarlos de la misma forma en que se validan en el servidor, ya que por ejemplo no tenemos acceso a las bases de datos de la aplicación, por lo que deberá volverse a validar por el servidor para realizar la validación completa. No obstante, es conveniente realizar esta validación en el cliente como una prevalidación, de forma que detecte siempre que sea posible los datos erróneos en el lado del cliente evitando así realizar una conexión innecesaria con el servidor.

Dado que la red es lenta, las operaciones que necesiten conectarse a la red serán costosas. Estas operaciones será conveniente que sean ejecutadas por hilos en segundo plano, y nunca deberemos establecer una conexión desde un callback. Además siempre que sea posible deberemos mostrar una barra de progreso mientras se realiza la operación, de forma que el usuario tenga constancia de que se está haciendo algo. También será conveniente permitir al usuario que interrumpa estas largas operaciones, siempre que la interrupción pueda hacerse y no cause inconsistencias en los datos.

Un aspecto interesante en los clientes J2ME es la posibilidad de incorporar personalización. La personalización consiste en recordar los datos y las preferencias del usuario, de forma que la aplicación se adapte a estas preferencias y el usuario no tenga que introducir estos datos en cada sesión. Podremos pedir esta información de personalización la primera vez que ejecuta la aplicación y almacenar esta información utilizando RMS o bien registrarla en el servidor de forma remota. Si tuviésemos esta información por duplicado, en local y en remoto, deberemos proporcionar mecanismos para sincronizar ambos registros.

Codificación de los datos

Entra la aplicación J2ME y el servidor deberán intercambiarse datos con una determinada codificación. Podemos movernos entre dos extremos: la codificación de los datos en binario y la codificación en XML.

La codificación binaria de los datos será eficiente y compacta. Será sencillo codificar información en este formato utilizando los objetos DataOutputStream y ByteArrayOutputStream. Tiene el inconveniente de que tanto el cliente como el servidor deberán conocer cómo está codificada la información dentro del array de bytes, por lo que estos componentes estarán altamente acoplados.

Si hemos definido una serialización para los objetos, podemos aprovechar esta serialización para enviarlos a través de la red. En este caso la serialización la hemos definido manualmente nosotros en un método del objeto, y no se hace automáticamente como en el caso de J2SE, por lo que deberemos tener cuidado de que en el objeto del cliente y en el del servidor se serialice y deserialice de la misma forma. Además, al transferir un objeto entre J2ME y J2EE deberemos asegurarnos de que este objeto utiliza solamente la parte común de la API de Java en ambas plataformas.

En el otro extremo, XML es un lenguaje complejo de analizar y la información ocupa más espacio. Como ventaja tenemos que XML es un lenguaje estándar y autodescriptivo, por lo que reduciremos el acoplamiento de cliente y servidor. Aunque en MIDP no se incluyen librerías para procesar XML, diversos fabricantes proporcionan sus propias implementaciones de las librerías de XML para J2ME. Podemos utilizar estas librerías para crear y analizar estos documentos en los clientes móviles.