Crear Login En Android Con Retrofit

Login Android Con Retrofit

¿Te suena?

Con este artículo comenzamos a completar la app SaludMock que propuse en el artículo Tutorial Retrofit En Android: Planificación Aplicación Médica.

Así que el primer avance que realizaremos será crear el login de usuarios con Retrofit.

¿Cómo hacerlo?

Con los siguientes pasos generales que te propongo concretar:

  1. Crear Proyecto SaludMock En Android Studio
  2. Configurar Retrofit En Android Studio
  3. Crear Screen De Login
  4. Testear Aplicación SaludMock Con Datos Falsos
  5. Crear Base De Datos De SaludMock
  6. Crear Servicio Web RESTful
  7. Realizar Petición POST Con Retrofit

¿Qué te parece?

Si estás listo y te interesa, entonces sigue leyendo…

Retrofit: Realizar Peticiones HTTP En Android

En el artículo pasado habíamos dicho que Retrofit simplifica la creación de un cliente HTTP que facilita al programador seguir el estilo REST.

Dado que es una librería, debemos comprender que clases, métodos y componentes serán los que usaremos para realizar una petición POST y loguear al afiliado.

Teniendo en cuenta esto, el siguiente es un resumen de pasos o especie de receta para usar Retrofit:

  1. Agrega las dependencias Gradle para Retrofit y su convertidor de formato.
  2. Agrega los permisos para usar la red y saber su estado en el AndroidManifest.xml
  3. Define los objetos planos Java (POJOs) que representan el cuerpo de la petición y respuesta asociados a la llamada HTTP.
  4. Crea una interfaz que represente al servicio REST, donde estén todas las peticiones a realizar sobre él.
  5. Crea una instancia de un adaptador HTTP que proporciona la librería a través de la clase Retrofit.
  6. A través de tu adaptador crea una instancia concreta de la interfaz añadida previamente.
  7. Invoca los métodos que definiste en la interfaz y ponlos a correr de forma asíncrona. Puedes tomar el resultado de la petición con la interfaz Callback.

Ahora, Retrofit usa un sistema de anotaciones para promover el comportamiento de las peticiones.

Las anotaciones más populares que tendremos serán las asociadas a los 4 métodos del CRUD:

  • @GET: Realiza una petición GET
  • @POST: Realiza una petición POST
  • @PUT: Realiza una petición PUT
  • @DELETE: Realiza un petición DELETE

Y si deseas personalizar más componentes de la petición tendrás:

  • @Headers: Añade las cabeceras que proporciones
  • @Body: Puedes usarlo para enviar el cuerpo de una petición POST/PUT que será serializado por el convertidor dictaminado
  • @FormUrlEncoded: Determina que una petición será enviada con parámetros en su URL. Cada parámetro lo declaras con @Field
  • @Path: Reemplaza un segmento de URL marcado con un identificador

No te preocupes si no comprendes por el momento. Una vez realices este tutorial te será más claro.

Descargar Proyecto Del Código Final

Si tienes dudas sobre como lucirá y funcionará la app final de este tuto, te dejo el siguiente video ilustrativo:

Ahora, si deseas desbloquear el link de descargar para obtener los códigos completos, sigue estas instrucciones:

Bien…

…comencemos con el desarrollo.

Crear Proyecto SaludMock En Android Studio

Primero lo primero.

Abre Android Studio y selecciona la opción Start a new Android Studio project para crear el nuevo proyecto:

Start new Android Studio project

A continuación, configura el proyecto con los siguientes datos:

Configurar SaludMock

Si te parece bien, desde ahora guarda los proyectos de mi web en D:\android-herpo\blog para que mantengamos un orden.

El siguiente paso es elegir la versión mínima de soporte de la app. Déjalo por defecto en 11.

Phone And Tablet

Cuando te pidan añadir una actividad inicial al proyecto, selecciona el tipo Basic Activity:

Basic Activity En Android Studio

Lo siguiente es personalizar la actividad inicial que se agregará como principal.

Esta no será la del login, si no la de citas médicas.

Así nombra a la actividad AppointmentsActivity para obtener este resultado:

Personalizar actividad inicial de citas médicas

Presiona Finish y tendrás el nuevo proyecto creado.

Configurar Retrofit En Android Studio

La integración de Retrofit en tu proyecto Android Studio requiere añadir la siguiente dependencia en tu archivo app/build.gradle:

dependencies {
   
    // Retrofit   
    compile 'com.squareup.retrofit2:retrofit:2.1.0'

}

Seguidamente debes elegir un convertidor para los formatos de intercambio que provengan del servicio web.

Debido a que nosotros usaremos la librería Gson para convertir las respuestas JSON en POJOs Java, entonces agregaremos estas dos dependencias:

dependencies {
   
    // Retrofit
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}

Retrofit se integra internamente con el convertidor para retornar en objetos planos sin que tú medies en ello.

Teniendo en cuenta esto, la pregunta es:

¿Retrofit soporta más convertidores?

¡Si!

La siguiente tabla muestra el convertidor junto a su dependencia:

Convertidor Dependencia
Gson com.squareup.retrofit2:converter-gson:2.1.0
Jackson com.squareup.retrofit2:converter-jackson:2.1.0
Moshi com.squareup.retrofit2:converter-moshi:2.1.0
Protobuf com.squareup.retrofit2:converter-protobuf:2.1.0
Wire com.squareup.retrofit2:converter-wire:2.1.0
Simple XML com.squareup.retrofit2:converter-simplexml:2.1.0

Perfecto.

Ya determinadas las dependencias necesarias para usar Retrofit, entonces ve a Tools > Android > Sync Project with Gradle Files para sincronizar la construcción.

Actualizar Permisos Del AndroidManifest.xml

Recuerda que realizaremos peticiones HTTP, lo que requiere añadir los permisos correspondientes al manifesto:

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

Elegir Paleta De Colores De La App

Usaremos azul para los colores primarios y verde para los acentos:

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#7FD3FA</color>
    <color name="colorPrimaryDark">#7FD3FA</color>
    <color name="colorAccent">#98D743</color>
</resources>

Crear Screen De Login

A continuación veremos cómo representar el boceto del Login que realizamos en un layout.

Además generaremos la lógica para realizar un login con datos ficticios que nos permita pasar a la creación del servicio web antes de realizar peticiones HTTP con Retrofit.

Crear Actividad De Login

Android Studio trae consigo una plantilla para actividades llamada Login Activity, la cual incorpora un diseño tradicional de formulario de login más la lógica de validación de credenciales asociadas al evento de un botón.

Una plantilla que nos ahorrará mucho trabajo, ¿no lo crees?

Con esto en mente, sitúate en el paquete Java de tu proyecto y ve a File > New > Activity > Login Activity:

Nueva actividad de login

Cuando te salga el asistente de configuración, deja como sigue las opciones:

Configurar actividad de login

Presiona Finish y tendrás la clase Java junto al layout prefabricado.

Preview de la actividad de login

Modificar Layout Del Formulario

El diseño creado tiene varios elementos que usaremos para el formulario.

Sin embargo, este no trae consigo un espacio para el logo o incluso una variación razonable para landscape.

Si observas la jerarquía de elementos actual verás lo siguiente:

Estructura XML del layout de login

El padre LinearLayout raíz tiene dos hijos directos: un indicador de progreso y el formulario.

Si queremos añadir el logo, entonces podemos insertar como segundo hijo un ImageView centrado horizontalmente. Al mismo tiempo usa el icono launcher para probar la ubicación de esta:

<ImageView
    android:id="@+id/image_logo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="16dp"
    android:layout_marginTop="8dp"
    app:srcCompat="@mipmap/ic_launcher" />

Adicionalmente debes cambiar el campo de texto para el número de identificación para que acepte solo valores numéricos.

Este tiene una restricción de 10 caracteres, por lo que puedes dejarlo así:

<EditText
    android:id="@+id/user_id"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/prompt_user_id"
    android:inputType="number"
    android:maxLength="10"
    android:maxLines="1"
    android:textColor="@android:color/white"
    android:textColorHint="@android:color/white" />

El código final sería:

activity_login.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.hermosaprogramacion.blog.saludmock.ui.LoginActivity">

    <!-- Login progress -->
    <ProgressBar
        android:id="@+id/login_progress"
        style="?android:attr/progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:theme="@style/WhiteProgress"
        android:visibility="gone" />

    <ImageView
        android:id="@+id/image_logo"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginTop="8dp"
        app:srcCompat="@drawable/saludmock_logo" />

    <ScrollView
        android:id="@+id/login_form"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:id="@+id/email_login_form"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <android.support.design.widget.TextInputLayout
                android:id="@+id/float_label_user_id"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:theme="@style/LoginTextField">

                <EditText
                    android:id="@+id/user_id"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/prompt_user_id"
                    android:inputType="number"
                    android:maxLength="10"
                    android:maxLines="1"
                    android:textColor="@android:color/white"
                    android:textColorHint="@android:color/white" />

            </android.support.design.widget.TextInputLayout>

            <android.support.design.widget.TextInputLayout
                android:id="@+id/float_label_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:theme="@style/LoginTextField"
                app:passwordToggleEnabled="true">

                <EditText
                    android:id="@+id/password"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/prompt_password"
                    android:imeActionId="@+id/login"
                    android:imeActionLabel="@string/action_sign_in_short"
                    android:imeOptions="actionUnspecified"
                    android:inputType="textPassword"
                    android:maxLines="1"
                    android:textColor="@android:color/white"
                    android:textColorHint="@android:color/white" />

            </android.support.design.widget.TextInputLayout>

            <Button
                android:id="@+id/email_sign_in_button"
                style="?android:textAppearanceSmall"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:text="@string/action_sign_in"
                android:textColor="@color/colorPrimary"
                android:textStyle="bold"
                app:backgroundTint="@android:color/white" />

        </LinearLayout>
    </ScrollView>
</LinearLayout>

Crear Variación Landscape Del Login

Ahora crearemos la variación horizontal del layout.

Para ello, ve a la pestaña Preview del layout, selecciona el icono del teléfono en la parte superior derecha y presiona Create Landscape Variation:

Crear variación landscape de layout del login

El resultado será la creación de un layout con el mismo contenido de login_activity.xml que será ejecutado cuando el dispositivo rote.

Algo más que añadir:

  • Necesitamos cambiar el atributo android:orientation del padre al valor horizontal
  • Usaremos el atributo android:layout_weight con el valor de 1 en el logo y el formulario para tener una división equitativa
  • Centraremos los contenidos de ambos hijos asignando el valor center al atributo android:layout_gravity

Aplicando los cambios tendrás esta definición XML:

res/layout-land/activity_login.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <!-- Login progress -->
    <ProgressBar
        android:id="@+id/login_progress"
        style="?android:attr/progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:theme="@style/WhiteProgress"
        android:visibility="gone" />

    <ImageView
        android:id="@+id/image_logo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="16dp"
        android:layout_marginTop="8dp"
        android:layout_weight="1"
        android:padding="16dp"
        app:srcCompat="@drawable/saludmock_logo" />

    <ScrollView
        android:id="@+id/login_form"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:layout_weight="1">

        <LinearLayout
            android:id="@+id/email_login_form"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:orientation="vertical">

            <android.support.design.widget.TextInputLayout
                android:id="@+id/float_label_user_id"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:theme="@style/LoginTextField">

                <EditText
                    android:id="@+id/user_id"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/prompt_user_id"
                    android:inputType="number"
                    android:maxLength="10"
                    android:maxLines="1"
                    android:textColor="@android:color/white"
                    android:textColorHint="@android:color/white" />

            </android.support.design.widget.TextInputLayout>

            <android.support.design.widget.TextInputLayout
                android:id="@+id/float_label_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:theme="@style/LoginTextField"
                app:passwordToggleEnabled="true">

                <EditText
                    android:id="@+id/password"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/prompt_password"
                    android:imeActionId="@+id/login"
                    android:imeActionLabel="@string/action_sign_in_short"
                    android:imeOptions="actionUnspecified"
                    android:inputType="textPassword"
                    android:maxLines="1"
                    android:textColor="@android:color/white"
                    android:textColorHint="@android:color/white" />

            </android.support.design.widget.TextInputLayout>

            <Button
                android:id="@+id/email_sign_in_button"
                style="?android:textAppearanceSmall"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:text="@string/action_sign_in"
                android:textColor="@color/colorPrimary"
                android:textStyle="bold"
                app:backgroundTint="@android:color/white" />

        </LinearLayout>
    </ScrollView>
</LinearLayout>

El resultado de la previa sería así:

Preview del layout del login en landscape

Modificar Strings Del Login

Lo siguiente es cambiar los strings que creó Android Studio automáticamente para la actividad de login.

Si abres strings.xml verás que todas están en inglés.

Así que adapta cada valor al español según tus preferencias.

En mi caso sería de esta forma:

<resources>
    <string name="app_name">SaludMock</string>
    <string name="action_settings">Settings</string>
    <string name="title_activity_login">Inicio de sesión</string>

    <!-- Strings related to login -->
    <string name="prompt_user_id">Número de identificación</string>
    <string name="prompt_password">Contraseña</string>
    <string name="action_sign_in">Iniciar sesión</string>
    <string name="action_sign_in_short">Iniciar sesión</string>
    <string name="error_invalid_user_id">Número de identificación inválido</string>
    <string name="error_incorrect_password">La contraseña es incorrecta</string>
    <string name="error_field_required">Este campo es requerido</string>
    <string name="error_server">Error en el servidor</string>
    <string name="error_incorrect_user_id">Número de identificación no registrado</string>
    <string name="error_network">Conexión de red no disponible</string>
    <string name="error_invalid_password">Contraseña inválida</string>
</resources>

Establecer Condición De Transición Login – Citas Médicas

Mira:

Si ejecutases el proyecto en este momento, la actividad que se vería será la de citas médicas a causa de la etiqueta android.intent.category.LAUNCHER.

Para exigir que se ejecute el login iremos al método onCreate() de AppointmentsActivity y agregaremos una condición mucho antes de que se infle todo el contenido de esta.

Por el momento no tenemos una expresión booleana consistente para hacerlo suceder, así que pondrás un if con el literal true sin más:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Redirección al Login
    if (true) {
        startActivity(new Intent(this, LoginActivity.class));
        finish();
        return;
    }

De esta forma serás redirigido a LoginActivity al ejecutar la aplicación.

Implementar Lógica Del Login

La forma en que pensamos realizar el login se basa en estos pasos:

Usuario Sistema
1. Ingresa su ID y contraseña (la contraseña debe estar oculta)
2. Envía los datos
3. Determina que el usuario tiene acceso comprobando sus credenciales
4. Inicia la sesión
5. Presenta la pantalla de citas médicas

Este es el camino feliz de nuestro caso de uso.

Para implementarlo primero debemos modificar la plantilla generada de LoginActivity.

Si observas su estructura, verás que existen varios métodos y miembros útiles:

Estructura De LoginActivity

No obstante, hay varias características que no necesitamos relacionadas a la creación de sugerencias basadas en los contactos del dispositivo.

Esos elementos puedes borrarlos.

En cuestión, los únicos comportamientos que nos ayudarán son:

  • attemptLogin()
  • isEmailValid()
  • isPasswordValid()
  • showProgress()

Veamos como reconstruir el flujo…

Definir miembros de LoginActivity

En primer lugar añadiremos un usuario y contraseña de pruebas que permitirán saber si nuestro algoritmo sirve.

Esto requiere que elimines el arreglo DUMMY_CREDENTIALS:

/**
 * Credenciales de pruebas
 * TODO: remuévelas cuando vayas a implementar una autenticación real.
 */
private static final String DUMMY_USER_ID = "0000000000";
private static final String DUMMY_PASSWORD = "dummy_password";

Existe una tarea asíncrona interna que simula el proceso de autenticación en un hilo separado llamada UserLoginTask.

Por el momento la dejaremos, lo que significa que su instancia mAuthTask vivirá:

/**
 * Keep track of the login task to ensure we can cancel it if requested.
 */
private UserLoginTask mAuthTask = null;

Lo siguiente es definir las referencias a los views que tendrán comportamientos de interfaz.

Ya sabemos que tenemos al logo, los campos de texto para el ID y contraseña, la barra de progreso y el formulario como tal:

// UI references.
private ImageView mLogoView;
private EditText mUserIdView;
private EditText mPasswordView;
private TextInputLayout mFloatLabelUserId;
private TextInputLayout mFloatLabelPassword;
private View mProgressView;
private View mLoginFormView;

Definir el método onCreate()

En onCreate() primero obtén las referencias de todos los views que declaramos como instancias:

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

    mLogoView = (ImageView) findViewById(R.id.image_logo);
    mUserIdView = (EditText) findViewById(R.id.user_id);
    mPasswordView = (EditText) findViewById(R.id.password);
    mFloatLabelUserId = (TextInputLayout) findViewById(R.id.float_label_user_id);
    mFloatLabelPassword = (TextInputLayout) findViewById(R.id.float_label_password);
    Button mSignInButton = (Button) findViewById(R.id.email_sign_in_button);
    mLoginFormView = findViewById(R.id.login_form);
    mProgressView = findViewById(R.id.login_progress);
}

Ahora estableceremos las escuchas para el evento de edición en el campo de texto del password y el click en el botón de login:

// Setup
mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
        if (id == R.id.login || id == EditorInfo.IME_NULL) {
            attemptLogin();
            return true;
        }
        return false;
    }
});

mSignInButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        attemptLogin();
    }
});

Los métodos populateAutoComplete() y mayRequestContacts() no lo necesitaremos, así que borralos.

Los métodos isEmailValid() e isPasswordValid()

La verificación de los valores de correo y contraseña que el usuario digita se realizan con estos métodos.

Pero no usaremos email si no un ID numérico.

Así que refactoriza isEmailValid() para que se llame isUserIdValid(). Y verifica que su tamaño sea exactamente 10.

Mira:

private boolean isUserIdValid(String userId) {
    return userId.length() == 10;
}

En el password dejaremos la regla que trae la plantilla predeterminada: que su tamaño no sea inferior o igual a 4.

private boolean isPasswordValid(String password) {
    return password.length() > 4;
}

Definir método attemptLogin()

Este método tal cual como está es muy bueno.

Ya que:

  1. Comprueba que no se hayan enviados datos aún (mAuthTask debe ser null)
  2. Restablece errores por si las anteriores credenciales los dejaron (setError(null))
  3. Obtiene los datos del usuario como variables String (userId y password)
  4. Transmite el foco al primer campo de texto con error (requestFocus())
  5. Valida si los campos de texto no están vacíos (TextUtils.isEmpty())
  6. Asigna los errores flotantes a los campos de texto (setError() con recursos string)
  7. Si todo sale bien, entonces muestra el progreso (showProgress()) y luego crea una tarea asíncrona con las credenciales validadas.

Si revisas el código, y tan solo cambias la asignación de errores a las etiquetas flotantes, tendrás:

private void attemptLogin() {
    if (mAuthTask != null) {
        return;
    }

    // Reset errors.
    mFloatLabelUserId.setError(null);
    mFloatLabelPassword.setError(null);

    // Store values at the time of the login attempt.
    String userId = mUserIdView.getText().toString();
    String password = mPasswordView.getText().toString();

    boolean cancel = false;
    View focusView = null;

    // Check for a valid password, if the user entered one.
    if (TextUtils.isEmpty(password)) {
        mFloatLabelPassword.setError(getString(R.string.error_field_required));
        focusView = mFloatLabelPassword;
        cancel = true;
    } else if (!isPasswordValid(password)) {
        mFloatLabelPassword.setError(getString(R.string.error_invalid_password));
        focusView = mFloatLabelPassword;
        cancel = true;
    }

    // Verificar si el ID tiene contenido.
    if (TextUtils.isEmpty(userId)) {
        mFloatLabelUserId.setError(getString(R.string.error_field_required));
        focusView = mFloatLabelUserId;
        cancel = true;
    } else if (!isUserIdValid(userId)) {
        mFloatLabelUserId.setError(getString(R.string.error_invalid_user_id));
        focusView = mFloatLabelUserId;
        cancel = true;
    }

    if (cancel) {
        // There was an error; don't attempt login and focus the first
        // form field with an error.
        focusView.requestFocus();
    } else {
        // Show a progress spinner, and kick off a background task to
        // perform the user login attempt.
        showProgress(true);
        mAuthTask = new UserLoginTask(userId, password);
        mAuthTask.execute((Void) null);
    }
}

Mostrar indicador de carga

Ya sabemos que showProgress() es el encargado de mostrar/ocultar la ProgressBar en el layout.

El código creado por la plantilla tiene la lógica correcta. Muestra la barra de progreso (View.VISIBLE) y oculta el formulario (View.GONE).

Sin embargo nosotros necesitamos ocultar el logo también, por lo que podemos reducirlo a lo siguiente:

private void showProgress(boolean show) {
    mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);

    int visibility = show ? View.GONE : View.VISIBLE;
    mLogoView.setVisibility(visibility);
    mLoginFormView.setVisibility(visibility);
}

Usar tarea asíncrona para simulación

UserLoginTask simula a través de una espera de n milisegundos la petición al servidor web.

Sin embargo cambiaremos el tercer parámetro por el tipo Integer, ya que reportaremos un error para el ID y otro para la contraseña:

public class UserLoginTask extends AsyncTask<Void, Void, Integer> {

Su constructor y miembros los dejaremos intactos:

private final String mUserId;
private final String mPassword;

UserLoginTask(String userId, String password) {
    mUserId = userId;
    mPassword = password;
}

El método doInBackground() comienza de forma correcta. Se adormece el hilo 2000 milisegundos para simular la petición web.

Sin embargo cambiaremos la comprobación de credenciales basados en los datos de prueba:

@Override
protected Integer doInBackground(Void... params) {

    try {
        // Simulate network access.
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        return 4;
    }

    if (!mUserId.equals(DUMMY_USER_ID)) {
        return 2;
    }

    if (!mPassword.equals(DUMMY_PASSWORD)) {
        return 3;
    }

    return 1;

}

Los errores son un diseño rápido cuyo significado es:

  1. Petición exitosa
  2. Número de identificación no registrado
  3. Password incorrecto
  4. Error del servidor

Con esta idea en mente, procesamos los resultados en onPostExecute() de la siguiente forma:

  1. Dirigimos al usuario a la actividad de citas médicas (showAppointmentsScreen())
  2. Se asigna error al campo de texto del id (mFloatLabelUserId)
  3. Se asigna error al campo de texto del password (mFloatLabelPassword)
  4. Se muestra un Toast con el error de servidor (showLoginError())

En código Java tendremos:

@Override
protected void onPostExecute(final Integer success) {
    mAuthTask = null;
    showProgress(false);

    switch (success) {
        case 1:
            showAppointmentsScreen();
            break;
        case 2:
        case 3:
            showLoginError("Número de identificación o contraseña inválidos");
            break;
        case 4:
            showLoginError(getString(R.string.error_server));
            break;
    }
}

Donde showAppointmentsScreen() y showLoginError() son métodos de LoginActivity con la siguiente definición:

private void showAppointmentsScreen() {
    startActivity(new Intent(this, AppointmentsActivity.class));
    finish();
}

private void showLoginError(String error) {
    Toast.makeText(this, error, Toast.LENGTH_LONG).show();
}

Verificar conexión de red

Debo agregar que verificar la disponibilidad de la red es vital para realizar el login, por lo que añadiremos un método llamado isOnline() con el siguiente contenido:

private boolean isOnline() {
    ConnectivityManager cm =
            (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

    NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
    return activeNetwork != null && activeNetwork.isConnected();
}

Y luego anteponer su resultado antes de llamar a attemptLogin():

// Setup
mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
        if (id == R.id.login || id == EditorInfo.IME_NULL) {
            if (!isOnline()) {
                showLoginError(getString(R.string.error_network));
                return false;
            }
            attemptLogin();
            return true;
        }
        return false;
    }
});

mSignInButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        if (!isOnline()) {
            showLoginError(getString(R.string.error_network));
            return;
        }
        attemptLogin();

    }
});

Limpiar actividad

Hasta el momento tendríamos todo lo necesario para simular la autenticación de nuestros usuarios, no obstante aún hay algunos métodos que no usaremos añadidos por Android Studio.

Así que elimina todo aquello que no tenga nada que ver con lo que haremos.

Testear Aplicación SaludMock Con Datos Falsos

Por último ejecuta la aplicación (Run) y contempla el resultado final de la interfaz:

Login Screen UI Android

La primer prueba será dejar el ID del usuario y la contraseña vacíos para ver si se trata la restricción.

Se requieren credenciales del login

Prueba los formatos de ambas credenciales:

Credenciales inválidas de usuario

También prueba enviar los datos sin conexión:

Conexión de red no disponible

Pon los datos con formato ideal, pero sin registro existente:

Usuario no registrado

Finalmente pon las credenciales de prueba correctamente y verifica que proceda a la pantalla de citas médicas.

Crear Base De Datos Remota De SaludMock

Bien, nuestro próximo paso es crear la base de datos que soporte la app.

Para ello usaremos el gestor MySQL.

Ya sabes que puedes conseguir esta herramienta instalando el paquete XAMPP.

De esta manera podrás seguir los siguientes pasos…

Definir Modelo De Datos Para El Login

Si tomamos como referencia el escenario actual solo tendremos una entidad relacionada: Los afiliados a la entidad promotora de salud.

Si deseáramos darle usuarios para que accedan a la app, entonces sería buena idea tener una entidad para los mismos.

Sin embargo un afiliado solo puede tener un usuario con el que puede acceder.

Lo que produce una relación 1:1.

Desde este punto de vista podríamos tan solo dotar a los afiliados de atributos que les permitan loguearse.

Así que simplificando en un DER tendríamos la siguiente tabla:

DER SaludMockSimple y conciso.

El problema en estas instancias no exige más… a menos que tú tengas otras restricciones en tu negocio.

Implementar Base De Datos En MySQL

Debido a que vamos a realizar las pruebas locamente, entonces entra a phpMyAdmin con la ruta localhost/phpmyadmin.

Ahora crea una nueva base de datos presionando el botón Nueva o New en el panel izquierdo. Asígnale como nombre "salud_mock" y presiona el botón de Crear.

Crear base de datos de saludmock en phpmyadmin

En seguida, selecciona la base de datos y ve a la pestaña SQL.

Allí pon la siguiente sentencia CREATE TABLE para añadir la tabla affiliate:

CREATE TABLE affiliate
(
    id VARCHAR(10) PRIMARY KEY NOT NULL,
    hash_password VARCHAR(256) NOT NULL,
    name VARCHAR(128) NOT NULL,
    address VARCHAR(128) NOT NULL,
    gender ENUM('F', 'M') NOT NULL,
    token VARCHAR(255) NOT NULL
);

Con eso ya tendrás parcialmente la base de datos para SaludMock.

Crear Servicio Web RESTful

En este punto usaremos PHP 5.6 para procesar las peticiones HTTP, enrutar los parámetros hacia los recursos definidos y retornar una respuesta JSON.

Ahora bien: nos basaremos en el login que creamos en el artículo Servicio Web RESTful Para Android Con Php, Mysql y Json.

Dentro de este contexto adaptaremos el mismo enrutamiento y diseño. Por ende, sería bueno que lo leyeras para no perderte.

Comencemos por definir la estructura de paquetes…

Definir Estructura Del Proyecto PHP

Crea un directorio llamado api.saludmock.com para el servicio RESTful dentro de \xampp\htdocs.

Esta será la raíz donde albergaremos todos los archivos vitales.

Para ser más exactos la estructura general de directorios se verá de esta forma:

Estructura de carpetas de proyecto PHP

El propósito de cada archivo y directorio es el siguiente:

Archivo/Directorio Descripción
v1 Señala que en su interior estará la versión 1 de la API
controllers Contiene los controladores para cada recurso
data Contiene los componentes relacionados con la base de datos local
utils Guarda elementos de utilidad y relación indirecta
views Contiene las clases encargadas de presentar las respuestas de la API
.htaccess Archivo de configuración Apache para sobrescribir las URLs
index.php Punto de entrada principal de las peticiones HTTP para generación de enrutamiento

Crear Archivo .htaccess

Para tener URLs limpias y sin extensiones crea un archivo .htaccess en v1 y agrega las siguientes reglas:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?PATH_INFO=$1 [L,QSA]
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Diseñar URIs Para Login

¿Cómo serán las URIs para crear la sesión de un afiliado?

Sencillo: podemos usar el plural de afiliados en inglés (affiliates) para establecer el recurso y luego añadir un segmento login.

Usaremos el método POST para determinar la creación de una sesión de usuario. Lo que en resumidas será:

POST http://localhost/blog/v1/api.saludmock.com/affiliates/login Crea una nueva sesión del afiliado

Dicha funcionalidad estará incompleta, si no establecemos una ubicación para guardar los afiliados que deseamos autenticar.

Por esta razón usaremos la palabra register para establecerlo como punto de llegada de los registros. Al igual que el login, este será una creación, por ende usamos POST:

POST http://localhost/blog/v1/api.saludmock.com/affiliates/register Guarda un nuevo afiliado

Diseñar Cuerpo De Peticiones Y Respuestas

La petición que apunte al endpoint del login en su expresión más simple debe enviar las credenciales del usuario.

En este tutorial no seguiremos un método de autenticación como Basic Auth, Digest u OAuth. Pero sería excelente que aprendas a usar uno de estos elementos para añadir complejidad a la seguridad de tu API.

Y además adquirir un certificado SSL para no pasar solo texto plano en tu petición.

Mira:

Enviaremos las credenciales en un objeto JSON en el cuerpo de la petición.

Veamos un ejemplo:

{
 "userId":"000000000",
 "password":"dummy_password"
}

De otro lado, la respuesta que recibiremos estará compuesta de los siguientes atributos:

(Me basé en estos tips de diseño de Stormpath)

  • status: Contiene el error HTTP de forma redundante para evitar el análisis de la respuesta completa
  • code: Código interno de error para la API
  • message: Mensaje de error corto para el usuario junto a una posible solución
  • moreInfo: Aquí pones una URL para apuntar a un recurso que muestre más información del error ocurrido.
  • developerMessage: Es el mensaje en detalle sobre el error ocurrido con datos que puedan ser de interés para el desarrollador. Nos servirá bastante para el modo debug de Android (Log.d())

Por ejemplo, si el número de identificación no estuviera registrado tendríamos:

{
 "status":401,
 "code":1001,
 "message":"El número de identificación no está registrado",
 "moreInfo":"http://localhost",
 "developerMessage":"No existe un registro en la tabla \"affiliate\" cuya columna \"id\" coincida"
}

En caso de que la autorización sea exitosa, entonces tendríamos:

{ 
 "id":9993230213,
 "name":"Carlos Lopez",
 "address":"Cra 34 #24-20",
 "gender":"M",
 "token":"$2y$10$KjIJ3BNQL.Z9CGGl1P1vBO.dMRMtKUun21k4oGtHL5.eUVnTh.W/C"
}

Todos los datos del afiliado junto al token nos permitirán inflar la interfaz y mantener la sesión abierta.

Peticiones y respuestas del registro de usuarios

La petición JSON de un nuevo usuario tendrá todos sus datos excepto el token, ya que lo generaremos internamente:

{ 
 "id":9993230213,
 "password":"ghyUlV",
 "name":"Carlos Lopez",
 "address":"Cra 34 #24-20",
 "gender":"M"
}

Si todo salió bien, entonces tendríamos un estado 201 con el siguiente cuerpo:

{
 "status" : "201"
 "message":"Afiliado registrado"
}

Elegir Formatos De Texto Para La API

El formato más popular es JSON, sin embargo también podemos dar soporte a XML como añadidura.

Para que el cliente pueda especificarlo en la petición, daremos la posibilidad de enviar un parámetro llamado format en la URL.

Por ejemplo:

http://localhost/blog/api.saludmock.com/v1/affiliates/login?format=xml

ó

http://localhost/blog/api.saludmock.com/v1/affiliates/register?format=json

El formato por defecto siempre será JSON, por lo que si deseas ese formato puedes omitir pegar el parámetro format.

Enrutamiento De Recursos En index.php

Como es sabido, el enrutamiento es el proceso de tomar la respuesta HTTP, analizar la URL, el método y las cabeceras para realizar una acción sobre nuestros recursos.

El lugar donde lo haremos será el index.php.

Así que agrega a v1 dicho archivo.

La lógica exacta que seguirá será:

  1. Procesar el parámetro format para determinar el formato de la respuesta (JSON/XML)
  2. Registrar un manejador de excepciones globales que surjan en la API
  3. Extraer los segmentos de la URL
  4. Determinar el recurso sobre el que se realizará la operación
  5. Ejecutar la acción sobre el recurso dependiendo del método HTTP especificado en el cliente
  6. Imprimir la respuesta en el formato establecido

Esto en código se implementa a:

index.php

<?php

require 'controllers/affiliates.php';
require 'views/XmlView.php';
require 'views/JsonView.php';
require 'utils/ApiException.php';

// Obtener valor del parámetro 'format' para el formato de la respuesta
$format = isset($_GET['format']) ? $_GET['format'] : 'json';

// Crear representación de la vista para el formato elegido
if (strcasecmp($format, 'xml') == 0) {
    $apiView = new XmlView();
} else {
    $apiView = new JsonView();
}

// Registrar manejador de excepciones
set_exception_handler(
    function (ApiException $exception) use ($apiView) {
        http_response_code($exception->getStatus());
        $apiView->render($exception->toArray());
    }
);

// Extraer segmento de la url
if (isset($_GET['PATH_INFO'])) {
    $urlSegments = explode('/', $_GET['PATH_INFO']);
} else {
    throw new ApiException(
        404,
        0,
        "El recurso al que intentas acceder no existe",
        "http://localhost",
        "No existe un resource definido en: http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
}

// Obtener recurso
$resource = array_shift($urlSegments);
$apiResources = array('affiliates');

// Comprobar si existe el recurso
if (!in_array($resource, $apiResources)) {
    throw $resourceNotFound;
}

// Transformar método HTTP a minúsculas
$httpMethod = strtolower($_SERVER['REQUEST_METHOD']);

// Determinar acción según el método HTTP
switch ($httpMethod) {
    case 'get':
    case 'post':
    case 'put':
    case 'delete':
        if (method_exists($resource, $httpMethod)) {
            $apiResponse = call_user_func(array($resource, $httpMethod), $urlSegments);
            $apiView->render($apiResponse);
            break;
        }
    default:
        // Método no permitido sobre el recurso
        $methodNotAllowed = new ApiException(
            405,
            0,
            "Acción no permitida",
            "http://localhost",
            "No se puede aplicar el método $_SERVER[REQUEST_METHOD] sobre el recurso \"$resource\"");
        $apiView->render($methodNotAllowed->toArray());

}

Crear Vistas JSON Y XML

Si observas el index.php verás cómo usamos la clase JsonView y XmlView para instancear a $apiView.

Estas clases son implementaciones concretas de View: una clase abstracta que representa el formato de impresión de la respuesta.

Basándonos en esta definición, entonces crea la clase View dentro de views y añádele un método para imprimir la respuesta llamado render().

<?php

/**
 * Clase base para la representación de las vistas
 */
abstract class View {
    public abstract function render($body);
}

Derivar Vista JSON

Ahora implementaremos una vista dedicada al formato JSON.

Así que añade a views una clase llamada JsonView y sobrescribe render().

La idea es usar json_encode() para transformar el array asociativo entrante en un objeto JSON:

<?php

require_once "View.php";

/**
 * Clase para imprimir en la salida respuestas con formato JSON
 */
class JsonView extends View {

    public function render($body) {
        // Set de estado de le respuesta
        if (isset($body["status"])) {
            http_response_code($body["status"]);
        }

        // Set del contenido de la respuesta
        header('Content-Type: application/json; charset=utf8');

        // Encodificado JSON
        $jsonResponse = json_encode($body, JSON_PRETTY_PRINT, JSON_UNESCAPED_UNICODE);

        if (json_last_error() != JSON_ERROR_NONE) {
            $internalServerError = new ApiException(
                500,
                0,
                "Error interno en el servidor. Contacte al administrador",
                "http://localhost",
                "Error de parsing JSON en JsonView.php. Causa: " . json_last_error_msg());
            throw $internalServerError;
        }

        echo $jsonResponse;

        exit;
    }
}

En parte, el complemento principal de la impresión debe llevar el estado HTTP para setearlo con http_response_code() y además usar como tipo de contenido application/json con UTF-8.

Derivar Vista XML

Al igual que el anterior caso, crea una clase llamada XmlView en views y derívala de View:

<?php

require_once "View.php";

/**
 * Clase para imprimir en la salida respuestas con formato XML
 */
class XmlView extends View
{

    public function render($body)
    {
        // Set de estado de le respuesta
        if (isset($body["status"])) {
            http_response_code($body["status"]);
        }

        // Set del contenido de la respuesta
        header('Content-Type: text/xml; charset=utf-8');

        $xml = new SimpleXMLElement('<apiResponse/>');
        self::arrayToXml($body, $xml);
        print $xml->asXML();

        exit;
    }

    public function arrayToXml($data, &$xml)
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if (is_numeric($key)) {
                    $key = 'item' . $key;
                }
                $subnode = $xml->addChild($key);
                self::arrayToXml($value, $subnode);
            } else {
                $xml->addChild("$key", htmlspecialchars("$value"));
            }
        }
    }
}

La clase SimpleXMLElement nos permitirá crear un nodo XML al cual le añadiremos los hijos dependiendo del array asociativo entrante.

Allí es donde arrayToXml() comienza a crear la jerarquía XML.

Manejar Errores De La API Con Excepciones

Cuando estemos controlando los errores que producen ciertos flujos de la API, es mucho más sencillo disparar una excepción en ese punto que retornar el resultado entre elementos.

Pensando en ello, creemos una excepción propia llamada ApiException en la carpeta utils que herede de Exception.

La idea es poner como atributos aquellos elementos de las respuestas que diseñamos anteriormente y así retornarlas:

<?php

/**
 * Excepción personalizada para el envío de respuestas
 */
class ApiException extends Exception {
    private $status;
    private $apiCode;
    private $userMessage;
    private $moreInfo;
    private $developerMessage;

    public function __construct($status, $code, $message, $moreInfo, $developerMessage) {
        $this->status = $status;
        $this->apiCode = $code;
        $this->userMessage = $message;
        $this->moreInfo = $moreInfo;
        $this->developerMessage = $developerMessage;
    }

    public function getStatus() {
        return $this->status;
    }

    public function getApiCode() {
        return $this->apiCode;
    }

    public function getUserMessage() {
        return $this->userMessage;
    }

    public function getMoreInfo() {
        return $this->moreInfo;
    }

    public function getDeveloperMessage() {
        return $this->developerMessage;
    }

    public function toArray() {
        $errorBody = array(
            "status" => $this->status,
            "code" => $this->apiCode,
            "message" => $this->userMessage,
            "moreInfo" => $this->moreInfo,
            "developerMessage" => $this->developerMessage
        );
        return $errorBody;
    }
}

Si retomas en archivo index.php, verás el lugar donde registramos el manejador de excepciones. Allí se llama al método toArray() para pasarlo a la vista y setear el estado HTTP proveniente de la excepción ($status).

Crear Controlador De Afiliados

El manejo del recurso affiliates será a través de una clase con el mismo nombre que administrará que hacer con el recurso dependiendo de la acción.

Así que crea la clase affiliates en la carpeta controllers y pon cuatro métodos para cada acción HTTP:

<?php

/**
 * Controlador del recurso "/affiliates"
 */
class affiliates {

    public static function get($urlSegments) {

    }

    public static function post($urlSegments) {

    }

    public static function put($urlSegments) {

    }

    public static function delete($urlSegments) {

    }

}

Si te fijas, cada método recibe un parámetro $urlSegments para procesar los segmentos de URL extras de la petición.

Cabe aclarar que en index.php usamos el método call_user_func() para llamar el método correspondiente de los controladores existentes según el recurso y el método HTTP.

De ese modo, si sigues este estilo de diseño, entonces todos tus controllers deben tener los mismos 4 métodos.

Procesar Petición POST Para Registro De Usuarios

Aquí nos preguntamos como registramos a un usuario, según las URIs, entradas y salidas diseñadas.

Es evidente que el código lo escribiremos en el método post() de affiliates.

Además piensa en que podemos tener los recursos "affiliates/register" y "affiliates/login".

Por lo que lo más conveniente es procesar los segmentos con un condicional:

public static function post($urlSegments) {
    switch ($urlSegments[0]) {
        case "register":
            return self::saveAffiliate();
            break;
        case "login":
            return self::authAffiliate();
            break;
        default:
            throw new ApiException(404, 0, "El recurso al que intentas acceder no existe",
                "http://localhost.com", "No se encontró el segmento \"affiliates/$urlSegments[0]\".");
    }
}

Donde saveAffiliate() será el método que registre a los afiliados en la base de datos MySQL y authAffiliate() será el que autentique al usuario.

Insertar Afiliado En La Base De Datos

¡Excelente!

Ahora solo debemos insertar los datos que vienen en la tabla affiliate.

El siguiente flujo describe muy bien las acciones a realizar entro de saveAffiliate():

  1. Obtener parámetros del POST y decodificar su formato JSON (o XML)
  2. Verificar integridad de los atributos del objeto
  3. Realizar operación INSERT en la tabla affiliate.
  4. Procesar el resultado de la base de datos para retornar una respuesta

En código tendríamos lo siguiente:

private static function saveAffiliate() {
    // Obtener parámetros de la petición
    $parameters = file_get_contents('php://input');
    $decodedParameters = json_decode($parameters);

    // Verificar integridad de datos
    // TODO: Implementar restricciones de datos adicionales
    if (!isset($decodedParameters["id"]) ||
        !isset($decodedParameters["password"]) ||
        !isset($decodedParameters["name"]) ||
        !isset($decodedParameters["address"]) ||
        !isset($decodedParameters["gender"])
    ) {
        // TODO: Crear una excepción individual por cada causa anómala
        throw new ApiException(400, 0,
            "Verifique los datos del afiliado tengan formato correcto",
            "http://localhost.com",
            "Uno de los atributos del afiliado no está definido en los parámetros");
    }

    // Insertar afiliado
    $dbResult = self::insertAffiliate($decodedParameters);

    // Procesar resultado de la inserción
    if ($dbResult) {
        return ["status" => 200, "message" => "Afiliado registrado"];
    } else {
        throw new ApiException(500, 2000,
            "Error del servidor",
            "http://localhost.com",
            "Error en la base de datos al ejecutar la inserción del afiliado.");
    }
}

Ahora, insertAffiliate() es quien opera la base de datos usando una conexión PDO, por lo que antes de ver su lógica primero definiremos el conector BD.

Crear Singleton Para Conexión PDO

La idea es crear la dependencia de un objeto PDO dentro de una clase manejadora llamada MysqlManager.

Para ello añádela en el paquete data y asegúrate de que siga el patrón singleton:

MysqlManager.php

<?php
/**
 * Clase que envuelve una instancia de la clase PDO
 * para el manejo de la base de controladores
 */

require_once 'login_mysql.php';


class MysqlManager {

    /**
     * Única instancia de la clase
     */
    private static $mysqlManager = null;

    /**
     * Instancia de PDO
     */
    private static $pdo;

    final private function __construct() {
        try {
            // Crear nueva conexión PDO
            self::getDb();
        } catch (PDOException $e) {
            // Manejo de excepciones
            throw new ApiException(
                500,
                0,
                "Error de conexión a base de datos",
                "http://localhost",
                "La conexión al usuario administrador de MySQL se vío afectada. Detalles: " . $e->getMessage());
        }
    }

    public static function get() {
        if (self::$mysqlManager === null) {
            self::$mysqlManager = new self();
        }
        return self::$mysqlManager;
    }

    public function getDb() {
        if (self::$pdo == null) {

            // Parámetros de PDO
            $dsn = sprintf('mysql:dbname=%s; host=%s', MYSQL_DATABASE_NAME, MYSQL_HOST);
            $username = MYSQL_USERNAME;
            $passwd = MYSQL_PASSWORD;
            $options = array(
                PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8",
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);

            self::$pdo = new PDO($dsn, $username, $passwd, $options);
        }

        return self::$pdo;
    }

    final protected function __clone() {
    }

    function _destructor() {
        self::$pdo = null;
    }
}

Definir Credenciales De MySQL

El constructor de PDO requiere 3 parámetros donde debemos incluir las credenciales de MySQL basados en el host al que accederemos y la base de datos a usar.

Como ya lo hice notar, MysqlManager tiene una sentencia require_once para un archivo llamado login_mysql.php.

Justo allí irán estas credenciales a manera de constantes.

Fíjate:

login_mysql.php

<?php
/**
 * Provee las constantes para conectarse a la base de datos
 * Mysql.
 */
// TODO: cambiar por dominio o IP en producción
define("MYSQL_HOST", "localhost");
define("MYSQL_DATABASE_NAME", "saludmock");
// TODO: cambiar en producción
define("MYSQL_USERNAME", "root");
// TODO: cambiar en producción
define("MYSQL_PASSWORD", "");

IMPORTANTE: Cambia estas credenciales cuando estés en producción. Recuerda que estamos usando nuestro PC doméstico como servidor de desarrollo, pero cuando contratas un proveedor los datos deben ajustarse a este ambiente.

Implementar Método insertAffiliate()

Con lo anterior hemos preparado el camino para insertar el afiliado, así que crea el método insertAffiliate() en affiliates y aplica tu conocimiento para:

  1. Extraer individualmente cada dato del afiliado
  2. Generar los atributos que requieran cálculos o tratos especiales (como la encriptación del password y la creación del token)
  3. Preparar la sentencia INSERT con los datos
  4. Ejecutar la sentencia preparada y retornar el resultado

Veamos:

private static function insertAffiliate($decodedParameters) {
    //Extraer datos del afiliado
    $id = $decodedParameters["id"];
    $password = $decodedParameters["password"];
    $name = $decodedParameters["name"];
    $address = $decodedParameters["address"];
    $gender = $decodedParameters["gender"];

    // Encriptar contraseña
    $hashPassword = password_hash($password, PASSWORD_DEFAULT);

    // Generar token
    $token = uniqid(rand(), TRUE);

    try {
        $pdo = MysqlManager::get()->getDb();

        // Componer sentencia INSERT
        $sentence = "inserto into affiliate (id, hash_password, name, address, gender, token)" .
            " values (?,?,?,?,?,?)";

        // Preparar sentencia
        $preparedStament = $pdo->prepare($sentence);
        $preparedStament->bindParam(1, $id);
        $preparedStament->bindParam(2, $hashPassword);
        $preparedStament->bindParam(3, $name);
        $preparedStament->bindParam(4, $address);
        $preparedStament->bindParam(5, $gender);
        $preparedStament->bindParam(6, $token);

        // Ejecutar sentencia
        return $preparedStament->execute();

    } catch (PDOException $e) {
        throw new ApiException(
            500,
            0,
            "Error de base de datos en el servidor",
            "http://localhost.com",
            "Ocurrió el siguiente error al intentar insertar el afiliado: " . $e->getMessage());
    }
}

REST Testing: Registro De Afiliados

¿Cómo vas?

La codificación ha sido significativa y es muy parecida al servicio REST de la agenda con contactos que ya hicimos.

Lo que haremos ahora es testear con la herramienta Postman, las peticiones de registro.

Para ello sigue estos pasos:

1. Abre Postman.

Abrir Postman

2. Configura la petición con las siguientes características:

  • Método: POST
  • URL: http://localhost/blog/v1/api.saludmock.com/affiliates/register

Método POST y URL de registro de afiliados

3. Ahora, ve a la pestaña Body, pásate al radio raw y luego selecciona el tipo JSON (application/json).

En la caja del cuerpo inserta los datos de un afiliado de prueba que vimos en el diseño de la petición.

A manera de ejemplo yo agregaré los siguientes:

{
    "id":"1234567890",
    "password":"mypassword",
    "name":"Fernando",
    "address":"Calle 23 #2",
    "gender":"M"
}

Con estas condiciones dadas, tendrás la interfaz así:

Nuevo afiliado con postman

4. Presiona Send y espera la siguiente respuesta:

{
 "status": 200,
 "message": "Afiliado registrado"
 }

Con esto tendrás el registro de afiliados completo.

A menos que tengas alguna excepción, la cual no deberías tardarte más de un minuto en resolver, debido a que el atributo developerMessage te dirá exactamente qué sucede.

IMPORTANTE: El servicio REST actual permite que cualquiera que conozca la URI de registro de afiliados y el formato de petición, pueda guardar usuarios. Si deseas limitar esto, te interesará mucho usar roles, permisos y recursos como te enseño en mi tutorial Tutorial De App Productos Parte 2: Login Y Servidor Virtual DigitalOcean.

Procesar Petición POST Para Login De Usuarios

Una vez creado un afiliado, ahora necesitamos autenticarlo y crear su sesión.

En el apartado anterior vimos que el método post() de affiliates llamaba al método authAffiliate(), cuando el segmento de URL complementario era "/login".

Lo que nos lleva a crear dicho método e implementar su lógica.

Añadir Lógica De Autenticación

Agrega el método authAffiliate() al controlador de afiliados y siguiendo el mismo formato que usamos en saveAffiliate(), aplica estos pasos:

  1. Obtener parámetros y decodificar su tipo JSON
  2. Validar integridad de datos de la petición
  3. Consultar si las credenciales coinciden con un registro de la base de datos
  4. Retornar la respuesta de la sesión creada o el error generado

Aunque el código PHP es parecido al anterior, te mostraré como quedarían las instrucciones:

private static function authAffiliate() {

    // Obtener parámetros de la petición
    $parameters = file_get_contents('php://input');
    $decodedParameters = json_decode($parameters, true);

    // Controlar posible error de parsing JSON
    if (json_last_error() != JSON_ERROR_NONE) {
        $internalServerError = new ApiException(500, 0,
            "Error interno en el servidor. Contacte al administrador",
            "http://localhost",
            "Error de parsing JSON. Causa: " . json_last_error_msg());
        throw $internalServerError;
    }

    // Verificar integridad de datos
    if (!isset($decodedParameters["id"]) ||
        !isset($decodedParameters["password"])
    ) {
        throw new ApiException(
            400,
            0,
            "Las credenciales del afiliado deben estar definidas correctamente",
            "http://localhost",
            "El atributo \"id\" o \"password\" o ambos, están vacíos o no definidos"
        );
    }

    $userId = $decodedParameters["id"];
    $password = $decodedParameters["password"];

    // Buscar usuario en la tabla
    $dbResult = self::findAffiliateByCredentials($userId, $password);

    // Procesar resultado de la consulta
    if ($dbResult != NULL) {
        return [
            "status" => 200,
            "id" => $dbResult["id"],
            "name" => $dbResult["name"],
            "address" => $dbResult["address"],
            "gender" => $dbResult["gender"],
            "token" => $dbResult["token"]
        ];
    } else {
        throw new ApiException(
            400,
            4000,
            "Número de identificación o contraseña inválidos",
            "http://localhost",
            "Puede que no exista un usuario creado con el id:$userId o que la contraseña:$password sea incorrecta."
        );
    }
}

Si observas, findAffiliateByCredentials() se encarga de verificar en la base de datos, si existe una afiliado con las credenciales entrantes.

Para definirlo sigue leyendo…

Consultar La Tabla De Afiliados Por Credenciales

Una vez obtenidos las credenciales del usuario, entonces creamos el método findAffiliateByCredentials() para recibirlos.

La idea es:

  1. Redactar el comando SELECT sobre affiliate para encontrar al usuario con el ID del parámetro
  2. Preparar la sentencia y ejecutarla
  3. Si hubo resultados, entonces comprobar el valor de la columna hash_password con la contraseña del parámetro
  4. Retornar el usuario (array asociativo) si ambas credenciales coinciden, o un valor de null.

Vayamos al código:

private static function findAffiliateByCredentials($userId, $password) {
    try {
        $pdo = MysqlManager::get()->getDb();

        // Componer sentencia SELECT
        $sentence = "SELECT * FROM affiliate WHERE id=?";

        // Preparar sentencia
        $preparedSentence = $pdo->prepare($sentence);
        $preparedSentence->bindParam(1, $userId, PDO::PARAM_INT);

        // Ejecutar sentencia
        if ($preparedSentence->execute()) {
            $affiliateData = $preparedSentence->fetch(PDO::FETCH_ASSOC);

            // Verificar contraseña
            if (password_verify($password, $affiliateData["hash_password"])) {
                return $affiliateData;
            } else {
                return null;
            }

        } else {
            throw new ApiException(
                500,
                5000,
                "Error de base de datos en el servidor",
                "http://localhost",
                "Hubo un error ejecutando una sentencia SQL en la base de datos. Detalles:" . $pdo->errorInfo()[2]
            );
        }

    } catch (PDOException $e) {
        throw new ApiException(
            500,
            0,
            "Error de base de datos en el servidor",
            "http://localhost.com",
            "Ocurrió el siguiente error al consultar el afiliado: " . $e->getMessage());
    }
}

Rest Testing: Login De Usuarios

De la misma forma que el registro de afiliados, abriremos Postman para testear el login.

En primer lugar crea una nueva pestaña y configura la petición así:

  • Método: POST
  • URL: http://localhost/blog/api.saludmock.com/v1/affiliates/login

Testing de login de afiliados URL

El siguiente paso es ir a la pestaña Body, seleccionar el radio raw y decidirnos por JSON (application/json).

Con esas características, pon en el área de texto un objeto JSON con las credenciales de afiliado como habíamos diseñado anteriormente:

{
 "id":"1234567890",
 "password":"mypassword"
}

Si todo salió bien, entonces tendrás una respuesta con estado 200 similar a la siguiente:

{
 "status": 200,
 "id": "1234567890",
 "name": "Fernando",
 "address": "Calle 23 #2",
 "gender": "M",
 "token": "19922585ab9d878a2d3.03088656"
}

Realizar Petición POST Con Retrofit

¡Muy bien!

Ya tenemos nuestro servicio REST para el registro y login de afiliados.

Aunque solo necesitaremos el login para que nuestra app parcial de SaludMock funcione.

Así que vamos a ver como modificar el sistema de autenticación falso que tenemos, por una petición POST real con Retrofit…

Crear Interfaz Java Para Representar REST Service

Como vimos en la receta inicial, el primer paso es añadir una interfaz que abstraiga el formato de las peticiones HTTP que recibirá nuestro servicio web.

Así que crea una nueva interfaz llamada SaludMockApi y define un método POST para el login de usuarios:

public interface SaludMockApi {

    // TODO: Cambiar host por "10.0.0.2" para Genymotion.
    // TODO: Cambiar host por "10.0.0.3" para AVD.
    // TODO: Cambiar host por IP de tu PC para dispositivo real.
    public static final String BASE_URL = "http://10.0.0.2/blog/api.saludmock.com/v1";

    @POST("affiliates/login")
    Call<Affiliate> login(@Body LoginBody loginBody);

}

La constante BASE_URL será la raíz que nos ayudará a crear las urls particulares de petición.

Cambia el host por los valores que te dejo en los comentarios TODO según el lugar donde ejecutes tu app.

Si te fijas en el método login() verás que usamos la anotación @POST para indicar que ese será la acción y la ruta parcial del recurso donde loguearemos al afiliado.

El retorno será un tipo Call<Affiliate>, donde Affiliate es un objeto plano Java para recoger los datos de la respuesta.

Y el parámetro será un objeto LoginBody anotado con @Body para indicar que será transmitido como cuerpo.

Crear Affiliate y LoginBody

Ambos son objetos Java que serán usados en la serialización y deserialzación JSON.

En el caso de Affiliate tendremos los atributos que resultan de la petición. Por ende crea la clase:

Affiliate.java

public class Affiliate {

    private String id;
    private String name;
    private String address;
    private String gender;
    private String token;

    public Affiliate(String id, String name, String address, String gender, String token) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.gender = gender;
        this.token = token;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

De acuerdo con el cuerpo de la petición diseñado en el servicio REST, LoginBody solo tendrá como atributos las credenciales de usuario.

Como resultado tendremos:

LoginBody.java

public class LoginBody {
    @SerializedName("id")
    private String userId;
    private String password;

    public LoginBody(String userId, String password) {
        this.userId = userId;
        this.password = password;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

IMPORTANTE: Usa la anotación @SerializedName para aclararle a Gson cuál será el nombre exacto del atributo JSON que será interpretado.

Definir Un Miembro De La Clase Retrofit

A continuación vamos a crear el adaptador de Retrofit en LoginActivity.

Abre la actividad y declara al inicio de la clase la variable mRestAdapter:

public class LoginActivity extends AppCompatActivity {

    private Retrofit mRestAdapter;

Luego ve a onCreate() e inicializa su contenido con su patrón Retrofit.Builder basado en la URL base y un convertidor Gson (GsonConverterFactory.create()):

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

    // Crear conexión al servicio REST
    mRestAdapter = new Retrofit.Builder()
            .baseUrl(SaludMockApi.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    //...

Crear Instancia De SaludMockApi

Bien. El paso a seguir es crear la implementación concreta del cliente HTTP.

Para ello usa el método create() de tu adaptador Retrofit y recoge la instancia en una variable global llamada mSaludMockApi:

public class LoginActivity extends AppCompatActivity {


    private SaludMockApi mSaludMockApi;

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

        // ...

        // Crear conexión a la API de SaludMock
        mSaludMockApi = mRestAdapter.create(SaludMockApi.class);

Realizar Llamada HTTP Con SaludMockApi

Ya para terminar vamos a modificar el método attemptLogin().

Como dije al inicio, puedes realizar la petición de forma asíncrona usando el método Call.enqueue().

Para recibir el resultado de este método, usa una callback del tipo Callback.

Ojo: Esto significa que ya no usaremos la tarea asíncrona UserLoginTask, en consecuencia bórrala y pon la invocación del método SaludMockApi.login().

Veamos como cambia el método:

private void attemptLogin() {

    // Reset errors.
    mFloatLabelUserId.setError(null);
    mFloatLabelPassword.setError(null);

    // Store values at the time of the login attempt.
    String userId = mUserIdView.getText().toString();
    String password = mPasswordView.getText().toString();

    boolean cancel = false;
    View focusView = null;

    // Check for a valid password, if the user entered one.
    if (TextUtils.isEmpty(password)) {
        mFloatLabelPassword.setError(getString(R.string.error_field_required));
        focusView = mFloatLabelPassword;
        cancel = true;
    } else if (!isPasswordValid(password)) {
        mFloatLabelPassword.setError(getString(R.string.error_invalid_password));
        focusView = mFloatLabelPassword;
        cancel = true;
    }

    // Verificar si el ID tiene contenido.
    if (TextUtils.isEmpty(userId)) {
        mFloatLabelUserId.setError(getString(R.string.error_field_required));
        focusView = mFloatLabelUserId;
        cancel = true;
    } else if (!isUserIdValid(userId)) {
        mFloatLabelUserId.setError(getString(R.string.error_invalid_user_id));
        focusView = mFloatLabelUserId;
        cancel = true;
    }

    if (cancel) {
        // There was an error; don't attempt login and focus the first
        // form field with an error.
        focusView.requestFocus();
    } else {
        // Mostrar el indicador de carga y luego iniciar la petición asíncrona.
        showProgress(true);

        Call<Affiliate> loginCall = mSaludMockApi.login(new LoginBody(userId, password));
        loginCall.enqueue(new Callback<Affiliate>() {
            @Override
            public void onResponse(Call<Affiliate> call, Response<Affiliate> response) {
                
            }

            @Override
            public void onFailure(Call<Affiliate> call, Throwable t) {
                
            }
        });
    }
}

El método onResponse() se ejecuta si llegó una respuesta HTTP normal. Y onFailure() se invoca cuando ocurre una excepción sobre la red o si hay malas prácticas en la lógica de Retrofit.

¿Qué debería ir en onResponse()?

Veamos las instrucciones:

  1. Ocultar el progreso
  2. Luego determinar si se produjo un error. Esto se hace con el método Response.isSuccessfull().
  3. Si lo hubo, necesitaremos comprobar si el formato es JSON.
    1. Si lo es, hacemos un parsing JSON para que retorne un POJO del mensaje de error (nueva clase ApiError)
    2. Si no, mostramos el mensaje de error con showLoginError()
    3. Frenamos el flujo del método.
  4. Si no, tomamos el objeto Affiliate que viene de la respuesta y almacenamos sus datos en las preferencias de Android. Esto con el fin de mantener la sesión abierta.

Observemos el código:

Call<Affiliate> loginCall = mSaludMockApi.login(new LoginBody(userId, password));
loginCall.enqueue(new Callback<Affiliate>() {
    @Override
    public void onResponse(Call<Affiliate> call, Response<Affiliate> response) {
        // Mostrar progreso
        showProgress(false);

        // Procesar errores
        if (!response.isSuccessful()) {
            String error;
            if (response.errorBody()
                    .contentType()
                    .subtype()
                    .equals("application/json")) {
                ApiError apiError = ApiError.fromResponseBody(response.errorBody());

                error = apiError.getMessage();
                Log.d("LoginActivity", apiError.getDeveloperMessage());
            } else {
                error = response.message();
            }

            showLoginError(error);
            return;
        }

        // Guardar afiliado en preferencias
        SessionPrefs.get(LoginActivity.this).saveAffiliate(response.body());

        // Ir a la citas médicas
        showAppointmentsScreen();
    }

    @Override
    public void onFailure(Call<Affiliate> call, Throwable t) {
        showProgress(false);
        showLoginError(t.getMessage());
    }
});

Del parámetro Response<Affiliate> usamos los siguientes métodos:

  • isSuccessful(): Es true si se obtienen códigos 2xx.
  • errorBody(): El contenido plano de una respuesta con error
  • message(): Mensaje de estado HTTP
  • body(): Es el cuerpo deserializado de la petición (objeto Affiliate), si esta fue exitosa.

En onFailure() tan solo ocultamos el progreso y mostramos el error.

Mantener La Sesión De Usuario Con Las Preferencias De Android

Finalmente…

…guardaremos los datos del afiliado en las preferencias de Android para mantener su sesión abierta.

¿Cómo lo hacemos?

Crea un paquete llamado prefs dentro de data y añade una clase con patrón singleton llamada SessionsPrefs:

public class SessionPrefs {

    private static SessionPrefs INSTANCE;

    public static SessionPrefs get() {
        if (INSTANCE == null) {
            INSTANCE = new SessionPrefs();
        }
        return INSTANCE;
    }

    private SessionPrefs() {
        
    }
    
}

La idea es implementar el método saveAffiliate() que vimos en la sección pasada para guardar la info del afiliado.

Veamos…

Definir Miembros

Los primeros miembros serán constantes para el nombre del archivo de preferencias y las claves de los valores, o sea:

public static final String PREFS_NAME = "SALUDMOCK_PREFS";
public static final String PREF_AFFILIATE_ID = "PREF_USER_ID";
public static final String PREF_AFFILIATE_NAME = "PREF_AFFILIATE_NAME";
public static final String PREF_AFFILIATE_ADDRESS = "PREF_AFFILIATE_ADDRESS";
public static final String PREF_AFFILIATE_GENDER = "PREF_AFFILIATE_GENDER";
public static final String PREF_AFFILAITE_TOKEN = "PREF_AFFILAITE_TOKEN";

Ahora va una instancia de SharedPreferences:

private final SharedPreferences mPrefs;

También será importante tener una bandera booleana que identifique si el usuario está o no logueado:

private boolean mIsLoggedIn = false;

Definir Constructor

Bien, lo siguiente es inicializar las preferencias en el constructor a través de un parámetro Context que invoque al método getSharedPreferences().

Además resetearemos el valor de mIsLoggedIn comprobando el contenido del token:

private SessionPrefs(Context context) {
    mPrefs = context.getApplicationContext()
            .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);

    mIsLoggedIn = !TextUtils.isEmpty(mPrefs.getString(PREF_AFFILAITE_TOKEN, null));
}

Obviamente el método get() para la instancia cambiará a la siguiente creación:

public static SessionPrefs get(Context context) {
    if (INSTANCE == null) {
        INSTANCE = new SessionPrefs(context);
    }
    return INSTANCE;
}

Facilitar Conocimiento Del Estado Del Usuario

En pocas palabras, crearemos un método público que permita saber si el afiliado está actualmente logueado o no:

public boolean isLoggedIn(){
    return mIsLoggedIn;
}

Crear Método De Guardado De Afiliado

Y ahora crear el método saveAffiliate() para que reciba un objeto Affiliate.

Guarda cada atributo y activa la bandera de logueo:

public void saveAffiliate(Affiliate affiliate) {
    if (affiliate != null) {
        SharedPreferences.Editor editor = mPrefs.edit();
        editor.putString(PREF_AFFILIATE_ID, affiliate.getId());
        editor.putString(PREF_AFFILIATE_NAME, affiliate.getName());
        editor.putString(PREF_AFFILIATE_ADDRESS, affiliate.getAddress());
        editor.putString(PREF_AFFILIATE_GENDER, affiliate.getGender());
        editor.putString(PREF_AFFILAITE_TOKEN, affiliate.getToken());
        editor.apply();

        mIsLoggedIn = true;
    }
}

Cerrar Sesión De Afiliado

Por último pon un método para eliminar la sesión del usuario.

Esto se logra dándole null a todas las preferencias y asignando el valor de falso a la bandera:

public void logOut(){
    mIsLoggedIn = false;
    SharedPreferences.Editor editor = mPrefs.edit();
    editor.putString(PREF_AFFILIATE_ID, null);
    editor.putString(PREF_AFFILIATE_NAME, null);
    editor.putString(PREF_AFFILIATE_ADDRESS, null);
    editor.putString(PREF_AFFILIATE_GENDER, null);
    editor.putString(PREF_AFFILAITE_TOKEN, null);
    editor.apply();
}

Actualizar Redirección Desde Login A Citas Médicas

Ve a la actividad AppointmentsActivity y modifica la condición de redirección que pusimos en onCreate().

Dale como expresión de condición al if el resultado del método SessionPrefs.isLoggedIn():

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Redirección al Login
    if (!SessionPrefs.get(this).isLoggedIn()) {
        startActivity(new Intent(this, LoginActivity.class));
        finish();
        return;
    }

De esta forma ya tendrás lista la app de SaludMock creada de forma parcial funcionando.

¡Bien hecho!

¿Quieres Otro Ejemplo De Login?

Si deseas crear un login más complejo con autorización básica, patrón MVP (Model-View-Presenter), arquitectura CLEAN, y un servicio REST mejor separado, entonces te gustará ver mi tutorial Tutorial De App Productos Parte 2: Login Y Servidor Virtual DigitalOcean.

Sé que te ayudará mucho en la organización, mantenimiento y testing de tu proyecto.

Y para finalizar…

Quiero preguntarte:

  • ¿Qué te pareció este tutorial?
  • ¿Te fue de utilidad?
  • ¿Estas realizando un proyecto similar?

Escribe tu apreciación en la caja de comentarios para dejarme saber qué piensas :)

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar