Las push notifications o notificaciones push son paquetes de datos enviados desde un servidor hacia sus clientes sin necesidad de una petición previa.
Firebase Cloud Messaging es una de las muchas plataformas que existen para generar este comportamiento.
Así que sigue leyendo este tutorial para que comprendas como este servicio de Google puede ayudarte a enviar mensajes push.
A continuación puedes ver el resultado final del tutorial:
Desbloquea el link de descarga del proyecto en Android Studio en la siguiente caja:
[sociallocker id=»7121″]
¿Qué es Firebase Cloud Messaging?
Firebase Cloud Messaging o FCM es un servicio de Google cuya funcion popular es el envío de push notifications desde una aplicación servidor hacia una aplicación cliente. Esto es posible gracias a la intervención del servicio de mensajería del servidor de Firebase.
Anteriormente este se conocía como Google Cloud Messaging o GCM, sin embargo la plataforma actualizó las características de la arquitectura con esta nueva versión, así que se recomienda comenzar o importar los proyectos lo más pronto posible con FCM.
¿Qué utilidades de uso frecuente tienen las notificaciones push en Android?
- Comunicar la posible sincronización con respecto a un nuevo cambio
- Crear recordatorios de servicios
- Promocionar descuentos en productos
- Notificación del estado de un pedido en un eCommerce
- Enviar alertas informativas relacionadas a la ubicación del usuario
- Petición de datos para feedback de servicios
- Implementar chats entre usuarios
- …
Como ves, los Android developers podemos sacarle gran provecho a esta situación, ya que se deja atrás el esquema de un servidor perezoso que espera por peticiones de los clientes.
Ahora el servidor puede notificar a los dispositivos con el fin de enganchar a los usuarios, disminuyendo el consumo de batería.
Upstream Messages
FCM no solo permite el envío de mensajes servidor-cliente (downstream messages).
También permite retornar por el mismo canal mensajes cliente-servidor. A estos se les conoce como upstream messages y son de gran utilidad para creación de apps de chat como WhatsApp.
Sin embargo en este tutorial solo veremos sobre downstream messages.
Arquitecture de FCM
Existen tres elementos en la interacción de FCM:
- Tu app servidor
- Tu app Android (cliente)
- El servidor de Firebase
Como se muestra en el diagrama anterior, la aplicación servidor envía una petición al servidor de Firebase. Luego la plataforma notifica a todos los dispositivos que estén registrados al proyecto.
Con esa sencilla relación, la receta a la hora de usar Firebase Cloud Messaging consiste de tres pasos:
- Configurar un proyecto FCM en la consola de Firebase
- Desarrollar tu app Android cliente
- Desarrollar tu app servidor
¡Comencemos!
Car Insurance App
Para desarrollar el contenido del tutorial se usará un ejemplo llamado Car Insurance. Se trata de una aplicación Android sencilla que sostiene el servicio de una compañía de seguros para automóviles.
Su construcción se basa en los siguientes requerimientos:
- Como cliente de Car Insurance, deseo iniciar sesión con mi email.
- Como supervisor de la compañía de seguros, deseo enviar notificaciones sobre las promociones y descuentos de la compañía a los usuarios.
El siguiente wireframe sencillo muestra las características:
En la scren de Login el usuario tiene tres puntos de interacción: Los campos de texto para tipear sus credenciales y el botón de inicio de sesión.
Si somos totalmente optimistas, una vez presionado el botón, el sistema de autenticación dará luz verde para pasar a la screen de Notificaciones, donde se irán apilando las notificaciones enviadas por el supervisor de ventas.
Arquitectura Modelo-Vista-Controlador (MVP)
Car Insurance se basará en el patrón Model-View-Presenter para mejorar la separación de componentes, incrementar la legibilidad en el testing de la app y optimizar el mantenimiento de sí misma.
Aunque no voy a entrar a explicar a fondo los fundamentos de MVP, te daré una descripción general y el funcionamiento de cada capa.
La interacción es sencilla:
- Vista: Muestra los datos al usuario y comunica las interacciones de este al presentador. Un ejemplo de implementación en Android son las Actividades o los fragmentos.
- Presentador: Agente intermediario entre la vista y el modelo. Carga los datos del modelo, los formatea y los muestra en la vista. También se encarga de actualizar el modelo si es necesario.
- Modelo: Genera la interacción necesaria para cargar los datos a proyectar en la app. Normalmente consulta fuentes de datos como SQLite, Web services, el disco local, etc. con el fin de proveer las entidades que alimentan la vista.
Si deseas saber más puedes consultar estas dos excelentes fuentes:
Instalar Google Play Services
Antes de comenzar con el desarrollo preparemos nuestro entorno de desarrollo.
1. En Android Studio inicia el SDK Manager a través del botón de acceso rápido.
3. Busca la categoría Extras y marca la línea Google Play Services. Luego presiona Install 1 package…
4. Acepta los términos de la licencia de Google Play Services e instala su paquete.
Al terminar la carga, tendrás disponible las librerías para el posterior uso de Firebase Cloud Messaging u otros servicios de Google (Google Maps, Analytics, Gmail, Apps Script, etc.)
Añadir Firebase A Tu Aplicación Android
1. Ve a Google Firebase Console.
2. Haz click en «CREAR NUEVO PROYECTO».
Si tienes un proyecto con el antiguo Google Cloud Messaging, clickea «o importar un proyecto de Google».
3. Pon como nombre al proyecto «Car Insurance» y selecciona tu país de origen. Luego confirma con «CREAR PROYECTO».
4. Haz click en «Añade Firebase a tu aplicación de Android».
5. Ubica el nombre del paquete de la app en el primer espacio. En mi caso es com.herprogramacion.carinsurance
. Luego haz click en «AÑADIR APLICACION».
Al igual que vimos en el tutorial de Google Maps API v2, la huella digital SHA-1 se obtiene desde la consola de comandos de tu sistema operativo con la utilidad keytool.
Abre la venta Terminal de Android Studio y ejecuta el siguiente comando ajustando la sintaxis a tu directorio local:
keytool -exportcert -list -v -alias <nombre-de-key> -keystore <ruta-de-keystore>
Para este ejemplo usaremos la keystore para modo debug, así que el comando tendría la siguiente forma:
keytool -exportcert -list -v -alias androiddebugkey -keystore "C:UsersTUUSUARIO.androiddebug.keystore"
El resultado sería una cadena similar a la siguiente:
Huellas digitales del Certificado: SHA1: 7A:33:08:75:0E:AD:20:67:45:6D:06:28:DE:6F:C3:C1:33:99:D3:68
Ahora ponlo en el campo «Certificado de firma de depuración SHA-1 (Opcional)».
6. Descarga a tu PC el archivo google-services.json que acaba de generar Firebase Console.
7. Como lo indica el segundo paso del asistente, mueve el archivo google-services.json
a la carpeta raíz del proyecto en Android Studio «Car Insurance».
Firebase Authentication Para Login En Android
Firebase Authentication es otro producto de Firebase para facilitar la identificación usuarios.
Ya sabes que es superimportante proteger la información de tus usuarios y salvarla de forma segura en la nube.
La principal ventaja de este servicio es la gran cantidad de procesos de autenticación que permite.
El SDK te deja realizar la autenticación con proveedores populares como Facebook, Twitter, Github y Google.
También provee un SDK para que integres el proceso de identificación con tu aplicación servidor si tienes cuentas personalizadas.
También te permite usar las credenciales básicas del usuario para generar la autenticación.
Este último será el que usaremos en el ejemplo actual.
1. Autenticación basada en email y password
Firebase Authentication permite a los android developers generar el registro y login de un usuario a través del email y password como se hace comúnmente.
Sin importar que método elijas, siempre se asignará un ID de usuario único que será empleado para el usuario. Este será guardado en la base de datos de la Console de Firebase.
Por esta vía Firebase console provee una sección de usuarios para administrar su creación y almacenamiento.
Incluso te facilita una serie de plantillas para procesar el double opt-in para los usuarios que se registren.
Veamos como habilitar este servicio.
Establecer método de email en Firebase Console
1. Abre Android Studio y añade la dependencia de Firebase Authentication en el archivo build.gradle del módulo.
dependencies { //... compile 'com.google.firebase:firebase-auth:9.0.2' }
2. Accede a la consola de Firebase y selecciona el proyecto Car Insurance.
3. Expande el Navigation Drawer y ve a DEVELOP > Auth.
4. A continuación encontrarás una sección de administración divida en tres pestañas:
- USUARIOS: Te permite administrar todos los usuarios registrados en tu proyecto Firebase. Aquí te será posible añadir, eliminar, deshabilitar cuentas y reenviar passwords.
- SIGN-IN METHOD: Permite habilitar y configurar los métodos de autenticación que usarás en tu app.
- PLANTILLAS DE CORREO ELECTRÓNICO: Permite administrar las autorespuestas por email que serán enviados a los nuevos usuarios para la confirmación o reset de password.
Con esto en mente, lo que harás es ir a SIGN-IN METHOD y presionar el primer provider Email/password.
En la siguiente sheet que se desprende, cambia el switch para habilitar el servicio y luego guarda la configuración.
Crear user de prueba
Sitúate en la pestañas USUARIOS y agrega un nuevo user.
Escribe un correo y contraseña para el nuevo usuario que tendrá acceso a Car Insurance:
Con eso tendrás el usuario que probaremos en la app cuyo ID ya ha sido asignado.
2. Crear Login en Android
El wireframe inicial muestra una screen de login que consta de dos campos de texto. Uno para el email y otro para el password.
Y el punto de interacción para iniciar la autorización es un raised button.
Para desarrollar esta característica crea un paquete llamado login
y agrega los siguientes archivos:
Archivo | Propósito |
---|---|
LoginActivity.java |
Actividad que actúa como controlador del login |
LoginFragment.java |
Fragmento que muestra el formulario de login como implementación de la vista |
LoginContract.java |
Define la capa de vista y la interacción con el presentador |
NotificationsPresenter.java |
Implementación de la capa de presentación para actualizar la vista |
LoginInteractor.java |
Interactor para autorizar a los usuarios y validar restricciones de datos y dependencias externas. |
Arquitectura
Los componentes definidos anteriormente interactúan de la siguiente manera.
- La vista recibe comunica al presentador cuando el usuario presiono el botón de login.
- El presentador le ordena al interactor que inicie el proceso de autenticación.
- El interactor hace una llamado al Firebase Authentication SDK para autenticar en el server.
- Firebase Authentication inicia un proceso asíncrono para enviar una petición al server y se queda esperando por la respuesta.
- Al llegar la respuesta se le transmite al interactor.
- El interactor notifica al presenter si el login fue exitoso o hubo algún fallo.
- El presenter actualiza la vista ya sea para alertar al usuario de los errores o para dar paso a la actividad de notificaciones.
El siguiente diagrama resume este comportamiento:
Representar el patrón MVP
En esta parte debes preguntarte:
¿Qué debe mostrar la vista cuando el usuario interactúe con los campos existentes?
La ruta feliz sería que el usuario introduzca su email/contraseña y al presionar el botón de login:
- Se muestre un indicador de progreso
- Se abra la screen de notificaciones
Pero existen factores que pueden alterar este flujo. Cosas como:
- Error en la sintaxis de email
- Error en la sintaxis del password
- Error en la autenticación con Firebase Authentication
- Error en la disponibilidad de la APK de Google Play Services
- Error en la disponibilidad de la red
Por el lado del presentador puedes preguntarte:
¿Qué fuentes de datos se necesitan consultar para realizar el login?
Aquí ya sabemos que solo será el servidor de Firebase para la autenticar con base a la petición que enviaremos.
Así que en resumen el contrato de interacciones quedaría así:
LoginContract.java
/** * Interacción MVP en Login */ public interface LoginContract { interface View extends BaseView<Presenter>{ void showProgress(boolean show); void setEmailError(String error); void setPasswordError(String error); void showLoginError(String msg); void showPushNotifications(); void showGooglePlayServicesDialog(int errorCode); void showGooglePlayServicesError(); void showNetworkError(); } interface Presenter extends BasePresenter{ void attemptLogin(String email, String password); } }
Donde BaseView
y BasePresenter
son interfaces con el comportamiento general que esperamos de todas las vistas y presentadores en la app.
BasePresenter.java
/** * Interfaz de comportamiento general de presenters */ public interface BasePresenter { void start(); }
BaseView.java
/** * Interfaz de comportamiento general de vistas */ public interface BaseView<T> { void setPresenter(T presenter); }
El controlador start()
ejecuta todos los comportamientos iniciales por defecto que la vista requiere del presentador.
Por otro lado setPresenter()
crea un vínculo view-presenter para realacionar ambas instancias.
Si quieres ver más implementaciones parecidas los repositorios de Google podrían serte de ayuda:
Android Architecture Blueprints [beta]
Si ves las interacciones de una forma más conveniente, entonces eres libre de experimentar e implementar el patrón de acuerdo a tus necesidades.
Crear formulario de login
Editar el layout de la activity de login
La actividad actúa como un contenedor simplificado. Esto significa que puedes usar un solo nodo para representar el contenido.
Usa el id login_container
para incrustar el fragmento a la hora de inflar el contenido.
activity_login.xml
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/login_container" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" 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" />
Editar layout del fragmento
Abre el layout de LoginFragment
y borra su contenido.
Para crear la jerarquía XML debes tener en cuenta que tendrás el logo y el formulario por debajo.
Por lo que un LinearLayout
vertical sería buena opción para la distribución.
Adicionalmente agrega una ProgressBar
con visibilidad gone
, para poder intercambiarlo con el formulario cuando el login se inicie.
Veamos:
fragment_login.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" 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"> <!-- Logo --> <TextView android:id="@+id/tv_logo" android:textColor="@android:color/white" android:layout_width="wrap_content" android:textStyle="bold" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginBottom="56dp" android:layout_marginTop="108dp" android:text="CAR INSURANCE" android:textAppearance="@style/TextAppearance.AppCompat.Display2" /> <!-- Indicador de progreso --> <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:visibility="gone" /> <!-- Formulario de login --> <LinearLayout android:id="@+id/login_form" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.design.widget.TextInputLayout android:id="@+id/til_email_error" android:textColorHint="@android:color/white" android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.design.widget.TextInputEditText android:id="@+id/tv_email" android:layout_width="match_parent" android:theme="@style/LoginEditText" android:layout_height="wrap_content" android:hint="@string/hint_email" android:inputType="textEmailAddress" android:maxLines="1" android:singleLine="true" /> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:id="@+id/til_password_error" android:layout_width="match_parent" android:textColorHint="@android:color/white" android:layout_height="wrap_content"> <android.support.design.widget.TextInputEditText android:id="@+id/tv_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/LoginEditText" android:hint="@string/hint_password" android:imeActionId="@+id/login" android:imeActionLabel="@string/action_sign_in_ime" android:imeOptions="actionUnspecified" android:inputType="textPassword" android:maxLines="1" android:singleLine="true" /> </android.support.design.widget.TextInputLayout> <Button android:id="@+id/b_sign_in" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginTop="16dp" android:text="@string/action_sign_in" android:textStyle="bold" android:theme="@style/AppTheme.AccentButton" /> </LinearLayout> </LinearLayout>
Implementa el fragmento de login
En el fragmento de login obtén las referencias de UI para:
- El indicador de progreso
- El formulario
- Los campos de texto y sus etiquetas flotantes
- El botón de SIGN-IN
Ahora maneja los siguientes eventos:
- Al comenzar a escribir en los EditTexts el error del TextInputLayout debe desaparecer. Esto con el fin de que el usuario distinga entre intentos de tipeo.
- Al presionar el botón
r_sign_in
debe ejecutarse el métodoattempLogin()
. Más adelante veremos que este método comunica al presentador la necesidad de loguear con las credenciales actuales.
LoginFragment.java
public class LoginFragment extends Fragment { private TextInputEditText mEmail; private TextInputEditText mPassword; private Button mSignInButton; private View mLoginForm; private View mLoginProgress; private TextInputLayout mEmailError; private TextInputLayout mPasswordError; public static LoginFragment newInstance(String param1, String param2) { LoginFragment fragment = new LoginFragment(); // Setup de argumentos en caso de que los haya return fragment; } public LoginFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { // Extracción de argumentos en caso de que los haya } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_login, container, false); mLoginForm = root.findViewById(R.id.login_form); mLoginProgress = root.findViewById(R.id.login_progress); mEmail = (TextInputEditText) root.findViewById(R.id.tv_email); mPassword = (TextInputEditText) root.findViewById(R.id.tv_password); mEmailError = (TextInputLayout) root.findViewById(R.id.til_email_error); mPasswordError = (TextInputLayout) root.findViewById(R.id.til_password_error); mSignInButton = (Button) root.findViewById(R.id.b_sign_in); // Eventos mEmail.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { mEmailError.setError(null); } @Override public void afterTextChanged(Editable editable) { } }); mPassword.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { mPasswordError.setError(null); } @Override public void afterTextChanged(Editable editable) { } }); mSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attemptLogin(); } }); return root; } @Override public void onAttach(Context context) { super.onAttach(context); } @Override public void onDetach() { super.onDetach(); } @Override public void onResume() { super.onResume(); } private void attemptLogin() { } }
Realizar transacción del fragmento
Ve a LoginActivity
y realiza una transacción add()
para agregar el fragmento al contenedor.
/** * Screen de login basada en el método email/password de Firebase */ public class LoginActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); LoginFragment loginFragment = (LoginFragment) getSupportFragmentManager() .findFragmentById(R.id.login_container); if (loginFragment == null) { loginFragment = LoginFragment.newInstance(); getSupportFragmentManager().beginTransaction() .add(R.id.login_container, loginFragment) .commit(); } } }
Usar el fragmento como vista
Implementa LoginContract.View
sobre LoginFragment
y agrega las firmas de los controladores.
public class LoginFragment extends Fragment implements LoginContract.View {
Luego sobrescribe los controladores:
@Override public void showProgress(boolean show) { mLoginForm.setVisibility(show ? View.GONE : View.VISIBLE); mLoginProgress.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void setEmailError(String error) { mEmailError.setError(error); } @Override public void setPasswordError(String error) { mPasswordError.setError(error); } @Override public void showLoginError(String msg) { Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show(); } @Override public void showPushNotifications() { startActivity(new Intent(getActivity(), NotificationsActivity.class)); getActivity().finish(); } @Override public void showGooglePlayServicesDialog(int codeError) { mCallback.onInvokeGooglePlayServices(codeError); } @Override public void showGooglePlayServicesError() { Toast.makeText(getActivity(), "Se requiere Google Play Services para usar la app", Toast.LENGTH_LONG) .show(); } @Override public void showNetworkError() { Toast.makeText(getActivity(), "La red no está disponible. Conéctese y vuelva a intentarlo", Toast.LENGTH_LONG) .show(); } @Override public void setPresenter(LoginContract.Presenter presenter) { if (presenter != null) { mPresenter = presenter; } else { throw new RuntimeException("El presenter no puede ser nulo"); } }
Mostrar diálogo de error de Google Play Services
En el código anterior, el método showGooglePlayServicesDialog()
llama al método onInvokeGooglePlayServices()
de un campo nombrado mCallback
.
Si viste mi artículo sobre creación de diálogo en Android, sabrás que este comportamiento permite comunicar la el fragmento con la actividad contenedora.
Para ello defines una escucha interna en el fragmento con los controladores necesarios. En nuestro caso necesitamos mostrar un diálogo de error sobre Google Play Services basado en el código de error:
private Callback mCallback; //... interface Callback { void onInvokeGooglePlayServices(int codeError); }
Luego en onAttach()
salvas la instancia de la actividad en mCallback
y onDetach()
le quitas la referencia.
@Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof Callback) { mCallback = (Callback) context; } else { throw new RuntimeException(context.toString() + " debe implementar Callback"); } } @Override public void onDetach() { super.onDetach(); mCallback = null; }
Lo siguiente es abrir LoginActivity
e implementas la interfaz.
public class LoginActivity extends AppCompatActivity implements LoginFragment.Callback {
Y finalmente implementas el controlador:
@Override public void onInvokeGooglePlayServices(int errorCode) { showPlayServicesErrorDialog(errorCode); } void showPlayServicesErrorDialog( final int errorCode) { Dialog dialog = GoogleApiAvailability.getInstance() .getErrorDialog( LoginActivity.this, errorCode, REQUEST_GOOGLE_PLAY_SERVICES); dialog.show(); }
Donde shoPlayServicesErrorDialog()
se apoya en el método de utilidad GooglePlayAvailability.getErrorDialog()
para crear un diálogo prefabricado que permita solucionar los inconvenientes de Google Play Services.
Por ejemplo:
Implementar presentador del login
Abre la clase LoginPresenter
para implementar LoginContract.Presenter
y añade todos los métodos a sobrescribir.
LoginPresenter.java
public class LoginPresenter implements LoginContract.Presenter, LoginInteractor.Callback { private final LoginContract.View mLoginView; private LoginInteractor mLoginInteractor; public LoginPresenter(@NonNull LoginContract.View loginView, @NonNull LoginInteractor loginInteractor) { mLoginView = loginView; loginView.setPresenter(this); mLoginInteractor = loginInteractor; } @Override public void start() { } @Override public void attemptLogin(String email, String password) { } @Override public void onEmailError(String msg) { } @Override public void onPasswordError(String msg) { } @Override public void onAuthSuccess() { } @Override public void onAuthFailed(String msg) { } @Override public void onBeUserResolvableError(int errorCode) { } @Override public void onGooglePlayServicesFailed() { } @Override public void onNetworkConnectFailed() { } }
Si te fijas bien, tenemos dos campos en la clase. Una instancia de LoginContract.View
y de LoginInteractor
.
El primero ya sabes es el vínculo hacia la vista, el cual entra como primer parámetro en el constructor y es ligado con su método setPresenter()
.
Por otro lado el interactor entra como segundo parámetro y es retenido en el atributo. Además tenemos la implementación de LoginInteractor.Callback
para la comunicación con el presenter.
Crear interactor del login
La función del interactor es pasarle el email y password al SDK de Firebase Authentication para realizar la autenticación en el servidor.
Luego procesa la respuesta y se la comunica al presentador para que este actualice la vista basado en el resultado.
1. Crea un método de entrada llamado login()
. Este debe recibir las credenciales y realizar las siguientes acciones:
isValidEmail(String)
,isValidPassword(String)
: Valida el contenido de las crendencialesisNetworkAvailable()
: Comprueba la disponibilidad de la redisGooglePlayServicesAvailable()
: Comprueba la disponibilidad de Google Play ServicessignInUser()
: Inicia la sesión del usuario
/** * Interactor del login */ public class LoginInteractor { private final Context mContext; private FirebaseAuth mFirebaseAuth; public LoginInteractor(Context context, FirebaseAuth firebaseAuth) { mContext = context; if (firebaseAuth != null) { mFirebaseAuth = firebaseAuth; } else { throw new RuntimeException("La instancia de FirebaseAuth no puede ser null"); } } public void login(String email, String password, final Callback callback) { // Check lógica boolean c1 = isValidEmail(email, callback); boolean c2 = isValidPassword(password, callback); if (!(c1 && c2)) { return; } // Check red if (!isNetworkAvailable()) { callback.onNetworkConnectFailed(); return; } // Check Google Play Service if (!isGooglePlayServicesAvailable(callback)) { return; } // Consultar Firebase Authentication signInUser(email, password, callback); } private boolean isValidPassword(String password, Callback callback) { return false; } private boolean isValidEmail(String email, Callback callback) { return false; } private boolean isNetworkAvailable() { return false; } private boolean isGooglePlayServicesAvailable(Callback callback) { return false; } private void signInUser(String email, String password, final Callback callback) { } }
Ten en cuenta que signInUser()
se llama solo si las tres restricciones han sido cumplidas.
2. Declara una interfaz llamada Callback
para comunicar los resultados al presenter.
interface Callback { void onEmailError(String msg); void onPasswordError(String msg); void onNetworkConnectFailed(); void onBeUserResolvableError(int errorCode); void onGooglePlayServicesFailed(); void onAuthFailed(String msg); void onAuthSuccess(); }
El propósito de cada uno es:
onEmailError(String)
: Reporta el presenter que hubo un error en el email. El parámetro es el mensaje que se mostrará al usuario.onPasswordError(String)
: Similar aonEmailError()
pero para el campo de la contraseña.onNetworkConnectFailed()
: Reporta al presenter la no disponibilidad de la red.onBeUserResolvableError()
: Reporta al presenter que hay un error de Play Services, pero es posible que el usuario pueda arreglarlo con un asistente de Android.onGooglePlayServicesFailed()
: Reporta al presenter un error de Play Services que no puede resolver el usuario.onAuthFailed()
: Reporta al presenter un error en la autenticación en Firebase.onAuthSuccess()
: Reporta al presenter que la autenticación en Firebase fue exitosa.
3. Validar email.
Aquí debes incluiré todas las business rules que tu app requiera.
Por el momento solo pondremos las básicas: El correo no debe estar vacío y debe cumplir con el patrón correspondiente.
private boolean isValidEmail(String email, Callback callback) { boolean isValid = true; if (TextUtils.isEmpty(email)) { callback.onEmailError("Escribe tu correo"); isValid = false; } if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { callback.onEmailError("Correo no válido"); isValid = false; } // Más reglas de negocio... return isValid; }
No olvides reportar al presenter con onEmailError()
los inconvenientes.
4. Validar password.
En este ejemplo la contraseña solo la restringiré a que no esté vacía.
private boolean isValidPassword(String password, Callback callback) { boolean isValid = true; if (TextUtils.isEmpty(password)) { callback.onPasswordError("Escribe tu contraseña"); isValid = false; } // Más reglas de negocio... return isValid; }
5. Verificar la conexión de red en Android.
Usa el componente ConnectivityManager
y obtén la información de la red activa actualmente con getActiveNetworkInfo()
. Luego comprueba si está conectada con isConnected()
.
private boolean isNetworkAvailable() { ConnectivityManager connMgr = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); }
6. Verificar si Google Play Services está disponible.
Asegurate de que exista la APK de Google Play Services usando el componente GoogleApiAvailability
y su método isGooglePlayServicesAvailable()
.
private boolean isGooglePlayServicesAvailable(Callback callback) { int statusCode = GoogleApiAvailability.getInstance() .isGooglePlayServicesAvailable(mContext); if (GoogleApiAvailability.getInstance().isUserResolvableError(statusCode)) { callback.onBeUserResolvableError(statusCode); return false; } else if (statusCode != ConnectionResult.SUCCESS) { callback.onGooglePlayServicesFailed(); return false; } return true; }
Este no retorna un booleano si no un código de estado, el cual determina si la APK no existe, si está desactualizada, deshabilitada, etc.
Para evitar comprobar con cada estado, usa isUserResolvableError()
para agrupar aquellos códigos que permiten que el usuario arregle el inconveniente. Si no es así entonces reporta con Callback.onGooglePlayServicesFailed()
.
Más adelante veremos cómo implementar singInUser()
.
Implementar el flujo de UI para la autenticación
1. Abre LoginActivity
y dentro de onCreate()
crea el presentador.
@Override protected void onCreate(Bundle savedInstanceState) { //... // Obtener instancia FirebaseAuth mFirebaseAuth = FirebaseAuth.getInstance(); LoginInteractor loginInteractor = new LoginInteractor(getApplicationContext(), mFirebaseAuth); mPresenter = new LoginPresenter(mLoginFragment, loginInteractor); }
2. Desde LoginFragment
inicia el presentador en onResume()
.
@Override public void onResume() { super.onResume(); mPresenter.start(); }
3. Allí mismo, modifica attemptLogin()
para que obtenga las credenciales y se las pase al presentador.
private void attemptLogin() { mPresenter.attemptLogin( mEmail.getText().toString(), mPassword.getText().toString()); }
4. Abre LoginPresenter y modifica attemptLogin() para que el interactor inicie la autenticación.
@Override public void attemptLogin(String email, String password) { mLoginView.showProgress(true); mLoginInteractor.login(email, password, this); }
Usa showProgress()
para mostrar la carga mientras ello sucede.
5. Sobrescribe todos los controladores de LoginInteractor.Callback
para reportar los estados de resultado a la vista.
LoginPresenter.java
/** * Presentador del login */ public class LoginPresenter implements LoginContract.Presenter, LoginInteractor.Callback { private final LoginContract.View mLoginView; private LoginInteractor mLoginInteractor; public LoginPresenter(@NonNull LoginContract.View loginView, @NonNull LoginInteractor loginInteractor) { mLoginView = loginView; loginView.setPresenter(this); mLoginInteractor = loginInteractor; } @Override public void start() { // Comprobar si el usuario está logueado } @Override public void attemptLogin(String email, String password) { mLoginView.showProgress(true); mLoginInteractor.login(email, password, this); } @Override public void onEmailError(String msg) { mLoginView.showProgress(false); mLoginView.setEmailError(msg); } @Override public void onPasswordError(String msg) { mLoginView.showProgress(false); mLoginView.setPasswordError(msg); } @Override public void onAuthSuccess() { mLoginView.showPushNotifications(); } @Override public void onAuthFailed(String msg) { mLoginView.showProgress(false); mLoginView.showLoginError(msg); } @Override public void onBeUserResolvableError(int errorCode) { mLoginView.showProgress(false); mLoginView.showGooglePlayServicesDialog(errorCode); } @Override public void onGooglePlayServicesFailed() { mLoginView.showGooglePlayServicesError(); } @Override public void onNetworkConnectFailed() { mLoginView.showProgress(false); mLoginView.showNetworkError(); } }
Corre la app y prueba las validaciones:
3. Validar credenciales del usuario con el Firebase Authentication SDK
Loguear usuario con email y password
Para iniciar la sesión de un usuario en Firebase realiza los siguientes pasos:
1. Obtén la instancia de FirebaseAuth
en el método onCreate()
de tu activity o fragment. Esto lo haces con el método getInstance()
. Guarda la referencia en un campo.
Este objeto es el punto de entrada del SDK de Firebase Authentication. Desde su definición se ejecuta todo el modelo asíncrono de programación para enviar/recibir datos al server de forma automática, facilitando la implementación al android developer.
En nuestro caso añadiremos un nuevo campo a LoginFragment
y luego asignaremos el singleton en onCreate()
.
//... private FirebaseAuth mFirebaseAuth; //... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { // Extracción de argumentos en caso de que los haya } // Obtener instancia FirebaseAuth mFirebaseAuth = FirebaseAuth.getInstance(); }
2. Crea una escucha de cambios de estado de inicio de sesión AuthStateListener
en onCreate()
. Luego regístrala al framework con FirebaseAuth.addAuthStateListener()
en start()
.
La eliminación del registro hazla en onStop()
con FirebaseAuth.removeAuthStateListener()
.
@Override public void onStart() { super.onStart(); mFirebaseAuth.addAuthStateListener(mAuthListener); } @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mFirebaseAuth.removeAuthStateListener(mAuthListener); } }
AuthStateListener
es llamada cuando se detecta un cambio en el estado de la sesión del usuario actual. Para controlar esto se usa onAuthStateChanged()
.
El controlador recibe una instancia de FirebaseAuth
para diferenciar el usuario al cual se le hace referencia si es que piensas usar varios proveedores.
3. Envía una petición de autenticación con FirebaseAuth.signInWithEmailAndPassword()
pasando como parámetros el email y password del usuario.
Este método retorna en un objeto Task<AuthResult>
. Donde Task
representa una operación asíncrona en Google Apis y AuthResult
contiene los datos del usuario si la autenticación fue un éxito.
Para saber en qué momento termina la operación, debes añadir una escucha OnCompleteListener
con el método addOnCompleteListener()
.
Esta trae consigo el controlador onComplete()
el cual se llama cuando la tarea se termina.
Nosotros ya sabemos que esta llamada va en el método signInUser()
de LoginInteractor
:
private void signInUser(String email, String password, final Callback callback) { mFirebaseAuth.signInWithEmailAndPassword(email, password) .addOnCompleteListener(new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { if (!task.isSuccessful()) { callback.onAuthFailed(task.getException().getMessage()); } else { callback.onAuthSuccess(); } } }); }
Usa el método isSuccesful()
para comprobar si la autenticación fue exitosa y reporta al presentador el resultado.
Si deseas obtener los datos del usuario usa AuthResult.getUser()
.
No obstante si el login fue exitoso, la escucha de cambios estado detectará este movimiento y podrás obtener los datos del usuario en ese lugar.
Cerrar sesión de un usuario
Car Insurance no implementará esta característica, pero si en algún momento deseas cerrar la sesión usa el método signOut()
:
FirebaseAuth.getInstance().signOut();
Añadir Firebase Cloud Messaging En Android
1. Incluye en tu archivo <proyecto>/build.gradle
la siguiente dependencia:
buildscript { repositories { jcenter() } dependencies { // ... classpath 'com.google.gms:google-services:3.0.0' } }
2. Incluye en tu archivo gradle de aplicación <proyecto>/<modulo>/build.gradle
las siguientes dependencias:
dependencies { compile 'com.google.firebase:firebase-messaging:9.0.2' } // Incluyela al final apply plugin: 'com.google.gms.google-services'
Al final sincroniza de nuevo la app con la acción emergente Sync Now.
Ambas ediciones de Gradle se ven resumidas en el paso 3 del asistente de Firebase. Confirma todo clickeando «FINALIZAR».
3. Debes ver en el escritorio una card con la información del nuevo proyecto creado.
Para futuras modificaciones presiona el botón de overflow y luego selecciona «Administrar».
Modifica Tu AndroidManifest.xml
Para que el FCM SDK funcione con el framework de Android debes agregar dos services. Uno para la captación de mensajes y otro para la actualización de tokens.
Crea un paquete java llamado fcm
para incluir estos elementos.
Agrega un Service que extienda de FirebaseMessagingService.
En Android Studio haz click derecho en la carpeta java
y luego selecciona New > Service > Service. Usa el nombre «FCMService» y confirma.
Con ello el service se agrega en el manifesto como etiqueta <service>
. Búscalo y agrega el siguiente intent filter:
<service android:name=".FCMService"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service>
Ahora abre la clase java del Service y extiéndela de FirebaseMessagingService
.
public class FCMService extends FirebaseMessagingService { ...
Agrega un Service que extienda de FirebaseInstanceIdService.
Crea un nuevo service como hiciste en el paso anterior y nómbralo «FCMInstanceIdService». Luego agrega el siguiente intent filter en el manifesto:
<service android:name=".FCMInstanceIdService"> <intent-filter> <action android:name="com.google.firebase.INSTANCE_ID_EVENT" /> </intent-filter> </service>
Por último extiende la clase de FirebaseInstanceIdService
.
public class FCMInstanceIdService extends FirebaseInstanceIdService { ...
Crear Screen De Notificaciones
Como viste en la descripción inicial de Car Insurance, las notificaciones push serán recibidas en una activity con un fragment cuyo contenido es una lista.
Con esto en mente debes crear un paquete Java con el nombre notifications
. En él debes agregar los siguientes elementos:
Archivo | Propósito |
---|---|
NotificationsActivity.java |
Actividad que actúa como screen de las notificaciones |
NotificationsFragment.java |
Fragmento que despliega una lista de notificaciones |
NotificationsAdapter.java |
Adaptador del RecyclerView de notificaciones |
NotificationsContract.java |
Interfaz para la representación general de la vista y el presentador |
NotificationsPresenter.java |
Presentador para cargar una lista de notificaciones |
Por otro lado, los componentes asociados a los datos los pondremos en un paquete llamado data.
Archivo | Propósito |
---|---|
PushNotification.java |
POJO java para representar la entidad de las notificaciones |
PushNotificationsRepository.java |
Repositorio temporal para el guardado de las notificaciones entrantes |
Arquitectura
La arquitectura de esta característica es similar al screen de login, solo que ahora interviene un repositorio ficticio de datos para almacenar las notificaciones push de forma temporal.
Analicemos el flujo:
- Usamos la consola de Firebase en la sección Notifications para crear y enviar un mensaje al servidor
- El Push Messaging Service lo recibe y lo despacha hacia la app Android, donde el Firebase Cloud Messaging internamente obtendrá el mensaje.
- Desde el FCM SDK envíamos un broadcast hacia la vista, donde estará esperando un BroadcastReceiver.
- Las vista comunica al presenter que llegó una nueva push notification
- El presenter se comunica con el repositorio de datos para salvar la instancia temporalmente.
- Una vez el repositorio haya guardado satisfactoriamente la entidad, se lo comunica al presenter.
- Con ello el presenter actualiza la lista en la vista, apilando el nuevo ítem.
Conectar la vista con el presentador
Los siguientes son comportamientos que debe tener la vista de las notificaciones al iniciarse la screen:
- Mostrar notificaciones
- Mostrar mensaje al no haber notificaciones
En cuanto al presentador debería:
- Cargar las notificaciones de la fuente de datos
- Registrar el cliente FCM
Este resumen produciría las siguientes interfaces.
NotificationsContract.java
/** * Conexión View - Presenter */ public interface NotificationContract { interface View extends BaseView<Presenter>{ void showNotifications(List<PushMessage> notifications); void showNoMessagesView(); } interface Presenter extends BasePresenter{ void registerAppClient(); void loadNotifications(); } }
Establecer el modelo
Definir entidades
Los datos que mostraremos en la vista se representan por la entidad PushNotification
. Esta contiene los siguientes atributos:
- id: Identificador para las notificaciones.
- title: Título de la promoción
- description: Descripción de la promoción
- expiryDate: Fecha de expedición
- discount: El valor del descuento en decimal (D= [0, 1.0]).
Con esto claro solo debemos crear una clase tradicional con un constructor, gets y sets.
PushNotification.java
/** * Representación de una promoción en forma de push notification */ public class PushNotification { private String id; private String mTitle; private String mDescription; private String mExpiryDate; private float mDiscount; public PushNotification() { id = UUID.randomUUID().toString(); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return mTitle; } public void setTitle(String title) { this.mTitle = title; } public void setDescription(String description) { this.mDescription = description; } public String getDescription() { return mDescription; } public void setExpiryDate(String expiryDate) { mExpiryDate = expiryDate; } public String getExpiryDate() { return mExpiryDate; } public void setDiscount(float discountValue) { mDiscount = discountValue; } public float getDiscount() { return mDiscount; } }
Crear repositorio de notificaciones
El repositorio seguirá un patrón de acceso de datos estático, del cual obtendremos y guardaremos notificaciones.
Para ello debes:
- Implementar un singleton que posea un
ArrayMap
de objetosPushNotification
. - Definir una interfaz de comunicación que retorne los datos del mapa.
- Establecer dos métodos. Uno para guardar las notificaciones y otro para retornar todos los elementos.
Con eso tendremos:
/** * Repositorio de push notifications */ public final class PushNotificationsRepository { private static ArrayMap<String, PushNotification> LOCAL_PUSH_NOTIFICATIONS = new ArrayMap<>(); private static PushNotificationsRepository INSTANCE; private PushNotificationsRepository() { } public static PushNotificationsRepository getInstance() { if (INSTANCE == null) { return new PushNotificationsRepository(); } else { return INSTANCE; } } public void getPushNotifications(LoadCallback callback) { callback.onLoaded(new ArrayList<>(LOCAL_PUSH_NOTIFICATIONS.values())); } public void savePushNotification(PushNotification notification) { LOCAL_PUSH_NOTIFICATIONS.put(notification.getId(), notification); } public interface LoadCallback { void onLoaded(ArrayList<PushNotification> notifications); } }
Fijate que getPushNotifications()
recibe una instancia de la interfaz de comunicación, la cual se espera sea creada de forma anónima en el presentador.
En cuanto a savePushNotification()
, recibe un objeto nuevo, el cual es guardado con put()
en la fuente.
La capa de Vista
Modificar layout del fragment de notificaciones
Abre el archivo res/layout/fragment_notifications.xml
y añade un RecyclerView
para representar la lista.
Si deseas mostrar un mensaje cuando no hayan elementos, entonces crea un Empty state pattern sencillo con un icono y texto descriptivo:
Con ambos elementos tendrás algo parecido a esto:
fragment_notifications.xml
<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.support.v7.widget.RecyclerView android:id="@+id/rv_notifications_list" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="LinearLayoutManager" tools:listitem="@layout/item_list_notification" /> <LinearLayout android:id="@+id/noMessages" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:orientation="vertical" tools:visibility="gone"> <ImageView android:id="@+id/noMessagesIcon" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" android:scaleType="fitXY" android:src="@drawable/ic_bell" android:tint="@android:color/darker_gray" /> <TextView android:id="@+id/noMessagesText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="8dp" android:gravity="center" android:text="@string/no_message_text" android:textAppearance="@style/TextAppearance.AppCompat.Small" /> </LinearLayout> </RelativeLayout>
Crear layout para el ítem de lista
El siguiente mock de alto nivel muestra el resultado final para la distribución de los views del ítem con respecto a los atributos descritos en la clase PushNotification
:
Como ves, el nodo es un CardView
para la creación de la tarjeta. Puedes ver un tuto sobre cómo usarlas aquí:
La distribución interna puedes realizarla en un RelativeLayout ya que tendremos ubicaciones atípicas.
En definitiva, crea un nuevo layout llamado item_list_notification.xml
y añade la siguiente definición XML:
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" app:cardUseCompatPadding="true" app:contentPaddingLeft="@dimen/activity_horizontal_margin" app:contentPaddingRight="@dimen/activity_horizontal_margin" app:contentPaddingTop="8dp" app:contentPaddingBottom="8dp" android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_marginTop="8dp" android:minHeight="?attr/listPreferredItemHeight" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tv_discount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:text="New Text" android:textAppearance="@style/TextAppearance.AppCompat.Display1" tools:text="50%" /> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_marginLeft="16dp" android:layout_toRightOf="@+id/tv_discount" android:text="New Text" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" tools:text="¡Cyberlunes!" /> <TextView android:id="@+id/tv_description" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_title" android:layout_marginLeft="16dp" android:layout_toRightOf="@+id/tv_discount" android:text="New Text" tools:text="Compra en línea ahora mismo y ahorra hasta un 50% en el seguro de tu automóvil" /> <TextView android:id="@+id/tv_expiry_date" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_description" android:layout_marginLeft="16dp" android:layout_marginTop="8dp" android:layout_toRightOf="@+id/tv_discount" android:text="New Text" android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textStyle="italic" tools:text="Valido hasta el 7/07/2016" /> <Button android:id="@+id/button" style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_expiry_date" android:layout_toRightOf="@+id/tv_discount" android:text="REDIMIR" android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textColor="?attr/colorPrimary" android:textSize="13sp" /> </RelativeLayout> </android.support.v7.widget.CardView>
Finalmente el mock del screen de las notificaciones se proyectaría así:
Crear adaptador de push notifications
Crea un adaptador para el RecyclerView clase que extienda de RecyclerView.Adapter
y sobrescribe los métodos de inflado y binding.
La idea es que se base en una fuente de datos basada en una colección de objetos PushNotification
.
PushNotificationsAdapter.java
/** * Adaptador de notificaciones */ public class PushNotificationsAdapter extends RecyclerView.Adapter<PushNotificationsAdapter.ViewHolder> { ArrayList<PushNotification> pushNotifications = new ArrayList<>(); public PushNotificationsAdapter() { } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); View itemView = inflater.inflate(R.layout.item_list_notification, parent, false); return new ViewHolder(itemView); } @Override public void onBindViewHolder(ViewHolder holder, int position) { PushNotification newNotification = pushNotifications.get(position); holder.title.setText(newNotification.getTitle()); holder.description.setText(newNotification.getDescription()); holder.expiryDate.setText(String.format("Válido hasta el %s", newNotification.getExpiryDate())); holder.discount.setText(String.format("%d%%", (int) (newNotification.getDiscount() * 100))); } @Override public int getItemCount() { return pushNotifications.size(); } public void replaceData(ArrayList<PushNotification> items) { setList(items); notifyDataSetChanged(); } public void setList(ArrayList<PushNotification> list) { this.pushNotifications = list; } public void addItem(PushNotification pushMessage) { pushNotifications.add(0, pushMessage); notifyItemInserted(0); } public class ViewHolder extends RecyclerView.ViewHolder { public TextView title; public TextView description; public TextView expiryDate; public TextView discount; public ViewHolder(View itemView) { super(itemView); title = (TextView) itemView.findViewById(R.id.tv_title); description = (TextView) itemView.findViewById(R.id.tv_description); expiryDate = (TextView) itemView.findViewById(R.id.tv_expiry_date); discount = (TextView) itemView.findViewById(R.id.tv_discount); } } }
Este adaptador tiene los siguientes tres métodos personalizados:
replaceData():
Actualiza los datos actuales por un nuevo conjunto.setList():
Asignador de ítems con checkeo de restricción nula.addItem()
: Añade un solo ítem en la posición 0.
Implementar la vista con el fragmento de notificaciones
El fragmento NotificationsFragment
debe implementar la interfaz de vista NotificationsContract.View
y luego sobrescribir los controladores.
NotificationsFragment.java
/** * Muestra lista de notificaciones */ public class PushNotificationsFragment extends Fragment implements PushNotificationContract.View { private RecyclerView mRecyclerView; private LinearLayout mNoMessagesView; private PushNotificationsAdapter mNotificatiosAdapter; private PushNotificationsPresenter mPresenter; public PushNotificationsFragment() { } public static PushNotificationsFragment newInstance() { PushNotificationsFragment fragment = new PushNotificationsFragment(); // Setup de Argumentos return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { // Gets de argumentos } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_notifications, container, false); mNotificatiosAdapter = new PushNotificationsAdapter(); mRecyclerView = (RecyclerView) root.findViewById(R.id.rv_notifications_list); mNoMessagesView = (LinearLayout) root.findViewById(R.id.noMessages); mRecyclerView.setAdapter(mNotificatiosAdapter); return root; } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Override public void onPause() { super.onPause(); } @Override public void showNotifications(ArrayList<PushNotification> notifications) { mNotificatiosAdapter.replaceData(notifications); } @Override public void showEmptyState(boolean empty) { mRecyclerView.setVisibility(empty ? View.GONE : View.VISIBLE); mNoMessagesView.setVisibility(empty ? View.VISIBLE : View.GONE); } @Override public void popPushNotification(PushNotification pushMessage) { mNotificatiosAdapter.addItem(pushMessage); } @Override public void setPresenter(PushNotificationContract.Presenter presenter) { if (presenter != null) { mPresenter = (PushNotificationsPresenter) presenter; } else { throw new RuntimeException("El presenter de notificaciones no puede ser null"); } } }
El código anterior realiza las siguientes acciones:
onCreateView()
: Asignamos el adaptador alRecyclerView
.onResume()
: Se inicia el presentador.showNotifications()
: Actualiza los datos del adaptador con las notificaciones existentes.showEmptyState()
: Cambia entre empty state y la lista. El métodoView.setVisible()
te lo permite junto a las constantesView.GONE
yView.VISIBLE
.setPresenter()
: Vincula la vista actual (fragmento) con el presentador.
Implementar presentador de notificaciones
Abre la clase NotificationsPresenter
e implementa la interfaz NotificationsContract.Presenter
.
public class NotificationsPresenter implements NotificationContract.Presenter {
Lo primero es añadir dos campos para la vista y otro para el componente FirebaseMessaging
.
private final NotificationContract.View mNotificationView; private final FirebaseMessaging mFCMInteractor;
El punto de entrada del constructor debe asignar ambas instancias de los campos. Adicionalmente haz efectivo el vínculo del presenter con la view con setPresenter.
private final NotificationContract.View mNotificationView; private final FirebaseMessaging mFCMInteractor;
Ahora sobrescribe el método base start()
con el registro de FCM con el método subscribeToTopic()
. Aunque yo uso el tema «promos», ya depende de ti como nombrar el tema y que condiciones usarás para asignarlo si es que tienes más de uno.
Lo siguiente es llamar a loadNotifications()
para iniciar la carga de datos.
@Override public void start() { registerAppClient(); loadNotifications(); }
@Override public void registerAppClient() { mFCMInteractor.subscribeToTopic("promos"); } @Override public void loadNotifications() { NotificationsRepository.getInstance().getNotifications( new NotificationsRepository.LoadNotificationsCallback() { @Override public void onNotificationsLoaded(List<PushMessage> notifications) { if (notifications.size() > 0){ mNotificationView.showEmptyState(false); mNotificationView.showNotifications(notifications); }else { mNotificationView.showEmptyState(true); } } } ); }
En loadNotifications()
llamamos el método getNofications()
del repositorio de notificaciones.
Esto requiere crear una escucha anónima del tipo LoadNotificationsCallback()
, donde se sobrescribirá el controlador onNotificationsLoaded()
, el cual retorna la lista de notificaciones existentes.
Luego hay dos reglas de la aplicación. Si hay más de una notificación, entonces poblamos la lista con View.showNotifications()
.
De lo contrario invocamos al empty state desde la vista con View.showEmptyState()
.
Preparar la actividad de notificaciones
La actividad de notificaciones debe:
- Realizar una transacción
add()
dePushNotificationsFragment
- Crear un instancia de
PushNotificationsPresenter
- Comprobar si hay un usuario con sesión iniciada. Si no es así, entonces debes redirigir la app a la
LoginActivity
.
PushNotificationsActivity.java
public class PushNotificationsActivity extends AppCompatActivity { private static final String TAG = PushNotificationsActivity.class.getSimpleName(); private PushNotificationsFragment mNotificationsFragment; private PushNotificationsPresenter mNotificationsPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_notifications); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); setTitle(getString(R.string.title_activity_notifications)); // ¿Existe un usuario logueado? if (FirebaseAuth.getInstance().getCurrentUser() == null) { startActivity(new Intent(this, LoginActivity.class)); finish(); } mNotificationsFragment = (PushNotificationsFragment) getSupportFragmentManager() .findFragmentById(R.id.notifications_container); if (mNotificationsFragment == null) { mNotificationsFragment = PushNotificationsFragment.newInstance(); getSupportFragmentManager() .beginTransaction() .add(R.id.notifications_container, mNotificationsFragment) .commit(); } mNotificationsPresenter = new PushNotificationsPresenter( mNotificationsFragment, FirebaseMessaging.getInstance()); } }
Enviar Mensajes con Firebase Notifications
Firebase Notifications es un servicio gratuito de la plataforma Firebase Cloud para permitir enviar downstream messages a las aplicaciones clientes.
Esta solución provee un panel de administración sencillo con los requerimientos mínimos de UI para enviar una notificación con personalización media.
Esto quiere decir, que se comporta como una app servidor para el envío de mensajes.
La usaremos en el ejemplo actual ya que no se requiere personalización compleja, pero si tus push notifications se basan en comportamientos ligados a un sistema personalizado, es mejor que crees tu propia app server.
Un plus es que te permite realizar tracking del número de mensajes enviados, leídos, programados, de la cantidad de usuarios activos, etc. de forma automática con el servicio Firebase Analytics.
Enviar notificaciones desde la consola Notifications
Ve a la consola de Firebase y selecciona el proyecto Car Insurance:
Ahora dirígete al panel derecho, busca la sección GROW y selecciona Notifications.
Con ello tendrás el área de acción para enviar notificaciones.
Para enviar tu primer mensaje presiona el raised button que dice «ENVÍA TU PRIMER MENSAJE».
A continuación verás un formulario para redactar el contenido de la notificación push y configurar su comportamiento.
Veamos el propósito de las propiedades más frecuentes.
Texto del mensaje
Es el cuerpo de la notificación como tal. El contenido primario con la mayoría de datos informativos para el usuario.
Etiqueta del mensaje
Nombre que actúa como identificador del mensaje en la base de datos interna de Firebase Notifications.
Aquí puedes determinar un estándar de nombrado para seguir un orden lógico y distintivo.
Úsalo con cuidado, ya que lo verás en cada ítem de lista en el área de Notifications.
Fecha de entrega
Determina el instante en el que enviarás la notificación push.
Tendrás dos opciones «Enviar ahora» y «Enviar más tarde».
La primera despacha lo más pronto posible el mensaje hacia los clientes Android.
La segunda te permite programar el periodo futuro para el envío. Esto es de gran utilidad por si tienes un plan de campañas mensual con diferentes avisos.
La configuración posterior la puedes basar en una fecha, hora y zona horaria.
Por defecto se usa la zona horario del dispositivo del usuario, pero puedes desmarcar el checkbox y elegir cualquier rango GMT.
Objetivo
Es el usuario o conjunto de usuarios a los cuáles se les enviará el mensaje.
La opción Segmento de usuarios hace referencia a un subconjunto de clientes filtrados por criterios como la Aplicación en la que están registrados, el tipo de Audiencia definida en Analytics, el Idioma del dispositivo y la Versión de la app o del FCM SDK usado.
La opción Tema se refiere al concepto Topic del registro en Firebase Messaging. Aquí aparecerá una lista de todos los topics registrados desde clientes Android para la selección apropiada.
Puede que la aparición de un nuevo topic en la lista demore hasta 24 horas. Así que no te preocupes si no ves los tuyos cuando recién inicias el envío de notificaciones.
En última instancia tenemos la opción Un único dispositivo. Permite enviar a un solo dispositivo identificado por su Token de registro de FCM.
Recuerda que la implementación de FirebaseInstanceIdService
te permite obtener el token al momento de ser creado o actualizado.
Eventos de conversión
Un evento de conversión es una interacción del usuario con la app, que dispara un trigger para el registro de dicha acción.
Esta propiedad hace parte del estudio del servicio Firebase Analytics. El conteo de los eventos de conversión puede ser visto en Analytics > EVENTOS.
Opciones avanzadas
La última sección se trata de opciones avanzadas para el complemento de la push notification.
Como dice el texto informativo inicial, todos los campos son opcionales.
Aunque el contenido de la mayoría es intuitivo, te dejo la descripción de cada uno:
- Título: Encabezado de la notificación para impacto en el usuario
- Datos personalizados: Una serie de pares clave-valor para enviar como atributos adicionales en el cuerpo de la notificación.
- Prioridad: Determina la importancia de envío de un mensaje.
- Sonido: Habilita/Deshabilita la reproducción del sonido de la notificación. Solo en iOS. En Android debemos añadir la característica al objeto
Notification
. - Fecha de caducidad: El tiempo que estará la notificación disponible en la base de datos de Firebase Notifications. Dejar sobrevivir el mensaje te permitirá duplicar su contenido para envíos posteriores que sean similares.
Procesar push notifications en Android
Acceder al token de registro de FCM
Antes de recibir la notificación, loguearemos el token de registro para tener en cuenta su existencia.
Para ello ve a IFirebaseInstanceIdService
y sobrescribe onRefreshToken()
con un logueo Log.d()
cuyo texto central es el resultado del método FirebaseInstanceId.getInstance().getToken()
.
IFirebaseInstanceIdService.java
public class IFirebaseInstanceIdService extends FirebaseInstanceIdService { private static final String TAG = IFirebaseInstanceIdService.class.getSimpleName(); public IFirebaseInstanceIdService() { } @Override public void onTokenRefresh() { String fcmToken = FirebaseInstanceId.getInstance().getToken(); Log.d(TAG, "FCM Token: " + fcmToken); sendTokenToServer(fcmToken); } private void sendTokenToServer(String fcmToken) { // Acciones para enviar token a tu app server } }
El método hipotético llamado sendTokenToServer()
enviaría en un futuro el token a nuestra app server para guardarlo en la tabla de usuarios en una base de datos personalizada.
Esto podría serte de utilidad si deseas operar con notificaciones para un grupo de dispositivos personalizado. Donde consultarías los tokens a tu servicio web para crear el grupo.
Recibir y manejar mensajes en una app Android
1. Ya sabes que IFirebaseMessagingService
recibe los mensajes en su controlador onMessageReceived()
. Así que tan solo queda procesar su contenido para incluirlo en la lista.
La idea es crear una notificación de usuario para que muestre al usuario la llegada de una nueva promoción en la status bar o en el lock screen. Incluso puedes usar la prioridad Alta para mostrarla de forma emergente.
IFirebaseMessagingService.java
public class IFirebaseMessagingService extends FirebaseMessagingService { private static final String TAG = IFirebaseMessagingService.class.getSimpleName(); @Override public void onMessageReceived(RemoteMessage remoteMessage) { Log.d(TAG, "¡Mensaje recibido!"); displayNotification(remoteMessage.getNotification(), remoteMessage.getData()); sendNewPromoBroadcast(remoteMessage); } private void sendNewPromoBroadcast(RemoteMessage remoteMessage) { Intent intent = new Intent(PushNotificationsFragment.ACTION_NOTIFY_NEW_PROMO); intent.putExtra("title", remoteMessage.getNotification().getTitle()); intent.putExtra("description", remoteMessage.getNotification().getBody()); intent.putExtra("expiry_date", remoteMessage.getData().get("expiry_date")); intent.putExtra("discount", remoteMessage.getData().get("discount")); LocalBroadcastManager.getInstance(getApplicationContext()) .sendBroadcast(intent); } private void displayNotification(RemoteMessage.Notification notification, Map<String, String> data) { Intent intent = new Intent(this, PushNotificationsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT); Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_car) .setContentTitle(notification.getTitle()) .setContentText(notification.getBody()) .setAutoCancel(true) .setSound(defaultSoundUri) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(pendingIntent); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } }
Para reportar la nueva aparición de una notificación a la UI usaremos el componente LocalBroadcastManager
.
Este permite enviar un Intent hacia otros componentes Android para procesar llamadas. La idea es enviar en los extras del intent los datos que conforman la notificación.
2. Ahora desde NotificationsFragment
registraremos un BroadcastReceiver
para recibir los intents. Su comportamiento nos dice que debemos registrarlo en onResume()
y eliminar el registro en onPause()
:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { // Gets de argumentos } mNotificationsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String title = intent.getStringExtra("title"); String description = intent.getStringExtra("description"); String expiryDate = intent.getStringExtra("expiry_date"); String discount = intent.getStringExtra("discount"); mPresenter.savePushMessage(title, description, expiryDate, discount); } }; } @Override public void onResume() { super.onResume(); mPresenter.start(); LocalBroadcastManager.getInstance(getActivity()) .registerReceiver(mNotificationsReceiver, new IntentFilter(ACTION_NOTIFY_NEW_PROMO)); } @Override public void onPause() { super.onPause(); LocalBroadcastManager.getInstance(getActivity()) .unregisterReceiver(mNotificationsReceiver); }
La instancia anónima creada en onCreate()
, sobrescribe onReceive()
, donde le diremos al presenter que guarde los datos en el repositorio.
Actualmente Firebase Notifications Console tiene algunos problemas con el envío de notificaciones cuando la app está cerrada o en background. Por lo que debemos esperar al equipo desarrollador para que nos provean de actualizaciones.
Enviar notificación push
Ve a la consola, compón un mensaje con las siguientes características y envíalo a todos los usuarios cuyo segmento sea el registro en com.herprogramacion.carinsurance
o el paquete que hayas elegido para tu app.
- Título: ¡Cyberlunes!
- Descripción: Compra en línea ahora mismo y ahorra hasta un 50% en el seguro de tu automóvil
- Etiqueta: Promo_#2_Junio_2016
- Datos personalizados: (expiry_date:30/06/2016), (disscount,0.5)
Cuando todo esté listo presiona «ENVIAR MENSAJE».
Deberías ver la aparición de la promoción en la lista y ver el icono del auto en la status bar.
Conclusión
Los nuevos servicios de Firebase tienen un potencial increíble por explotar.
Aunque el tema central de este tutorial era el envío de notificaciones push con Firebase Cloud Messaging, vimos que Firebase Authentication es un excelente servicio que nos facilita el login con múltiples proveedores en nuestra app.
También vimos que es posible integrar con Firebase Analytics para obtener informes con las métricas que consideres más importantes en el balance de tu app.
Así que ahora todo depende de ti conocer más a fondo este servicio. Aunque hay varios elementos que necesitan trabajo y aún presentan bugs, de seguro es un servicio que vale la pena agregar a nuestra caja de herramientas.