Blog de Amazon Web Services (AWS)

Creando aplicaciones nativas en Android con AWS

Por Kevin Cortés, Arquitecto de Soluciones AWS Argentina

 

Hoy podemos afirmar que todos contamos con nuestros dispositivos móviles para lo que sea: pedir delivery, un auto o mismo también entretenerse con una buena película de viernes por la noche. El celular se convirtió en una herramienta indispensable para nuestro día a día. Y así también, AWS ha sabido entender esa demanda.

En este post le mostraremos cómo diseñar e implementar una aplicación móvil en Android utilizando SDKs y distintos servicios de AWS. La aplicación contará con usuarios que tendrán que autenticarse ingresando un correo electrónico y contraseña, validando esta información mediante un código de 6 dígitos generado en forma aleatoria. Una vez ingresado, el usuario podrá realizar distintas funciones desde la aplicación enviando solicitudes a una API Rest con un token de autenticación.

A su vez, este blog estará acompañado de un código publicado en el repositorio de ejemplos de AWS que puede utilizarse como referencia.

El siguiente diagrama de arquitectura representa cómo interactúan los servicios de AWS involucrados en esta publicación.

 

 

Para esta ocasión, utilizaremos los siguientes servicios:

  • AWS Amplify
  • Amazon API Gateway
  • Amazon Cognito
  • AWS Lambda
  • Amazon DynamoDB

AWS Amplify es un conjunto de productos y herramientas que permite a los desarrolladores web móviles y de frontend crear e implementar aplicaciones seguras y escalables. Con Amplify, puede configurar los backends de las aplicaciones en cuestión de minutos y conectarlos a su aplicación en solo unas pocas líneas de código.

Con ello, crearemos los grupos de usuarios en Cognito. Éste servicio le permite, de manera rápida y sencilla incorporar el control de acceso, la inscripción y el inicio de sesión de los usuarios. Por otra parte, también admite el inicio de sesión mediante proveedores de identidad social, como Apple, Facebook, Google y Amazon, y proveedores de identidad empresarial a través de SAML 2.0 y OpenID Connect. Para este caso práctico, los usuarios deberán registrarse para acceder a nuestra aplicación.

Más aún, la API Rest será expuesta públicamente con el servicio de API Gateway que recibirá solicitudes HTTPS autenticadas por Cognito. De esta forma, las solicitudes que resulten exitosas tienen que estar autorizadas mediante el token provisto por Cognito para ejecutarse. Caso contrario, la API devolverá el código de error ‘403 – Forbidden’.

La API solo mostrará información esencial del cliente, como por ejemplo su nombre y apellido. Dichos registros estarán asociados a un identificador único provisto por Cognito que identifica unívocamente al usuario. Los datos serán guardados en una tabla de DynamoDB, una base de datos no relacional de clave-valor que ofrece rendimiento en milisegundos de un solo dígito a cualquier escala.

Lo primero a realizar será crear un nuevo proyecto en Android Studio. Inicialmente el proyecto contará con un solo Activity. Éste es un componente principal de la interfaz en Android, y se asocia con una ventana donde se define la interfaz para que el usuario interaccione con los componentes creados en él (un texto, un botón, un cuadro de diálogo, entre otros).

La aplicación contará con dos Activities. Cuando la aplicación sea ejecutada, por defecto lanzará el InitActivity, que contendrá un FrameLayout. Este sirve para contener a los fragments. Un fragment podría definirse como una porción de la interfaz que puede añadirse a un Activity de forma independiente y tiene el fin de ser reutilizado en otras actividades

Para la ejecución del post, vamos a necesitar dos Activities:

  • InitActivity: Contendrá la lógica del Login, Register y CodeValidation.
  • HomeActivity: Navegará entre distintas interfaces (fragments) para visualizar información de la API una vez el usuario haya ingresado.

 

 

Una vez creado el primer Activity, deberá configurar Amplify en nuestro proyecto. Para ello, primero deberá descargar la CLI para interactuar desde la terminal:

npm install -g @aws-amplify/cli

 

Posteriormente se debe configurar el proyecto con un nombre, una región y un usuario IAM (Identity and Access Management) con los permisos que sean necesarios. Para realizarlo, ejecute el siguiente comando:

amplify configure

 

Si vamos al servicio de Amplify desde la consola de AWS, se puede observar que ya hay una app disponible.

 

 

A continuación, hay que hacer modificaciones al proyecto de Android Studio. Para ello, se debe agregar a build.gradle las siguientes líneas:

 

android {

    compileOptions {

        // Support for Java 8 features

        coreLibraryDesugaringEnabled true

        sourceCompatibility JavaVersion.VERSION_1_8

        targetCompatibility JavaVersion.VERSION_1_8

    }

}

 

dependencies {

    // Amplify core dependency

    implementation 'com.amplifyframework:core:1.6.8'


    // Support for Java 8 features

    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'

}

 

Para comenzar a provisionar recursos en AWS, primero debe inicializar el proyecto situado en el directorio root con el comando:

amplify init

 

Y en el método onCreate de InitActivity agregar el siguiente fragmento de inicialización de Amplify en el código:

 

try {

            Amplify.addPlugin(new AWSCognitoAuthPlugin());

            AmplifyConfiguration config = AmplifyConfiguration.builder(getApplicationContext()).devMenuEnabled(false).build();

            Amplify.configure(config, getApplicationContext());

 
            Log.i("Tutorial", "Initialized Amplify");

        } catch (AmplifyException e) {

            Log.e("Tutorial", "Could not initialize Amplify", e);

        }

 

Una vez llegado a este punto, ya puede a comenzar a hacer deploy de nuestros servicios en AWS utilizando la SDK de Amplify desde nuestro código. El próximo paso es configurar la autenticación.

 

Agregando autenticación a nuestra App

Como se mencionó en el paso anterior, initActivity realiza el render de 3 tipos de fragmentos: Login, Register y CodeValidation. Cada uno cumplirá la función como su nombre lo indica. En esta ocasión, nos centraremos en cómo implementar el componente de login.

La aplicación deberá contar con permisos adecuados y acceso a Internet, por lo que en el archivo AndroidManifest.xml debemos agregar:

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

 

El próximo paso es añadir a nuestro proyecto de Amplify el recurso de autenticación. Lo logramos ejecutando un comando y estableciendo los parámetros que queremos. En éste caso será por default. El comando es:

amplify add auth

 

Si navegamos nuevamente al servicio de Amplify desde la consola de AWS, dentro de nuestro proyecto vamos observar que hay un recurso de backend creado.

 

 

Este recurso es administrado por Cognito y al dirigirse al servicio de Cognito, observaremos que se creó un pool de usuarios:

 

 

 

Regresando nuevamente al código, se podrá ver que se creó un nuevo directorio llamado amplify con la configuración de nuestro proyecto y autenticación.

Por otra parte, se tiene que agregar las dependencias de Cognito al código de Java. En build.gradle agregamos la siguiente línea

implementation 'com.amplifyframework:aws-auth-cognito:1.6.8'

 

Nuestro objetivo es que la aplicación inicialmente muestre 4 campos: 2 entradas de texto para usuario y contraseña respectivamente, y dos botones para ingresar o ir al sitio de registración. Toda la lógica será manejada por LoginFragment en InitActivity. Para ello, crearemos un layout con dos TextInputEditText. Por último, también se deben agregar 2 botones: para ir al fragment de registro y otro para hacer el login hacia Cognito. En la siguiente captura de pantalla podemos ver un ejemplo del objetivo de layout que necesitaremos.

 

 

Volviendo nuevamente al código del fragment, dentro del método onCreateView se deben declarar las variables de los dos TextInputEditText para obtener el texto tanto del mail como de la contraseña. También declararemos a los botones de Login y Register para definir las acciones posteriormente.

 

final TextInputEditText passwordEditText = view.findViewById(R.id.password_edit_text);

final TextInputEditText usernameEditText = view.findViewById(R.id.username_edit_text);

MaterialButton nextButton = view.findViewById(R.id.next_button);

MaterialButton registerButton = view.findViewById(R.id.register_button);



nextButton.setOnClickListener(new View.OnClickListener() {

    @Override

    public void onClick(View view) {
 

        String email = usernameEditText.getText().toString();

        String password = passwordEditText.getText().toString();


        Amplify.Auth.signIn(

                email,

                password,

                result -> {

                    if ( result.isSignInComplete() ){

                        Intent intent = new Intent(getActivity(), HomeActivity.class);

                        startActivity(intent);

                    } else {

                        Log.d("Amplify-Login", "User Sign In is not complete.");

                    }

                },

                error -> {

                    Log.e("Amplify-Login", error.toString());

                }

        );

    }

});


registerButton.setOnClickListener(new View.OnClickListener() {

    @Override

    public void onClick(View view) {

        ((NavigationHost) getActivity()).navigateTo(new RegisterFragment(), false); // Navigate to the next Fragment

    }

});

 

 

Si el login resulta exitoso, se redirige a la otra actividad que creamos: HomeActivity. En ella, vamos a mostrar información básica de las llamadas a la API. Nuestro próximo paso es configurar la API Rest utilizando Amazon API Gateway, AWS Lambda y Amazon DynamoDB.

En el servicio API Gateway, vamos a generar una nueva api (android-app-api) y creamos un nuevo recurso llamado profile, con 2 métodos: GET y POST. Cada método hará referencia a dos funciones lambda distintas: una que consulta sobre la base de datos en DynamoDB y otra que persiste los datos, respectivamente. En paralelo, ambas funciones tienen un rol de ejecución que les permite contar con el nivel de privilegio necesario: hacer consultas e inserciones sobre nuestra tabla en Amazon DynamoDB. Lo siguiente a realizar es verificar que ambos roles tengan los permisos adecuados de lectura y escritura.

 

 

Una vez configurados los métodos y los recursos, debemos configurar los autorizadores: esto es asociar el pool de usuarios de Cognito a nuestra API. Caso contrario expondremos la API abierta a cualquier usuario. En la sección Authorizers crearemos uno nuevo y añadimos el pool creado por Amplify previamente. En Token Source debemos añadir el nombre del header con el que la API interpretará al token, en este caso es “Authorization”.

 

 

Luego de guardar, debemos editar el request, cambiando el Authorization de “None” por el autorizador creado anteriormente.

 

 

La ejecución del método debería ser similar a la siguiente captura:

 

 

Debemos realizar un deploy de la API para que aplique los cambios.

 

 

En la sección stages vamos a ver la URL provista.

 

 

A continuación debemos crear nuestra tabla en la base de datos. En este blog no nos centraremos en cómo crear una tabla en DynamoDB aunque existen referencias de cómo hacerlo en el siguiente link. Como se comentó previamente, vamos a crear una tabla en DynamoDB y utilizaremos un String con clave “user_id” como Primary Key. Esta hará referencia al ID del usuario de Cognito, por lo que garantizamos que no existirán duplicados de este campo.

 

 

Finalmente necesitamos una función que haga de conector entre la API y la base de datos. En este caso te presentaré un ejemplo de la función asociada al método GET. La función toma el user_id del request enviado por API Gateway. De esa forma, luego hace una búsqueda por dicho valor. En caso de que no exista, devuelve un error genérico.

Una función Lambda de ejemplo que trae valores de la tabla sería:

 

import json

import boto3

from botocore.exceptions import ClientError

from boto3.dynamodb.conditions import Key, Attr


dynamodb = boto3.resource('dynamodb')

table = dynamodb.Table('UserData') #Name of your table created in DynamoDB

 

def lambda_handler(event, context):
   
    user_id = event["requestContext"]["authorizer"]["claims"]["event_id"]

    try:

        response = table.get_item(Key={'user_id': user_id})

    except ClientError as e:

        print(e)

        response_to_lambda = {

            "statusCode" : 500,

            "body" : "Something went wrong. Please, check your data again"

        }

    else:

        response_to_lambda = {

            "statusCode" : 200,

            "body" : json.dumps(response["Item"])

        }


    return response_to_lambda

 

Llegado a este punto vamos a tener una API con distintos recursos y métodos (GET, POST) autenticados, realizando invocaciones a funciones Lambda y estas hacen llamadas a DynamoDB.

El próximo paso es modificar la activity HomeActivity y crear un fragmento llamado UserProfileFragment. Esto será mostrado en la aplicación si el login es exitoso. Por otra parte, el layout del fragment deberá contar con 4 TextInputEditText para que dentro de cada uno podamos incluir la información otorgada por la API.

 

 

Situados en el método onCreateView del fragment, debemos instanciar los 4 tipos de TextInputEditText:

 

@Override

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_user_data, container, false); 

        TextInputEditText user_id = view.findViewById(R.id.user_data_user_id);

        TextInputEditText email = view.findViewById(R.id.user_data_user_email);

        TextInputEditText name = view.findViewById(R.id.user_data_user_name);

        TextInputEditText surname = view.findViewById(R.id.user_data_user_surname);


        APIInfo api = new APIInfo();

 

        Amplify.Auth.fetchAuthSession(

                result -> {

                    AWSCognitoAuthSession cognitoAuthSession = (AWSCognitoAuthSession) result;

 

                    switch(cognitoAuthSession.getIdentityId().getType()) {

                        case SUCCESS:

                            String accessToken = cognitoAuthSession.getUserPoolTokens().getValue().getIdToken();

 
                            api.getPersonalInfo(getContext(), accessToken, new UserInfoRunInterface() {

                                @Override

                                public void run(User user) {

                                    user_id.setText(user.getUserID());

                                    email.setText(user.getEmail());

                                    name.setText(user.getName());

                                    surname.setText(user.getSurname());

                                }

                            });
 

                            break;

 

                        case FAILURE:

                            Log.e("Amplify-Authentication", "IdentityId not present because: " + cognitoAuthSession.getIdentityId().getError().toString());

                    }

                },

                error -> {

                    Log.d("Amplify-Authentication", error.toString());

                }

        );

        return view;

    }

 

La aplicacion deberá ejecutar el código de Amplify para que retorne un token otorgado por Cognito cuando el usuario ingresó por primera vez inicialmente. Si no hubo errores, hace una llamada a la API con el método getPersonalInfo (detallado más abajo en este documento). A este método debemos pasarle 3 parámetros: el contexto del fragmento, el token y la implementación de una interfaz con un método run. Dentro de este método debemos configurar el setText de cada uno de los TextInputEditText con los datos provistos por la API.

El método getPersonalInfo es un método dentro de otra clase para simplificar las llamadas a la API y para que el modelo de los fragmentos quede más desacoplado de la misma.

 

public void getPersonalInfo(Context appContext, String userToken, UserInfoRunInterface userInfoRunInterface) {

        RequestQueue queue = Volley.newRequestQueue(appContext);

        String url = APIConstants.getAPIPersonalInfo();


        JSONObject obj = new JSONObject();

        JsonObjectRequest jsObjRequest = new JsonObjectRequest(Request.Method.GET, url, obj,

                new Response.Listener<JSONObject>() {

                    @Override

                    public void onResponse(JSONObject response) {

                        User u = User.parseUser(response);

                        //Call the interface’s method

                        userInfoRunInterface.run(u);

                    }

                },

                new Response.ErrorListener() {

                    @Override

                    public void onErrorResponse(VolleyError error) {

                        Log.e("API", error.toString());

                    }

                }

        ) {

            @Override

            public Map<String, String> getHeaders() throws AuthFailureError {

                Map<String, String>  params = new HashMap<String, String>();

                //Because it is an authenticated method, we must add this header with the token provided.

                params.put("Authorization", userToken); 

                return params;

            }

        };

        queue.add(jsObjRequest);

    }

 

Conclusiones

La mayor parte de las empresas y startups están pensando en mobile precisamente porque hoy todos contamos con un teléfono celular y es imprescindible para nuestras tareas cotidianas. La finalidad de este blog es que puedas guiarte con conceptos básicos de cómo agregar algunas funcionalidades a la app que suelen ser difíciles inicialmente. Por ejemplo, en caso de querer configurar autenticación por usuarios debemos tener en cuenta múltiples factores en los que se destacan principalmente seguridad y una base de datos cuya información deberá estar encriptada. Vimos que con Cognito eso se podía resolver en cuestión de minutos, agregándole además una mayor capa de seguridad como el segundo factor de validación de código.

Por otra parte, configurar nuevos recursos en la nube y administrarlos desde nuestro proyecto o código suele aparentar ser difícil. Constatamos que con Amplify eso ya no es un problema porque con simples comandos y una SDK intuitiva, podemos obtener todos los recursos al alcance de segundos.

Para finalizar, creamos una API Rest segura cuyo acceso deberá ser mediante tokens de usuarios autenticados y cuyo objetivo es que el usuario navegue en la aplicación y consecuentemente se hagan llamados al backend. Gracias a API Gateway no sólo pudimos hacer esa configuración fácilmente, sino que además nos ofrece seguridad y escalabilidad.

El objetivo de este post es una guía para comenzar a desarrollar una primera aplicación nativa en Android con recursos de AWS con ayuda del código subido en el repositorio público en aws-samples. Si bien en el blog hemos aclarado qué son algunos conceptos de Android (como Activity o Fragment), suponemos que quien lo lea tenga algún conocimiento básico en dicha área. De todas formas, siguiendo el blog, revisando las documentaciones de los servicios y el código se podría ejecutar sin inconvenientes.

 

Para más información, consulte la documentación oficial de AWS Amplify.

 

 


Sobre los autores

Kevin Cortés es arquitecto de soluciones en AWS. Trabaja asistiendo y apoyando a los clientes del sector público en su “viaje a la nube” de AWS, trabajando en proyectos que involucran arquitecturas escalables y serverless. Tiene gran interés en las áreas de desarrollo mobile, web, IOT y analítica de datos.