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.