Tutorial de Programación Android – Login con NodeJS, Express y Json Web Token – Creando la app móvil

Tutorial de Programación Android: Login con NodeJS, Express y Json Web Token – Creando la app móvil: Nuestra LoginActivity

Continuando con la serie de artículos que componen este tutorial de Programación Android, vamos a comenzar con la parte del diseño de la aplicación para Android que va a interactuar con el servidor que tenemos creado. Para ello vamos a crear un nuevo proyecto en Android Studio. Después de darle nombre y elegir la versión mínima de Android para la que funcionará, nos va a pedir que elijamos el tipo de Activity que queremos añadir a nuestro proyecto. Por comodidad vamos a escoger LoginActivity, de esta manera, cuando arranquemos nuestra app, lo primero que veremos será una pantalla de login que nos pedirá nuestro email y password. Posteriormente cambiaremos la activity de entrada de nuestra app, pero de momento lo dejaremos así.

Esperamos a que Android Studio genere todo el código y vamos a añadir tres dependencias a nuestro proyecto para simplificarnos un poco las cosas. La primera ButterKnife que nos va ayudar a manejar los componentes de las vistas de nuestra aplicación de una manera más sencilla. La segunda es Ion para manejar las peticiones que realicemos a nuestro servidor y la tercera Gson para serializar y deserializar las peticiones y respuestas  de nuestro servidor y convertirlas en objetos manejables. Para añadirlas editamos el archivo build.gradle de nuestra app que quedaría de la siguiente manera, las dependencias añadidas se muestran en negrita:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        applicationId "pedropapblomoral.com.nodeloginandroid"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:design:25.3.1'
    compile 'com.koushikdutta.ion:ion:2.+'
    compile 'com.jakewharton:butterknife:8.5.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
    compile 'com.google.code.gson:gson:2.8.0'
    testCompile 'junit:junit:4.12'
}

Vamos a aprovechar gran parte del código que ha generado Android Studio al crear el proyecto. Como pretendemos ser muy ordenaditos, vamos a crear un par de packages en nuestra aplicación, que vamos a llamar clases y utilidades.

Dentro de la carpeta clases crearemos una nueva clase Java, llamada Respuesta.Class, que contendrá una clase con dos propiedades y que nos servirá para manejar las respuestas del servidor a nuestra petición de Login. El código de esta clase es tan simple como este:

package pedropapblomoral.com.nodeloginandroid.clases;


public class Respuesta {
    private String message;
    private String token;

    public String getMessage() {
        return message;
    }

    public String getToken() {
        return token;
    }
}

En la carpeta utilidades tendremos una clase denominada constantes que utilizaremos para guardar las variables que usaremos a lo largo del desarrollo:

package pedropapblomoral.com.nodeloginandroid.utilidades;

public class constantes {
    public static final String BASE_URL = "http://192.168.1.103:1515/api/v1/";
    public static final String TOKEN = "token";
    public static final String EMAIL = "email";
    public static final String LOG_TAG = "node-login-android";
}

Y ahora vamos con el código de nuestra LoginActivity que nos ha creado Android Studio. vamos a provechar gran parte del mismo, haciendo uso de las dependencias que hemos añadido. Así que vamos a verlo por partes. Comenzaremos ilustrando el uso que hemos hecho de Butter Knife, que es muy simple:

public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> {

    /**
     * Id to identity READ_CONTACTS permission request.
     */
    private static final int REQUEST_READ_CONTACTS = 0;

 
    private View mProgressView;
    private View mLoginFormView;
    private SharedPreferences mSharedPreferences;
    @BindView(R.id.password) EditText mPasswordView;
    @BindView(R.id.email) AutoCompleteTextView mEmailView;
    @BindView(R.id.email_sign_in_button) Button mEmailSignInButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // Set up the login form.
        //mEmailView = (AutoCompleteTextView) findViewById(R.id.email);
        ButterKnife.bind(this);
        populateAutoComplete();

        //mPasswordView = (EditText) findViewById(R.id.password);
        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;
            }
        });

//        Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button);
        mEmailSignInButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                attemptLogin();
            }
        });

        mLoginFormView = findViewById(R.id.login_form);
        mProgressView = findViewById(R.id.login_progress);
        initSharedPreferences();
    }

Como se puede apreciar, el funcionamiento es muy sencillo. Mediante anotaciones definimos y asignamos las vistas y luego en el método onCreate de nuestra Actividad con

ButterKnife.bind(this);

nos ahorramos todos los findViewById. Ahora vamos a ver como utilizamos Ion y Gson para hacer nuestro login, cuando el usuario hace click en el botón login:

    public void UserLogin (String email, String password){
        final String mEmail;
        final String mPassword;
        mEmail = email;
        mPassword = password;
        String credentials = mEmail + ":" + mPassword;
        String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);


Ion.with(getApplicationContext())
                .load("POST",constantes.BASE_URL+"/autentificar")
                .setHeader("Authorization", basic)
                .setLogging("ION_VERBOSE_LOGGING", Log.VERBOSE)
                .asString()
                .withResponse()
                .setCallback(new FutureCallback<Response<String>>() {
                    @Override
                    public void onCompleted(Exception e, Response<String> result) {
                        // do stuff with the result or error
                        // print the response code, ie, 200
                        int status;
                        status=result.getHeaders().code();
                        System.out.println(result.getHeaders().code());
                        // print the String that was downloaded
                        System.out.println(result.getResult());
                        showProgress(false);
                        if (e != null) {

                            e.printStackTrace();

                            Toast.makeText(getApplicationContext(), "Error loading user data", Toast.LENGTH_LONG).show();
                            return;
                        }

                        Log.d(LOG_TAG, result.toString() );
                        final Gson gson = new Gson();
                        Respuesta respuesta = gson.fromJson(result.getResult(),Respuesta.class);
                        if(status==200){

                            Toast.makeText(getApplicationContext(), respuesta.getMessage(), Toast.LENGTH_LONG).show();
                            SharedPreferences.Editor editor = mSharedPreferences.edit();
                            editor.putString(constantes.TOKEN,respuesta.getToken());
                            editor.putString(constantes.EMAIL,respuesta.getMessage());
                            editor.apply();

                        }
                        if (status==404 || status == 401){
                            Toast.makeText(getApplicationContext(), respuesta.getMessage(), Toast.LENGTH_LONG).show();
                        }


                        mEmailView.setText(null);
                        mPasswordView.setText(null);
                    }
                });


    }

Primero creamos la cadena de credenciales que vamos a enviar a nuestro servidor y la codificamos en Base64. Después hacemos uso de Ion, indicando que vamos a hacer una petición POST, añadiendo como cabecera las credenciales.Convertimos el resultado de la respuesta en un objeto de nuestra clase Respuesta mediante Gson. Leemos el estado de la respuesta que hemos enviado desde nuestro servidor. En caso de que sea ‘200’, guardamos el email y el token del usuario. En caso contrario, el estado será ‘404’ si el usuario no existe en nuestra base de datos o ‘401’ si el password no es correcto. El código completo de nuestra LoginActivity, con sus correspondientes validaciones previas quedaría de la siguiente manera:

public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> {

    /**
     * Id to identity READ_CONTACTS permission request.
     */
    private static final int REQUEST_READ_CONTACTS = 0;


    private View mProgressView;
    private View mLoginFormView;
    private SharedPreferences mSharedPreferences;
    @BindView(R.id.password) EditText mPasswordView;
    @BindView(R.id.email) AutoCompleteTextView mEmailView;
    @BindView(R.id.email_sign_in_button) Button mEmailSignInButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ButterKnife.bind(this);
        populateAutoComplete();
        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;
            }
        });

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

        mLoginFormView = findViewById(R.id.login_form);
        mProgressView = findViewById(R.id.login_progress);
        initSharedPreferences();
    }

    private void populateAutoComplete() {
        if (!mayRequestContacts()) {
            return;
        }

        getLoaderManager().initLoader(0, null, this);
    }

    private boolean mayRequestContacts() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return true;
        }
        if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
            return true;
        }
        if (shouldShowRequestPermissionRationale(READ_CONTACTS)) {
            Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE)
                    .setAction(android.R.string.ok, new View.OnClickListener() {
                        @Override
                        @TargetApi(Build.VERSION_CODES.M)
                        public void onClick(View v) {
                            requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS);
                        }
                    });
        } else {
            requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS);
        }
        return false;
    }

    /**
     * Callback received when a permissions request has been completed.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        if (requestCode == REQUEST_READ_CONTACTS) {
            if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                populateAutoComplete();
            }
        }
    }


    /**
     * Attempts to sign in or register the account specified by the login form.
     * If there are form errors (invalid email, missing fields, etc.), the
     * errors are presented and no actual login attempt is made.
     */
    private void attemptLogin() {
        // Reset errors.
        mEmailView.setError(null);
        mPasswordView.setError(null);

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

        boolean cancel = false;
        View focusView = null;

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

        // Check for a valid email address.
        if (TextUtils.isEmpty(email)) {
            mEmailView.setError(getString(R.string.error_field_required));
            focusView = mEmailView;
            cancel = true;
        } else if (!isEmailValid(email)) {
            mEmailView.setError(getString(R.string.error_invalid_email));
            focusView = mEmailView;
            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);
            UserLogin(email,password);
//            mAuthTask = new UserLoginTask(email, password);
//            mAuthTask.execute((Void) null);
        }
    }

    private boolean isEmailValid(String email) {
        //TODO: Replace this with your own logic
        if (TextUtils.isEmpty(email) || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {

            return false;

        } else {

            return  true;
        }
    }

    private boolean isPasswordValid(String password) {
        //TODO: Replace this with your own logic
        if (TextUtils.isEmpty(password) && password.length() <= 4) {

            return false;

        } else {

            return true;
        }

    }

    /**
     * Shows the progress UI and hides the login form.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    private void showProgress(final boolean show) {
        // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
        // for very easy animations. If available, use these APIs to fade-in
        // the progress spinner.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);

            mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
            mLoginFormView.animate().setDuration(shortAnimTime).alpha(
                    show ? 0 : 1).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
                }
            });

            mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
            mProgressView.animate().setDuration(shortAnimTime).alpha(
                    show ? 1 : 0).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
                }
            });
        } else {
            // The ViewPropertyAnimator APIs are not available, so simply show
            // and hide the relevant UI components.
            mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
            mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
        }
    }

    @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        return new CursorLoader(this,
                // Retrieve data rows for the device user's 'profile' contact.
                Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI,
                        ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION,

                // Select only email addresses.
                ContactsContract.Contacts.Data.MIMETYPE +
                        " = ?", new String[]{ContactsContract.CommonDataKinds.Email
                .CONTENT_ITEM_TYPE},

                // Show primary email addresses first. Note that there won't be
                // a primary email address if the user hasn't specified one.
                ContactsContract.Contacts.Data.IS_PRIMARY + " DESC");
    }

    @Override
    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
        List<String> emails = new ArrayList<>();
        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            emails.add(cursor.getString(ProfileQuery.ADDRESS));
            cursor.moveToNext();
        }

        addEmailsToAutoComplete(emails);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> cursorLoader) {

    }

    private void addEmailsToAutoComplete(List<String> emailAddressCollection) {
        //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list.
        ArrayAdapter<String> adapter =
                new ArrayAdapter<>(LoginActivity.this,
                        android.R.layout.simple_dropdown_item_1line, emailAddressCollection);

        mEmailView.setAdapter(adapter);
    }


    private interface ProfileQuery {
        String[] PROJECTION = {
                ContactsContract.CommonDataKinds.Email.ADDRESS,
                ContactsContract.CommonDataKinds.Email.IS_PRIMARY,
        };

        int ADDRESS = 0;
        int IS_PRIMARY = 1;
    }

    /**
     * Represents an asynchronous login/registration task used to authenticate
     * the user.
     */
    private void initSharedPreferences() {

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
    }
    public void UserLogin (String email, String password){
        final String mEmail;
        final String mPassword;
        mEmail = email;
        mPassword = password;
        String credentials = mEmail + ":" + mPassword;
        String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);


        Ion.with(getApplicationContext())
                .load("POST",constantes.BASE_URL+"/autentificar")
                .setHeader("Authorization", basic)
                .setLogging("ION_VERBOSE_LOGGING", Log.VERBOSE)
                .asString()
                .withResponse()
                .setCallback(new FutureCallback<Response<String>>() {
                    @Override
                    public void onCompleted(Exception e, Response<String> result) {
                        // do stuff with the result or error
                        // print the response code, ie, 200
                        int status;
                        status=result.getHeaders().code();
                        System.out.println(result.getHeaders().code());
                        // print the String that was downloaded
                        System.out.println(result.getResult());
                        showProgress(false);
                        if (e != null) {

                            e.printStackTrace();

                            Toast.makeText(getApplicationContext(), "Error loading user data", Toast.LENGTH_LONG).show();
                            return;
                        }

                        Log.d(LOG_TAG, result.toString() );
                        final Gson gson = new Gson();
                        Respuesta respuesta = gson.fromJson(result.getResult(),Respuesta.class);
                        if(status==200){

                            Toast.makeText(getApplicationContext(), respuesta.getMessage(), Toast.LENGTH_LONG).show();
                            SharedPreferences.Editor editor = mSharedPreferences.edit();
                            editor.putString(constantes.TOKEN,respuesta.getToken());
                            editor.putString(constantes.EMAIL,respuesta.getMessage());
                            editor.apply();

                        }
                        if (status==404 || status == 401){
                            Toast.makeText(getApplicationContext(), respuesta.getMessage(), Toast.LENGTH_LONG).show();
                        }


                        mEmailView.setText(null);
                        mPasswordView.setText(null);
                    }
                });

    }

}

Próximamente veremos en nuestro Tutorial de Programación Android: Login con NodeJS, Express y Json Web Token cómo hacer uso del token que nos ha devuelto para realizar mas peticiones a nuestro servidor y refinaremos algo más nuestra app móvil.

Si lo deseas, puedes consultar el resto de capítulos de este tutorial de Programación Android:

[wphtmlblock id=»517″]
5/5 - (1 voto)

Pedro Pablo Moral

Licenciado en ADE. Experto Universitario en Gestión de RRHH por competencias-

Deja una respuesta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.