Parte 2: Android + AppEngine + Cloud Datastore

En la parte 1 se preparó una aplicación Android básica. Además se generó una aplicación web como backend con un endpoint para usuario. En esta segunda parte se terminará el endpoint de salvar notas (de manera muy breve) y se pasará a integra la aplicación Android con los dos endpoints.

La Parte 1 esta aqui.

Endpoint de Notas

Siguiendo los pasos de la creación del endpoint User, se creará el endpoint para salvar notas. Así, se creara la clase Note

public class Note {

    private Long mId;
    private String mContent;
    private Long mUserId;
    private Date mDate;

    public Long getId() {
        return mId;
    }

    public void setId(Long id) {
        mId = id;
    }

    public String getContent() {
        return mContent;
    }

    public void setContent(String content) {
        mContent = content;
    }

    public Long getUserId() {
        return mUserId;
    }

    public void setUserId(Long userId) {
        mUserId = userId;
    }

    public Date getDate() {
        return mDate;
    }

    public void setDate(Date date) {
        mDate = date;
    }
}

Además se creará la clase NoteEndpoint que recibirá las solicitudes de manipulación de notas, similar a usuario, obtener una nota, insertar nota y listar notas.

/** An endpoint class we are exposing */
@Api(name = "noteEndpoint", version = "v1", namespace = @ApiNamespace(ownerDomain = "savenotes.backend.fr4gus.com", ownerName = "savenotes.backend.fr4gus.com", packagePath=""))
public class NoteEndpoint {

    // TODO in memory implementation to test endpoints
    static Map<Long, Note> mLocalNotes = new HashMap<Long, Note>();
    static long mLastId = 0;

    private static final Logger LOG = Logger.getLogger(NoteEndpoint.class.getName());

    /**
     * This method gets the <code>Note</code> object associated with the specified <code>id</code>.
     * @param id The id of the object to be returned.
     * @return The <code>Note</code> associated with <code>id</code>.
     */
    @ApiMethod(name = "getNote")
    public Note getNote(@Named("id") Long id) {
        LOG.info("Calling getNote method");
        return mLocalNotes.get(id);
    }

    /**
     * This inserts a new <code>Note</code> object.
     * @param note The object to be added.
     * @return The object to be added.
     */
    @ApiMethod(name = "insertNote")
    public Note insertNote(Note note) {
        LOG.info("Calling insertNote method");

        synchronized (mLocalNotes) {
            mLastId++;
            mLocalNotes.put(mLastId,note);
            note.setId(mLastId);
        }

        return note;
    }

    /**
     * This returns all stored <code>Note</code> objects stored in the service
     * @return a list of all notes
     */
    @ApiMethod(name = "listNotes", path = "note/list")
    public List<Note> listNotes() {
        ArrayList<Note> notes = new ArrayList<Note>();
        notes.addAll(mLocalNotes.values());
        return notes;
    }
}

Se actualiza web.xml para agregar la clase de NoteEnpoint

   <servlet>
        <servlet-name>SystemServiceServlet</servlet-name>
        <servlet-class>com.google.api.server.spi.SystemServiceServlet</servlet-class>
        <init-param>
            <param-name>services</param-name>
            <param-value>com.fr4gus.backend.savenotes.UserEndpoint, com.fr4gus.backend.savenotes.NoteEndpoint</param-value>
        </init-param>
    </servlet>

Integración Android + GAE

Antes de poder utilizar las clases de la librería cliente para los endpoints. Es necesario primero agregar dependencias a nuestro proyecto. En este link, se muestra los pasos necesarios para agregar las dependencias.

También es posible obtener los pasos, al crear las librerías cliente, ya que vienen con una documentación. Aquí tienen un resumen de los cambios:

En build.gradle

repositories {
    mavenCentral()
    mavenLocal() // Agregar el repo local de maven
}

dependencies {
    compile ([group: 'com.google.api-client', name: 'google-api-client-android', version: '1.18.0-rc'])
    // Si quieren usar Gson pongan este
    compile ([group: 'com.google.api-client', name: 'google-http-client-gson', version: '1.18.0-rc'])

    // Si quieren usar Jackson pongan este
    compile ([group: 'com.google.api-client', name: 'google-http-client-jackson2', version: '1.18.0-rc'])

}

Si no quieren usar ni Gson ni Jackson, pueden importar AndroidJsonFactory si su proyecto tienen un API level mínimo de 11, importando: import com.google.api.client.extensions.android.json.AndroidJsonFactory;

Se recomienda ver la documentación, ya que esta cuenta con información de como agregar esas dependencias de manera más óptima, eliminando ciertos paquetes que no se ocupan.

Se creó una clase Application para poder guardar el usuario actual (usando el patrón Singleton)

public class SaveNotesApplication extends Application {
    static SaveNotesApplication sInstance;

    User mCurrentUser;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
    }

    public static SaveNotesApplication getInstance() {
        return sInstance;
    }

    public User getCurrentUser() {
        return mCurrentUser;
    }

    public void setCurrentUser(User currentUser) {
        mCurrentUser = currentUser;
    }
}

Con esto podemos acceder al usuario actual desde cualquier parte de la aplicación. Importante recordar que se ocupa actualizar el archivo AndroidManifest.xml en el tag <application> y agregar el atributo android:name con el valor .SaveNotesApplication.

Antes de poder enviar la nota se ocupa obtener el usuario primero. Para ello se creará una actividad padre, para que todas las actividades que hereden de ella puedan acceder al usuario o en su defecto, solicitarlo al servicio getUser si ya está registrado o insertUser si no ha sido registrado aún.

public class BaseActivity extends ActionBarActivity {
    public static final String TAG = "SaveNotes";

    public static final String APP_NAME = SaveNotesApplication.getInstance().getString(R.string.app_name);
    public static final String TEST_ROOT_URL = "http://192.168.1.140:8080/_ah/api";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // DeviceUtil.getDeviceID simplemente llama a Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID) 
        final String deviceId = DeviceUtil.getDeviceId(this);

        User currentUser = SaveNotesApplication.getInstance().getCurrentUser();
        if (currentUser == null) {
            new AsyncTask<String,Void,Boolean>(){
                @Override
                protected Boolean doInBackground(String... params) {
                    UserEndpoint.Builder userBuilder = new UserEndpoint.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(),null);
                    userBuilder.setApplicationName(APP_NAME);
                    userBuilder.setRootUrl(TEST_ROOT_URL);
                    userBuilder..setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
                        @Override
                        public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest) throws IOException {
                            abstractGoogleClientRequest.setDisableGZipContent(true);
                        }
                    });
                    UserEndpoint userService = userBuilder.build();

                    try {
                        User user = userService.findUser(deviceId).execute();
                        SaveNotesApplication.getInstance().setCurrentUser(user);
                    } catch (IOException e) {
                        Log.e(TAG,e.getMessage(),e);
                        return false;
                    }

                    return true;
                }
            }.execute(deviceId);
        }
    }
}

En la línea 18 se nota la utilización del patrón Builder, con el cual se “configura” la instancia para acceder al endpoint de usuario.

En la línea 19 se establece el nombre de la aplicación. Este valor va como una cabecera en el request http, y permite mantener estadisticas de que tipo de cliente está utilizando los endpoints. Esto es importante cuando se tiene una version web u en otra plataforma como iOS del mismo cliente.

En la línea 20 se configura, para efectos de pruebas locales, la dirección del equipo donde se correrá una version local del servicio de App Engine. Esto es útil para las pruebas locales, pues por defecto intentará contactar a los servicios hospedados en la infraestructura de Google. Es de suma importancia aclarar que por defecto, el servidor se levanta utilizando “localhost” (127.0.0.1) por lo que solo escuchará request del mismo loop. Como vamos a querer acceder al servicio local desde otros equipos desde la misma red, es necesario modificar el build.gradle del módulo de backend para que escuche solicitudes de otros equipos. Esto se hace de manera sencilla agregando un valor nuevo al enclosure “appengine”. Se debe agregar httpAddress = ’0.0.0.0′ por lo que el enclosure se verá similar a este:

appengine {
  httpAddress = '0.0.0.0'
  downloadSdk = true
  appcfg {
    oauth2 = true
  }
  endpoints {
    getClientLibsOnBuild = true
    getDiscoveryDocsOnBuild = true
  }
}

De la línea 21 a la 26, solo para pruebas locales, se configura el GoogleClientRequest, para deshabilitar la compresión de las solicitudes usando GZip. Esto por que la instancia local no maneja GZip y si ejecutan el cliente sin eso, les va a dar un EOFException en el servidor.

Si notaron, hay un método nuevo llamado UserEndpoint.findUser(String s). Este metodo se agregó para poder buscar el usuario con el deviceId, y así ver si el dispositivo está registrado. Si no está registrado, lo registra inmediatamente, ahorrando un llamado extra al endpoint.

Ya con el usuario disponible, se guarda en la clase Application y se puede continuar con el flujo normal. Si el usuario ya se encontraba asignado en la aplicación, simplemente no se llama al servicio.

A la clase NoteActivity hay que actualizarle el método OnClickListener para que llame al servicio de insertar notas. Nótese que se configura el Builder del endpoint de manera similar al de Usuario.

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.action_save) {
            String noteContent = mNoteContent.getText().toString();

            // Do nothing if content is empty
            if (TextUtils.isEmpty(noteContent))
                return;

            new AsyncTask<String, Void, Boolean>() {
                @Override
                protected Boolean doInBackground(String... params) {
                    String content = params[0];

                    NoteEndpoint.Builder noteBuilder = new NoteEndpoint.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null);

                    noteBuilder.setApplicationName(APP_NAME);

                    //TODO Use only for local test. Remove for production
                    noteBuilder.setRootUrl(TEST_ROOT_URL);
                    noteBuilder.setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
                        @Override
                        public void initialize(AbstractGoogleClientRequest<?> abstractGoogleClientRequest) throws IOException {
                            abstractGoogleClientRequest.setDisableGZipContent(true);
                        }
                    });

                    NoteEndpoint noteService = noteBuilder.build();

                    Note note = new Note();
                    note.setContent(content);

                    User currentUser = SaveNotesApplication.getInstance().getCurrentUser();
                    note.setUserId(currentUser.getId());
                    try {
                        noteService.insertNote(note).execute();
                    } catch (IOException e) {
                        Log.e(TAG, e.getMessage(), e);
                        return false;
                    }
                    return true;
                }

                @Override
                protected void onPostExecute(Boolean result) {
                    if (result){
                        mNoteContent.setText("");
                    } else {
                        Toast.makeText(NotesActivity.this, R.string.error_send_note, Toast.LENGTH_SHORT).show();
                    }
                }
            }.execute(noteContent);

        }
    }

Así de simple se llaman a los endpoints de un API implementado con Google App Engine. Simplemente se utiliza el Builder para crear la instancia del cliete del endpoint y luego se llama al método, pasándole los parámetros necesarios y luego llamando al método execute. Esto último se debe hacer en un hilo aparte al del UI por que lo bloquearía, razón por la cual en este ejemplo se utilzó un AsyncTask.

Siguientes pasos

Si bien con esto se completaría la integración de una aplicación Android con un API implementado en Google App Engine, creo conveniente explicar en la tercera y última parte de éste tutorial, como utilizar Objectify para poder guardar los datos en el Datastore. Principalmente por que me parece interesante la librería que facilita enormemente el acceso al Datastore, y así cerrar el ejemplo con algo funcional.

Pueden acceder al código fuente del ejemplo completo en Github: https://github.com/fr4gus/SaveNotes

Parte 1: Android + AppEngine + Cloud Datastore

Introducción

La aplicaciones móviles van evolucionando y ahora más que nunca deben ofrecer valor agregado con la utilización de la nube o inclusive delegar a servicios externos tareas más complejas. Actualmente existen diversas opciones para proveer a las aplicaciones de poder computacional en la nube y en este tutorial se utilizarán las herramientas que Google facilita: Google App Engine y Cloud Datastore.

Google App Engine

Antes del boom de los servicios en “la nube”  los desarrolladores o las empresas requerían mantener su propia infraestructura. Ahora existen muchas compañias que ofrecen su propia infraestrucutra y proveen de diferentes frameworks para poder instalar aplicaciones sin preocuparse del como.

Google ofrece su infraestrucura a través de App Engine que es una plataforma como servicio (PaaS del inglés Platform as a Serviced) que permite construir aplicaciones y correrlas en dicha infraestrucutra. GAE se encuentra disponible en los sabores: Java, Python, PHP y Go. El servicio es gratuito hasta cierta “cuota”. Las cuotas se cuentan por “solicitudes por día”, y en el momento que su aplicación sobrepase dicha cuota, deberá pagar. Para más informacion sobre GAE puede consultar la documentación oficial (1).

Cloud Data

Google ofrece 3 servicios para almacenamiendo de datos. La primera es Cloud SQL que es prácticamente un MySQL pero hospedado en la infraestructura de Google. La segunda Google Storage que es más bien para almacenamiento de archivos (tipo Drive) y finalmente Cloud Datastore para almacenar datos no relacionales (NoSQL).

El propósito de este tutorial es condensar la información de diferentes fuentes y aglomerar los pasos necesarios para poder proveer de backed a una aplicación Android utilizando Google App Engine (GAE).

Requerimientos:

  • Conocimientos básicos de desarrollo de aplicaciones Android
  • Conocimientos básicos de desarrollo de aplicaciones JavaEE (Servlets, JSP).

Parte 1. Aplicación Android

Si ya cuenta con una aplicación Android y desea integrarla con GAE puede pasar a la parte 2 (si no tiene una aplicación web en GAE). Se creará una aplicación sencilla para ayudar con el ejemplo.

En este caso se hará una aplicación para guardar “notas” que no es más que un simple texto que luego podemos visualizar en la Web.

La actividad para guardar notas tendrá el siguiente layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context=".NotesActivity">

    <TextView
        android:id="@+id/add_note_description"
        android:text="@string/add_note_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <EditText
        android:id="@+id/note_content"
        android:layout_below="@+id/add_note_description"
        android:layout_width="match_parent"
        android:layout_above="@+id/action_save"
        android:layout_height="match_parent"/>

    <Button
        android:id="@+id/action_save"
        android:text="@string/add_note"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</RelativeLayout>

Y se verá de la siguiente manera:

Save Notes Layout

El código de la actividad se verá de momento de la siguiente manera:

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class NotesActivity extends ActionBarActivity implements View.OnClickListener {
    EditText mNoteContent;
    Button mSaveNote;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_notes);

        mNoteContent = (EditText) findViewById(R.id.note_content);
        mSaveNote = (Button) findViewById(R.id.action_save);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.action_save) {
            String noteContent = mNoteContent.getText().toString();

            // TODO call save on backend

            // TODO when save is finished clear the textview and show a toast message
        }
    }
}

Parte 2. Servicios en GAE

En Android Studio se debe agregar un nuevo módulo, que contendrá la implementación del backend de la aplicación. Simplemente vaya a File -> New Module y seleccione el módulo “App Engine Java Endpoints Module”. Si desea agregar Google Cloud Messaging, seleccione la opción anterior.

new_backend_module

Esto va a crear un nuevo módulo en el proyecto. Otro de los efectos es que el build.gradle del módulo de la aplicación de Android va a ser modificado con la siguiente línea:

 dependencies {
    ...
    compile project(path: ':backend', configuration: 'android-endpoints')
}

Esto significa que el módulo de Android (app en el ejemplo) va a poder utlizar al módulo de GAE (backend en el ejemplo) como si tuviera las librerías de cliente. Si estuvieran los módulos en proyectos separados o utilizando otra herramienta distinta de Android Studio o más bien sin gradle, se deben compilar las librerias clientes. Esto genera unos archivos zip, uno por cada endpoint registrado y el codigo fuente hay que agregarlo a nuestro proyecto de Android. ¿Tedioso verdad? Por eso es mejor tenerlo así en Android Studio.

A la fecha que se escribió este tutorial, había un pequeño problema con la plantilla de módulos de GAE en Android Studio. Yo tuve un problema luego de crear el módulo y es por un problema con el plugin de appengine. Si se fija en el build.gradle del módulo, vera un comentario con una nota sobre esto. Para resolverlo (bajar el sdk y las respectivas dependencias) simplemente deben correr en una terminal el siguiente comando en la raíz del proyecto: ./gradlew [modulename]:appengineRun. En este caso sería ./gradlew backend:appengineRun. Si no tienen el sdk de App Engine o las librarías, con este comando lo podrán obtenerlo.

El módulo GAE viene con un codigo tipo plantilla, para ejemplificar el uso del módulo. Vamos a crear nuestro ejemplo y explicar las anotaciones en conjunto para ahorra tiempo.

Agregar y listar

Vamos a hacer dos endpoints. Un endpoint para poder salvar notas y listarlas. Otro endpoint para registrar usuarios. Para simplificarlo un poco, vamos a hacerlo anónimo, y de auto registro utilizando un identificador único por dispositivo.

Para no hacer muy largo el ejemplo vamos a utilizar en nuestro App solo el de insertar, pero podemos usar el de listar para corroborar que los datos están siendo guardados utilizando el dashboard admin de la instancia en GAE.

Primero hay que crear las clases del modelo que van a ser los DTOs (Data Transfer Objects) y que representan la información que vamos a almacenar. En nuestro caso ocupamos dos. User y Note. User va a tener un id único en el sistema y el device_id. Note va a tener un string con el contenido de la nota, un id único, la fecha de creación y el id interno del usuario.

Para hacer las cosas fáciles Android Studio cuenta con un comando para crear endpoints. Nada más seleccionamos la clase a la cual queremos hacer el endpoint y le damos “Generate Endpoints“:

generate_endpoint

Esto generará una clase llamada <nombre-clase>Endpoint. Por ejemplo si lo hacemos para User, será UserEndpoint. Y se verá similar a esto:

/** An endpoint class we are exposing */
@Api(name = "userEndpoint", version = "v1", namespace = @ApiNamespace(ownerDomain = "savenotes.backend.fr4gus.com", ownerName = "savenotes.backend.fr4gus.com", packagePath=""))
public class UserEndpoint {

    // Make sure to add this endpoint to your web.xml file if this is a web application.

    private static final Logger LOG = Logger.getLogger(UserEndpoint.class.getName());

    /**
     * This method gets the <code>User</code> object associated with the specified <code>id</code>.
     * @param id The id of the object to be returned.
     * @return The <code>User</code> associated with <code>id</code>.
     */
    @ApiMethod(name = "getUser")
    public User getUser(@Named("id") Long id) {
        // Implement this function

        LOG.info("Calling getUser method");
        return null;
    }

    /**
     * This inserts a new <code>User</code> object.
     * @param user The object to be added.
     * @return The object to be added.
     */
    @ApiMethod(name = "insertUser")
    public User insertUser(User user) {
        // Implement this function

        LOG.info("Calling insertUser method");
        return user;
    }
}

Es importante verificar que el endpoint se encuentre registrado en el archivo web.xml que se encuentra por lo general aqui:

verify_web_xml

Se debe verificar que el endpoint se registró. Si no es así, agregarlo manualmente para hacerlo visible.

verify_endpoint

Para probar rápidamente el endpoint, vamos primero a crear un almacenamiento en memoria con un HashMap. En getUser vamos a hacer un get del map y en insertUser vamos a hacer put. Además vamos a agregar un nuevo método en el endpoint, para poder listar los uuarios registrados. La clase UserEndpoint tendrá las siguiente modificaciones.

1. Agregar el map para almacenamiento en memoria. Propiedades de la clase mLocalUsers y mLastId. La variable mLastId nos servira como id autoincremental de los usuarios.

    static Map<Long, User> mLocalUsers = new HashMap<Long, User>();
    static long mLastId = 0;

2. Modificar gerUser para obtener el usuario del map.

        User user = mLocalUsers.get(id);
        LOG.info("Returning user with id " + user.getDeviceId());

3. Modificar insertUser para guardar el user en el map.

        synchronized (mLocalUsers) {
            mLastId++;
            user.setId(mLastId);
            mLocalUsers.put(mLastId, user);

        }

4. Agregar el método listUser para listar los usuarios del map.

    @ApiMethod(name = "listUsers", path = "user/list")
    public List<User> listUser(){
        ArrayList<User> users = new ArrayList<User>();
        users.addAll( mLocalUsers.values());
        return users;
    }

Como notarán, listUser tiene un parámetro extra en la anotación. “path” esta redifiniendo la ruta por defecto para acceder este endpoint. Esto se debe a que sin él, getUser y listUser van a tener la misma firma a nivel de servicio. Van a ser endpoints con la misma ruta “user/” que por medio de GET van a traer objetos tipo User, por lo que va a crear un conflicto. Es por ello que es necesario modificar la ruta del list.

Si recuerdan la nota sobre el issue con el plugin de GAE y Android Studio, ejecutamos el commando ./gradlew backend:appengineRun. Este comando hace levantar la instancia local de GAE. Si acceden a http://localhost:8080/ les aparecera una página como la siguiente:

 gae_landing

Dentro de las opciones en el menú superior, hay uno que nos interesa que es el Google Cloud Endpoints API Explorer. Con este explorador pueden hacer llamados a los endpoints y probarlos directamente. Por ejemplo podemos meter un usuario:

add_user

Que corresponde al siguiente request y response.

add_user_request_response

Como ven, desarrollar para Google App Engine es relativamente sencillo. Recuerden que hay disponibles otros lenguajes si Java no es de su preferencia, pero como este tutorial está orientado para Android Devs, parece adecuado hacer la implementación en Java también.

Para la segunda parte se hará la integración con el app de Android y posteriormente como extra, como utilizar Google Objectify para acceder al Cloud Datastore de una manera sencilla.

Primer Media Maratón

Después de correr varias carreras entre 5k y 10k había tomado la decisión de entrarle a la primera media maraton. También había corrido algunas carreras con obstáculos de 12k y 15k y según yo ya estaba listo.

Cuando le comentaba a la gente cual carrera iba a hacer (La Correcaminos) todos me motivaban diciendo que esta carrera es perfecta para empezar (y lo es) pues es mayoritariamente en descenso y plana.

Empecé un plan de esos de Endomondo, pero al pasar las semanas se me complicaba seguirlos al pie de la letra, creo que perfectamente falle entre un 30%-50% de las sesiones, pero igual estaba envalentonado, pues como también estoy nadando, según yo tenía buen aire.

Las dos semanas anterior a la carrera estaba bastante nervioso y me empezaron a dar “achaques”, algunos dolorsillos o molestias en las pantorrillas y las rodillas. Me imagino que me estaba autosaboteando mentalmente. Por dicha no me resfrié como me suele pasar antes de una carrera.

El Sábado previo a la carrera, tuvimos entrenamiento en G-Swim de 3k, y aunque salí agotado no creí que me fuera afectar. Aún ahora no se si tuvo alguna influencia.

Los primeros 10km me fui con un paso muy fuerte (para mí jeje) aproximadamente 5:30 min/Km y creo que este fue el mayor error de como ataqué la carrera. Cuando llegamos a Zapote (donde arranca la de 10km) sentí un burst de adrenalina por que aún no habían salido y estaban todos ahi apoyando  a los de 21. Pero luego de que se me pasó ya sentía algo de agotamiento aunque creo que parte fue mental por que estaba rebasando mis propios límites y estaba entrando a lo “desconocido”.

La primera señal de agotamiento lo sentí al pasar la Asamblea Legislativa, que tiene una pendiente desde la Corte hasta el Parque Nacional y además cierto grado de dificultad por que algunas partes tienen un empedrado. Pero luego de ahí todo es prácticamente bajada por el Parque Morazán y hasta La Coca Cola.

La segunda señal, y la definitiva, fue al entrar al Paseo Colón que tiene un falso plano por la Toyota y ya veía yo que las piernas no me respondían como quería. Digamos que del torso para arriba andaba bien, pues el pulso no supero los 180 ppm lo que significa que no estaba haciendo un esfuerzo máximo, pero tampoco podía exigirle más a mis piernas.

Al llegar a La Sabana ya empezaba a duduar que lo iba a lograr, máxime que sentí un fuerte golpe psicológico al ver que ya me estaban alcanzando los de 10k (tontera mía la verdad) y que el paso que llevaba iba disminuyendo. Gracias al apoyo de mis compañeros de Guppys que venían acompañando en bicicleta no me dejé por vencido, pero al doblar por el Colegio de Médicos y Cirujanos definitivamente sabía que la subida por Sabana Norte iba a estar bien dura.

Para empeorar el asunto desde el Estadio Nacional ya sentía un calambre fuerte en el músculo de la tibia izquierda, que iba y venía. Hace mucho tiempo había tenido la oportunidad de compartir con los Scouts y me aprendí algunas canciones que se usan para marchar, que me ayudaron muchísimo para concentrarme en avanzar.

Al doblar por la Nissan, sentí unas ganas enormes de llorar, no sé si por que temía no terminar tan cerca o por que lo iba a lograr. Yo hubiera llorado perfectamente, de no ser que sentía un nudo enorme en la garganta y me costaba mucho respirar. Pude sobreponerme y avanzar hasta la entrada ya del cierre de la carrera.

Creo que ya venia más a velocidad de caminar que de correr, pero aún así tuve que seguir en trote. Intenté caminar, pero mis piernas no tenían fuerza ya por lo que me dió miedo caerme. Seguí trotando y al llegar a la meta de nuevo el calambre, que me hizo cojear hasta la meta. Por ahí oí mi nombre (era mi mamá) pero no quise ni voltearme a ver por miedo a caerme o perder el último impulso.

Al cruzar la meta sentí gran alivio, pero ya las piernas no daban y caminé con dificultad para recibir el refrigerio y la hidratación. Aún así me sentí bien por haber logrado mi meta. Intenté localizar a amigos que sabían que también la habían corrido, pero en mi estado preferí buscar a mi familia una vez recogida la tan merecida medalla para irme a descanzar.

No hubiera logrado esta carrera sin la motivación de mis amigos de Guppys y el apoyo de mi familia.

Correcaminos

Nine Patch Remotos

Una de las herramientas más útiles dentro del arsenal del framework de Android son las imagenes nine-patch. Estas permiten a una imagen adaptarse a su contenedor para una misma densidad. El ejemplo por excelencia son los botones. Si se utiliza una imagen para el boton, por lo general es una imagen con un tamaño predeterminado. Esta imagen tal vez se vea muy bien cuando el dispositivo se encuentra en posicion vertical, pero cuando se gira el dispositivo, el boton mantiene su tamaño y no se “estira”.

Button Landscape

Boton en orientacion horizontal

Button Portrait

Boton en orientacion vertical

 

Cuando se sabe cual imagen se va a utilizar, esta imagen es alojada junto con el resto de la aplicaicon en el APK. Se puede utilizar una herramienta del mismo SDK para crear las marcar necesarias para indicarle al sistema operativo que partes de la imagen deben estirarse e inclusive cual va a ser la zona disponible para albergar contenido.

Boton con nine patch

Todo muy bien hasta aquí. Pero ¿Qué pasaría si se ocupa obtener un boton de manera remota? Es decir, al momento de hacer la aplicación, aún no es posible saber como va a ser el botón, por lo que no sería posible usar la herramienta anterior.

“Compile” vs “Source”

Cuando se crea un nine-patch con la herramienta, esto genera un archivo png, con la terminación 9.png. Este archivo es similar al original, pero tiene una transparencia de 1px con ciertas marcas. Cuando se compila el proyecto, esto genera por debajo el “chunk” metadata necesario para poder manipular la imagen en el UI.

La clase Bitmap tiene un metodo llamado getNinePatchChunk que permite obtener ese metadata. La clase NinePatch puede verificar si el metadata es válido y de ser así, se puede hacer un NinePatchDrawable.

 
InputStream stream = .. //whatever Bitmap bitmap = BitmapFactory.decodeStream(stream);
byte[] chunk = bitmap.getNinePatchChunk();
boolean result = NinePatch.isNinePatchChunk(chunk);
NinePatchDrawable patchy = new NinePatchDrawable(bitmap, chunk, new Rect(), null); 

Para poder hacer esto funcional, en un escenario con la imagen remota, se ocuparía que el servidor pueda enviar una versión compilada de la imagen. Esto presenta el problema de que no existe una herramienta oficial para “compilar” un nine-patch. Si bien existen herramientas de terceros que lo permiten, pueden haber situaciones donde esto no sea posible.

Este fue el caso que se presentó recientemente, donde se solicitó soportar nine-patch para unos botones, cuyas imagenes venian de manera remota. Para ello se creo un drawable nuevo que pudiera emular el nine patch. Eso si, debe recibir los componentes por separado. Por ahora no es posible recibir una imagen con las marcas de la herramienta del nine-patch y solo soporta 3 parches, aunque en un futuro se planea agregar soporte para los 9 en total.

La parte mas importante de este componente es el método draw. Que simplemente pinta los parches de los bordes (izquierda y derecha) y replica el parche del centro tantas veces como sea necesario para llenar el espacio.

	@Override
	public void draw(Canvas canvas) {
		int left = 0;
		int mTopRightWidth = 0;

		if (mTopLeftBG != null) {
			Rect dst = new Rect(0, 0,
					(int) (mTopLeftBG.getWidth() * mDensityScale),
					(int) (mTopLeftBG.getHeight() * mDensityScale));

			canvas.drawBitmap(mTopLeftBG, null, dst, mPaint);
			left = (int) (mTopLeftBG.getWidth() * mDensityScale);
		}

		if (mTopRightBG != null) {
			Rect dst = new Rect(getBounds().right
					- ((int) (mTopRightBG.getWidth() * mDensityScale)), 0,
					getBounds().right,
					((int) (mTopRightBG.getHeight() * mDensityScale)));

			canvas.drawBitmap(mTopRightBG, null, dst, mPaint);
			mTopRightWidth = (int) (mTopRightBG.getWidth() * mDensityScale);
		}

		if (mTopCenterBG != null) {
			int bitmapScaledWidth = (int) (mTopCenterBG.getWidth() * mDensityScale);
			for (int i = left; i < getBounds().right - mTopRightWidth; i += bitmapScaledWidth) {
				Rect dst = new Rect(i, 0, i + bitmapScaledWidth,
						(int) (mTopCenterBG.getHeight() * mDensityScale));
				canvas.drawBitmap(mTopCenterBG, null, dst, mPaint);
			}
		}
	}

Para poder soportar diferentes densidades, se requiere que la imagen sea para la densidad máxima (en este momento xxhdpi), por ejemplo una imagen que tenga 144px de alto. A partir de ahi a la hora de recibir la imagen se escala hacia abajo deacuerdo a la densidad del dispositivo.

Próximamente voy a publicar un proyectos en github de ejmplo y el código fuente del PatchDrawable.

Punta Leona ¡Misión cumplida!

 

Mas de un año después de empezar a nadar (poco más de 170km nadados), llegó el momento: La primera competencia de aguas abiertas en Punta Leona.

Llegamos pasadas la 7am (salimos a las 5:3am de Tibás) al Hotel Punta Leona, no sin antes desayunar en la Soda El Guacimo un pinto con huevo y tortilla aliñada. Seguro por ser tan temprano no encontramos mayor tumulto en la entrada y en un dos por tres, estabamos ya en Playa Mantas.

El equipo de Guppys, G-Swim ya tenia preparado unos toldos para reuniros. Poco a poco fueron llegando más compañeros y se acercaba la hora. Por ahi de las 9 empezaron a repartir las gorras, indicando el heat que nos asignaron y el chip. Empezamos mal, por que no logre ponerme el tatuaje temporal del número que va en la pantorrilla, y terminé con el número puesto con marcador “permanente” (lo pongo en comillas por que luego de pasar la vaselina, casi se borra).

A las 10:30 todo el equipo se reunió en la playa para estirar y hacer unos calentamientos en el mar. La marea estaba subiendo y habia cierto oleaje. Además, la orilla tenia mucha basura con palos, ramas y hojas. Entramos primero para nadar hasta el último vertice del trazado. A la vuelta se me pegaron unos hilos en la muñeca izquierda y el antebazo. Ya me estaba preocupando un poco.

La segunda vez, era hacer un triangulo desde la orilla y las dos primeras boyas. Todo bien en la entrada, pero de nuevo a la vuelta me pegaron fuerte unos hilos (hasta se me quedaron en el brazo al salir). Me estaba preocupando más. No podia participar de la carrera con las camisetas que uso normalmente en los entrenamientos por que me tapan el número del brazo. Ni modo, me puse la mitad del tarro de vaselina y así me fui.

Como en los eventos de atletismo, me puse muy ansioso en el corral de salida. Estaba en el último heat, por lo que debia esperar la salida de mucha gente delante mio. Rosados, Verdes y nosotros los Naranjas (cada heat tenia asignado un color de gorra diferente). Nos juntamos los del equipo que estabamos en el mismo heat y todos terapiandonos para calmarnos y transmitirnos buenas vibras.

Sale el primer grupo… me pongo a hacer ejercicios de respiracion. Sale el segundo grupo, me topo a mi estimadísimo amigo Pablo Viquez que se alistaba para salir en el heat de los 3000m. Ahi hablamos un poco y me pude distraer un poco de mi mismo nervisismo. Nos llaman a los Naranjas y ahi nos vamos acomodando. Como nos explicaron nuestros entrenadores, nos fuimos a lado izquierdo.

Finalmente nos colocamos en la salida y suena el pitazo. Empezamos a avanzar corriendo y una ola nos recibio de buenas a primeras. Logro alejarme del grupo mas hacia la izquierda y sigo avanzando hasta que me topo con la famosa grada de la playa. Todo tranquilo y aprovecho para empezar a nadar. Todo el trayecto hasta la primera boya bien. Iba mentalizando en nadar tranquilo y suave. Me pasan dos y yo tranquilo. Fue hasta que llegamos al primer vertice que hubo cierto roce, pero nada de que preocuparse. Me aleje un poco y le puse para poder ubicarme mejor.

Miré hacia adelante para ubicar la siguiente boya y al sentirme bien decido ponerle un poquito mas. Sigo al mismo ritmo pero tratando de aumentar mi alcance en los brazos, jalar un poco más fuerte el agua y asegurarme de terminar la brazada bien. Al rato de avanzar, llegamos a la mitad del segundo segmento. Noto que mucha gente empieza a girar a la derecha. Yo no podia creer que ya hubieramos llegado al otro vertice. Bajo la velocidad y trato de escuchar a los kayakistas. “Tienen que seguir hasta la otra boya”, y me doy cuenta de que varios se fueron por el camino incorrecto y nada, pues a seguir. Esa segunda mitad estuvo durilla por que había fuerte oleaje, que provocaba que ciertas brazadas no fueran efectivas, o me hundiera en el agua en el momento de respirar. Por dicha ya estaba acostumbrado a eso y pude seguir sin mayor complicación.

Ya para ese entonces habia rebasado a varios verdes y al llegar al segundo vertice decido aumentar la velocidad. Ya venia con un ritmo mayor que del primer vertice por la misma emocion de haber rebasado a varios verdes y encontrarme casi solo al llegar al segundo vertice. Trato de ubicar la meta, y a ponerle. Me preocupaba un poco los hilos, pero “fuck it” ya iba de salida. No ma habia topado un solo hilo y ya no me importaba si me los topaba en ese momento.

A la mitad del tercer segmento alcanzo a un grupo de verdes. Tuve que zigzagear varias veces para poder encontrar huecos y tratar de rebasarlos. Lo logro, pero ya me estaba quemando un poco. Me pasa el primer Gris (los de los 3000m) y decido ponerle en la patada (también para ayudar a la sangre a distribuirse bien para el momento de pasarse a posicion vertical para correr y no marearme). Sigo avanzando y ya veo más cerca la meta. Me entra una ansiedad enorme que logro controlar, y sigo manteniendo el ritmo con patada fuerte.

Llegando casi a la orilla tenía a una muchacha  adelante y a la izquierda. Nos empuja una ola a la orilla y ya nos podemos poner de pie. Noto que ella va a salir caminando y empiezo a correr fuerte para cruzar la meta. Creo que escuche mi nombre, no sé quien me vio pero gracias por aplaudir :). Ya pasando la meta me trato de recuperar del morón que me pegué y recibo mi primera medalla de aguas abiertas.

Debo de agradecer primero a mi familia por apoyarme en estas aventuras. Luego a mi equpo G-Swim por acogerme a dos meses de la competencia y hacerme sentir parte del mismo desde el principio. También agradecerle a mis amigos que practican deporte, por que todos me han motivado con sus logros a perseguir los míos. A Marianella Cordero que, con su blog, me inspiró muchísimo con su experiencias en atletísmo. También a Lorraine Rate y la gente de H2Open Magazine (recomendado por cierto) por su amistad a la distancia, buenos consejos y el excelente plan que seguí para poder conseguir la confianza y condición para poder estar capacitado para este reto. También a los amigos de AguaLibre, el grupillo de locos, que nos vamos siempre a Playa Herradura a entrenar. Apliqué todos y cada uno de los consejos que me dieron.

No puedo describir la emoción de conseguir ésta meta. No gané la competencia pero hice un tiempo mejor del que esperaba. Mañana me toca El Cruce de la Leona y ahora me toca descansar.

 GG!

Llegando a la meta