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″]