Tutorial de Programación Android: Login con NodeJS, Express y Json Web Token, refinando nuestra app móvil: la BaseActivity
Continuamos con este Tutorial de Programación Android: Login con NodeJS, Express y Json Web Token. Una vez tenemos la parte del login de nuestra aplicación funcionando, vamos a poner la cosa un poco más elegante. Vamos a crear una BaseActivity de la que extenderemos el resto de Activitys de nuestra aplicación. Esta BaseActivity se encargará de todas las tareas comunes de nuestra aplicación. Además manejará el funcionamiento de la Barra Superior y del Navigation Drawer o Panel Lateral de Navegación.
Para empezar vamos a crear los layouts que componen nuestra BaseActivity. Comenzamos por el base_activity.xml:
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout 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:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="pedropablomoral.com.nodeloginandroid.BaseActivity" tools:openDrawer="start"> <include layout="@layout/app_bar_base" android:layout_width="match_parent" android:layout_height="match_parent" /> <android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/nav_header_base" app:menu="@menu/activity_base_drawer" /> </android.support.v4.widget.DrawerLayout>
Como podemos ver, el elemento raíz de nuestro xml es un DrawerLayout, que contiene a su vez, otras dos vistas. En este caso es importante el orden en que las incluimos. La primera subvista es lo que aparecerá cuando el panel lateral esté cerrado. La segunda hace referencia a la vista con el panel abierto. Ambas ocuparán el total de la pantalla.
Siguiendo este orden, vamos a ver el detalle de la vista que insertamos con el tag include:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:fitsSystemWindows="true" tools:context="pedropablomoral.com.nodeloginandroid.BaseActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimaryDark" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <include layout="@layout/content_base" /> </android.support.design.widget.CoordinatorLayout>
En este caso el elemento raiz es un CoordinatorLayout, por si en un futuro queremos controlar el comportamiento de nuestra actionbar. Dentro de este colocamos en primer lugar nuestra actionbar y volvemos a incluir otro layout, que será tan solo un RelativeLayout en el que inflaremos las vistas de cada Activity que extendamos de esta baseActivity:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:id="@+id/contenedor" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="pedropablomoral.com.nodeloginandroid.BaseActivity" tools:showIn="@layout/app_bar_base"> </RelativeLayout>
Con esto tenemos configurado el aspecto que tendrá nuestra app, cuando el menú de navegación lateral esté cerrado, que debería ser similar a este:
Ahora vamos a volver al primer layout, el base_activity.xml y vamos a ver la segunda subvista del mismo, que es un NavigationView y que se corresponde con el aspecto que tendrá nuestra aplicación cuando el panel de navegación se encuentre abierto. Este menú consta de dos partes: un header y un menú. El header es otro layout, nav_header_base.xml, que contiene un ImageView con el icono de nuestra aplicación y dos TextView, contenidos en un LinearLayout al que hemos dado 160 dp de altura. De esta manera configuramos la apariencia de nuestro NavigationDrawer cuando está abierto. Por otro lado, el menú no es más que un recurso del tipo menú que hemos creado para ser inflado en nuestro panel de Navigación
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="@dimen/nav_header_height" android:background="@color/colorAccent" android:gravity="bottom" 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" android:theme="@style/ThemeOverlay.AppCompat.Dark"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" android:text="@string/node_login_android" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/pedropablomoral_com" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_camera" android:icon="@android:drawable/ic_menu_edit" android:title="Inicio" /> <item android:id="@+id/nav_gallery" android:icon="@android:drawable/ic_menu_view" android:title="@string/perfil" /> <item android:id="@+id/nav_manage" android:title="Logout" /> </group> <item android:title="Varios"> <menu> <item android:id="@+id/nav_share" android:title="Acción 1" /> <item android:id="@+id/nav_send" android:title="Acción 2" /> </menu> </item> </menu>
Con esto queda configurada la parte de la vista de nuestra BaseActivity. Continuando con esta parte del tutorial de Programación Android, nos queda ver como manejar toda la lógica de la navegación de nuestra app e integrar todo aquello que necesitemos en ella, para que el resto de Activitys que creemos hereden de esta que hemos creado. En principio, además de la navegación de la app, todas las activitys que hereden de nuestra BaseActivity, van a comprobar que existe un token de usuario y que este token no haya expirado. Si ambas condiciones se cumplen, mostrarán la activity en la que nos encontremos. En caso de que no haya token o de que este haya expirado, devolverán al usuario a la pantalla de login.
El código responsable de la navegación por la aplicación a través del panel lateral es el siguiente:
@Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } else if (id == R.id.nav_gallery) { Intent intent = new Intent(this, PerfilActivity.class); startActivity(intent); } else if (id == R.id.nav_manage) { Logout(); } else if (id == R.id.nav_share) { } else if (id == R.id.nav_send) { } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; }
En nuestra baseActivity almacenaremos también el token que acompañará a todas las peticiones que realicemos mediante Ion a nuestro servidor Express. Este token se encuentra grabado en las SharedPreferences de nuestra aplicación. El código completo de la BaseActivity será el siguiente:
public class BaseActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { private SharedPreferences mSharedPreferences; private BaseActivity thisActivity; public String token; public String email; protected void onCreateDrawer() { initSharedPreferences(); /** Comprueba si hay token y si no ha expirado */ if (!token.equals("") && ComprobarToken()) { setContentView(R.layout.activity_base); thisActivity = this; Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.addDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); initSharedPreferences(); } else { Logout(); } } @Override protected void onResume (){ super.onResume(); initSharedPreferences(); if (!token.equals("") && ComprobarToken()) { } else { Logout(); } } private void Logout() { /** Vacia las variables token y email, limpia el historial de la aplicacion y devuelve al usuario a la pantalla de login */ SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.putString(constantes.TOKEN, ""); editor.putString(constantes.EMAIL, ""); editor.apply(); finish(); Intent i = new Intent(this, LoginActivity.class); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(i); } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @SuppressWarnings("StatementWithEmptyBody") @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { Intent intent = new Intent(this, MainActivity.class); startActivity(intent); } else if (id == R.id.nav_gallery) { Intent intent = new Intent(this, PerfilActivity.class); startActivity(intent); } else if (id == R.id.nav_manage) { Logout(); } else if (id == R.id.nav_share) { } else if (id == R.id.nav_send) { } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } private void initSharedPreferences() { /** Asignamos los valores correspondientes a las avriables token y email * para que estén disponibles en todas las Activities que extiendan * de nuestra baseActivity y poder incluir el token en todas las peticiones * al servidor*/ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); token = mSharedPreferences.getString(constantes.TOKEN, ""); email = mSharedPreferences.getString(constantes.EMAIL, ""); } private boolean ComprobarToken() { /** Comprueba si el token no ha expirado */ try { Claims claims = Jwts.parser() .setSigningKey(constantes.SECRET.getBytes()) .parseClaimsJws(token).getBody(); System.out.println("body: " + claims.toString()); System.out.println("Issuer: " + claims.getIssuer()); System.out.println("Expiration: " + claims.getExpiration()); return true; } catch (ExpiredJwtException ex) { System.out.println("exception : " + ex.getMessage()); return false; } } }
Nos queda ver como nuestros usuarios pueden consultar su perfil. El layout de esta activity tiene tan solo dos campos de texto que se rellenan con el email y el nombre del usuario que se ha logueado. La aplicación hace la petición de perfil a nuestro servidor, insertando en la cabecera el token que nos proporcionó el servidor al hacer login. Luego, mediante Gson, transforma la respuesta en un objeto de la clase Usuario. El layout y la activity quedan de la siguiente manera:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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:visibility="visible" tools:context="pedropablomoral.com.nodeloginandroid.PerfilActivity" tools:layout_editor_absoluteY="81dp" tools:layout_editor_absoluteX="0dp"> <LinearLayout android:layout_width="368dp" android:layout_height="wrap_content" android:orientation="vertical" tools:layout_editor_absoluteX="8dp" tools:layout_editor_absoluteY="8dp"> <TextView android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="email" tools:layout_editor_absoluteX="16dp" tools:layout_editor_absoluteY="47dp" /> <TextView android:id="@+id/username" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="username" tools:layout_editor_absoluteX="16dp" tools:layout_editor_absoluteY="16dp" /> </LinearLayout> </android.support.constraint.ConstraintLayout>
public class PerfilActivity extends BaseActivity { private Usuario usuario; @BindView(R.id.email) TextView mEmailView; @BindView(R.id.username) TextView mUserView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.onCreateDrawer(); RelativeLayout placeHolder = (RelativeLayout) findViewById(R.id.contenedor); getLayoutInflater().inflate(R.layout.activity_perfil, placeHolder); ButterKnife.bind(this); GetPerfil(); } private void GetPerfil() { Ion.with(getApplicationContext()) .load("GET", constantes.BASE_URL+"/usuarios/"+email) .setHeader("x-access-token", token) .setLogging("ION_VERBOSE_LOGGING", Log.VERBOSE) .asString() .withResponse() .setCallback(new FutureCallback<Response<String>>() { @Override public void onCompleted(Exception e, Response<String> result) { int status; status=result.getHeaders().code(); System.out.println(result.getHeaders().code()); System.out.println(result.getResult()); 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(); usuario = gson.fromJson(result.getResult(),Usuario.class); mEmailView.setText(usuario.getEmail()); mUserView.setText(usuario.getUsername()); } }); } }
Y hasta aquí esta quinta parte del Tutorial de Programación Android: Login con NodeJS, Express y Json Web Token.
[wphtmlblock id=»517″]