Está en la página 1de 239

Tutorial Para Desarrollar Una App

Android Para Productos, Clientes Y


Pedidos
(Parte #4)

¡Bienvenido a la cuarta parte de nuestro proyecto App Productos !

Espero te haya sido de agrado la tercera parte y su contenido, al generar la screen de


detalle para los productos, incorporar la base de datos SQLite como fuente local de
datos e incrementar el servicio web para consultar un producto por su detalle.

En esta ocasión, desarrollaremos una de las funcionalidades vitales de la aplicación.

La creación de factuas/pedidos por parte del vendedor.

Al realizar la investigación previa del problema, intentar cubrir las necesidades de


los lectores de Hermosa Programación y a su vez, mantener un alcance lúdico
consistente en tiempo y explicación; los objetivos por alcanzar en este tutorial son
los siguientes:

 Crear Navigation Drawer


 Crear screen de lista de pedidos/facturas
 Crear screen de lista de clientes
 Actualizar screen de lista de productos para permitir un modo de selección
 Crear screen de agregar facturas
 Crear screen para añadir/editar ítems de factura

Para ser honesto, esta parte será más dispendiosa que las anteriores.

Pero te aseguro que valdrá la pena seguirla, ya que se vienen enfoques muy
interesantes para solucionar la creación de pedidos.

¿Te suena bien?


¡Entonces comencemos :D !
Contenido
Planeando Lo Que Haremos 7

Paso #1. Análisis De Requerimientos 8


Listar Facturas 8
Listar Clientes 10
Crear Pedidos 11
Modelo De Datos 12

Paso #2. Preparar Bocetos 13


Boceto Para Navigation Drawer 13
Bocetos Para Lista Clientes 15
Boceto Para Lista De Facturas 17
Bocetos Para Creación De Factura 18
Boceto Para Añadir/Editar Un Ítem 20

Paso #4. Crear Menú Desplegable (Navigation Drawer) 24


Nav Drawer Con Múltiples Actividades 24
Tarea #1. Crear Actividad Padre 26
Tarea #2 Modificar Layouts De Las Actividades 27
Tarea #3. Implementar Navigation Drawer 34
Tarea #4. Crear Actividades 48

Paso #4. Crear Lista De Clientes 64


Diseñar Arquitectura 64
Tarea #1. Crear Entidades 66
Tarea #2. Crear Almacén De Clientes En Caché 69
Tarea #3. Crear Repositorio De Clientes 77
Tarea #4. Crear Interactor Para Obtener Clientes 80
Tarea #5. Crear Presentador De Clientes 82
Tarea #6. Crear Vista De Lista De Clientes 85
Tarea #7. Crear Actividad De Clientes 100

Paso #5. Crear Lista De Facturas 103


Diseñar Arquitectura 103
Tarea #1. Crear Entidades 105
Tarea #2. Crear Caché 111
Tarea #3. Crear Repositorio 119
Tarea #4. Crear Interactor 123
Tarea #5. Definir Microinteracciones MVP 125
Tarea #6. Crear Presentador 126
Tarea #7. Crear Fragmento 129
Tarea #8. Completar Actividad De Facturas 145

Paso #7. Crear Facturas 147


Diseñar Arquitectura 147
Tarea #1. Crear Entidades 152
Tarea #2. Crear Interactor Para Añadir Facturas 155
Tarea #3. Definir Microinteracciones MVP 158
Tarea #4. Crear Caché De Ítems De Factura 161
Tarea #5. Crear Fragmento 170
Tarea #6. Crear Presentador 198
Tarea #7. Modificar Actividad 204

Paso #8. Añadir/Editar Ítems De Una Factura 210


Diseñar Arquitectura 210
Tarea #1. Definir Microinteracciones MVP 213
Tarea #2. Crear Contrato MVP 214
Tarea #3. Crear Fragmento 215
Tarea #4. Crear Presentador 226
Tarea #4. Modificar Actividad 231

Conclusión 234
¿Cuáles Son Los Pasos A Seguir? 235
Planeando Lo Que Haremos
El orden de los pasos que seguiremos para desarrollar el puñado de características
para obtener una entrega sigue siendo igual al inicio cuando fue planteada la
metodología:

1. Análisis
2. Bocetos
3. Recursos y herramientas
4. Desarrollo
5. Fuentes de datos reales

Así que empecemos por entender lo que requiere el usuario basado en la naturaleza
de la farmacia hipotética…
Paso #1. Análisis De Requerimientos
En esencia tenemos tres casos de usos a desarrollar:

 Listar facturas
 Listar clientes
 Crear facturas

Cada uno de ellos puede variar en función de las reglas de negocio del problema que
estés abordando, por lo que debes estar muy atento en el momento que requieras
añadir tus propias conjeturas.

Listar Facturas
Veamos una breve descripción de esta característica:

El vendedor requiere escanear la lista de todos los pedidos para


revisar la información pactada en alguno de ellos.

Subcaso: Este podrá buscar por nombre del cliente y filtrar los
resultados por fecha, monto y tipo de pago.

Como ves, el alcance es sencillo y concreto.

A eso sumémosle algunas reglas de negocio y aclaraciones:

 Las facturas pueden ser listadas por todo tipo de usuario (vendedor,
supervisor y administrador)
 La factura no está sujeta al seguimiento de su entrega, ya que esta es
inmediata y personal (no hay domicilios)
 Los tres filtros son inclusivos (se adicionan solo los resultados que cumplan
con las características simultáneamente)
 Una vez realizado el pago completo de la factura, esta procede a guardarse y
a descontar del inventario los ítems
De la otra mano, los datos que guardaremos de cada factura son los siguientes:

 ID: Identificador único de la factura


 Número de factura: Es un identificador numérico que diferencia
visualmente a cada factura del resto
 Cliente: Persona que compró medicamentos de la farmacia
 Pagos: Forma en que se realizó la operación de compra (efectivo, debito,
crédito)
 Fecha de creación: Fecha que consta el día y la hora exacta en que la compra
fue exitosa.
 Monto total: Valor total que el cliente pagó por el pedido
 Estado: Indica la situación actual de la factura. Los valores disponibles serán.

Donde los estados de la factura son:

 Borrador: Estado inicial al crear una factura


 Cancelada: Cuando el cliente se retracta la cancelación de su manifestación
de compra
 Pagada: Cuando el cliente ha pagado el total y se le han entregado sus
artículos en la caja
 Enviada: Cuando el vendedor ha notificado a sus compañeros de caja la
preparación del cliente para pagar

En otros modelos de negocio a lo mejor necesites:

 Fechas de cada transición: Si la esperanza de vida del estado requiere


seguimiento, entonces no olvides añadir los respectivos campos de fecha para
tomar su duración.
 Peso total: Estima la cantidad total de gramos que pesa el pedido en conjunto
para cómputos de tarifas.
 Descuentos: Si existe la política de descuentos directos y personalizados,
entonces requerirás tener su información para calcular los totales.

Otra parte importante de un pedido serían los ítems que pactaron comprar.

Por lo general se le conocen como detalles del pedido; pero también podremos
tratarlos con varios alias: líneas del pedido, productos del pedido, ítems del pedido,
productos relacionados al pedido, etc.
El nombre que elijas depende de tu gestión documental y glosario entre las partes
comprometidas con el software.

En mi caso me referiré a ellos como ítems de la factura.

Ahora bien, ¿Qué atributos nos indica la farmacia que debe tener persistencia?

Fíjate:

 Factura: Factura a la que pertenece


 Producto: Es el ítem del inventario que se agregó
 Índice de secuencia: La posición del ítem en orden incremental con respecto
a los demás
 Cantidad: Número de unidades del producto en mención
 Precio: Valor establecido del producto al crear el pedido
 Total: Es el importe total al multiplicar el precio por la cantidad

Listar Clientes
De forma similar a la necesidad de revisar pedidos, los clientes también son
requeridos:

El vendedor requiere listar los clientes existentes para validar el


histórico de compras de los visitantes y así mismo poder asociarle
pedidos.

Subcaso: Este puede realizar una búsqueda rápida por nombre y


filtrar por fecha en que ordenó, productos que ha comprado y el
total gastado en la farmacia.

Los siguientes son los datos que la farmacia desea almacenar por cada cliente:

 Número de Cliente: Es un identificador del cliente para diferenciarles entre



 Nombre: Nombres y apellidos del cliente
 Teléfono: Número telefónico de contacto del cliente
 Dirección: Indica la ubicación del cliente en la ciudad
 Ciudad: Municipio de residencia actual del cliente
 Fecha de conversión: Fue el día en que se convirtió en cliente de la farmacia
 Otros detalles: Información extra del cliente

Crear Pedidos
Descripción:

El vendedor requiere crear un nuevo pedido cuando el cliente le


manifieste su intención de compra.

Este puede agregar y eliminar productos del pedido tanto como


desee; cambiar la cantidad de unidades exigidas por el cliente; y
determinar el tipo de pago que el usuario usará en la compra.

Reglas de negocio:

 Es política de la farmacia que el cliente debe estar previamente creado antes


de guardar el pedido.
 Solo se acepta un tipo de pago
 No pueden agregarse productos cuyo stock no sea mayor a 0
 Los productos con descuento o promociones, son creadas como productos
individuales
 El valor del impuesto es del 18% para todos los productos y no se puede
modificar manualmente

No lo olvides:

Has una correcta investigación para profundizar y descubrir sobre las reglas de
negocio del tu problema.

A lo mejor en la mayor cantidad de elementos coincidamos, pero sin duda alguna


habrá más requerimientos, reglas y datos que diferencian ligera o marcadamente
nuestros problemas.
Habiéndote remarcado esto, fíjate en las ideas que tengo para la interfaz…

Modelo De Datos
En resumidas cuentas, las definiciones anteriores de las entidades pueden
relacionarse y establecerse en el siguiente diagrama entidad-relación:

Donde las terminologías de las tablas son:

 Customer: Cliente
 Invoice: Factura
 Invoice Item: Ítem de factura
Paso #2. Preparar Bocetos
A continuación te muestro los wireframes de las características previamente
descritas.

Boceto Para Navigation Drawer


Recordemos que debido a la inclusión de nuevas entidades de información a la app,
es necesario tener un control de UI que permita la navegación principal entre dichos
elementos.

Y como bien sabes, el Navigation Drawer es un patrón excepcional para solucionar


esta situación.

El boceto muestra una cabecera con una foto de perfil del vendedor junto a su
nombre y correo de acceso.

Por el lado de las opciones de menú, tenemos un primer grupo con las entidades de
negocio centrales: Facturas, Productos y Clientes.

El segundo grupo tiene un par de acciones a nivel de lógica de la aplicación como es


el cambio de ajustes y el cierre de sesión.

Aunque los puntos de interacción son intuitivos, aclararé su alcance:

Punto de interacción Reacción

Tap en opción Facturas Abre la screen de lista de facturas

Tap en opción de Abre la screen de lista de productos


Productos

Tap en opción de Clientes Abre la screen de lista de clientes

Tap en opción de Ajustes Abre la screen de preferencias (no implementado en


este tutorial)
Tap en opción de Cerrar Cierra la sesión del vendedor
sesión

Bocetos Para Lista Clientes

El alcance de la lista de clientes se basa en el refresco de la lista por el momento.


El subcaso para filtros y búsqueda no los trataremos en este tutorial.

En cuanto al diseño, la pantalla para la lista de clientes muestra el nombre y el


teléfono de contacto.

Algunas ideas adicionales para mostrar en la info del cliente serían:

 Monto total gastado


 Número de facturas creadas
 Fecha de última compra
 Identificador de cliente

En cuanto a sus puntos de interacción:

Punto de interacción Reacción

Tap en toggle de menú Se abre el menú horizontal (Navigation Drawer)


horizontal / swipe horizontal

Tap en action button de lupa Se despliega action view para búsqueda (sin
implementación en este tutorial)

Tap en floating action button Inicio de screen para creación de nuevo cliente
(sin implementación en este tutorial)

Swipe to refresh Se refresca la lista de cliente desde el servidor


(sin implementación en este tutorial)
Boceto Para Lista De Facturas

Al igual que en la lista de clientes, obviaremos la búsqueda, filtros y ordenamientos


en esta lista.

En la vista proveeremos los siguientes datos por cada ítem:

 Cliente
 Número de la factura
 Fecha de creación
 Cantidad de ítems comprados
 Valor total
 Estado

El único punto de interacción funcional del que dispondremos, será la creación con
Tap en el FAB.
(Obviamente la apertura y cierre del Nav. Drawer)

Bocetos Para Creación De Factura


En esta característica tenemos una dinámica diferente.

En esencia, la creación de la factura se da por el siguiente formulario:

Este diseño permite:

 Asociar el cliente al que se la hará la factura


 Mostrar el número generado automáticamente de la factura (si lo deseas,
puedes condicionar para que la entrada pueda ser manual)
 Añadir ítems basados en los productos existentes
 Mostrar los resultados totales

Aunque elementos como los descuentos, impuestos, fechas de vencimiento y el


pedido asociado no se encuentran presentes debido a la naturaleza del negocio, queda
a tu disposición ampliar el formulario por si lo requieres.

Te recomiendo sigas con el diseño de cards para separar el formato por áreas de
contenidos visibles.

Observemos los eventos asociados:

Punto de interacción Reacción

Tap en botón Up y Se muestra diálogo para confirmar el descarte de los


Back cambios creados. Si se acepta, no se guardan, de lo
contrario se mantiene en la interfaz

Tap en campo de Abre la scren de clientes en modo de selección


cliente

Tap en botón “Añadir Abre la screen para adición de ítems de factura


Item”

Tap en un ítem añadido Abre la screen para edición de ítems de factura

Tap en el botón de Refresca la lista de ítems para evidenciar la eliminación


remoción (×) del ítem
Consecuencia de las Se actualiza la card que sostiene los importes totales de
operaciones de ítems la factura

Tap en botón de check Intenta guardar la factura.


(✓)

Boceto Para Añadir/Editar Un Ítem


Cuando es presionado el punto de interacción representado con el botón Añadir
ítem, la siguiente screen es presentada:
Este formulario permite seleccionar el producto que pide el cliente a través del
primer campo de texto y permite seleccionar la cantidad necesitada a través de un
número entero.

Punto de interacción Reacción

Tap en Up/Back Button Vuelve a la screen de nueva factura sin efecto alguno

Tap en action button de Confirma la configuración exitosa del nuevo ítem y se


marca de chequeo retorna a la screen de nueva factura
Campo de texto debajo Abre la screen de lista de productos, permitiendo la
de la etiqueta Cliente selección de un elemento para retornarlo a la screen
presente

Foco en campo de Permite escribir un número entero que representa la


Cantidad cantidad exigida del producto

Si tienes otro atributo que necesites (descuentos manuales, impuestos,


modificación manual de los datos del producto, etc.), siéntete libre de
incorporarlo.

Una vez el ítem es agregado correctamente, este es apilado con un diseño como el
siguiente:
La presentación contiene la imagen del producto, el nombre, la cantidad elegida,
el precio actual y el total parcial hacia la derecha.

Dentro de este se encuentra un punto de interacción para remover el ítem a través de


un icono de equis.
Paso #3. Crear Menú Desplegable
(Navigation Drawer)
De los bocetos pasamos directamente al desarrollo de las pantallas de la app.

Aquí asumiremos primero la creación del navigation drawer para materializar el


primer mockup.

Si aún no sabes cómo implementar este componente te recomiendo leer mi artículo


NavigationView: Navigation Drawer Con Material Design antes de seguir con esta
sección.

En él encontrarás la receta para incorporar su diseño y leer eventos del usuario.

Una vez lo hayas hecho puedes seguir con facilidad los siguientes pasos.

Nav Drawer Con Múltiples Actividades


El ejemplo que te proporcioné anteriormente, los ejemplos de la documentación
oficial de Android y varios ejemplos de la web te mostrarán como usar solo
fragmentos para representar cada sección del Nav Drawer.

En ocasiones este enfoque es apropiado para las limitaciones de algunos problemas,


sin embargo en nuestro caso necesitamos enviar al usuario a una actividad diferente
por cada sección del menú.

Esto quiere decir que deseamos que cada actividad tenga el mismo comportamiento
en su NavigationView.

La pregunta aquí es:

¿Cómo implementar el menú en varias actividades?

Y la respuesta más simple sería:


Pongo un DrawerLayout en el layout de cada actividad; luego copio y pego los
eventos del menú en cada clase.

Obvio no está mal, esta solución cumple con el cometido final.

No obstante, si queremos mejorar la legibilidad y el mantenimiento, podemos usar


una herencia de clases.

Es decir, crear una actividad padre que controle las acciones del navigation drawer y
luego hacer que ProductsActivity, InvoicesActivity y CustomersActivity hereden sus
características y comportamientos.

El siguiente diagrama de clases nos muestra esta idea:

Ahora, nos queda crear la actividad padre BaseActivity para que asuma las
responsabilidades del navigation drawer.

En resumen, las tareas de programación a llevar a cabo son las siguientes:

1. Crear actividad padre


2. Modificar layout de la actividad de productos
3. Crear cabecera del menú
4. Crear recurso de menú para las secciones
5. Procesar eventos del navigation drawer
6. Implementar herencia sobre actividades

Así que vamos a completar cada una de ellas.


Tarea #1. Crear Actividad Padre
Agrega una nueva actividad al paquete raíz del proyecto (o si lo deseas ponla en un
nuevo paquete llamado navdrawer).

Nómbrala BaseActivity.

En adición a eso, debes saber que esta actividad no tendrá un layout propio.

¿Por qué?

Bueno, ella se basa en el layout de cada una de sus hijas. Por lo que supone que existirá
un componente DrawerLayout y NavigationView creado previamente, el cual se
pasa por el supermétodo onCreate().

Lo que quiere decir que puedes borrar el layout que Android Studio incluye en la
creación.

Con ello tendrías tan solo el código base de la actividad:

public class BaseActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nav_drawer);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

FloatingActionButton fab = (FloatingActionButton)


findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action",
Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}

}
Tarea #2 Modificar Layouts De Las Actividades

2.1 Envolver Con Un DrawerLayout El Contenido

Abre el layout de la actividad correspondiente y envuelve toda la definición XML


con un nodo <DrawerLayout> como se muestra en el siguiente código:

<?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">

<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"

<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="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

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

<FrameLayout
android:id="@+id/id_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
</android.support.design.widget.CoordinatorLayout>

</android.support.v4.widget.DrawerLayout>
2.2 Añadir NavigationView

Recuerda que el segundo componente del DrawerLayout es el NavigationView para


representar el contenido del menú deslizante.

Como es sabido, este va por debajo del contenido principal.

Así que agregamos el siguiente fragmento al layout:

...
<!--Menú horizontal-->
<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" />

</android.support.v4.widget.DrawerLayout>

Dos cosas a resaltar:

 En todas las actividades que vaya a estar el nav drawer, usa el mismo
indicador nav_view para que la actividad padre se refiera al mismo
componente
 La etiqueta aún no está completa, falta el menú, la cabecera y demás etiquetas
de personalización si así lo deseas

Debido a que usaremos ese mismo fragmento de código en varios layouts, no queda
mal aislarlo en un layout individual para reutilizarlo.

Así que agrega un nuevo layout llamado navigation_view.xml y luego úsalo con la
etiqueta <include>:

<!--Menú horizontal-->
<include layout="@layout/navigation_view" />
2.3. Crear Cabecera Del Navigation Drawer

En el diseño de la cabecera tenemos una imagen de corte circular para la foto del
vendedor junto al nombre del mismo y su correo (o cualquier dato particular que
elijas).

Sabiendo esto, crea un nuevo layout llamado nav_header.xml y agrega la siguiente


definición:

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:background="@color/colorPrimary"
android:orientation="vertical"
android:gravity="bottom"
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/profile_image"
android:layout_width="@dimen/navdrawer_profile_image_size"
android:layout_height="@dimen/navdrawer_profile_image_size"
android:paddingTop="@dimen/keyline_1_minus_8dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_account_circle" />

<TextView
android:id="@+id/salesman_name"
android:layout_width="match_parent"
android:paddingTop="@dimen/keyline_1_minus_8dp"
android:layout_height="wrap_content"
android:tag="SalesmanName"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="Victor García" />

<TextView
android:id="@+id/salesman_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="victorg@farmacia.com" />
</LinearLayout>

Este layout usa nuevos recursos de dimensión en el archivo dimens.xml, los cuales es
muestran a continuación:
<dimen name="keyline_1_minus_8dp">8dp</dimen>

<!-- Nav Drawer -->


<dimen name="navdrawer_profile_image_size">64dp</dimen>

Además del vector ic_account_circle.xml que representa la imagen por defecto del
perfil, el cual puedes obtener desde el siguiente link.

Volviendo al tema, Android Studio te mostrará la siguiente previsualización cuando


copies el contenido:
2.4 Crear Recurso De Menú Para Ítems

Lo siguiente es añadir un recurso a res/menú para constituir las secciones del nav
drawer que especificamos en el boceto.

Es sencillo, tan solo recuerda usar la etiqueta <item> en para cada sección y
relacionar su título desde un recurso string.

Con eso claro, crea el archivo drawer_items.xml e incluye esta definición:

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="@+id/invoices_nav_item"
android:icon="@drawable/ic_currency"
android:title="@string/orders_nav_item" />
<item
android:id="@+id/products_nav_item"
android:icon="@drawable/ic_basket"
android:title="@string/products_nav_item" />
<item
android:id="@+id/customers_nav_item"
android:icon="@drawable/ic_account"
android:title="@string/customers_nav_item" />

</group>

<group
android:id="@+id/drawer_group_2"
android:checkableBehavior="none">
<item
android:id="@+id/settings_nav_item"
android:icon="@android:color/transparent"
android:title="@string/settings_nav_item" />
<item
android:id="@+id/log_out_nav_item"
android:icon="@android:color/transparent"
android:title="@string/log_out_nav_item" />
</group>
</menu>

Como ves, son dos grupos. El primero para las secciones principales de datos y el
segundo para los ajustes y cierre de sesión como habíamos bocetado.
Si echas un vistazo a la pestaña de diseño podrás ver la jerarquía más clara:
Tarea #3. Implementar Navigation Drawer
Bien, ahora centrémonos en BaseActivity que será el centro de operaciones para el
nav drawer.

Pero… ¿Cuál es el ámbito de esta actividad?

Mira:

Esta clase manejará el ciclo de una actividad pero sin embargo no deseamos que
infle un layout, por lo que tampoco nos interesa crear instancias de ella.

Como conclusión, marcarla como abstract sería nuestro siguiente paso:

public abstract class BaseActivity extends AppCompatActivity {

Definir Configuración De Los Ítems

Crearemos un campo enumerado con el fin de tomar las referencias de los IDs del
menú para los ítems activables en el drawer.

public abstract class BaseActivity extends AppCompatActivity {

public enum NavDrawerItemEnum {


INVOICES(R.id.invoices_nav_item),
PRODUCTS(R.id.products_nav_item),
CUSTOMERS(R.id.customers_nav_item),
INVALID(0);

private int id;

NavDrawerItemEnum(int navItemId) {
id = navItemId;
}

public int getId() {


return id;
}
}

¿Cuál es el objetivo de hacer esto?


Entregar el ID relacionado con las actividades hijas al momento de marcar un ítem.

Lo haremos efectivo creando un método protegido llamado getNavDraweritem():

protected NavDrawerItemEnum getNavDrawerItem() {


return NavDrawerItemEnum.INVALID;
}

De esta forma lo sobrescribiremos en cada actividad hijas y retornaremos el ID del


enumerado correspondiente.

Inicializar Actividad

Ahora, en el método onCreate() no obtendremos instancias de los views ya que este


es ejecutado como un supermétodo por las subclases.

De manera que puedes usar este controlador para añadir otras características
compartidas como la gestión de sesión de usuario, tracking de eventos,
sincronización, etc.

En nuestro caso, moveremos la verificación de un usuario logueado que teníamos en


ProductsActivity.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: Controlar otras características compartidas adicionales

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

Asignar Toolbar A La Actividad

También nos es posible manejar la asignación de la Toolbar como Action Bar de las
actividades hijas.
Para ello crearemos el método getToolbar() para conseguir la instancia y asignarla
de una vez con setSupportActionBar():

public Toolbar getToolbar(int toolbar) {


if (mToolbar == null) {
mToolbar = (Toolbar) findViewById(toolbar);
if (mToolbar != null) {
setSupportActionBar(mToolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
}
return mToolbar;
}

El parámetro que recibe es el ID de la toolbar que se asignará. El cual nos será de


utilidad a la hora de cubrir el patrón master-detail.

Este método podemos llamarlo inicialmente en onSetContentView() luego de que el


layout de la actividad hija haya sido inflado.

@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
getToolbar();
}

Obtener Referencia Del Navigation Drawer

En el mismo método, obtenemos la instancia del NavigationView, el DrawerLayout e


inflamos la cabecera.

mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);


mNavigationView = (NavigationView) findViewById(R.id.nav_view);
// Inflar cabecera del nav drawer
mNavHeader = mNavigationView.inflateHeaderView(R.layout.nav_header);

El objetivo de inflar la cabecera es poder tener acceso a su referencia, para acceder


cómodamente a los views hijos. Esto nos permitirá poblarla con los datos del
usuario.
Ok.

Hemos tenido un avance inicial que actuará como base en la preparación del menú
horizontal.

…¿recuerdas los aspectos para llevarlo a cabo?

Si no es así, te los menciono:

 Cambiar icono de la navegación en la Toolbar por un icono de tres barras


horizontales
 Poblar la sección de la cabecera con datos de la cuenta del usuario
 Abrir la actividad relacionada con la sección seleccionada
 Cerrar drawer si el Back Button es presionado
 Mantener la selección de la sección actual en apertura/cierre del drawer o
cambios de configuración

Veamos las anteriores acciones en ejecución.

Cambiar Botón De Navegación

Vamos a onPostCreate() y usamos una instancia de ActionBarDrawerToggle.

Esta clase nos permite ligar la ActionBar al DrawerLayout de manera sencilla.

Incluso maneja los eventos (addDrawerListener()) de apertura/cierre si se lo pasamos


al drawer como DrawerListener.

Además ejecuta una animación del icono de navegación.

Para usarlo, creamos una instancia cuyo constructor recibe la actividad a ligar, el
drawer, la toolbar a setear como action bar y dos textos para referencia de
navegación de la apertura y cierre.

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

// Setear toolbar para dos paneles


getToolbar(R.id.toolbarList);

mDrawerToggle = new ActionBarDrawerToggle(


this,
mDrawerLayout,
mToolbar,
R.string.open_drawer_desc,
R.string.close_drawer_desc) {
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
setDrawerIndicatorEnabled(true);
}

@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
}
};
mDrawerLayout.addDrawerListener(mDrawerToggle);
mDrawerToggle.syncState();
}

Llamamos a syncState() con el fin de sincronizar el indicador con el drawer.

Asimismo invocamos a getToolbar() con el ID de la toolbar para la actividad de


productos cuyo panel de lista tiene una toolbar llamada R.id.toolbarList.

Sin embargo en el momento que deseemos dos paneles en otra actividad debemos
usar este mismo ID o invocar a getToolbar() desde la actividad hija en el momento
apropiado.

Procesar Eventos En Las Secciones

Para intervenir en las interacciones del usuario al tocar las secciones del menú
usamos la escucha OnNavigationItemSelectedListener.

Solo la asignamos con NavigationView.setNavigationItemSelectedListener() y


ejecutamos las acciones basados en el controlador onNavigationItemSelected().

Si relees las interacciones del boceto, verás que las acciones a ejecutar ya están
definidas.
Abriremos las actividades correspondientes a cada sección menos en la opción
“Cerrar Sesión”, donde ejecutaremos el cierre de la sesión actual del vendedor.

Debido a que el procesamiento de la selección es una tarea común, entonces


crearemos un método que tome el parámetro del ítem afectado con el fin de aislar el
tratamiento.

Observemos la representación:

@Override
public void setContentView(int layoutResID) {

mNavigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem
item) {
onNavDrawerItemClicked(item);
return true;
}
});

Donde onNavDrawerItemClicked() tiene como responsabilidad las siguientes


instrucciones:

1. Cambiar la apariencia del ítem a un estado de marcado


2. Aplicar la lógica para cada sección
3. Cerrar el drawer

Como resultado tendremos:

private void onNavDrawerItemClicked(MenuItem item) {


// Marcamos la sección
item.setChecked(true);

// Procesamos el comportamiento según la sección elegida


itemSelected(item.getItemId());

// Cerrarmos el drawer
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(GravityCompat.START);
}
}

Continuando, el método itemSelected() recibe el ID del ítem de menú presionado.

Su propósito es decidir con una estructura switch que operaciones ejecutar


dependiendo de la opción:

private void itemSelected(int navDrawerItemId) {


switch (navDrawerItemId) {
case R.id.invoices_nav_item:
launchSectionActivity(InvoicesActivity.class);
break;
case R.id.products_nav_item:
launchSectionActivity(ProductsActivity.class);
break;
case R.id.customers_nav_item:
launchSectionActivity(CustomersActivity.class);
break;
case R.id.settings_nav_item:
// TODO: Ejecutar tu actividad de ajustes
Snackbar.make(findViewById(android.R.id.content),
"Ajustes", Snackbar.LENGTH_SHORT).show();
break;
case R.id.log_out_nav_item:
// Cerrar sesión
logOut();
break;
}
}

Para las opciones que requieran el inicio de una sesión he creado un método con el
nombre launchSectionActivity():

private void launchSectionActivity(Class activityToLaunch) {


Intent intent = new Intent(this, activityToLaunch);
startActivity(intent);
finish();
}

Cerrar sesión desde las preferencias de usuario

Para cerrar la sesión llamamos al singleton de las preferencias de usuario y


ejecutamos el método delete() . Acto seguido iniciamos la actividad del login:
private void logOut() {
UserPrefs.getInstance(getApplicationContext()).delete();
launchSectionActivity(LoginActivity.class);
}

Poblar La Cabecera Con Datos De Cuenta

Ahora pondremos el correo del vendedor en el view de texto que tenemos en la


cabecera.

Si lo deseas, puedes añadir más views a la cabecera y setear información adicional del perfil
usuario, e incluso añadir ciertas acciones.

Así que generemos un nuevo método privado tipo void que cubra esta necesidad.

Su nombre será setupUserBox().

private void setupUserBox() {


// TODO: Poblar más views, agregar más acciones

// Poner email
TextView userNameView = (TextView)
mNavHeader.findViewById(R.id.salesman_name);
String userName = UserPrefs.getInstance(this).getUserName();
userNameView.setText(userName);
}

En el que tomamos la referencia del text view destinado para el nombre y le


asignamos el correo con UserPrefs.getUserName():

Modificación en preferencias de usuario

Declaramos el método para obtener el usuario en IUserPreferences:

public interface IUserPreferences {

...
String getUserName();
}

Y lo concretamos en UserPrefs así:

public class UserPrefs implements IUserPreferences {

...

@Override
public String getUserName() {
return mSharedPreferences.getString(PREF_USERNAME, null);
}
}

Marcar Ítem De Menú Por Defecto

Cada vez que se clickea en una sección de contenido en el drawer, el ítem debemos
marcar el ítem para mantener el conocimiento de ubicación.

Esto lo logramos con NavigationView.setCheckedItem().

Simplificaremos su invocación creando el método setNavDrawerCheckedItem():

private void setNavDrawerCheckedItem() {


mNavigationView.setCheckedItem(getNavDrawerItem().getId());
}

El cual será llamado al final de setContentView() junto a setupUserBox():

@Override
public void setContentView(int layoutResID) {

setNavDrawerCheckedItem();
setupUserBox();
}
Cerrar Drawer Si El Back Button Es Presionado

Este paso es sencillo.

Ya sabemos que para procesar el evento de presión del Back Button se usa el
método onBackPressed().

De tal modo que sobrescribamos el método y llamemos a closeDrawers() si el


drawer se encuentra abierto:

@Override
public void onBackPressed() {
if(mDrawerLayout.isDrawerOpen(GravityCompat.START)){
mDrawerLayout.closeDrawer(GravityCompat.START);
}else {
super.onBackPressed();
}
}

Poner Up Button En La Toolbar

En ocasiones necesitaremos que aparezca el up button en vez del icono del drawer
por razones de funcionalidad.

Un claro ejemplo de ello será cuando deseemos seleccionar un ítem de una actividad
de lista que también se muestra como sección del drawer.

Para resolver este inconveniente, crearemos un método protected llamado


setToolbarAsUp(), el cual deshabilitará el icono del drawer para que aparezca el Up y
luego seteamos una escucha personalizada que venga como parámetro.

protected void setToolbarAsUp(View.OnClickListener clickListener) {


if (mDrawerToggle == null) {
return;
}
mDrawerToggle.setDrawerIndicatorEnabled(false);
mDrawerToggle.setToolbarNavigationClickListener(clickListener);
}

Así que cuando necesitemos habilitar este botón, sobrescribimos el método y listo.
Tarea #4. Crear Actividades
A continuación vamos a crear las actividades que necesitaremos en los casos de uso
de este tutorial.

Estas son:

 Actividad de clientes
 Actividad de facturas
 Actividad adición/edición facturas
 Actividad adición/edición ítems de factura

De acuerdo al flujo establecido en los bocetos tendremos las siguientes relaciones


interacciones.

Facturas > Adición/Edición Facturas

Adición/Edición Facturas > Clientes

Adición/Edición Facturas > Adición de Items

Adición/Edición Facturas > Edición de Items

Adición/Edición Items > Productos

Determinar estas relaciones nos permite declarar los códigos de petición que habrá
en cada actividad.

Por otra parte, es necesario crear cada uno de los paquetes para alberga las
actividades y sus componentes.

 addeditinvoice: paquete para la añadir/editar facturas


 addeditinvoiceitem: paquete para añadir/editar ítems de factura
 customers: lista de clientes
 invoices: lista de facturas

1. Poner Productos En El Nav Drawer

Abre la actividad de productos y extiéndela de BaseActivity.

Sobrescribe el método getNavDrawerItem() para que retorne el valor PRODUCTS y el


drawer reconozca el ID del ítem en el navigation view:

public class ProductsActivity extends BaseActivity {

//...

@Override
protected NavDrawerItemEnum getNavDrawerItem() {
return NavDrawerItemEnum.PRODUCTS;
}
}

Obviamente al cambiar su layout como lo dijimos en las secciones anteriores


tendremos:

<?xml version="1.0" encoding="utf-8"?>


<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start"
android:fitsSystemWindows="true">

<include layout="@layout/content_products" />

<!--Menú horizontal-->
<include layout="@layout/navigation_view" />

</android.support.v4.widget.DrawerLayout>
2. Crear Actividad De Facturas

Haz click derecho sobre invoices y añade una actividad básica con New > Activity
> Basic Activity llamada InvoicesActivity.

En su configuración asegúrate de nombrar al layout activity_invoices.xml.


Cabe aclarar que el título a usar es “Facturas” en mi caso. Ya tú decides cual es el
término a conveniencia para tu app.
Además ponemos Java en la opción Source Language y presionamos Finish.

Modificar Layout

Acto seguido abre dicho layout y escribe el código XML para el diseño propuesto en
la sección de bocetos.

Recuerda que necesitamos los mismos elementos: App Bar, contenido principal y un
FAB. Y debemos envolverlo con un DrawerLayout.

Por lo que basta con este código:

activity_invoices.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:openDrawer="start">

<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<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="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

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

<FrameLayout
android:id="@+id/invoices_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fabSize="normal"
app:srcCompat="@drawable/ic_plus" />

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

<!--Menú horizontal-->
<include layout="@layout/navigation_view" />
</android.support.v4.widget.DrawerLayout>

Modificar Clase De La Actividad

Para esta actividad haremos los siguientes ajustes:

 Poner constante para código de interacción con la creación de facturas


 Extenderla de BaseActivity
 Sobrescribir el método getNavDrawerItem() para que retorne INVOICES

Así:

InvoicesActivity.java

public class InvoicesActivity extends BaseActivity{

public static final int REQUEST_ADD_INVOICE = 1;

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

@Override
protected NavDrawerItemEnum getNavDrawerItem(){
return NavDrawerItemEnum.INVOICES;
}
}
Crear Action Buttons

En el alcance del boceto se ve un botón de búsqueda en la toolbar, por ende creemos


un nuevo archivo de menú llamado invoices_menu.xml y añadámoslo:

Invoices_menu.xml

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:orderInCategory="1"
android:title="@string/action_search"
app:showAsAction="ifRoom" />
</menu>

Obviamente añadimos el controlador respectivo a la actividad para inflarlo en la


interfaz:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.invoices_menu, menu);
return true;
}

3. Crear Actividad De Clientes

Añade la clase en el paquete principal customers y nombra su layout como


activity_customers.xml.

Para facilitar su creación usa la opción que sale de presionar click derecho en el
paquete, New > Activity > Basic Activity

Modificar Layout

La jerarquía inicial que Android Studio nos provee es muy acorde a lo que
necesitamos.
Tenemos una App Bar, el contenido principal aislado en otro layout y un FAB.

Pero debido a que necesitamos al navigation drawer, agrega como nodo raíz un
componente DrawerLayout e inserta un NavigationView:

<?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"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<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="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

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

<FrameLayout
android:id="@+id/customers_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fabSize="normal"
app:srcCompat="@drawable/ic_plus" />

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

<!--Menú horizontal-->
<include layout="@layout/navigation_view" />
</android.support.v4.widget.DrawerLayout>

Si deseas verlo más plástico podrás ver la jerarquía visualmente en la ventana


Component Tree:

Modificar Clase De La Actividad

Lo siguiente es hacer que la actividad herede de BaseActivity:

public class CustomersActivity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_customers);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

FloatingActionButton fab = (FloatingActionButton)


findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// TODO: Abrir actividad de creación de clientes
}
});
}

}
Y como esta actividad pertenecerá al drawer, sobrescribimos el método para retorno
de la enumeración con CUSTOMERS:

@Override
protected NavDrawerItemEnum getNavDrawerItem() {
return NavDrawerItemEnum.CUSTOMERS;
}

Crear Action Buttons

En el apartado de bocetos vimos que la action bar tiene consigo la opción de


búsqueda.

Crea un nuevo recurso de menú y añade este elemento (aprovecha para incorporar
otros que tengas por cuenta propia):

customers_menu.xml

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:orderInCategory="1"
android:title="@string/action_search"
app:showAsAction="ifRoom" />
</menu>

Incorpora este menú agregando el método onCreateOptionsMenu() en la actividad:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.customers_menu, menu);
return true;
}
4. Crear Actividad Para Añadir/Editar Factura

Ahora añadimos la actividad AddEditInvoiceActivity a través del asistente, le


asignamos el título “Nueva Factura” y dejamos al archivo de layout como
activity_add_invoice.xml.

Modificar Layout

El propósito del layout de esta actividad es proveer la Toolbar y un espacio para


alojar al fragmento.

Por esta razón dejaremos los nodos autogenerados CoordinatorLayout, AppBarLayout y


Toolbar.

El FloatingActionButton lo eliminaremos y reemplazaremos la etiqueta <include> por


un FrameLayout para el fragmento:

activity_add_edit_invoice.xml

<?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="com.hermosaprogramacion.premium.appproductos.addeditinvoic
e.AddEditInvoiceActivity">

<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="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>

<FrameLayout
android:id="@+id/add_edit_invoice_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

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

Por el momento limpiamos la toma de referencia del FAB y agregamos la constante


de petición que hará InvoicesActivity.

También necesitamos habilitar el botón up de la toolbar.

Para ello declararemos un campo tipo ActionBar y luego usaremos a los métodos
setDisplayHomeAsUpEnabled() y setDisplayShowHomeEnabled() en onCreate().

AddEditInvoiceActivity.java

public class AddEditInvoiceActivity extends AppCompatActivity


implements DiscardChangesDialog.DiscardDialogListener {

public static final int REQUEST_ADD_INVOICE = 1;


private ActionBar mActionBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_edit_invoice);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mActionBar = getSupportActionBar();
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayShowHomeEnabled(true);
}

}
Añadir Acciones De La Toolbar

Agregaremos un action button con icono de check para guardar la factura.

Con esto en mente crearemos el recurso de menú add_edit_invoice_menu.xml, el


cual en su interior tendrá una sola etiqueta <item>.

¿Sus características?

Nombre Acción > “Guardar”, Icono > Check, ID > action_save_invoice

Veamos:

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save_invoice"
android:icon="@drawable/ic_check"
android:orderInCategory="1"
android:title="@string/action_save_invoice"
app:showAsAction="ifRoom" />
</menu>

Posteriormente, infla el menú con onCreateOptionsMenu():

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.add_edit_invoice_menu, menu);
return true;
}

5. Crear Actividad Para Añadir/Editar Ítems De Factura

Crea la clase Java de la actividad a través del asistente y su secuencia New >
Activity > Basic Activity.

Al introducir su configuración general introduce estos valores:


 Activity Name: AddEditInvoiceItemActivity
 Layout Name: activity_add_edit_invoice_item
 Title: Añadir Nuevo Ítem

Modificar Layout

Al igual que con la actividad de añadir facturas, limpiaremos el layout para que
reciba el fragmento únicamente:

activity_add_edit_invoice_invoice.xml

<?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="com.hermosaprogramacion.premium.appproductos.addeditinvoiceitem.pr
esentation.AddEditInvoiceItemActivity">

<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="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

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

<FrameLayout
android:id="@+id/add_edit_invoice_item_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>
Agregar Campos Y Constantes

Agregaremos 3 códigos para las acciones descritas anteriormente y tomaremos la


instancia de la Toolbar en forma de ActionBar:

Además habilitamos el Up Button con los métodos setDisplayHomeAsUpEnabled() y


setDisplayShowHomeEnabled().

AddEditInvoiceItemActivity.java

public class AddEditInvoiceActivity extends AppCompatActivity


implements DiscardChangesDialog.DiscardDialogListener {

public static final int REQUEST_ADD_INVOICE_ITEM = 1;


public static final int REQUEST_PICK_CUSTOMER = 2;
public static final int REQUEST_EDIT_INVOICE_ITEM = 3;

private ActionBar mActionBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_edit_invoice);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mActionBar = getSupportActionBar();
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayShowHomeEnabled(true);

Añadir Acciones De La Toolbar

Creamos el recurso de menú add_edit_invoice_item_menu.xml, para alojar el


action button de guardar.

Configurémoslo de esta forma.

Nombre Acción > “Guardar”, Icono > Check, ID > action_save_item_invoice


Miremos el código del menú:

<?xml version="1.0" encoding="utf-8"?>


<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save_invoice_item"
android:icon="@drawable/ic_check"
android:orderInCategory="1"
android:title="@string/action_save_invoice_item"
app:showAsAction="ifRoom" />
</menu>

Seguido, inflemos el menú:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.add_edit_invoice_item_menu, menu);
return true;
}

Manejar Eventos De Up Y Back Button

Sobrescribamos el método onSupportNavigationUp() para que llame a


onBackPressed():

@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
Paso #4. Crear Lista De Clientes
Esta característica es genérica debido a que emplearemos la arquitectura CLEAN
junto al patrón MVP en la capa de presentación como lo hemos venido haciendo.

Simplemente ya debe ser para ti mecánico e intuitivo.

Diseñar Arquitectura
Iniciando por la presentación tendremos un presentador y vista sin ninguna
alteración:

Por el lado del dominio tendremos un caso de uso para obtener clientes.

Obviamente esto requiere de un objeto de dominio que represente al cliente en la


lectura y escritura.

Y en los datos por el momento tendremos un repositorio para los clientes y una
fuente de datos en caché que sostenga la información mientras probamos las
características.
Tarea #1. Crear Entidades
El primer movimiento que debemos hacer es la crear el insumo principal para la
característica: la clase para los clientes.

Al basarnos en nuestro modelo de datos previo, sabremos que ya existen atributos


que podemos establecer.

Añade la nueva clase Customer a data/entities y escribe sus atributos.

No está de más decirte que las propiedades get/set pueden autogenerarse con Click
derecho dentro de la clase > Generate… > Getter and Setter:

public class Customer {


private String mId;
private String mName;
private String mPhone;
private String mAddress;
private String mCity;
private String mRegisterDate;
private String mOtherDetails;

public Customer(String id, String name,


String phone, String address,
String city, String registerDate,
String otherDetails) {
mId = id;
mName = name;
mPhone = phone;
mAddress = address;
mCity = city;
mRegisterDate = registerDate;
mOtherDetails = otherDetails;
}

public Customer(String name,


String phone, String address,
String city, String registerDate,
String otherDetails) {
this(UUID.randomUUID().toString(),// ID random automático
name,
phone,
address,
city,
registerDate,
otherDetails);

public String getId() {


return mId;
}

public void setId(String id) {


this.mId = id;
}

public String getName() {


return mName;
}

public void setName(String name) {


this.mName = name;
}

public String getPhone() {


return mPhone;
}

public void setPhone(String phone) {


this.mPhone = phone;
}

public String getAddress() {


return mAddress;
}

public void setAddress(String address) {


this.mAddress = address;
}

public String getCity() {


return mCity;
}

public void setCity(String city) {


this.mCity = city;
}

public String getRegisterDate() {


return mRegisterDate;
}

public void setRegisterDate(String registerDate) {


this.mRegisterDate = registerDate;
}

public String getOtherDetails() {


return mOtherDetails;
}

public void setOtherDetails(String otherDetails) {


this.mOtherDetails = otherDetails;
}
}

El segundo constructor genera automáticamente el identificador del cliente a través


de la clase UUID.
Esto es para mantener la integridad del conglomerado de clientes existentes en la
fuente local.

Una vez tengamos acceso al servidor podremos reemplazar el ID temporal impuesto.


Tarea #2. Crear Almacén De Clientes En Caché
Esta fuente de datos cumple con el mantenimiento de los clientes en la memoria del
dispositivo para optimizar la lectura de la lista.

Es similar a como lo hicimos con los productos, la cual nos ayudó a crear filtros y
ordenamientos sin tener que pasar por sentencias SQL o intervenir con el servicio
web.

Crear Interfaz ICacheCustomersStore

Aquí defines las operaciones estándar que serán realizadas sobre los objetos del
modelo.

¿Cuáles serán?

 Obtener clientes (recuerda usar Query para crear selecciones personalizadas)


 Añadir clientes
 Eliminar clientes
 Determinar si la caché está disponible

En código, añade un nuevo componente interface al paquete data/cache y agrega


métodos para el acceso de clientes.

public interface ICacheCustomersStore {


List<Customer> getCustomers(Query query);

void addCustomer(Customer customer);

void deleteCustomers();

boolean isCacheReady();
}

Crear Implementación CacheCustomersStore

Muy bien, lo que sigue es concretar la representación de la interfaz en una clase.


Dicho elemento usará un mapa de Customer que actúa como la fuente en memoria de
los clientes, sobre la cual se realizarán las operaciones definidas.

Además esta implementación seguirá el patrón singleton con el fin de limitar su


existencia de instancia a una sola, debido a que no necesitamos pluralidades a lo
largo de la app.

Así que agrega al mismo paquete la nueva clase CacheCustomersStore:

public class CacheCustomersStore implements ICacheCustomersStore {


/**
* Única instancia de la clase
*/
private static CacheCustomersStore INSTANCE = null;

/**
* Este campo representa la caché fundamental de clientes
*/
HashMap<String, Customer> mCachedCustomers = null;

// Se previene la creación de instancias


private CacheCustomersStore() {

/**
* Se crea o retorna la única instancia
*/
public static CacheCustomersStore getInstance() {
if (INSTANCE == null) {
INSTANCE = new CacheCustomersStore();
}
return INSTANCE;
}

@Override
public List<Customer> getCustomers(Query query) {
return null;
}

@Override
public void addCustomer(Customer customer) {

@Override
public void deleteCustomers() {

}
@Override
public boolean isCacheReady() {
return false;
}
}

Implementar Operaciones Sobre Clientes

Una vez creada la relación de realización de la caché, añadiremos las instrucciones


sobre las operaciones definidas.

getCustomers(Query query)

En primera instancia, la obtención de clientes es la selección a través del objeto


Query sobre la lista de elementos que existen en el mapa.

Si recuerdas en las partes pasadas, usamos el concepto de Selector para ejecutar la


selección de datos en un objeto que cubriera el filtrado, el ordenamiento y la
paginación.

Lo bueno fue que hicimos unas clases de alcance global que comprendían cualquier
objeto de dominio.

Por lo que nos queda sencillo crear el selector para clientes.

¿Qué necesitas hacer?

Primero, crea en el paquete customes/domain/criteria el selector CustomersSelector y


establece una relación de realización con ListSelector:

public class CustomersSelector implements ListSelector<Customer>{

private final Query mQuery;

public CustomersSelector(Query query) {


mQuery = query;
}

@Override
public List<Customer> selectListRows(List<Customer> items) {
return null;
}
}

(Los selectores se complementan de Query para determinar las características de


selección)

Ahora en el controlador selectListRows() recorre la lista de clientes entrante y


redúcela a la mínima expresión con los atributos de la consulta:

@Override
public List<Customer> selectListRows(List<Customer> items) {
// Clientes finales
List<Customer> resultingCustomers;

// Especificación inicial
final MemorySpecification<Customer> spec =
(MemorySpecification<Customer>) mQuery.getSpecification();

// Comparador inicial
Comparator<Customer> comp = mNameComparator;

// Filtrar
resultingCustomers = filterItems(items, spec);

// Ordenar
if (mQuery.getFieldSort() != null) {
switch (mQuery.getFieldSort()) {
case NAME_CUSTOMER_FIELD:
comp = mNameComparator;
break;
}
}
Collections.sort(resultingCustomers, comp);

// Paginar
resultingCustomers = CollectionsUtils.getPage(resultingCustomers,
mQuery.getPageNumber(), mQuery.getPageSize());

return resultingCustomers;
}

Donde el método filterItems() usa la especificación junto al método


Collections2.filter():

@NonNull
private ArrayList<Customer> filterItems(List<Customer> items, final
MemorySpecification<Customer> spec) {
Collection<Customer> filteredItems =
Collections2.filter(items, new Predicate<Customer>() {
@Override
public boolean apply(Customer customer) {
return spec == null || spec.isSatisfiedBy(customer);
}
});

return new ArrayList<>(filteredItems);


}

Y el comparador asignado a la constante NAME_CUSTOMER_FIELD ("name") compara los


campos del nombre:

// Comparador para el nombre del cliente


private Comparator<Customer> mNameComparator = new Comparator<Customer>()
{
@Override
public int compare(Customer o1, Customer o2) {
if (mQuery.getSortOrder() == Query.ASC_ORDER) {
return o1.getName().compareTo(o2.getName());
} else {
return o2.getName().compareTo(o1.getName());
}
}
};

Añadido a eso, es muy importante tener una especificación por defecto para
seleccionar todos los clientes por si no se indica ninguna selección desde la vista.
Crea dentro del paquete domain/criteria dicha especificación llamada
AllCustomersSpec:

public class AllCustomersSpec implements MemorySpecification<Customer> {


@Override
public boolean isSatisfiedBy(Customer item) {
return true;
}
}

Después de la programación anterior, vuelve al método getCustomers() de la caché y


ejecuta la selección:

@Override
public List<Customer> getCustomers(Query query) {
// Se obtienen clientes en forma de lista
List<Customer> customers =
Lists.newArrayList(mCachedCustomers.values());

// Selección de clientes
CustomersSelector selector = new CustomersSelector(query);
return selector.selectListRows(customers);
}

addCustomer()

Agregar un cliente es mucho más sencillo. Tan solo usa el método put() del mapa de
origen con el identificador del cliente entrante:

@Override
public void addCustomer(Customer customer) {
if (mCachedCustomers == null) {
mCachedCustomers = new LinkedHashMap<>();
}
mCachedCustomers.put(customer.getId(), customer);
}

deleteCustomers()

Tan fácil como dar clear() al mapa:

@Override
public void deleteCustomers() {
if (mCachedCustomers == null) {
mCachedCustomers = new LinkedHashMap<>();
}
mCachedCustomers.clear();
}
isCacheReady()

Sabremos si la fuente de datos está lista si el mapa no es nulo:

@Override
public boolean isCacheReady() {
return mCachedCustomers!=null;
}

Añadir Clientes De Prueba

Con el objetivo de tener información rápida para poblar nuestra UI, añadiremos 10
clientes de prueba para la farmacia.

En consecuencia, inicializa el mapa con los ítems en el constructor:

private CacheCustomersStore() {
mCachedCustomers = new LinkedHashMap<>();
createTestCustomer(new Customer("Carlos Villamaria", "444 6661",
"Cra 56", "Armenia", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Miriam Stevia", "444 6662",
"Cra 45", "Cali", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Pedro Cigueña", "444 6663",
"Cra 46", "Manizales", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Sara Vives", "444 6664",
"Cra 57", "Cali", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Jose Lopez", "444 6665",
"Cra 58", "Armenia", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Claudia Suarez", "444 6666",
"Cra 59", "Armenia", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Esteban Soto", "444 6667",
"Cra 60", "Cali", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Samanta Lleras", "444 6668",
"Cra 51", "Barranquilla", DateTimeUtils.getDateTime(),
null));
createTestCustomer(new Customer("Jean Estupiñan", "444 6669",
"Cra 52", "Pereira", DateTimeUtils.getDateTime(), null));
createTestCustomer(new Customer("Marcela Esturias", "444 6670",
"Cra 53", "Medellín", DateTimeUtils.getDateTime(), null));

/**
* Añade un nuevo cliente a la caché
*/
private void createTestCustomer(@NonNull Customer customer) {
Preconditions.checkNotNull(customer, "customer no puede ser null");
mCachedCustomers.put(customer.getId(), customer);
}

El método createTestCustomer() nos ayuda a simplificar la asignación a través de


put().

Mientras que en el constructor la fecha de creación es generada a través de una


nueva clase de utilidades llamada DateTimeUtils:

public class DateTimeUtils {

public static String getDateTime() {


DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",
Locale.getDefault());
return df.format(Calendar.getInstance().getTime());
}
}
Tarea #3. Crear Repositorio De Clientes
Ya hemos construido repositorios antes y sabemos que este es un patrón para
facilitar el acceso a los objetos de dominio.

En este caso necesitamos centralizar la administración sobre los clientes sin importar
desde que fuente de datos provengan.

Es por eso que debemos determinar los tipos de acceso y la lógica de


sincronización.

Mejor dicho:

¿Qué debe hacer nuestro repositorio?

De momento requerimos obtener la lista de clientes disponibles.

Por otro lado, el hecho de que tengamos implementado únicamente el almacén en


memoria pospone la definición de las reglas de sincronización.

En ese orden de ideas veamos la codificación.

Crear Interfaz ICustomersRepository

Añade al paquete customers/data la interfaz para el repositorio y añade tan solo un


método que simbolice la obtención de clientes basada en un objeto Query:

(No olvides agregar una callback para considerar el desconocimiento del tiempo que
tarda una lectura)

public interface ICustomersRepository {


interface GetCustomersCallback {
void onCustomersLoaded(List<Customer> customers);

void onDataNotAvailable(String errorMsg);


}

void getCustomers(@NonNull Query query, GetCustomersCallback


callback);
}

Crear Implementación CustomersRepository

Bien, en este punto sabemos que debemos generar una relación de realización con la
interfaz.

Además debemos agregar como campo el almacén de clientes en caché y los demás
almacenes que tengamos creados.

También cabe resaltar que esta clase seguirá el patrón singleton así que agrega las
características que ya hemos visto para ello.

En resumen, el código inicialmente debería verse así:

public class CustomersRepository implements ICustomersRepository {

private static CustomersRepository INSTANCE = null;

private CacheCustomersStore mCacheCustomersStore;

private CustomersRepository(@NonNull CacheCustomersStore


cacheCustomersStore) {
mCacheCustomersStore = checkNotNull(cacheCustomersStore);
}

public static CustomersRepository getInstance(CacheCustomersStore


cacheCustomersStore) {
if (INSTANCE == null) {
INSTANCE = new CustomersRepository(cacheCustomersStore);
}
return INSTANCE;
}

@Override
public void getCustomers(@NonNull Query query, GetCustomersCallback
callback) {

}
}
getCustomers()

El acceso de lectura a clientes es simplemente una propagación de obtención a la


fuente de datos en caché.

Lo que significa que llamarás a su método getProducts() respectivamente:

@Override
public void getCustomers(@NonNull Query query, GetCustomersCallback
callback) {

// TODO: Incluir refresco de datos ordenado por el usuario

// Retornar datos existentes en caché


if (mCacheCustomersStore.isCacheReady()) {

callback.onCustomersLoaded(mCacheCustomersStore.getCustomers(query));
}
}

Importante: Aún no consideramos el gesto de refresco manual por parte del


usuario, pero en el próximo tutorial debemos incluir su procesamiento en este
método.
Tarea #4. Crear Interactor Para Obtener Clientes
El interactor que obtiene los clientes es llamado desde el presentador con el fin de
conseguir una lista de elementos desde el repositorio.

En esencia este necesita un método de ejecución de dicha acción (“obtener clientes”)


y además propagar con una callback la tardanza de esta.

Crear interfaz IGetCustomers

Añade la interfaz al paquete domain/usecases y representa es ejecución con un


método execute():

public interface IGetCustomers {

interface ExecuteCallback {
void onSuccess(List<Customer> customers);

void onError(String error);

void execute(@NonNull Query query, boolean refresh, ExecuteCallback


callback);
}

Los parámetros del método reciben el objeto de consulta, una bandera para
determinar si hay que refrescar los datos y la callback para tratar la tarea de forma
indeterminada.

Alternativamente los interactores (casos de uso o también servicios de negocio)


pueden ser escritos desde una clase base que materializa la entrada y salida de la
operación como clases adicionales (Input y Output).

Si te interesa este tratamiento puedes consultar la clase UseCase del proyecto de


arquitectura CLEAN de Google.
Crear Implementación GetCustomers

Añade la clase al mismo paquete y genera la realización.

Si analizas el diagrama de clases, el repositorio de clientes será un campo que debes


añadir:

public class GetCustomers implements IGetCustomers {


private final ICustomersRepository mCustomersRepository;

public GetCustomers(ICustomersRepository customersRepository) {


mCustomersRepository = checkNotNull(customersRepository,
"customersRepository no puede ser null");
}

@Override
public void execute(@NonNull Query query, boolean refresh,
ExecuteCallback callback) {

}
}

execute()

Escribe como instrucción la llamada al repositorio para obtener los clientes.

Eres libre de escribir validaciones para los parámetros de entrada si los necesitas o
añadir acciones derivadas para los datos de salida.

@Override
public void execute(@NonNull Query query, boolean refresh, final
ExecuteCallback callback) {
checkNotNull(query, "query no puede ser null");
checkNotNull(callback, "callback no puede ser null");

mCustomersRepository.getCustomers(query, new
ICustomersRepository.GetCustomersCallback() {
@Override
public void onCustomersLoaded(List<Customer> customers) {
checkNotNull(customers, "customers no puede ser null");
callback.onSuccess(customers);
}

@Override
public void onDataNotAvailable(String errorMsg) {
callback.onError(errorMsg);
}
});
}

Es vital que comprendas la propagación que realizamos entre la callback del


repositorio y la del interactor para mantener la línea de tiempo en que se obtuvo una
respuesta.

Tarea #5. Crear Presentador De Clientes


El presentador de la lista de clientes debe responder a los eventos que el usuario
genera para consultar el repositorio.

Sin embargo el único evento a controlar es el inicio de la vista (onResume()), ya que


por el momento lo demás puntos de interacción no los desarrollaremos.

A continuación veremos la codificación.

Crear Interfaz CustomersListMvp

Este interfaz contiene la interfaz de la vista y el presentador para simplificar el uso


de esta asociación.

Así que agrégala dentro de customers/presentation:

public interface CustomersListMvp {


interface View {

interface Presenter {

}
}
Ahora, según el análisis de las interacciones de usuario, la única acción que podrá
desencadenar el usuario por el momento es la carga de la lista de clientes.

Es decir, un método void que reciba los parámetros de consulta (la orden de refresco,
filtros, búsqueda, ordenamientos, etc.):

interface Presenter {
void loadCustomers(boolean refresh);
}

Crear Implementación CustomersPresenter

Muy bien, ahora tan solo queda crear la implementación.

¿De que consta?

Si ves el diagrama de clases relucen dos campos: la vista relacionada y el caso de


uso a ejecutar.

Obviamente el constructor ha de recibir estas instancias.

Pero hay más:

Al igual que en ProductsPresenter, requeriremos tener:

 Una bandera para determinar si es primera carga (esto nos sirve para evitar
mostrar la barra de progreso por si ya tenemos datos en la lista)
 Un índice de la página actual para sostener el paginado
 Una constante para fijar el tamaño de la página de clientes

Además no olvides la realización de su interfaz representativa.

Con todo ello juntos tendrás:

public class CustomersPresenter implements CustomersListMvp.Presenter {

private static final int CUSTOMERS_PAGE_SIZE = 20;

private boolean mIsFirtLoad = true;


private int mCurrentPage = 1;

private CustomersListMvp.View mCustomersView;


private IGetCustomers mGetCustomers;

public CustomersPresenter(CustomersListMvp.View customersView,


IGetCustomers getCustomers) {
mCustomersView = checkNotNull(customersView,
"customersView no puede ser null");
mGetCustomers = checkNotNull(getCustomers,
"getCustomers no puede ser null");
}

@Override
public void loadCustomers(boolean refresh) {

}
}

loadCustomers()

El cuerpo de este código no solo se trata de la ejecución del caso de uso, sino del
cuidado de la experiencia de usuario.

Es decir, el cómo orquestar una secuencia de efectos en la vista para articular una
buena carga de clientes.

Con la lista de productos ya habíamos visto dicha receta:

1. Se muestra un indicador de carga (o de página siguiente)


2. Al terminar la obtención de clientes:
a. Éxito
i. Se ocultan los indicadores
ii. Se muestran los clientes en la lista
b. Error
i. Se ocultan los indicadores
ii. Se muestra mensaje de error

Al traducirlo a código tendríamos lo siguiente:

@Override
public void loadCustomers(boolean refresh) {
// Lógica para mostrar indicadores
if (refresh || mIsFirstLoad) {
// TODO: Mostrar indicador de carga
mCurrentPage = 1; // Reset del páginado
} else {
// TODO: Mostrar indicador de página siguiente
mCurrentPage++; // Preparar página siguiente
}

// Resumir consulta de clientes


Query query = new Query(
/* Filtro */ new AllCustomersSpec(),
/* Orden */ CustomersSelector.NAME_CUSTOMER_FIELD,
Query.ASC_ORDER,
/* Paginado */ mCurrentPage, CUSTOMERS_PAGE_SIZE);

// Ejecutar caso de uso "Obtener Clientes"


mGetCustomers.execute(query, refresh, new
IGetCustomers.ExecuteCallback() {
@Override
public void onSuccess(List<Customer> customers) {
// TODO: Remover indicador de carga
// TODO: Mostrar lista de clientes
mIsFirstLoad = false; // Off de primera carga
}

@Override
public void onError(String error) {
// TODO: Ocultar indicador de carga
// TODO: Ocultar indicador de página siguiente
// TODO: Mostrar error
}
});
}

Tarea #6. Crear Vista De Lista De Clientes


Lo siguiente es crear la vista que proyectará los elementos definidos en el boceto
establecido junto a los efectos visuales necesarios.
Definir Interfaz CustomersListMvp.View

Abre la interfaz del MVP que creamos anteriormente y piensa en los métodos que
tendrá.

En las características pasadas te había dicho que la vista contiene métodos que
alteran a sus views, otros que inician elementos de interfaz o efectos visuales y
aquellos que inherentemente se asocian con dependencias hacia el presentador.

Básicamente podemos copiar la mayoría de métodos que tenemos en


ProductsMvp.View:

public interface CustomersListMvp {


interface View {
void showCustomers(List<Customer> customers);

void showLoadingState(boolean show);

void showEmptyState();

void showCustomersError(String msg);

void showCustomersPage(List<Customer> customers);

void showLoadMoreIndicator(boolean show);

void showEndlessScroll(boolean show);

void setPresenter(CustomersListMvp.Presenter presenter);

interface Presenter {
void loadCustomers(boolean refresh);
}
}

En el boceto escribimos que existe un FAB para crear un nuevo cliente. La apertura
de la actividad para esa acción puede ser descrita como otro método de la vista, pero
no la pondré debido al alcance de este tutorial.
Crear Implementación CustomersFragment

Agrega un nuevo fragmento al paquete de presentación y relaciónale un layout


llamado fragment_customers.xml.

public class CustomersFragment extends Fragment {

public CustomersFragment() {

public static CustomersFragment newInstance() {


CustomersFragment fragment = new CustomersFragment();
return fragment;
}

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

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_customers, container,
false);
}

Luego definamos que elementos visuales contendrá el fragmento.

Si nos devolvemos al boceto, veremos un contenido similar a la lista de productos.

Tendremos un RecyclerView para los clientes junto a un FAB flotante en la parte


inferior derecha.

Además del layout para los ítems de la lista que proyecta el nombre y su número
telefónico.

En ese sentido, primero crea el layout de los ítems basándote en dos textos para la
info del cliente:
<?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="?android:attr/listPreferredItemHeight"
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:paddingBottom="@dimen/list_item_padding"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/list_item_padding">

<TextView
android:id="@+id/customer_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1_minus_8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
tools:text="Carlos Angulo" />

<TextView
android:id="@+id/customer_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="444 6544" />
</LinearLayout>

El resultado de la previsualización tendría que ser el siguiente:

Seguido añade modifica el layout del fragmento para que contenga las siguientes
características:
 La posibilidad de realizar Swipe to refresh
 Una lista
 Un view para mensajes de vacío

Acoplando el resultado sería el siguiente:

<android.support.v4.widget.SwipeRefreshLayout
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/swipe_to_refresh_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

<RelativeLayout
android:id="@+id/customers_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/customers_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F0F0F0"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/customer_item" />

<LinearLayout
android:id="@+id/no_customers_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/keyline_1_minus_8dp"
android:gravity="center"
android:text="@string/no_customers_message"
android:textSize="20sp" />
</LinearLayout>
</RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>

Con la previa tendríamos:


En cuanto tenemos la interfaz lista, es hora de completar sus interacciones en la
clase del fragmento.

El primer paso sería poner todos los campos.

Estos son: views a manipular y dependencias del diagrama de clases (presentador).

public class CustomersFragment extends Fragment {

private SwipeRefreshLayout mSwipeToRefreshView;


private RecyclerView mCustomersList;
// TODO: Referencia de adaptador de clientes
private View mNoCustomersView;

private CustomersListMvp.Presenter mCustomersPresenter;

...
}
Como lo expresa el comentario TODO, aún nos falta el adaptador de la lista, así que
efectuemos su creación.

¿Qué debes tener en cuenta?

Inspírate en el adaptador ProductsAdapter para tomar ventaja del código ya creado


para tener endless scroll en la lista.

Esto es:

 Crear dos view holders: uno para el ítem normal y otro para el indicador de
carga
 Usar la interfaz DataLoading sobre el adaptador para comunicar al fragmento
si se están cargando datos y si es permitido agregar otra página
 Agrega los método auxiliares para agregar ítems y llamar a los métodos
notify*()

A partir de este razonamiento y el código existente el resultado sería este:

CustomersAdapter.java

public class CustomersAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>


implements DataLoading {

private final static int TYPE_ITEM = 1;


private final static int TYPE_NEXT_PAGE_INDICATOR = 2;

private List<Customer> mItems;

private boolean mIsLoading = false;


private boolean mEndless = false;

public interface CustomerItemListener {


void onCustomerClick(Customer clickedCustomer);

private CustomerItemListener mItemListener;

public CustomersAdapter(List<Customer> customers, CustomerItemListener itemListener) {


setList(customers);
mItemListener = itemListener;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view; // View a retornar

if (viewType == TYPE_NEXT_PAGE_INDICATOR) {
view = inflater.inflate(R.layout.item_loading_footer, parent, false);
return new CustomersAdapter.NextPageIndicatorHolder(view);
}

view = inflater.inflate(R.layout.customer_item, parent, false);


return new CustomersAdapter.CustomersHolder(view, mItemListener);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case TYPE_ITEM:
bindCustomersHolder((CustomersHolder) holder, position);
break;
case TYPE_NEXT_PAGE_INDICATOR:
bindNextPageIndicatorHolder((NextPageIndicatorHolder) holder, position);
break;
}
}

@Override
public int getItemViewType(int position) {
int itemsSize = mItems.size();

// Lógica para determinar cuándo usar los tipos de views


if (position < itemsSize && itemsSize > 0) {
return TYPE_ITEM;
}
return TYPE_NEXT_PAGE_INDICATOR;
}

@Override
public int getItemCount() {
return mItems.size() + (mIsLoading ? 1 : 0);
}

private Customer getItem(int position) {


return mItems.get(position);
}

private void setList(List<Customer> customers) {


mItems = Preconditions.checkNotNull(customers);
}

private void bindCustomersHolder(CustomersHolder holder, int position) {


Customer customer = mItems.get(position);
holder.name.setText(customer.getName());
holder.phone.setText(customer.getPhone());
}

private void bindNextPageIndicatorHolder(NextPageIndicatorHolder holder, int position) {


boolean showNextPageIndicator = position > 0 && mIsLoading && mEndless;
holder.progress.setVisibility(showNextPageIndicator ? View.VISIBLE :
View.INVISIBLE);
}

public void replaceItems(List<Customer> customers) {


setList(customers);
notifyDataSetChanged();
}

public void addItems(List<Customer> customers) {


mItems.addAll(customers);
}

private int getNPIPosition() {


return mIsLoading ? getItemCount() - 1 : RecyclerView.NO_POSITION;
}

public void dataStartedLoading() {


if (mIsLoading) {
return;
}

mIsLoading = true; // Carga de datos On

// Notificar inserción del indicador de nueva página


new Handler().post(new Runnable() {
@Override
public void run() {
notifyItemInserted(getNPIPosition()); // NPI (Next Page Indicator)
}
});

public void dataFinishedLoading() {


if (!mIsLoading) {
return;
}

mIsLoading = false; // Carga de datos Off

// Notificar eliminación del indicador de nueva página


new Handler().post(new Runnable() {
@Override
public void run() {
notifyItemRemoved(getNPIPosition());
}
});
}

public void setEndless(boolean endless) {


mEndless = endless;
}

@Override
public boolean isLoadingData() {
return mIsLoading;
}

@Override
public boolean isThereMoreData() {
return mEndless;
}

private class CustomersHolder extends RecyclerView.ViewHolder implements


View.OnClickListener {

public TextView name;


public TextView phone;

private CustomerItemListener mItemListener;

public CustomersHolder(View itemView, CustomerItemListener itemListener) {


super(itemView);
name = (TextView) itemView.findViewById(R.id.customer_name);
phone = (TextView) itemView.findViewById(R.id.customer_phone);
mItemListener = itemListener;

itemView.setOnClickListener(this);
}

@Override
public void onClick(View view) {
int position = getAdapterPosition();
Customer customer = getItem(position);
mItemListener.onCustomerClick(customer);
}
}

private class NextPageIndicatorHolder extends RecyclerView.ViewHolder {


public ProgressBar progress;

public NextPageIndicatorHolder(View view) {


super(view);
progress = (ProgressBar) view.findViewById(R.id.progressBar);
}
}

Ok, con esto ya puedes agregar el campo al fragmento:

public class CustomersFragment extends Fragment {

private CustomersAdapter mCustomersAdapter;

Con todos los componentes asociados al fragmento es posible sobrescribir sus


comportamientos tanto del ciclo de vida como los que le dota la
CustomersListMvp.View.

Así que manos a la obra:

onCreate()

Inicias el adaptador con 0 elementos y habilitas la contribución a la Action Bar:

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

mCustomersAdapter = new CustomersAdapter(new ArrayList<Customer>(0));


setHasOptionsMenu(true);
}

onCreateView()

Obtén todas las referencias de la interfaz de usuario:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_customers,
container, false);

mSwipeToRefreshView = (SwipeRefreshLayout)
rootView.findViewById(R.id.swipe_to_refresh_view);
mCustomersList = (RecyclerView)
rootView.findViewById(R.id.customers_list);
mNoCustomersView = rootView.findViewById(R.id.no_customers_view);

setUpCustomersList();
setUpRefreshView();

return rootView;
}

private void setUpCustomersList() {


mCustomersList.setAdapter(mCustomersAdapter);

final LinearLayoutManager layoutManager =


(LinearLayoutManager) mCustomersList.getLayoutManager();

// Se agrega escucha de scroll infinito


mCustomersList.addOnScrollListener(
new InfinityScrollListener(mCustomersAdapter, layoutManager)
{
@Override
public void onLoadMore() {
mCustomersPresenter.loadCustomers(false);
}
});
}

private void setUpRefreshView() {


mSwipeToRefreshView.setColorSchemeColors(
ContextCompat.getColor(getActivity(), R.color.colorPrimary),
ContextCompat.getColor(getActivity(), R.color.colorAccent),
ContextCompat.getColor(getActivity(),
R.color.colorPrimaryDark));

mSwipeToRefreshView.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
mCustomersPresenter.loadCustomers(true);
}
});
}

Añadí setUpCustomersList() y setUpRefreshView() para simplificar la preparación de


la lista y el view de refresco.

onResume()

Este método se puede relacionar con la orden directa del usuario de ver la
información.

Por lo que usas el presentador para satisfacer la necesidad con su método de carga
de clientes:

@Override
public void onResume() {
super.onResume();
mCustomersPresenter.loadCustomers(false);
}

showCustomers()

Fácil: usa el método de reemplazo de datos del adaptador y muestra el view de la


lista (se añade el método showList() para esto).

@Override
public void showCustomers(List<Customer> customers) {
mCustomersAdapter.replaceItems(customers);
showList(true);
}

private void showList(boolean show) {


mCustomersList.setVisibility(show ? View.GONE : View.VISIBLE);
mNoCustomersView.setVisibility(show ? View.VISIBLE : View.GONE);
}
showLoadingState()

Activa la animación de carga del refresh layout:

@Override
public void showLoadingState(final boolean show) {
if (getView() == null) {
return;
}

mSwipeToRefreshView.post(new Runnable() {
@Override
public void run() {
mSwipeToRefreshView.setRefreshing(show);
}
});
}

showEmptyState()

Muestra el view de ausencia de clientes y oculta la lista:

@Override
public void showEmptyState() {
showList(false);
}

showCustomersError()

Crea un Toast para avisar al usuario los posibles errores a la hora de cargar los
clientes:

@Override
public void showCustomersError(String msg) {
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG)
.show();
}
showCustomersPage()

Añade la lista entrante (página) como datos adicionales al adaptador:

@Override
public void showCustomersPage(List<Customer> customers) {
mCustomersAdapter.addItems(customers);
showList(true);
}

showLoadMoreIndicator()

Inicia la inserción del indicador de carga para una nueva página si el parámetro de
entrada es true, de lo contrario dile al adaptador que finalice este proceso.

@Override
public void showLoadMoreIndicator(boolean show) {
if(show){
mCustomersAdapter.dataStartedLoading();
}else {
mCustomersAdapter.dataFinishedLoading();
}
}

showEnlessScroll()

Habilitamos el scrolling infinite:

@Override
public void showEndlessScroll(boolean show) {
mCustomersAdapter.setEndless(show);
}
setPresenter()

Si haces memoria, este método lo hemos usado siempre en los presentadores para
introducir la dependencia del presentador en el fragmento, ya que desde el
constructor no es recomendable hacerlo debido a la naturaleza dinámica del mismo.

Tan solo añade asigna el valor entrante a la instancia que tenemos como campo:

@Override
public void setPresenter(CustomersListMvp.Presenter presenter) {
mCustomersPresenter = checkNotNull(presenter, "presenter no puede ser
null");
}

Resolver TODOs Del Presentador

Una vez resuelto el fragmento, vuelve a CustomersPresenter y reemplaza los


comentarios TODO por la llamada a los métodos correspondientes de la vista:

@Override
public void loadCustomers(final boolean manualRefresh) {

// Lógica para mostrar indicadores


if (manualRefresh || mIsFirstLoad) {
mView.showLoadingState(true);
mCurrentPage = 1; // Reset del páginado
} else {
mView.showLoadMoreIndicator(true);
mCurrentPage++; // Preparar página siguiente
}

// Resumir consulta de clientes


Query query = new Query(
/* Filtro */ new AllCustomersSpec(),
/* Orden */ CustomersSelector.NAME_CUSTOMER_FIELD, Query.ASC_ORDER,
/* Paginado */ mCurrentPage, CUSTOMERS_PAGE_SIZE);

// Ejecutar caso de uso "Obtener Clientes"


mGetCustomers.execute(query, manualRefresh || mIsFirstLoad, new
IGetCustomers.ExecuteCallback() {
@Override
public void onSuccess(List<Customer> customers) {
mView.showLoadingState(false);

// Lógica si no hay resultados


if (customers.isEmpty()) {
if (manualRefresh || mIsFirstLoad) {
mView.showEmptyState();
} else {
mView.showLoadMoreIndicator(false);
}
mView.showEndlessScroll(false);
} else {
if (manualRefresh || mIsFirstLoad) {
mView.showCustomers(customers);
} else {
mView.showLoadMoreIndicator(false);
mView.showCustomersPage(customers);
}
mView.showEndlessScroll(true);
}

mIsFirstLoad = false; // Off


}

@Override
public void onError(String error) {
mView.showLoadingState(false);
mView.showLoadMoreIndicator(false);
mView.showCustomersError(error);
}
});
}

Tarea #7. Crear Actividad De Clientes


¡Excelente!

Ya tenemos todos los componentes de la arquitectura creados y listos para ser


ensamblados.

Por esta razón crearemos la actividad que será el punto de entrada del flujo principal
para que estos componentes cobren vida.

Finalmente instancia todos componentes de la capa de presentación para crear la


interfaz:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_customers);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

FloatingActionButton fab = (FloatingActionButton)


findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// TODO: Abrir actividad de creación de clientes
}
});

// Obtener fragmento por si ya está instalado


CustomersFragment fragment = (CustomersFragment)

getSupportFragmentManager().findFragmentById(R.id.customers_container);

// Añadir fragmento si no existe aún


if (fragment == null) {
fragment = CustomersFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.customers_container, fragment)
.commit();
}

// Instancear presentador
CustomersPresenter presenter = new CustomersPresenter(
fragment,
DependencyProvider.provideGetCustomers());

// Satisfacer dependencia del presentador en el fragmento (vista)


fragment.setPresenter(presenter);
}

Como ves, el caso de uso “obtener clientes” es creado desde el contenedor de


dependencias (DependecyProvider) con el siguiente método:

public static GetCustomers provideGetCustomers(){


return new GetCustomers(provideCustomersRepository());
}

private static CustomersRepository provideCustomersRepository() {


return
CustomersRepository.getInstance(CacheCustomersStore.getInstance());
}

Para concluir esta sección, ejecuta el proyecto y testea la apertura de la actividad de


clientes.

Verifica que los clientes se carguen correctamente y si lo deseas añade más objetos
de dominio para visualizar el endless scroll (incluye un delay en la carga de datos
para que sea visible)
Paso #5. Crear Lista De Facturas
En este paso tendremos demasiadas similitudes con la lista de clientes en cuestione
de presentación.

Por lo que obviaré algunos elementos explicativos sobre esta capa.

Diseñar Arquitectura
En el dominio tendremos la entidad Factura.

Ya que son el resultado final propuesto para el caso de uso.

¿Y qué pasa con los ítems que se agregan a una factura, como se representan?

Los ítems/líneas de factura harán parte de la misma.

Es decir que compondrán a factura y si deseamos acceder a ellos será a través de


esta.

El siguiente diagrama simple representa la relación:

Interfaz De Usuario

A la hora de mostrar una factura en la lista no requeriremos leer todos sus atributos.

Recordemos que hay datos del cliente en cada ítem de la lista.


Por esta razón y por practicidad agregaremos las entidades InvoiceUi y CustomerUi
con el fin de resumir la entidad a su mínima expresión para ser desplegada.

Esta producirá objetos de solo lectura, por lo que sus métodos mutadores deberían
ser privados cuando la codifiquemos:
Tarea #1. Crear Entidades
Ya tenemos una idea de los atributos que usaremos para cada entidad debido al
modelo de datos construido.

En consecuencia vayamos al paquete invoices/domain/entities y agreguemos las


clases:

CustomerUi

Esta entidad contiene solo los datos del cliente que necesitamos ver en la factura.

Debido a que es un objeto de solo lectura, sus métodos set*() serán privados para
evitar cualquier variación en el dominio.

public class CustomerUi {


private String mName;
private String mPhone;
private String mAddress;
private String mCity;

public CustomerUi(String name,


String phone,
String address,
String city) {
mName = name;
mPhone = phone;
mAddress = address;
mCity = city;
}

private void setName(String name) {


this.mName = name;
}

private void setPhone(String phone) {


this.mPhone = phone;
}

private void setAddress(String address) {


this.mAddress = address;
}

private void setCity(String city) {


this.mCity = city;
}

public String getName() {


return mName;
}

public String getPhone() {


return mPhone;
}

public String getAddress() {


return mAddress;
}

public String getCity() {


return mCity;
}
}

InvoiceUi

Agregamos los datos a visualizar en la lista y le añadimos una referencia a una


entidad CustomerUi.

public class InvoiceUi {


private String mId;
private String mNumber;
private Date mDate;
private float mTotalAmount;
private String mState;
private int mTotalItems;
private CustomerUi mCustomerUi;

public InvoiceUi(String id,


String number,
CustomerUi customerUi,
Date date,
float totalAmount,
String state,
int totalItems) {
mId = id;
mNumber = number;
mCustomerUi = customerUi;
mDate = date;
mTotalAmount = totalAmount;
mState = state;
mTotalItems = totalItems;
}

private void setId(String id) {


mId = id;
}

private void setNumber(String number) {


mNumber = number;
}

private void setDate(Date date) {


mDate = date;
}

private void setTotalAmount(float totalAmount) {


mTotalAmount = totalAmount;
}

private void setState(String state) {


mState = state;
}

public String getId() {


return mId;
}

public String getNumber() {


return mNumber;
}

public Date getDate() {


return mDate;
}

public float getTotalAmount() {


return mTotalAmount;
}

public String getState() {


return mState;
}

public String getCustomerName() {


return mCustomerUi.getName();
}

public int getTotalItems() {


return mTotalItems;
}
}
Invoice

La entidad para facturas usará internamente una lista de ítems de factura y además
tendrá una referencia a la info parcial del cliente asociado:

public class Invoice {


public static final String STATE_VALUE_DRAFT = "Borrador";
public static final String STATE_VALUE_PAID = "Pagada";
public static final String STATE_VALUE_CANCELED = "Cancelada";
public static final String STATE_VALUE_SENT = "Enviada";

private String mId;


private String mCustomerId;
private String mNumber;
private Date mDate;
private float mTotalAmount;
private String mState;
private List<InvoiceItem> mInvoiceItems;

public Invoice(String id,


String customerId,
String number,
Date date,
float totalAmount,
String state,
List<InvoiceItem> invoiceItems) {
mId = id;
mCustomerId = customerId;
mNumber = number;
mInvoiceItems = invoiceItems;
mDate = date;
mTotalAmount = totalAmount;
mState = state;
}

public Invoice(String customerId,


Date date,
List<InvoiceItem> items,
float totalAmount) {
this(
UUID.randomUUID().toString(),
customerId,
"INV-" + UUID.randomUUID().toString(),
date,
totalAmount,
STATE_VALUE_DRAFT,
items
);
}
public Invoice(String id,
String customerId,
String number,
Date date,
String state) {

this(
id,
customerId,
number,
date, 0, state,
new ArrayList<InvoiceItem>(0)
);
}

public String getId() {


return mId;
}

public void setId(String id) {


this.mId = id;
}

public String getNumber() {


return mNumber;
}

public void setNumber(String number) {


this.mNumber = number;
}

public Date getDate() {


return mDate;
}

public void setDate(Date date) {


this.mDate = date;
}

public float getTotalAmount() {


return mTotalAmount;
}

public void setTotalAmount(float totalAmount) {


this.mTotalAmount = totalAmount;
}

public String getState() {


return mState;
}

public void setState(String state) {


this.mState = state;
}

public int numberOfItems() {


return mInvoiceItems.size();
}

public String getCustomerId() {


return mCustomerId;
}

public void addInvoiceItem(InvoiceItem item) {


mTotalAmount += item.getTotal();
mInvoiceItems.add(item);
}

public void removeInvoiceItem(InvoiceItem item) {


mTotalAmount -= item.getTotal();
mInvoiceItems.remove(item);
}

public boolean emptyCustomer() {


return Strings.isNullOrEmpty(mCustomerId);
}

public boolean noItems() {


return mInvoiceItems.isEmpty();
}
}

El primer constructor sería general para la lectura de objetos ya creados


exitosamente.

En el segundo sería para la escritura local. Donde podemos ver la autogeneración


del identificador y número de la factura con el fin de tener referencias locales
mientras no tengamos conexión al servidor. Adicionalmente inicializamos el estado
con el concepto de “Borrador”.

Y el tercero es una variación del segundo, solo que no tendremos ítems de factura
inicialmente.
Tarea #2. Crear Caché
Creemos los componentes del almacén en caché para sostener los datos más
recientes mientras la app esté abierta.

2.1 Crear Interfaz

Crea una nueva interfaz llamada ICacheInvoicesStore en data/cache y pon como


métodos las que piensas usar en tu app.

public interface ICacheInvoicesStore {

interface LoadInvoicesUiCallback{
void onInvoicesUiLoaded(List<InvoiceUi> invoiceUis);
void onDataNotAvailable();
}

List<Invoice> getInvoices(Query query);

void getInvoicesUis(Query query, LoadInvoicesUiCallback callback);

void addInvoice(Invoice invoice);

void deleteInvoices();

boolean isCacheReady();
}

Es interesante resaltar que habrá un método get para obtener las facturas completas
con sus detalles y otro para la versión simplificada.

2.2 Crear Implementación

Crea la clase CacheInvoicesStore.

No olvides:

 Implementar ICacheInvocesStore
 Usar el patrón singleton
 Usar un mapa para materializar el origen de datos
 Recibir el repositorio de clientes como dependencia

public class CacheInvoicesStore implements ICacheInvoicesStore {

private static CacheInvoicesStore INSTANCE = null;

HashMap<String, Invoice> mCachedInvoices = null;

private ICustomersRepository mCustomersRepo;

private CacheInvoicesStore(ICustomersRepository customersRepository) {


mCustomersRepo = Preconditions.checkNotNull(customersRepository,
"customersRepository no puede ser null");

mCachedInvoices = new LinkedHashMap<>();

public static CacheInvoicesStore getInstance(ICustomersRepository


customersRepository) {
if (INSTANCE == null) {
INSTANCE = new CacheInvoicesStore(customersRepository);
}

return INSTANCE;
}

@Override
public List<Invoice> getInvoices(Query query) {
return null;
}

@Override
public void getInvoicesUis(final Query query, final LoadInvoicesUiCallback
callback) {

@Override
public void addInvoice(Invoice invoice) {
}

@Override
public void deleteInvoices() {
}

@Override
public boolean isCacheReady() {
return mCachedInvoices != null;
}
}

2.3 Obtener Facturas

En este método obtendremos las facturas basándonos en el objeto Query entrante.

Lo primero sería crear el selector InvoicesSelector dentro de


invoices/domain/criteria:

public class InvoicesSelector implements ListSelector<Invoice> {

public static final String DATE_INVOICE_FIELD = "date";

private Query mQuery;

public InvoicesSelector(Query query) {


mQuery = query;
}

@Override
public List<Invoice> selectListRows(List<Invoice> items) {
// Facturas finales
List<Invoice> resultingInvoices;

// Especificación inicial
final MemorySpecification<Invoice> spec =
(MemorySpecification<Invoice>) mQuery.getSpecification();

// Comparador inicial
Comparator<Invoice> comp = mDateComparator;

// Filtrar
resultingInvoices = filterItems(items, spec);

// Ordenar
if (mQuery.getFieldSort() != null) {
switch (mQuery.getFieldSort()) {
case DATE_INVOICE_FIELD:
comp = mDateComparator;
break;
}
}
Collections.sort(resultingInvoices, comp);

// Paginar
resultingInvoices = CollectionsUtils.getPage(resultingInvoices,
mQuery.getPageNumber(), mQuery.getPageSize());
return resultingInvoices;
}

@NonNull
private ArrayList<Invoice> filterItems(List<Invoice> items,
final
MemorySpecification<Invoice> spec) {
Collection<Invoice> filteredItems =
Collections2.filter(items, new Predicate<Invoice>() {
@Override
public boolean apply(Invoice invoice) {
return spec == null ||
spec.isSatisfiedBy(invoice);
}
});

return new ArrayList<>(filteredItems);


}

// Comparador para fecha


private Comparator<Invoice> mDateComparator = new
Comparator<Invoice>() {
@Override
public int compare(Invoice o1, Invoice o2) {
if (mQuery.getSortOrder() == Query.ASC_ORDER) {
return o1.getDate().compareTo(o2.getDate());
} else {
return o2.getDate().compareTo(o1.getDate());
}
}
};
}

(Crea el selector para InvoiceUi de forma similar)

Ahora completa el método getInvoices() con la comprobación de la query entrante.

Luego obtén todos los valores con values() del mapa y luego aplícales la consulta
con el selector:

@Override
public List<Invoice> getInvoices(Query query) {
checkNotNull(query, "query no puede ser null");

// Se obtienen facturas en forma de lista


List<Invoice> invoices =
Lists.newArrayList(mCachedInvoices.values());

// Selección de facturas
InvoicesSelector selector = new InvoicesSelector(query);
return selector.selectListRows(invoices);
}

2.3 Obtener Facturas Para UI


En este método vamos a reunir todos los los IDs de los clientes que se tienen en las
facturas y luego realizamos una consulta para obtenerlos.

@Override
public void getInvoicesUis(final Query query, final LoadInvoicesUiCallback
callback) {

List<String> customersIds = new ArrayList<>(0);

for (Invoice invoice : mCachedInvoices.values()) {


customersIds.add(invoice.getCustomerId());
}

// Clientes por conjunto de IDs


Query customerQuery = new Query(new CustomersByIds(customersIds));

mCustomersRepo.getCustomers(
customerQuery,
new ICustomersRepository.GetCustomersCallback() {
@Override
public void onCustomersLoaded(List<Customer> customers) {
List<InvoiceUi> invoiceUis = new ArrayList<>();

for (Customer customer : customers) {


Invoice invoice =
findInvoiceByCustomerId(customer.getId());
InvoiceUi invoiceInfo = joinInvoiceCustomer(customer,
invoice);

invoiceUis.add(invoiceInfo);
}

InvoicesInfoSelector selector = new


InvoicesInfoSelector(query);

callback.onInvoicesUiLoaded(selector.selectListRows(invoiceUis));
}

@Override
public void onDataNotAvailable(String errorMsg) {
callback.onDataNotAvailable();
}
});
}
Como ves, convertimos (joinInvoiceCustomer()) la intersección de ambas entidades
en un nuevo objeto InvoiceUi para añadirlo a un array de salida que será pasado en la
callback.

@NonNull
private InvoiceUi joinInvoiceCustomer(Customer customer, Invoice invoice)
{
CustomerUi customerUi = new CustomerUi(
customer.getName(),
customer.getPhone(),
customer.getAddress(),
customer.getCity());

return new InvoiceUi(


invoice.getId(),
invoice.getNumber(),
customerUi,
invoice.getDate(),
invoice.getTotalAmount(),
invoice.getState(),
invoice.numberOfItems());
}

Al momento de consultar necesitaremos una especificación general. Así que


crearemos a InvoicesUiNoFilter:

public class InvoicesUiNoFilter implements MemorySpecification<InvoiceUi>


{
@Override
public boolean isSatisfiedBy(InvoiceUi item) {
return true;
}
}

Esta vez es necesario crear un método para mapear de una entidad Invoice a una
InvoiceInfo. Lo que nos permitió facilitar la traducción en la lectura.

2.4 Añadir Facturas

Usa el método put() del mapa de origen con el identificador del cliente entrante:
@Override
public void addInvoices(Invoice invoice) {
if(mCachedInvoices ==null){
mCachedInvoices= new LinkedHashMap<>();
}

mCachedInvoices.put(invoice.getId(), invoice);
}

2.5 Borrar Facturas

Usa clear() en el mapa:

@Override
public void deleteInvoices() {
if(mCachedInvoices ==null){
mCachedInvoices= new LinkedHashMap<>();
}

mCachedInvoices.clear();
}

2.6 Verificar Disponibilidad De La Cache

Sabremos si la fuente de datos está lista si el mapa no es nulo:

@Override
public boolean isCacheReady() {
return mCachedInvoices != null;
}

2.7 Añadir Facturas De Prueba


Pondremos 10 facturas con N ítems en su interior dentro del constructor. De tal
forma que nos sirva para probar la interfaz.
private CacheInvoicesStore(ICustomersRepository customersRepository) {
mCustomersRepo = Preconditions.checkNotNull(customersRepository,
"customersRepository no puede ser null");

mCachedInvoices = new LinkedHashMap<>();

String[] customerIds = {
"100001", "100002", "100003",
"100004", "100005", "100006",
"100007", "100008", "100009",
"100010"};
String[] invoiceStates = {
Invoice.STATE_VALUE_CANCELED,
Invoice.STATE_VALUE_DRAFT,
Invoice.STATE_VALUE_PAID,
Invoice.STATE_VALUE_SENT};
String[] productIds = {
"0002-4464", "0003-1968", "0006-0106", "0006-0705", "0006-
4999",
"0007-4140", "0009-0370", "0009-3169", "0013-0102", "0013-
2586"};

Random random = new Random();

// Se generan 10 facturas de prueba


for (int i = 0; i < 10; i++) {

String invoiceId = UUID.randomUUID().toString();


Invoice invoice = new Invoice(
invoiceId,
customerIds[random.nextInt(10)],
"INV-" + (i + 1),
DateTimeUtils.getTime(),
invoiceStates[random.nextInt(4)]);

// Items
for (int j = 0; j < random.nextInt(20) + 1; j++) {
InvoiceItem invoiceItem = new InvoiceItem(
productIds[random.nextInt(10)],
random.nextInt(5) + 1,
random.nextFloat() * 10
);

invoice.addInvoiceItem(invoiceItem);
}

mCachedInvoices.put(invoice.getId(), invoice);
}

}
Tarea #3. Crear Repositorio
Para este repositorio tenemos prevista la lectura de las facturas completas y en forma
parcial.

Además necesitamos una operación para añadirlas al momento que vayamos a crear
la screen de creación.

Con ello en mente, creemos los componentes.

Crear Interfaz IInvoicesRepository

Añade al paquete invoices/data la interfaz IInvoicesRepository y genera dos


métodos para las lecturas y uno de inserción:

public interface IInvoicesRepository {


interface GetInvoicesCallback {
void onInvoicesLoaded(List<Invoice> invoices);

void onDataNotAvailable(String errorMsg);


}

interface GetInvoicesUiCallback {
void onInvoicesInfoLoaded(List<InvoiceUi> invoicesInfos);

void onDataNotAvailable(String errorMsg);


}

void getInvoices(@NonNull Query query, @NonNull GetInvoicesCallback


callback);

void getInvoicesUis(@NonNull Query query, @NonNull GetInvoicesUiCallback


callback);

void saveInvoice(@NonNull Invoice invoice);


}

Crear Implementación InvoicesRepository

Genera la clase concreta de la interfaz anterior y ajústala con el patrón singleton:


public class InvoicesRepository implements IInvoicesRepository {

private static InvoicesRepository INSTANCE = null;

private ICacheInvoicesStore mCacheInvoicesStore;

private InvoicesRepository(@NonNull ICacheInvoicesStore


cacheInvoicesStore) {
mCacheInvoicesStore = checkNotNull(cacheInvoicesStore);
}

public static InvoicesRepository getInstance(ICacheInvoicesStore


cacheInvoicesStore) {
if (INSTANCE == null) {
INSTANCE = new InvoicesRepository(cacheInvoicesStore);
}

return INSTANCE;
}

@Override
public void getInvoices(@NonNull Query query, @NonNull
GetInvoicesCallback callback) {

@Override
public void getInvoicesInfos(@NonNull Query query, @NonNull
GetInvoicesInfoCallback callback) {

@Override
public void saveInvoice(@NonNull Invoice invoice) {

}
}

Obtener Facturas

Obtén las facturas desde la caché:

@Override
public void getInvoices(@NonNull Query query, @NonNull
GetInvoicesCallback callback) {
if (mCacheInvoicesStore.isCacheReady()) {

callback.onInvoicesLoaded(mCacheInvoicesStore.getInvoices(query));
}
}

Obtener Facturas Para UI

Obtén las facturas parciales desde la caché:

@Override
public void getInvoicesUis(@NonNull Query query, @NonNull final
GetInvoicesUiCallback callback) {
if (mCacheInvoicesStore.isCacheReady()) {
mCacheInvoicesStore.getInvoicesUis(query,
new ICacheInvoicesStore.LoadInvoicesUiCallback() {
@Override
public void onInvoicesUiLoaded(List<InvoiceUi>
invoiceUis) {
callback.onInvoicesInfoLoaded(invoiceUis);
}

@Override
public void onDataNotAvailable() {
callback.onDataNotAvailable("");
}
});
}
}

Guardar Facturas

Guarda la factura entrante en la caché:

@Override
public void saveInvoice(@NonNull Invoice invoice) {
Preconditions.checkNotNull(invoice);

mCacheInvoicesStore.addInvoice(invoice);
}
Tarea #4. Crear Interactor
Crearemos un interactor para cargar las facturas parciales desde el repositorio.

Esto nos ayudará a generar la lista en la vista para el vendedor de la farmacia.

Crear interfaz

Añade la interfaz IGetInvoicesForUi al paquete domain/usecases con el método


execute() para expresar la lectura de objetos InvoiceUi:

public interface IGetInvoicesForUi {


interface ExecuteCallback {
void onSuccess(List<InvoiceUi> invoicesForUi);

void onError(String error);

void execute(@NonNull Query query, boolean refresh,


IGetInvoicesForUi.ExecuteCallback callback);
}

Crear Implementación

Crea la clase GetInvoicesForUi e implementar la interfaz anterior y asociar el


repositorio como campo para la consulta de objetos de dominio.

public class GetInvoicesForUi implements IGetInvoicesForUi {

private IInvoicesRepository mInvoicesRepository;

public GetInvoicesForUi(@NonNull IInvoicesRepository invoicesRepository) {


mInvoicesRepository = checkNotNull(invoicesRepository,
"invoicesRepository no puede ser null");
}

@Override
public void execute(@NonNull Query query, boolean refresh, final ExecuteCallback
callback) {
}
}
Obtener Facturas Para UI

Agrega las validaciones necesarias para tus parámetros de entrada y luego ejecuta la
obtención de facturas parciales desde el repositorio.

@Override
public void execute(@NonNull Query query, boolean refresh, final ExecuteCallback
callback) {
checkNotNull(query, "query no puede ser null");
checkNotNull(callback, "callback no puede ser null");

mInvoicesRepository.getInvoicesUis(query,
new IInvoicesRepository.GetInvoicesUiCallback() {
@Override
public void onInvoicesInfoLoaded(List<InvoiceUi> invoicesForUi) {
checkNotNull(invoicesForUi, "invoicesInfos no puede ser
null");
callback.onSuccess(invoicesForUi);
}

@Override
public void onDataNotAvailable(String errorMsg) {
callback.onError(errorMsg);
}
});
}
Tarea #5. Definir Microinteracciones MVP
En el boceto vimos que necesitamos la carga de la lista de facturas al inicio de la
screen y la apertura de la screen de creación de facturas al presionar el fab button.

Un comportamiento básico, por el cual ya hemos pasado varias veces.

En esta instancia es posible generalizar los presentadores y vistas que tengan semejanzas
destacables. Así que no dudes en hacerlo.

En todo caso, no está de más definir con anterioridad que deberían hacer nuestros
elementos de presentación.

Por el lado de la vista tenemos:

 Mostrar facturas en la lista


 Abrir la actividad de creación de nuevas facturas
 Mostrar indicador de carga
 Mostrar estado vacío
 Mostrar error de carga de facturas
 Mostrar página para el endless scroll
 Mostrar indicador de carga de página
 Habilitar endless scroll
 Mostrar un mensaje de éxito al guardar una nueva factura

Y en el presentador tendremos las reacciones para:

 Cargar facturas
 Crear una nueva factura
 Manejar resultado de la creación de factura

Para establecer este comportamiento en código, creamos la interfaz InvoicesListMvp


en invoices/presentation y describimos cada acción:

public interface InvoicesListMvp {


interface View {
void showInvoices(List<InvoiceUi> invoices);
void showAddInvoice();

void showLoadingState(boolean show);

void showEmptyState();

void showInvoicesError(String msg);

void showInvoicesPage(List<InvoiceUi> invoices);

void showLoadMoreIndicator(boolean show);

void showEndlessScroll(boolean show);

void showSuccessfullySavedMessage();

void setPresenter(Presenter presenter);


}

interface Presenter {
void loadInvoices(boolean refresh, boolean resume);

void addNewInvoice();

void manageSavingResult(int requestCode, int resultCode);


}
}

Tarea #6. Crear Presentador


Crea la clase InvoicesPresenter en el mismo paquete.

Identificar sus campos es de rutina.

Tenemos la vista y el caso de uso para obtener facturas.

Y además los miembros para mantener el paginado y detectar la primera carga de


facturas.

Rápidamente podrás armar el siguiente esqueleto:


public class InvoicesPresenter implements InvoicesListMvp.Presenter {

private InvoicesListMvp.View mView;


private IGetInvoicesForUi mGetInvoicesForUi;

private static final int INVOICES_PAGE_SIZE = 20;

private int mCurrentPage = 1;

private boolean mIsFirstLoad = true;

public InvoicesPresenter(InvoicesListMvp.View invoicesView, IGetInvoicesForUi


getInvoicesInfos) {
mView = checkNotNull(invoicesView, "invoicesView");
mGetInvoicesForUi = checkNotNull(getInvoicesInfos, "getInvoicesUis");
}

@Override
public void loadInvoices(final boolean manualRefresh, final boolean resume) {

@Override
public void addNewInvoice() {
}

@Override
public void manageSavingResult(int requestCode, int resultCode) {
}
}

Cargar Facturas

Completar este método no debe ser problema para ti.

Básate en el presentador de clientes y escribe el flujo para mostrar las facturas en la


vista o los errores correspondientes:

@Override
public void loadInvoices(final boolean manualRefresh, final boolean
resume) {

final boolean normalLoad = manualRefresh || mIsFirstLoad || resume;

if (normalLoad) {
mView.showLoadingState(true);
mCurrentPage = 1; // Reset del páginado
} else {
mView.showLoadMoreIndicator(true);
mCurrentPage++; // Preparar página siguiente
}

// Crear consulta de facturas parciales


Query query = new Query(
/* Filtro */ new InvoicesUiNoFilter(),
/* Orden */ InvoicesUiSelector.DATE_INVOICE_FIELD,
Query.DESC_ORDER,
/* Paginado */ mCurrentPage, INVOICES_PAGE_SIZE);

// Ejecutar caso de uso "Obtener Facturas parciales"


mGetInvoicesForUi.execute(query, manualRefresh || mIsFirstLoad,
new IGetInvoicesForUi.ExecuteCallback() {
@Override
public void onSuccess(List<InvoiceUi> invoicesForUi) {
mView.showLoadingState(false);

if (invoicesForUi.isEmpty()) {
if (normalLoad) {
mView.showEmptyState();
} else {
mView.showLoadMoreIndicator(false);
}
mView.showEndlessScroll(false);

} else {
if (normalLoad) {
mView.showInvoices(invoicesForUi);
} else {
mView.showLoadMoreIndicator(false);
mView.showInvoicesPage(invoicesForUi);
}
mView.showEndlessScroll(true);
}

mIsFirstLoad = false; // Off


}

@Override
public void onError(String error) {
mView.showLoadingState(false);
mView.showLoadMoreIndicator(false);
mView.showInvoicesError(error);
}
});

Aquí lo importante es destacar que la primera carga (mFirstLoad) y el refresco


manual (manualRefresh) pueden ordenar al repositorio que sea consultado el servidor
por datos actualizados.

En cambio la carga por restauración (resume) se enfoca en recargar desde la base de


datos local ya que solo hubo un refresco por parte de Android ante la vista.
Creación De Factura

El contenido de este método es simple ya que solo necesitamos una instrucción para
la vista de inicio de la screen de creación de facturas.

@Override
public void addNewInvoice() {
mView.showAddInvoice();
}

Manejar Resultado De Guardado

Comprobamos si la petición desde InvoicesActivity hacia AddEditInvoiceActivity


fue exitosa. Si es así, entonces se muestra un mensaje de guardado correcto:

@Override
public void manageSavingResult(int requestCode, int resultCode) {
if (InvoicesActivity.REQUEST_ADD_INVOICE == requestCode
&& Activity.RESULT_OK == resultCode) {
mView.showSuccessfullySavedMessage();
}
}

Tarea #7. Crear Fragmento


Añade el fragmento para la lista de facturas para concretar la vista.

Nombra su layout fragment_invoices.

public class InvoicesFragment extends Fragment {

public InvoicesFragment() {
}
public static InvoicesFragment newInstance() {
InvoicesFragment fragment = new InvoicesFragment();
return fragment;
}

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

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_invoices, container,
false);
}

Diseñar Layout Para La Lista

El layout de los ítems para las facturas posee 2 grupos de 3 textos como lo refleja el
boceto.

En este caso nos viene bien un RelativeLayout o un ConstraintLayout.

Por ende, crea un nuevo layout llamado invoice_item.xml y si deseas agrega mi


definición XML:

<?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:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/keyline_1">

<TextView
android:id="@+id/invoice_customer_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Robinson Pineda" />

<TextView
android:id="@+id/invoice_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="@dimen/keyline_1_minus_8dp"
android:ellipsize="end"
android:maxLength="20"
android:maxLines="1"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/invoice_customer_name"
tools:text="INV-000001" />

<TextView
android:id="@+id/invoice_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/invoice_number"
tools:text="06/06/2017" />

<TextView
android:id="@+id/invoice_total_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:gravity="end"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="$120" />

<TextView
android:id="@+id/invoice_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_1_minus_8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/invoice_total_amount"
tools:text="Pagada" />

<TextView
android:id="@+id/invoice_total_items"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/invoice_state"
tools:text="8 ítems" />

<android.support.constraint.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</android.support.constraint.ConstraintLayout>

La forma en que se verá es la siguiente:


Diseñar Layout Del Fragmento

Inmediatamente modifica el layout del fragmento para que acepte refresco, tenga un
estado vacío y la lista:

<android.support.v4.widget.SwipeRefreshLayout
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/swipe_to_refresh_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

<RelativeLayout
android:id="@+id/invoices_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/invoices_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/keyline_1"
android:paddingTop="@dimen/keyline_1"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/invoice_item" />

<LinearLayout
android:id="@+id/no_invoices_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/keyline_1_minus_8dp"
android:gravity="center"
android:text="@string/no_invoices_message"
android:textSize="20sp" />
</LinearLayout>
</RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>

Con la previa tendríamos:


Declarar Campos

Una vez completo el layout, vuelve al fragmento y agrega los campos relacionados.

public class InvoicesFragment extends Fragment implements


InvoicesListMvp.View {

private InvoicesListMvp.Presenter mInvoicesPresenter;

private SwipeRefreshLayout mSwipeToRefreshView;


private RecyclerView mInvoicesList;
private InvoicesAdapter mInvoicesAdapter;
private View mNoInvoicesView;

Como ves, tendremos solo el presentador y los views que usaremos.

Crear Adaptador

Para el adaptador de facturas puedes hacer una copia del adaptador de clientes y tan
solo reemplazar el view holder para obtener las referencias particulares.

Obviamente esto implica cambiar onBindViewHolder() para relacionar los datos con
los views, pero en general toda la lógica se mantiene igual:

InvoicesAdapter.java

public class InvoicesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>


implements DataLoading {

private final static int TYPE_ITEM = 1;


private final static int TYPE_NEXT_PAGE_INDICATOR = 2;

private final Resources mResources;

private List<InvoiceUi> mItems;

private boolean mIsLoading = false;


private boolean mEndless = false;

public InvoicesAdapter(Context context, List<InvoiceUi> invoicesInfos) {


mResources = context.getResources();
setList(invoicesInfos);
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());

View view; // View a retornar

if (viewType == TYPE_NEXT_PAGE_INDICATOR) {
view = inflater.inflate(R.layout.item_loading_footer, parent, false);
return new InvoicesAdapter.NextPageIndicatorHolder(view);
}

view = inflater.inflate(R.layout.invoice_item, parent, false);


return new InvoiceHolder(view);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case TYPE_ITEM:
bindInvoiceHolder((InvoiceHolder) holder, position);
break;
case TYPE_NEXT_PAGE_INDICATOR:
bindNextPageIndicatorHolder((NextPageIndicatorHolder) holder, position);
break;
}
}

@Override
public int getItemViewType(int position) {
int itemsSize = mItems.size();

// Lógica para determinar cuándo usar los tipos de views


if (position < itemsSize && itemsSize > 0) {
return TYPE_ITEM;
}
return TYPE_NEXT_PAGE_INDICATOR;
}

@Override
public int getItemCount() {
return mItems.size() + (mIsLoading ? 1 : 0);
}

private void setList(List<InvoiceUi> invoices) {


mItems = Preconditions.checkNotNull(invoices);
}

private void bindInvoiceHolder(InvoiceHolder holder, int position) {


InvoiceUi invoice = mItems.get(position);

String totalItems = mResources.getQuantityString(R.plurals.item_plurals,


invoice.getTotalItems(), invoice.getTotalItems());
String totalAmount = String.format(Locale.ROOT, "$%.2f", invoice.getTotalAmount());
String date = DateTimeUtils.formatDate(invoice.getDate(),
DateTimeUtils.DATE_ONLY_PATTERN);

holder.customerName.setText(invoice.getCustomerName());
holder.number.setText(invoice.getNumber());
holder.date.setText(date);
holder.totalAmount.setText(totalAmount);

int backgroundColor = mResources.getColor(R.color.white);


switch (invoice.getState()) {
case Invoice.STATE_VALUE_DRAFT:
backgroundColor = mResources.getColor(R.color.state_draft);
break;
case Invoice.STATE_VALUE_PAID:
backgroundColor = mResources.getColor(R.color.state_paid);
break;
case Invoice.STATE_VALUE_CANCELED:
backgroundColor = mResources.getColor(R.color.state_canceled);
break;
case Invoice.STATE_VALUE_SENT:
backgroundColor = mResources.getColor(R.color.state_sent);
break;
}
holder.state.setText(invoice.getState());
holder.state.setTextColor(backgroundColor);
holder.totalItems.setText(totalItems);
}

private void bindNextPageIndicatorHolder(NextPageIndicatorHolder holder, int position) {


boolean showNextPageIndicator = position > 0 && mIsLoading && mEndless;
holder.progress.setVisibility(showNextPageIndicator ? View.VISIBLE :
View.INVISIBLE);
}

public void dataStartedLoading() {


if (mIsLoading) {
return;
}

mIsLoading = true;

new Handler().post(new Runnable() {


@Override
public void run() {
notifyItemInserted(getNextPageIndicatorPosition());
}
});

public void dataFinishedLoading() {


if (!mIsLoading) {
return;
}

mIsLoading = false;

new Handler().post(new Runnable() {


@Override
public void run() {
notifyItemRemoved(getNextPageIndicatorPosition());
}
});
}

public void setEndless(boolean endless) {


mEndless = endless;
}

public void replaceItems(List<InvoiceUi> invoices) {


setList(invoices);
notifyDataSetChanged();
}

public void addItems(List<InvoiceUi> invoices) {


mItems.addAll(invoices);
}

private int getNextPageIndicatorPosition() {


return mIsLoading ? getItemCount() - 1 : RecyclerView.NO_POSITION;
}

@Override
public boolean isLoadingData() {
return mIsLoading;
}

@Override
public boolean isThereMoreData() {
return mEndless;
}

private class InvoiceHolder extends RecyclerView.ViewHolder {

TextView customerName;
TextView number;
TextView date;
TextView totalAmount;
TextView state;
TextView totalItems;

InvoiceHolder(View itemView) {
super(itemView);
customerName = (TextView) itemView.findViewById(R.id.invoice_customer_name);
number = (TextView) itemView.findViewById(R.id.invoice_number);
date = (TextView) itemView.findViewById(R.id.invoice_date);
totalAmount = (TextView) itemView.findViewById(R.id.invoice_total_amount);
state = (TextView) itemView.findViewById(R.id.invoice_state);
totalItems = (TextView) itemView.findViewById(R.id.invoice_total_items);
}

private class NextPageIndicatorHolder extends RecyclerView.ViewHolder {


ProgressBar progress;

NextPageIndicatorHolder(View view) {
super(view);
progress = (ProgressBar) view.findViewById(R.id.progressBar);
}
}
}

Manejar Creación Inicial Del Fragmento

Te sitúas en onCreate() e inicializas el adaptador con 0 elementos y habilitas la


contribución a la Action Bar:

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

mInvoicesAdapter = new InvoicesAdapter(new


ArrayList<InvoiceInfo>(0));
setRetainInstance(true);
setHasOptionsMenu(true);
}

Manejar La Creación De Interfaz

Obtenemos todas las referencias de la interfaz de usuario en onCreateView().


Entre ellas tendremos el FloatingActionButton de la actividad (getActivity()), al cual
le asignaremos una escucha OnClickListener para llamar el método addNewInvoice()
del presentador:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_invoices, container,
false);

mSwipeToRefreshView = (SwipeRefreshLayout)
rootView.findViewById(R.id.swipe_to_refresh_view);
mInvoicesList = (RecyclerView) rootView.findViewById(R.id.invoices_list);
mNoInvoicesView = rootView.findViewById(R.id.no_invoices_view);

setUpInvoicesList();
setUpRefreshView();

FloatingActionButton fab = (FloatingActionButton)


getActivity().findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mInvoicesPresenter.addNewInvoice();
}
});

if (savedInstanceState != null) {
showList(true);
}
return rootView;
}

Donde ya sabemos que setUpInvoicesList() y setUpRefreshView() setean las


características iniciales de la lista y el refresh view:

private void setUpInvoicesList() {


mInvoicesList.setAdapter(mInvoicesAdapter);

final LinearLayoutManager layoutManager =


(LinearLayoutManager) mInvoicesList.getLayoutManager();

// Se agrega escucha de scroll infinito


mInvoicesList.addOnScrollListener(
new InfinityScrollListener(mInvoicesAdapter, layoutManager) {
@Override
public void onLoadMore() {
mInvoicesPresenter.loadInvoices(false, false);
}
});
}

private void setUpRefreshView() {


mSwipeToRefreshView.setColorSchemeColors(
ContextCompat.getColor(getActivity(), R.color.colorPrimary),
ContextCompat.getColor(getActivity(), R.color.colorAccent),
ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark));
mSwipeToRefreshView.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
mInvoicesPresenter.loadInvoices(true, false);
}
});
}

Para el evento de cargar más datos al final de la lista (onLoadMore()) hacemos una
llamada al presentador con el fin de que cargue mas datos.

Al igual que al momento de realizar un refresco con el gesto Swipe.

Cargar Facturas

Cada que el fragmento se vuelve disponible para interacciones del usuario


llamaremos a la carga de facturas:

@Override
public void onResume() {
super.onResume();
mInvoicesPresenter.loadInvoices(false, true);
}

Como ves, enviamos true en el segundo parámetro notificando al presentador que se


hará una carga ordenada por Android.

Mostrar Facturas

Reemplaza los datos del adaptador y muestra la lista.

@Override
public void showInvoices(List<InvoiceUi> invoices) {
mInvoicesAdapter.replaceItems(invoices);
showList(true);
}

private void showList(boolean show) {


mInvoicesList.setVisibility(show ? View.VISIBLE : View.GONE);
mNoInvoicesView.setVisibility(show ? View.GONE : View.VISIBLE);
}

Iniciar Actividad De Creación De Facturas

En este método va la apertura de la actividad que permitirá la creación de una nueva


factura.

Esto será con el método startActivityForResult() donde especificaremos la petición


para crear una factura.

@Override
public void showAddInvoice() {
Intent intent = new Intent(getContext(),
AddEditInvoiceActivity.class);
startActivityForResult(intent, InvoicesActivity.REQUEST_ADD_INVOICE);
}

Mostrar Indicador De Carga

Activa la animación de carga del refresh layout:

@Override
public void showLoadingState(final boolean show) {
if (getView() == null) {
return;
}

mSwipeToRefreshView.post(new Runnable() {
@Override
public void run() {
mSwipeToRefreshView.setRefreshing(show);
}
});
}
Mostrar Estado Vacío

Muestra el view de ausencia de clientes y oculta la lista:

@Override
public void showEmptyState() {
showList(false);
}

Mostrar Error De Carga De Facturas

Crea un Toast para avisar al usuario los posibles errores a la hora de cargar los
clientes:

@Override
public void showInvoicesError(String msg) {
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG)
.show();
}

Mostrar Página Nueva De Facturas

Añade la lista entrante (página) como datos adicionales al adaptador:

@Override
public void showInvoicesPage(List<InvoiceUi> invoices) {
mInvoicesAdapter.addItems(invoices);
showList(true);
}
Mostrar Indicador De Cargar Más

Inicia la inserción del indicador de carga para una nueva página si el parámetro de
entrada es true, de lo contrario dile al adaptador que finalice este proceso.

@Override
public void showLoadMoreIndicator(boolean show) {
if (!show) {
mInvoicesAdapter.dataFinishedLoading();
} else {
mInvoicesAdapter.dataStartedLoading();
}
}

Mostrar Mensaje De Factura Guardada Correctamente

Mostraremos una SnackBar con el mensaje “Factura creada correctamente” (pon


el texto en un elemento <string> dentro de values/strings.xml con el nombre de
message_sucessfully_saved_invoice):

@Override
public void showSuccessfullySavedMessage() {
Snackbar.make(getActivity().findViewById(android.R.id.content),
R.string.message_successfully_saved_invoice,
Snackbar.LENGTH_LONG).show();
}

Asignar Presentador A La Vista

Tan solo asigna el valor entrante a la instancia que tenemos como campo:

@Override
public void setPresenter(InvoicesListMvp.Presenter presenter) {
mInvoicesPresenter = checkNotNull(presenter, "presenter no puede ser
null");
}
Tarea #8. Completar Actividad De Facturas
Como de costumbre terminamos con la creación de la actividad para crear toda la
arquitectura.

Generar Dominio Y Datos

Abre el proveedor de dependencias DependecyProvider y agrega un método para la


instanciación del caso de uso de facturas y otro para el repositorio.

public static GetInvoicesInfos provideGetInvoices() {


return new GetInvoicesInfos(provideInvoicesRepository());
}

private static InvoicesRepository provideInvoicesRepository() {


return InvoicesRepository.getInstance(

CacheInvoicesStore.getInstance(provideCustomersRepository()));
}

Añadir Presentación

Acto seguido añade la transacción del fragmento con la lista de facturas, crea la
instancia del presentador y asócialos entre sí dentro de onCreate().

InvoicesFragment fragment = (InvoicesFragment)

getSupportFragmentManager().findFragmentById(R.id.invoices_container);

setTitle(R.string.invoices);

if (fragment == null) {
fragment = InvoicesFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.invoices_container, fragment)
.commit();
}

InvoicesPresenter presenter = new InvoicesPresenter(


fragment,
DependencyProvider.provideGetInvoices());
fragment.setPresenter(presenter);

¡Ufff, muy bien!

Ahora es el turno de ejecutar la aplicación para verificar el caso de uso versus las
interacciones del boceto.

Revisa que todo vaya bien y la UI se vea como la siguiente:


Paso #7. Crear Facturas
Llegamos al último caso de uso de la app Android en esta parte del tutorial.

Aquí la dinámica es diferente, ya que el objetivo gira entorno a la creación de


facturas.

Es decir, una inserción en nuestras fuentes de datos.

Veamos como asumir este reto…

Diseñar Arquitectura
¿Qué significado tiene agregar una factura en el dominio de tu proyecto?

La respuesta a esa pregunta puede tener infinidad de variaciones debido a que los
ambientes de negocio no son exactamente iguales.

No obstante, de forma general podemos declarar que:

Crear una factura implica la asociación de un cliente que manifiesta la decisión de


compra con respecto a uno o varios productos/servicios del negocio.

En esencia para App Productos ese sería la regla base de este caso de uso y sobre la
cual trabajaremos el desarrollo.

Por lo que antes de que comencemos analiza tu situación.

Piensa en tus necesidades:

― ¿Mi facturación o generación de pedidos tiene procesos adicionales?

― ¿Hay factores externos que restrinjan el crear una factura?

― ¿Qué otras entidades intermediarias debería considerar en este proceso?


Etc.

Una vez hayas discernido entre ideas, tendrás la capacidad de expandir el siguiente
diseño que a construir.

Definir Entidades

Si revisas las interacciones del boceto para añadir una factura se sigue este flujo:

1. El vendedor inicia la creación


2. Busca al cliente si ya existe o lo crea en caso contrario
3. (Opcional) Escribe el número de factura
4. (Opcional) Cambia la fecha de creación
5. (Opcional) Modificación de otros atributos
6. Añade ítems a la factura
a. Selecciona un producto de la lista disponible. (Opcional: Puede crearlo
si no existe)
b. Selecciona la cantidad del producto
c. (Opcional) Aplica un descuento
d. (Opcional) Aplica un impuesto
e. (Opcional) Aplicación de más factores de cálculo y reglas de negocio

Basado en ello podemos cubrir la interfaz a través la siguiente composición MVP:


Usamos la nomenclatura AddEdit, ya que la edición la trataremos en la misma actividad
cuando vayamos a asumir este caso de uso (requeriremos el detalle de la factura creado
previamente)

Ahora.

Los ítems de factura en la lista tendrán asociados datos del producto debido a que
tiene que haber una relación interna antes de llegar a la interfaz.

Por tanto, representaremos esta información como una entidad de UI llamada


InvoiceItemUi, la cual se compondrá de ProductUi.

De esta forma tendremos objetos de solo lectura que alimenten la vista:

Definir Casos De Uso

Al editar o crear hay dos reglas de negocio para la aplicación disponibles:

 Crear la factura
 Obtener el cliente seleccionado

Sabiendo esto tenemos la ventaja de tener creado un caso de uso para obtener
clientes.

Por el otro lado, para la creación de la factura si debemos considerar una nueva
entidad.

Poniéndolo en un diagrama tenemos:


Definir Orígenes De Datos

Por último se encuentra la capa de datos, donde usaremos de nuevo el repositorio de


facturas pero con el fin de agregar entidades.

Y recuerda que en el alcance de App Productos parte 4 solo tendremos una fuente de
facturas en memoria.

Por otro lado, los ítems de factura en el proceso de creación/edición de facturas


tienen una esperanza de vida corta, por lo que solo generaremos un administrador
en caché para que las retenga solo por el lapso de tiempo que se demore el usuario
en esta característica.

¿Tienes claro todo hasta el momento?


Vale, si la explicación ha sido clara pasemos a leer los siguientes pasos de
programación.
Tarea #1. Crear Entidades
En el dominio de este caso de uso es necesario añadir métodos de validación para la
factura, con el fin de contrarrestar las entradas inconsistentes del usuario.

Con esto me refiero a dos reglas básicas: la asociación obligatoria de un cliente y


la existencia de al menos un ítem.

Así que añadimos dos métodos dentro de Invoice para representarlas:

public boolean emptyCustomer() {


return Strings.isNullOrEmpty(mCustomerId);
}

public boolean noItems() {


return mInvoiceItems.isEmpty();
}

Ahora creemos las entidades InvoiceUi y ProductUi dentro de


addeditinvoice/domain/ con los atributos que necesitaremos:

ProductUi.java

public class ProductUi{


private String mName;
private int mStock;
private String mImageUrl;

public ProductUi(String name,


int stock,
String imageUrl) {
mName = name;
mStock = stock;
mImageUrl = imageUrl;
}

private void setName(String name) {


this.mName = name;
}

private void setStock(int stock) {


mStock = stock;
}
private void setImageUrl(String imageUrl) {
this.mImageUrl = imageUrl;
}

public String getName() {


return mName;
}

public int getStock() {


return mStock;
}

public String getImageUrl() {


return mImageUrl;
}

InvoiceItemUi.java

public class InvoiceItemUi {


private String mProductId;
private int mQuantity;
private float mPrice;
private float mTotal;
private int mItemNumber;
private ProductUi mProductUi;

public InvoiceItemUi(String productId,


int quantity,
float price,
float total,
int itemNumber,
ProductUi productUi) {
mProductId = productId;
mQuantity = quantity;
mPrice = price;
mTotal = total;
mItemNumber = itemNumber;
mProductUi = productUi;
}

private void setQuantity(int mQuantity) {


this.mQuantity = mQuantity;
}

private void setPrice(float mPrice) {


this.mPrice = mPrice;
}
private void setTotal(float mTotal) {
this.mTotal = mTotal;
}

private void setProductId(String mProductId) {


this.mProductId = mProductId;
}

public String getProductId() {


return mProductId;
}

public int getQuantity() {


return mQuantity;
}

public float getItemPrice() {


return mPrice;
}

public float getTotal() {


return mTotal;
}

public String getProductName() {


return mProductUi.getName();
}

public String getProductImageUrl() {


return mProductUi.getImageUrl();
}

public int getProductStock() {


return mProductUi.getStock();
}

public int getItemNumber() {


return mItemNumber;
}
}
Tarea #2. Crear Interactor Para Añadir Facturas
En este paso vamos crear la representación del caso de uso para añadir facturas.

Pero antes de codificar determinemos entradas, salidas y procesos a la hora de


contactar la capa de datos.

¿Entradas?

1. Una nueva instancia Invoice construida en el formulario de creación de facturas


2. Parámetros adicionales para afectar la lógica de las fuentes de datos

¿Procesos?

 Hacer llamada de la operación de guardado en el repositorio de datos con los


insumos de la entrada
 Aplicar restricciones de lógica de negocios y validaciones necesarias

¿Salidas?

Éxito: Retorna la misma instancia entrante como símbolo de integridad


Falla: Un mensaje de error para el usuario

Comprendido esto, avancemos hacia la codificación de la interfaz y la clase.

Crear interfaz ISaveInvoice

Añade la interfaz al paquete invoices/domain/usecases con el método execute().

Este recibirá la factura a guardar y una instancia de la callback interna para


determinar el momento en que se termina la operación:

public interface ISaveInvoice {

interface ExecuteCallback {
void onSuccess(Invoice invoice);

void onError(String error);


}

void execute(@NonNull Invoice invoice, ExecuteCallback callback);


}

Crear Implementación SaveInvoice

Crea la clase SaveInvoice e implementa la interfaz anterior.

Adicionalmente añade el repositorio de facturas como campo para obtener su


instancia a través de un nuevo constructor:

public class SaveInvoice implements ISaveInvoice {


private IInvoicesRepository mInvoicesRepo;

public SaveInvoice(IInvoicesRepository invoicesRepo) {


mInvoicesRepo = checkNotNull(invoicesRepo);
}

@Override
public void execute(@NonNull Invoice invoice,
ExecuteCallback callback) {
}
}

Insertar Factura

Recuerda que este método representa el proceso del caso de uso.

Así que agregamos las validaciones correspondientes para la factura, la enviamos al


repositorio y al final retornamos como exitosa la salida.

@Override
public void execute(@NonNull Invoice invoice,
ExecuteCallback callback) {
Preconditions.checkNotNull(invoice, "invoice no puede ser null");
mInvoicesRepo.saveInvoice(invoice);

callback.onSuccess(invoice);
}

¿Y la respuesta fallida?
Al tener la fuente de datos en caché no debemos preocuparnos por latencias o
códigos de error que se generan en fuentes como servidores o base de datos locales.
Tarea #3. Definir Microinteracciones MVP
Del boceto podemos extraer todos los comportamientos a programar de la vista y el
presentador.

Miremos:

La vista relacionada al formulario debe:

 Mostrar nombre del cliente


 Mostrar error de cliente no seleccionado
 Mostrar error de ausencia de ítems
 Redireccionar a la actividad de clientes
 Redireccionar a la actividad para crear/editar ítems
 Redireccionar a la actividad de facturas
 Mostrar ítems de factura
 Mostrar importes totales de la factura
 Mostrar error de creación fallida

En cuanto al presentador:

 Guardar factura: Recibe cada uno de los atributos provenientes de las


entradas sencillas del usuario. En el caso de App Productos solo tendremos el
ID del cliente y la fecha.
 Seleccionar cliente: Ordena a la vista abrir la actividad de clientes en modo
de selección
 Añadir ítem de factura: Ordena a la vista abrir la actividad de creación de
ítems
 Editar ítem de factura: Ordena a la vista abrir la actividad de edición de ítems.
Se le pasa como parámetro el ID del producto relacionado al ítem.
 Borrar ítem de factura: Elimina de la caché de ítems el elemento con la ID
de producto entrante
 Manejar resultado de la actividad de clientes: Carga el cliente con el ID
resultante de la actividad de clientes
 Manejar resultados de creación/edición: Realiza las acciones necesarias ante
una creación o edición exitosa
 Cargar ítems des factura: Obtiene los ítems existentes y ordena a la vista
mostrarlos
 Restaurar estado: Restaura la selección del cliente en cambios de
configuración
Concordante a estas interacciones pasamos a escribir la interfaz AddEditInvoiceMvp
dentro de addeditinvoice/presentation.

public interface AddEditInvoiceMvp {

interface View {
void showCustomer(String name);

void showCustomerError();

void showItemsError();

void showInvoicesScreen(String invoiceId);

void showCustomersScreen();

void showEditInvoiceItemScreen(@NonNull String productId);

void showAddInvoiceItemScreen();

void showInvoiceItems(List<InvoiceItemUi> invoiceItemUis);

void showInvoiceAmounts(String subtotal, String tax, String


total);

void showSaveError(String error);

void setPresenter(Presenter presenter);


}

interface Presenter {
void saveInvoice(String customerId, Date date);

void selectCustomer();

void addNewInvoiceItem();

void editInvoiceItem(String productId);

void deleteInvoiceItem(String productId);

void manageCustomerPickingResult(String customerId);

void manageAdditionResult();

void manageEditionResult();

void loadInvoiceItems();

void restoreState(@NonNull String customerId);


}
}
Tarea #4. Crear Caché De Ítems De Factura
La caché de ítems de factura será la encargada de:

 Obtener ítems de factura


 Obtener ítems de factura en versión UI
 Obtener ítem de factura en versión UI
 Guardar facturas
 Borrar facturas
 Obtener importes totales

El origen de los objetos será un mapa que asocie cada ítem al ID del producto con el
fin de evitar repeticiones en la adición.

4.1 Crear Interfaz

Crea la interfaz ICacheInvoiceItemsStore dentro de addeditinvoiceitem/data y


escribe cada uno de las acciones anteriores como métodos.

Importante: Debido a que conseguir un ítem de factura asociado a un producto


requiere una consulta al repositorio de productos, se deben crear callbacks para las
cargas de objetos InvoiceItemUi.

public interface ICacheInvoiceItemsStore {


interface LoadInvoiceItemsUiCallback {

void onInvoiceItemsLoaded(List<InvoiceItemUi> invoiceItemUis);

void onDataNotAvailable();

interface GetInvoiceItemUiCallback {

void onInvoiceItemUiLoaded(InvoiceItemUi invoiceItemUi);

void onDataNotAvailable();

List<InvoiceItem> getInvoiceItems();

void getInvoiceItemUi(@NonNull String productId, @NonNull GetInvoiceItemUiCallback


callback);

void getInvoiceItemsUi(@NonNull LoadInvoiceItemsUiCallback callback);


void saveInvoiceItem(@NonNull InvoiceItem invoiceItem);

void deleteInvoiceItem(@NonNull String productId);

void deleteAll();

float getTotal();

float getSubtotal();

float getTax();

4.2 Crear Implementación

Ahora creamos un singleton llamado CacheInvoiceItemsStore con la implementación


de la anterior interfaz.

Además declara como campos:

 Un mapa para los ítems


 Una referencia al repositorio de productos
 3 variables flotantes para el total, el impuesto y el subtotal

Es decir:

public class CacheInvoiceItemsStore implements ICacheInvoiceItemsStore {


private static final float TAX_VALUE = 0.18f;

private static CacheInvoiceItemsStore ourInstance = null;

private Map<String, InvoiceItem> mCachedInvoiceItems = new LinkedHashMap<>();

private static IProductsRepository mProductsRepository;

// Totales
private float mTotal;
private float mTax;
private float mSubtotal;

public static CacheInvoiceItemsStore getInstance(IProductsRepository productsRepository)


{
if (ourInstance == null) {
ourInstance = new CacheInvoiceItemsStore(productsRepository);
}
return ourInstance;
}

private CacheInvoiceItemsStore(IProductsRepository productsRepository) {


mProductsRepository = Preconditions.checkNotNull(productsRepository);
mTotal = mSubtotal = mTax = 0;
}
}

La constante TAX_VALUE es el valor relativo del porcentaje que usaremos, para


calcular el impuesto en el ambiente de App Productos.

Normalmente este valor deberías almacenarlo en las preferencias y actualizarlo de


acuerdo a los cambios del servidor.

Lo cual te quede de tarea cuando hayas definido las reglas y valores para el negocio
que estudias.

Seguidamente pasemos a definir cada método.

Guardar Ítem De Factura

Usamos el método put() para guardar el ítem que viene como parámetro. Asociando
su ID de producto como key.

@Override
public void saveInvoiceItem(@NonNull InvoiceItem invoiceItem) {
InvoiceItem oldInvoiceItem =
mCachedInvoiceItems.put(invoiceItem.getProductId(), invoiceItem);

if (oldInvoiceItem != null) { // Edición


float diff = oldInvoiceItem.getTotal() - invoiceItem.getTotal();
calculateAmounts(-diff);
} else {// Adición
calculateAmounts(invoiceItem.getTotal());
generateItemNumbers();
}
}

Basados en el retorno de put() procedemos de dos formas:

 Valor: se considera una edición, por lo que restamos los totales del ítem
antiguo menos el nuevo. Luego calculamos los totales acumulando la
diferencia.
 Null: se considera una adición, por lo que calculamos los totales al acumular
el total del ítem entrante. Seguido generamos los números de línea de cada
ítem.
El cálculo de totales sería una acumulación dentro de calculateAmounts():

private void calculateAmounts(float itemTotal) {


mTotal += itemTotal;
mTax = mTotal * TAX_VALUE;
mSubtotal = mTotal - mTax;
}

Y la generación de números de línea un for regresivo invocando el mutador del


atributo:

private void generateItemNumbers() {


ArrayList<InvoiceItem> values = getValues();
for (int i = mCachedInvoiceItems.size() - 1; i > 0; i--) {
values.get(i).setItemNumber(i + 1);
}
}

Obtener Totales

Simplemente retornamos las variables correspondientes:

@Override
public float getTotal() {
return mTotal;
}

@Override
public float getSubtotal() {
return mSubtotal;
}

@Override
public float getTax() {
return mTax;
}

Obtener Ítems De Factura

Devolvemos los valores del mapa a través del método values().

@Override
public List<InvoiceItem> getInvoiceItems() {
return getValues();
}

@NonNull
private ArrayList<InvoiceItem> getValues() {
return Lists.newArrayList(mCachedInvoiceItems.values());
}

Obtener Ítems De Factura Para UI

En este método tenemos que materializar la relación entre un ítem de factura y un


producto a través del ID del producto.

Las acciones a seguir son las siguientes:

1. Crear una query para seleccionar productos por múltiples IDs


2. Al obtener el resultado favorable, recorremos la lista de productos buscando
cada ítem asociado
3. Se pasan los datos de la entidad Product a una nueva entidad InvoiceItemUi
4. Se añade el ítem a una lista de resultados
5. Se ordenan por número de ítem y se retornan

Veamos:

@Override
public void getInvoiceItemsUi(@NonNull final LoadInvoiceItemsUiCallback callback)
{

final List<InvoiceItemUi> invoiceItemsUi = new ArrayList<>(0);

ArrayList<String> codes = getKeys();


Query query = new Query(new ProductsByCode(codes));

mProductsRepository.getProducts(query, new
IProductsRepository.GetProductsCallback() {
@Override
public void onProductsLoaded(List<Product> products) {
for (Product product : products) {
InvoiceItem invoiceItem = findInvoiceItem(product.getCode());

invoiceItemsUi.add(joinInvoiceItemAndProduct(invoiceItem,
product));
}

sortByNumberItem(invoiceItemsUi);

callback.onInvoiceItemsLoaded(invoiceItemsUi);
}

@Override
public void onDataNotAvailable(String error) {
callback.onDataNotAvailable();
}
});

La especificación ProductsByCode (créala en products/domain/criteria) recibe una


lista de IDs de productos y verifica si cada producto de la fuente de datos tiene
alguno de esos valores.

public class ProductsByCode implements MemorySpecification<Product>,


ProviderSpecification {

private List<String> mCodes;

public ProductsByCode(@NonNull List<String> codes) {


mCodes = Preconditions.checkNotNull(codes);
}

@Override
public boolean isSatisfiedBy(Product item) {
boolean satisfied = false;
for(String code: mCodes){
satisfied = satisfied || code.equals(item.getCode());
}
return satisfied;
}

@Override
public Uri asProvider() {
// TODO: Implementar
return null;
}
}

Los valores de los IDs se obtienen con el método getKeys() que usa a keySet() del
mapa para retornar las llaves de los ítems:

@NonNull
private ArrayList<String> getKeys() {
return Lists.newArrayList(mCachedInvoiceItems.keySet());
}

Para obtener el ítem de factura asociado con el ID, usamos el método


findInvoiceItem():
private InvoiceItem findInvoiceItem(final String code) {
List<InvoiceItem> invoiceItems = getValues();
return Iterables.find(invoiceItems, new Predicate<InvoiceItem>() {
@Override
public boolean apply(InvoiceItem invoiceItem) {
return code.equals(invoiceItem.getProductId());
}
});
}

El ordenamiento por número de línea lo logramos con sortByNumberItem() al usar un


elemento Comparator que compare los atributos mItemNumber de cada elemento:

private void sortByNumberItem(List<InvoiceItemUi> invoiceItemsUi) {


Collections.sort(invoiceItemsUi, new Comparator<InvoiceItemUi>() {
@Override
public int compare(InvoiceItemUi o1, InvoiceItemUi o2) {
return o1.getItemNumber() - o2.getItemNumber();
}
});
}

Con todo esto juntos la información de ambas entidades con


joinInvoiceItemAndProduct():

private InvoiceItemUi joinInvoiceItemAndProduct(InvoiceItem invoiceItem,


Product product) {
ProductUi productUi = new ProductUi(
product.getName(),
product.getUnitsInStock(),
product.getImageUrl());
InvoiceItemUi itemUi = new InvoiceItemUi(
invoiceItem.getProductId(),
invoiceItem.getQuantity(),
invoiceItem.getPrice(),
invoiceItem.getTotal(),
invoiceItem.getItemNumber(),
productUi);
return itemUi;
}
Obtener Ítem De Factura Para UI

Procedemos de la misma forma que con la carga de varios ítems. Solo que esta vez
procesamos el único elemento retornado.

@Override
public void getInvoiceItemUi(@NonNull final String productId, @NonNull final
GetInvoiceItemUiCallback callback) {
List<String> codes = new ArrayList<>();
codes.add(productId);

Query query = new Query(new ProductsByCode(codes));

mProductsRepository.getProducts(query, new
IProductsRepository.GetProductsCallback() {
@Override
public void onProductsLoaded(List<Product> products) {
if (products.isEmpty()) {
callback.onDataNotAvailable();
} else {
Product product = products.get(0);
InvoiceItem itemInvoice = findInvoiceItem(product.getCode());

callback.onInvoiceItemUiLoaded(joinInvoiceItemAndProduct(itemInvoice, product));
}
}

@Override
public void onDataNotAvailable(String error) {
callback.onDataNotAvailable();
}
});
}

Eliminar Ítem De Factura

Usamos el método remove() del mapa, luego calculamos totales y generamos


números de línea:

@Override
public void deleteInvoiceItem(@NonNull String productId) {
InvoiceItem removedItem = mCachedInvoiceItems.remove(productId);
calculateAmounts(-removedItem.getTotal());
generateItemNumbers();
}
Limpiar La Caché

Este método lo usaremos al momento de salir de la creación/edición de la factura, ya


que necesitaremos la fuente de datos limpia si deseamos calcular un nuevo conjunto
de ítems.

Así que usamos el método clear() del mapa y reseteamos a 0 los importes totales.

@Override
public void deleteAll() {
mCachedInvoiceItems.clear();
mTotal = mSubtotal = mTax = 0;
}
Tarea #5. Crear Fragmento

5.1 Crear Clase

Haz click derecho en el paquete addinvoice y crea el fragmento a través de New >
Fragment > Fragment (Blank).

Cuando estés en el asistente configúralo así:

 Fragment Name: AddEditInvoiceFragment


 Layout Name: fragment_add_edit_invoice
 Include fragment factory methods: Si
 Include interface callbacks: No

Y finaliza presionando Finish.

Aunque la plantilla trae código útil, por el momento:

 Limpiaremos los parámetros agregados por defecto


 Aplicaremos la realización de View en el fragmento
 Añadiremos el campo de relación con el presentador

AddInvoiceFragment.java

public class AddEditInvoiceFragment extends Fragment implements


AddEditInvoiceMvp.View {

private AddEditInvoiceMvp.Presenter mPresenter;

public AddEditInvoiceFragment() {
// Required empty public constructor
}

public static AddEditInvoiceFragment newInstance() {


AddEditInvoiceFragment fragment = new AddEditInvoiceFragment();
return fragment;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_add_invoice, container,
false);

return root;
}
}

5.2 Crear Layout Para Ítems De Factura

Para poblar la lista de las líneas de la factura crearemos un nuevo layout llamado
invoice_item_list_item.xml, cuyo diseño estará basado en el boceto estudiado con
anterioridad:

Root: Los 5 views existentes podemos organizarlos bajo un ConstraintLayout:

<?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="@dimen/invoice_item_height"
android:foreground="?android:attr/selectableItemBackground">

</android.support.constraint.ConstraintLayout>

Donde la altura estará definida en el recurso de dimensión invoice_item_height.

<dimen name="invoice_item_height">72dp</dimen>
Imagen del producto: Para la imagen del producto con forma circular, usaremos la
librería CircleImageView de Henning Dodenhof.

La cual requiere que pongamos en nuestro archivo build.gradle (módulo) la


siguiente dependencia:

dependencies {
compile
"de.hdodenhof:circleimageview:$rootProject.circleImageViewVersion"

La variable circleImageViewVersion podemos encontrarle en el build de proyecto


(2.2.0 en el momento que escribo el tutorial).

<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/product_image"
android:layout_width="@dimen/image_product_invoice_item_size"
android:layout_height="@dimen/image_product_invoice_item_size"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />

image_product_invoice_item tienen la siguiente definición:

<dimen name="image_product_invoice_item_size">48dp</dimen>

Guías: Pondremos una directriz vertical y otra horizontal para tomar referencias de
límites en el layout.

Las ubicaremos a un 50% del espacio:

<android.support.constraint.Guideline
android:id="@+id/horizontal_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />

<android.support.constraint.Guideline
android:id="@+id/vertical_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />

Botón De Eliminación: Agregaremos un ImageButton sin bordes y le asignaremos el


icono “close” de los vector assets de Android o lo descargamos desde aquí.

Este view estará alineado a la derecha del padre y centrado verticalmente.

<ImageButton
android:id="@+id/remove_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="@dimen/button_remove_size"
android:layout_height="@dimen/button_remove_size"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:tint="@color/gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close" />

Textos: Agregamos 3 Text Views para representar el nombre, precio x cantidad y el


total del ítem de factura.

Simplemente los restringimos como vemos en el boceto usando los atributos de


limitación:

<TextView
android:id="@+id/product_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textSize="12sp"
app:layout_constraintBottom_toTopOf="@+id/horizontal_guideline"
app:layout_constraintEnd_toStartOf="@+id/vertical_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/product_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintWidth_default="wrap"
tools:text="Producto Nombre"
android:layout_marginLeft="8dp" />

<TextView
android:id="@+id/product_quantity_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
app:layout_constraintEnd_toStartOf="@+id/vertical_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/product_image"
app:layout_constraintTop_toTopOf="@+id/horizontal_guideline"
tools:text="$20.45 × 1" />

<TextView
android:id="@+id/line_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/remove_button"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="$20.45" />

Con esto tendríamos la siguiente preview:


5.3 Diseñar Layout

Ahora la cuestión es:

¿Cómo será el diseño del layout?

Obviamente la respuesta está en el boceto de esta screen.

Al prestar atención en la estructura visualizamos que es una jerarquía con varias


cards al mismo nivel. Cada una representa un bloque de contenido con ciertos
atributos de la factura.

Teniendo en cuenta ese hecho, una de las soluciones XML para este caso sería:
<android.support.v4.widget.NestedScrollView
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:clipToPadding="false"

tools:context="com.hermosaprogramacion.premium.appproductos.addeditinvoice.presentation.AddE
ditInvoiceFragment">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical">

<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1"
android:layout_marginTop="@dimen/keyline_1"
app:contentPaddingBottom="@dimen/keyline_1"
app:contentPaddingLeft="@dimen/keyline_1"
app:contentPaddingRight="@dimen/keyline_1"
app:contentPaddingTop="@dimen/keyline_1">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/customer_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/customer_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/colorPrimary" />

<android.support.design.widget.TextInputLayout
android:id="@+id/customer_text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1">

<TextView
android:id="@+id/customer_field"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"

android:hint="@string/customer_field" />
</android.support.design.widget.TextInputLayout>

<TextView
android:id="@+id/invoice_number_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/invoice_number_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/colorPrimary" />

<TextView
android:id="@+id/invoice_number_field"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/invoice_number_field" />

</LinearLayout>
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1"
app:contentPaddingBottom="@dimen/keyline_1"
app:contentPaddingLeft="@dimen/keyline_1"
app:contentPaddingRight="@dimen/keyline_1"
app:contentPaddingTop="@dimen/keyline_1">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/creation_date_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/creation_date_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/colorPrimary" />

<TextView
android:id="@+id/invoice_date_field"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
tools:text="09/01/2018" />

<TextView
android:id="@+id/other_field_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Otro"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/colorPrimary" />

<EditText
android:id="@+id/other_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Otro campo"
android:imeOptions="actionDone" />
</LinearLayout>
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/invoice_items_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginTop="@dimen/keyline_1"
android:text="@string/invoice_item_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="@color/colorPrimary"
android:layout_marginStart="@dimen/keyline_1" />

<View style="@style/Divider" />

<android.support.v7.widget.RecyclerView
android:id="@+id/invoice_items_list"
android:layout_width="match_parent"
android:overScrollMode="never"
android:layout_height="wrap_content"
tools:listitem="@layout/invoice_item_list_item"
android:layout_marginBottom="@dimen/keyline_1"
app:layoutManager="LinearLayoutManager" />

<android.support.design.widget.TextInputLayout
android:id="@+id/add_item_button_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1">

<Button
android:id="@+id/add_item_button"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/keyline_1"
android:paddingTop="@dimen/keyline_1"
android:text="@string/add_item_button" />
</android.support.design.widget.TextInputLayout>

</LinearLayout>
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_1"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1">
<include layout="@layout/content_invoice_totals" />
</android.support.v7.widget.CardView>
</LinearLayout>

</android.support.v4.widget.NestedScrollView>
Como ya ves es un complejo de un scroll view con un linear layout con 4 cards en el
interior:

En la card de los totales tenemos una etiqueta <include> que hace referencia a un
layout separado llamado content_invoice_totals.xml, el cual contiene un
ConstraintLayout para distribuir los importes y sus etiquetas:

<?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="wrap_content">

<TextView
android:id="@+id/subtotal_value"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical|end"
android:paddingEnd="@dimen/keyline_1"
android:paddingRight="@dimen/keyline_1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="$230" />

<TextView
android:id="@+id/tax_value"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical|end"
android:paddingEnd="@dimen/keyline_1"
android:paddingRight="@dimen/keyline_1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@+id/total_value"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintTop_toBottomOf="@id/subtotal_value"
tools:text="$30" />

<TextView
android:id="@+id/total_value"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:background="@color/colorPrimary"
android:gravity="center_vertical|end"
android:paddingEnd="@dimen/keyline_1"
android:paddingRight="@dimen/keyline_1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintTop_toBottomOf="@+id/tax_value"
tools:text="$200" />

<TextView
android:id="@+id/subtotal_label"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingLeft="@dimen/keyline_1"
android:paddingStart="@dimen/keyline_1"
android:text="@string/subtotal_label"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintBottom_toTopOf="@+id/tax_label"
app:layout_constraintEnd_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<View
android:id="@+id/divider1"
style="@style/Divider"
android:layout_marginTop="?attr/listPreferredItemHeightSmall"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tax_label"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingLeft="@dimen/keyline_1"
android:paddingStart="@dimen/keyline_1"
android:text="@string/tax_label"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintBottom_toTopOf="@+id/total_label"
app:layout_constraintEnd_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtotal_label" />

<TextView
android:id="@+id/total_label"
android:layout_width="0dp"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:background="@color/colorPrimary"
android:gravity="center_vertical"
android:paddingLeft="@dimen/keyline_1"
android:paddingStart="@dimen/keyline_1"
android:text="@string/total_label"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/vertical_center_guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tax_label" />

<android.support.constraint.Guideline
android:id="@+id/vertical_center_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.50" />

</android.support.constraint.ConstraintLayout>

Si ves la preview de ambos layout tendrías:


5.4 Definir Campos

El siguiente paso es añadir el campo de relación del presentador y las instancias de


los views que manipularemos.

public class AddEditInvoiceFragment extends Fragment implements


AddEditInvoiceMvp.View {

// Keys de estados
private static final String BUNDLE_CUSTOMER_ID =
"BUNDLE_CUSTOMER_ID";

private AddEditInvoiceMvp.Presenter mPresenter;

private TextView mCustomerField;


private TextView mDateField;
private Button mAddItemButton;
private RecyclerView mInvoiceItemsList;
private InvoiceItemAdapter mInvoiceItemAdapter;
private TextView mSubtotalText;
private TextView mTaxText;
private TextView mTotalText;
private TextInputLayout mAddItemButtonWrapper;
private TextInputLayout mCustomerFieldWrapper;

private String mCustomerId;


private Date mDate;

Si los campos no te son claros, déjame te explico:

La constante BUNDLE_CUSTOMER_ID la usaremos como key para guardar el estado más


adelante.

mPresenter es la relación con el presentador para invocar sus comportamientos.

El bloque siguiente son todos los views del layout que usaremos de algún modo.

Y al final tenemos dos variables para retener el ID del cliente seleccionado y la


fecha tomada.

5.5 Crear Adaptador Para Ítems De Factura

Recordemos que la lista de líneas de factura recibe dos eventos:

 El click en el ítem completo para editar


 El click en el botón derecho para remover el ítem

Por esta razón la interfaz interna que le añadiremos (ItemListener) debe poseer dos
métodos controladores.

De resto, el inflado y binding es un proceso que ya tenemos mecanizado.

Por ende creemos una nueva clase con el nombre InvoiceItemAdapter dentro de
addeditinvoice.

Luego usa el siguiente código:

public class InvoiceItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

public interface ItemListener {

void onItemClick(InvoiceItemUi invoiceItem);

void onRemoveItemClick(InvoiceItemUi invoiceItem);


}

private final Context context;

private List<InvoiceItemUi> invoiceItems;

private ItemListener listener;

public class InvoiceItemViewHolder extends RecyclerView.ViewHolder


implements View.OnClickListener {
public ImageView productImage;
public TextView productName;
public TextView productQuantityXPrice;
public TextView lineTotal;
public ImageButton removeButton;

public InvoiceItemViewHolder(LayoutInflater inflater, ViewGroup parent) {


super(inflater.inflate(R.layout.invoice_item_list_item, parent, false));
productImage = (ImageView) itemView.findViewById(R.id.product_image);
productName = (TextView) itemView.findViewById(R.id.product_name);
productQuantityXPrice = (TextView)
itemView.findViewById(R.id.product_quantity_price);
lineTotal = (TextView) itemView.findViewById(R.id.line_total);
removeButton = (ImageButton) itemView.findViewById(R.id.remove_button);
removeButton.setOnClickListener(this);
itemView.setOnClickListener(this);
}

@Override
public void onClick(View view) {
int i = getAdapterPosition();

if (i != RecyclerView.NO_POSITION) {
InvoiceItemUi item = getItem(i);

if (view.getId() == R.id.remove_button) {
listener.onRemoveItemClick(item);
} else {
listener.onItemClick(item);
}
}
}
}

public InvoiceItemAdapter(Context context, List<InvoiceItemUi> invoiceItems,


ItemListener listener) {
this.context = context;
this.invoiceItems = invoiceItems;
this.listener = listener;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new InvoiceItemViewHolder(LayoutInflater.from(parent.getContext()), parent);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
InvoiceItemViewHolder vh = (InvoiceItemViewHolder) holder;
InvoiceItemUi item = getItem(position);

Glide.with(context)
.load(item.getProductImageUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.centerCrop()
.into(vh.productImage);

vh.productName.setText(item.getProductName());

String priceXQuantity = String.format(Locale.ROOT, "$%.2f × %s",


item.getItemPrice(), item.getQuantity());
vh.productQuantityXPrice.setText(priceXQuantity);

String total = String.format(Locale.ROOT, "$%.2f", item.getTotal());


vh.lineTotal.setText(total);
}

@Override
public int getItemCount() {
return invoiceItems.size();
}

public void replaceData(List<InvoiceItemUi> invoiceItemUis) {


invoiceItems = invoiceItemUis;
notifyDataSetChanged();
}

private InvoiceItemUi getItem(int position) {


return invoiceItems.get(position);
}
}

Como ves, la interfaz para clicks sobre los ítems trae consigo a onItemClick() para
clicks sobre todo el ítem y onRemoveClick() para el click sobre el botón de remoción.

Añadido a eso.

La lógica que tenemos sobre onClick() del view holder establece una diferencia
entre los clicks a través de getId().

Así sabremos si el click es en el ítem total o en el botón.

5.6 Configurar Vista De Interfaz De Usuario

Ahora trabajemos sobre onCreateView().

Donde asociaremos las escuchas para los eventos correspondientes a la reacciones


del presentador:

 Selección de cliente
 Añadir ítem
 Editar item
 Guardar factura

Observemos:

Campo de cliente

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_add_invoice, container,
false);

// Cliente
mCustomerFieldWrapper = (TextInputLayout)
root.findViewById(R.id.customer_text_input);
mCustomerField = (TextView) root.findViewById(R.id.customer_field);
mCustomerField.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mPresenter.selectCustomer();
}
});

Tomamos las referencias y asignamos una escucha de clicks al campo.

En onClick() llamamos a selectCustomer() del presentador.

Campo de fecha

Tomaremos su referencia y luego con setText() inicializaremos su valor desplegado


a través de DateTimeUtils.getDateTime().

Pero antes actualizaremos getDateTime() para que reciba el patrón necesario para
mostrar en la UI propuesta:

public class DateTimeUtils {

public static final String DATE_ONLY_PATTERN = "dd/MM/yyyy";


public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

public static String getDateTime(String pattern) {


DateFormat df = new SimpleDateFormat(pattern,
Locale.getDefault());
return df.format(Calendar.getInstance().getTime());
}
}

Con ello:

// Fecha
mDateField = (TextView) root.findViewById(R.id.invoice_date_field);
String currentDateString =
DateTimeUtils.getDateTime(DateTimeUtils.DATE_ONLY_PATTERN);
mDateField.setText(currentDateString);

Botón para añadir ítem

Tomamos la referencia del campo y el wrapper. Luego añadimos una escucha de


clicks, en la cual llamaremos a addNewInvoiceItem().

// Botón ADD
mAddItemButtonWrapper = (TextInputLayout)
root.findViewById(R.id.add_item_button_wrapper);
mAddItemButton = (Button) root.findViewById(R.id.add_item_button);
mAddItemButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mPresenter.addNewInvoiceItem();
}
});

Lista de ítems de factura

Tomamos la referencia del recycler view y creamos el adaptador.

En los eventos de la escucha para items invocamos la edición y eliminación del


presentador:

// Items
mInvoiceItemsList = (RecyclerView)
root.findViewById(R.id.invoice_items_list);
DividerItemDecoration decoration = new
DividerItemDecoration(mInvoiceItemsList.getContext(),
DividerItemDecoration.VERTICAL);
mInvoiceItemAdapter = new InvoiceItemAdapter(getContext(),
new ArrayList<InvoiceItemUi>(0),
new InvoiceItemAdapter.ItemListener() {
@Override
public void onItemClick(InvoiceItemUi invoiceItem) {
mPresenter.editInvoiceItem(invoiceItem.getProductId());
}

@Override
public void onRemoveItemClick(InvoiceItemUi invoiceItem) {
mPresenter.deleteInvoiceItem(invoiceItem.getProductId());
}
});
mInvoiceItemsList.setAdapter(mInvoiceItemAdapter);
mInvoiceItemsList.addItemDecoration(decoration);

Importes totales

De nuevo usamos findViewById() y obtenemos las 3 referencias de la sección de


totales.

Las que inicializamos con el valor 0.

// Importes totales
mSubtotalText = (TextView) root.findViewById(R.id.subtotal_value);
mTaxText = (TextView) root.findViewById(R.id.tax_value);
mTotalText = (TextView) root.findViewById(R.id.total_value);
mSubtotalText.setText("$0");
mTaxText.setText("$0");
mTotalText.setText("$0");

5.7 Cargar Ítems De Factura

Invocaremos el método loadInvoiceItems() en onResume() del fragmento para


mantener actualizada la lista de ítems:

@Override
public void onResume() {
super.onResume();
mPresenter.loadInvoiceItems();
}
5.8 Guardar Factura

Agrega este método para recibir el evento del icono de “check” que tendremos en la
Toolbar de la actividad.

La reacción del mismo será la ejecución de saveInvoice() por parte del presentador.

Donde los valores de los parámetros se obtendrán así:

 customerId:Pasamos el campo global mCustomerId


 date: Pasamos el resultado getText().toString() del campo de fecha

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_save_invoice) {
mPresenter.saveInvoice(
mCustomerId,
mDate
);
}
return super.onOptionsItemSelected(item);
}

5.9 Mostrar Nombre Del Cliente

Asigna el texto entrante al campo del cliente y deshabilita el error del wraper:

@Override
public void showCustomer(String name) {
mCustomerFieldWrapper.setErrorEnabled(false);
mCustomerField.setText(name);
}
5.10 Mostrar Error De Cliente No Seleccionado

Avisa al usuario que no ha seleccionado un cliente para la factura en el campo de


texto.

@Override
public void showCustomerError() {
mCustomerFieldWrapper.setErrorEnabled(true);
mCustomerFieldWrapper.setError("El cliente es requerido");
requestViewFocus(mCustomerFieldWrapper);
}

Usamos requestViewFocus() para desplazar la visual hacia el wraper:

private void requestViewFocus(View view) {


ViewParent parent = view.getParent();
parent.requestChildFocus(view, view);
}

5.11 Mostrar Error De Ausencia De Items

Avisa al usuario que no ha seleccionado al menos un ítem.

@Override
public void showItemsError() {
mAddItemButtonWrapper.setErrorEnabled(true);
mAddItemButtonWrapper.setError("Al menos un item es requerido");
requestViewFocus(mAddItemButtonWrapper);
}

5.12 Redirigir A Actividad De Facturas

Hacemos que la actividad actual termine para dirigirnos a la actividad de facturas y


avisar de una creación exitosa.

@Override
public void showInvoicesScreen(String invoiceId) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}

5.13 Mostrar Error De Guardado

Crea un Toast para avisarle al usuario que la factura no pudo ser guardada.
Importante que guardes el mensaje en los recursos:

@Override
public void showSaveError(String error) {
Toast.makeText(getActivity(), error, Toast.LENGTH_SHORT).show();
}

5.14 Iniciar Actividad De Clientes

Antes de ejecutar la actividad de clientes es necesario pensar en que es lo que


deseamos.

Y eso es: retornar ID del cliente.

¿De qué forma hacerlo?

Veamos:

Acción 1. Abre CustomersActivity, luego establece una constante para la


nueva acción de selección de clientes, dos para los extras y otra para la
solicitud:

// Acción de selección de clientes


public static final String ACTION_PICK_CUSTOMER
= "com.hermosaprogramacion.action.ACTION_PICK_CUSTOMER";

Acción 2. Dentro de la escucha de clicks del adaptador en CustomersFragment


comprueba si el intent entrante es de tipo ACTION_PICK_CUSTOMER.

Si es el caso, entonces usa el método setResult() para enviar los extras y


seguido terminar la actividad.
mCustomersAdapter = new CustomersAdapter(new
ArrayList<Customer>(0),
new CustomerItemListener() {
@Override
public void onCustomerClick(Customer clickedCustomer) {

if (CustomersActivity.ACTION_PICK_CUSTOMER.equals(
getActivity().getIntent().getAction())) {

showAddEditInvoiceScreen(clickedCustomer.getId());
}
}
});

El método showAddEditInvoiceScreen() tendría la asignación del ID del cliente


seleccionado al intent de respuesta y la terminación de la actividad:

private void showAddEditInvoiceScreen(String customerId) {


Intent intent = new Intent();

intent.putExtra(CustomersActivity.EXTRA_CUSTOMER_ID,customerId);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
}

Acción 3. Sobrescribe el método Fragment.onActivityResult() en


AddEditInvoiceFragment para llamar al método manageCustomerPickingResult()
del presentador y guardar el ID del cliente.

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode) {
case AddEditInvoiceActivity.REQUEST_PICK_CUSTOMER:
if (Activity.RESULT_OK == resultCode) {
mCustomerId =
data.getStringExtra(CustomersActivity.EXTRA_CUSTOMER_ID);
mPresenter.manageCustomerPickingResult(mCustomerId);
}
break;

}
}

Acción 4. Condiciona la aparición del botón del drawer e intercámbialo por el


Up button en la acción de selección.
Para esto primero crearemos un método booleano llamado isActionPick()
dentro de la actividad de clientes.

El resultado es la comparación entre la acción del intent entrante y la


constante ACTION_PICK_CUSTOMER.

private boolean isActionPick(){


return ACTION_PICK_CUSTOMER.equals(getIntent().getAction());
}

Luego crearemos el método showActionPickNavigation(). En su interior


llamaremos al método setToolbarAsUp() de BaseActivity y le pasaremos una
escucha de clicks que reaccione con onBackPressed():

private void showActionPickNavigation() {


if (isActionPick()) {
setToolbarAsUp(new View.OnClickListener() {
@Override
public void onClick(View view) {
onBackPressed();
}
});
}
}

Seguido, sobrescribimos onResume() de la actividad de clientes y lo


invocamos:

@Override
protected void onResume() {
super.onResume();
showActionPickNavigation();
}

Esto mostrará el Up Button para ir hacia atrás en vez del icono del drawer
cuando estemos en modo de selección.
Terminadas los pasos anteriores, ve a showCustomersScreen() e inicia la actividad de
clientes con startActivityForResult().

Y agrégale la acción ACTION_PICK_CUSTOMER:

@Override
public void showCustomersUi() {
Intent intent = new Intent(getActivity(), CustomersActivity.class);
intent.setAction(CustomersActivity.ACTION_PICK_CUSTOMER);
startActivityForResult(intent,
CustomersActivity.REQUEST_PICK_CUSTOMER);
}

5.15 Mostrar Actividad Para Creación De Ítems

Iniciamos la actividad con la petición REQUEST_ADD_INVOICE_ITEM:

@Override
public void showAddInvoiceItemScreen() {
Intent requestIntent = new Intent(getActivity()
, AddEditInvoiceItemActivity.class);
startActivityForResult(requestIntent
, AddEditInvoiceActivity.REQUEST_ADD_INVOICE_ITEM);
}

5.16 Mostrar Actividad Para Edición De Items

Mostramos la actividad de creación pero esta vez le pasamos el ID del producto


asociado al item de facturaseleccionado que viene como parámetro.

Además pasamos el código REQUEST_EDIT_INVOICE_ITEM.

Y obviamente iniciamos la actividad con startActivityForResult().

@Override
public void showEditInvoiceItemScreen(@NonNull String productId) {
Intent requestIntent = new Intent(getContext(),
AddEditInvoiceItemActivity.class);
requestIntent.putExtra(AddEditInvoiceActivity.EXTRA_PRODUCT_ID, productId);
startActivityForResult(requestIntent,
AddEditInvoiceActivity.REQUEST_EDIT_INVOICE_ITEM);
}

Luego en onActivityResult() procesamos el resultado de la creación


(manageAdditionResult()) y edición (manageEditionResult()) del ítem desde el
presentador:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case AddEditInvoiceActivity.REQUEST_PICK_CUSTOMER:
if (Activity.RESULT_OK == resultCode) {
mCustomerId =
data.getStringExtra(CustomersActivity.EXTRA_CUSTOMER_ID);
mPresenter.manageCustomerPickingResult(mCustomerId);
}
break;
case AddEditInvoiceActivity.REQUEST_ADD_INVOICE_ITEM:
if (Activity.RESULT_OK == resultCode) {
mPresenter.manageAdditionResult();
}
break;
case AddEditInvoiceActivity.REQUEST_EDIT_INVOICE_ITEM:
if (Activity.RESULT_OK == resultCode) {
mPresenter.manageEditionResult();
}
break;
}
}

5.17 Mostrar Ítems De Factura


Reemplazamos los datos del adaptador con replaceData() y deshabilitamos el error
de asusencia de items por si se había mostrado antes:

@Override
public void showInvoiceItems(List<InvoiceItemUi> invoiceItemUis) {
mInvoiceItemAdapter.replaceData(invoiceItemUis);
mAddItemButtonWrapper.setErrorEnabled(false);
}
5.18 Mostrar Importes Totales De La Factura

Sobrescribimos el método showInvoiceAmounts() para asignar los valores String


entrantes a los text views correspondientes:

@Override
public void showInvoiceAmounts(String subtotal, String tax, String total)
{
mSubtotalText.setText(subtotal);
mTaxText.setText(tax);
mTotalText.setText(total);
}

5.19 Guardar Estado Actual Del Fragmento

La selección del cliente trae consigo el identificador del mismo para retenerlo en el
campo mCustomerId.

Sin embargo al haber un cambio de configuración, la instancia del fragmento


perderá su valor.

Para resolver este inconveniente sobrescribimos a onSaveInstaceState() y


guardamos el valor en el parámetro de entrada tipo Bundle.

Por supuesto que con anterioridad debemos haber definido la constante para la clave
del valor.

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(BUNDLE_CUSTOMER_ID, mCustomerId);
}

Luego lo recuperaremos al final de onCreateView() y llamamos al método


restoreState() del presentador:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// ...
// Restablecer estado anterior
if(savedInstanceState!=null){
mCustomerId = savedInstanceState.getString(BUNDLE_CUSTOMER_ID);
mPresenter.restoreState(mCustomerId);
}

setHasOptionsMenu(true);
return root;
}

5.20 Relacionar Presentador A La Vista

Como ya lo practicamos antes, asigna la instancia del presentador:

@Override
public void setPresenter(AddInvoiceMvp.Presenter presenter) {
mPresenter = Preconditions.checkNotNull(presenter);
}
Tarea #6. Crear Presentador

6.1 Crear Implementación

Genera la clase AddEditInvoicePresenter en addeditinvoice/presentation y añade la


estructura básica de implementación del contrato.

public class AddEditInvoicePresenter implements


AddEditInvoiceMvp.Presenter {

6.2 Definir Campos

De acuerdo a la sección de arquitectura los campos de relación son:

 La vista
 El caso de uso para obtener el cliente
 El caso de uso para guardar la factura
 Caché de ítems de factura

Observemos:

public class AddEditInvoicePresenter implements


AddEditInvoiceMvp.Presenter {

// Relaciones
private AddEditInvoiceMvp.View mView;
private IGetCustomers mGetCustomer;
private ISaveInvoice mSaveInvoice;
private ICacheInvoiceItemsStore mCache;

public AddEditInvoicePresenter(@NonNull AddEditInvoiceMvp.View view,


@NonNull IGetCustomers getCustomer,
@NonNull ISaveInvoice saveInvoice,
@NonNull ICacheInvoiceItemsStore
cache) {
mView = Preconditions.checkNotNull(view);
mGetCustomer = Preconditions.checkNotNull(getCustomer);
mSaveInvoice = Preconditions.checkNotNull(saveInvoice);
mCache = Preconditions.checkNotNull(cache);
}

Veamos el contenido de los métodos.

6.3 Manejar Resultado De Selección De Cliente

Para manejar el resultado de seleccionar un cliente, necesitamos ejecutar al caso de


uso para obtener clientes.

No obstante, crearemos una especificación para retornar un cliente por su ID.

Añade al paquete customers/domain/criteria una nueva clase que implemente la


interfaz MemorySpecification llamada CustomerById, donde su método
isSatisfiedBy() genere coincidencias a partir del ID:

public class CustomerById implements MemorySpecification<Customer> {


private String mCustomerId;

public CustomerById(String customerId) {


mCustomerId = Preconditions.checkNotNull(customerId,
"customerId no puede ser null");
}

@Override
public boolean isSatisfiedBy(Customer item) {
return mCustomerId.equals(item.getId());
}
}

Con esto ya es posible obtener el cliente y mostrar su nombre en la vista


(loadCustomer()):

@Override
public void manageCustomerPickingResult(final String customerId) {
loadCustomer(customerId);
}

private void loadCustomer(String customerId) {


Query query = new Query(new CustomerById(customerId),
null, 0, 0, 0);

mGetCustomer.execute(query, false, new


IGetCustomers.ExecuteCallback() {
@Override
public void onSuccess(List<Customer> customers) {
if (!customers.isEmpty()) {
mView.showCustomer(customers.get(0).getName());
}
}

@Override
public void onError(String error) {
mView.showCustomerError();
}
});
}

6.4 Manejar Resultados De Creación/Edición

Los resultados de crear y editar no se ven reflejados con datos resultantes de la


actividad AddEditInvoiceItemActivity, por lo que los dejaremos vacíos.

Pero si en tu caso requieres procesar alguna acción adicional, te quedan a


disposición.

@Override
public void manageAdditionResult() {

@Override
public void manageEditionResult() {

}
6.5 Guardar Factura

Aquí seguiremos los siguientes pasos:

1. Crear una nueva instancia Invoice con los parámetros de entrada


2. Validar la integridad de los parámetros que lo requieran. Lanza un mensaje de
error si no se cumplen
3. Ejecutar el caso de uso para guardar la entidad.
a. Si todo sale bien, limpiamos la caché de ítems y redirigimos al usuario
hacia la lista de facturas
b. De lo contrario mostramos un error

Las instrucciones anteriores en código se verían así:

@Override
public void saveInvoice(String customerId, Date date) {

Invoice newInvoice = new Invoice(


customerId,
date,
mCache.getInvoiceItems(),
mCache.getTotal());

boolean invalidUserInput = false;

if (newInvoice.emptyCustomer()) {
mView.showCustomerError();
invalidUserInput = true;
}

if (newInvoice.noItems()) {
mView.showItemsError();
invalidUserInput = true;
}

if (!invalidUserInput) {
mSaveInvoice.execute(newInvoice, new
ISaveInvoice.ExecuteCallback() {
@Override
public void onSuccess(Invoice invoice) {
mCache.deleteAll();
mView.showInvoicesScreen(invoice.getId());
}

@Override
public void onError(String error) {
mView.showSaveError(error);
}
});
}
}

6.6 Seleccionar Cliente

El cuerpo de este es súper fácil, ya que le decimos a la vista que ejecute la screen de
los clientes:

@Override
public void selectCustomer() {
mView.showCustomersScreen();
}

6.7 Crear Ítem

Exactamente igual que el anterior. Ordenamos a la vista abrir la screen para añadir
ítem de factura.

@Override
public void addNewInvoiceItem() {
mView.showAddInvoiceItemScreen();
}

6.8 Editar Ítem

Ordenamos a la vista iniciar la actividad de edición para ítems de factura:

@Override
public void editInvoiceItem(String productId) {
mView.showEditInvoiceItemScreen(productId);
}
6.9 Eliminar Ítem De Factura

Borramos el ítem de la cache y recargamos los ítems para UI:

@Override
public void deleteInvoiceItem(final String productId) {
// Eliminar ítem
mCache.deleteInvoiceItem(productId);

// Cargar ítems restantes


loadInvoiceItemsUi();
}

private void loadInvoiceItemsUi() {


mCache.getInvoiceItemsUi(new
ICacheInvoiceItemsStore.LoadInvoiceItemsUiCallback() {
@Override
public void onInvoiceItemsLoaded(List<InvoiceItemUi> invoiceItemUis) {

// Actualizar lista de items


mView.showInvoiceItems(invoiceItemUis);

// Actualizar totales
mView.showInvoiceAmounts(
formatAmount(mCache.getSubtotal()),
formatAmount(mCache.getTax()),
formatAmount(mCache.getTotal()));
}

@Override
public void onDataNotAvailable() {

}
});
}

El método loadInvoiceItemsUi() obtiene de la caché los ítems existentes en forma de


InvoiceItemUi y los muestra con showInvoiceItems().

Luego formatea los totales albergados en caché con formatAmount() para al final
mostrarlos:

private String formatAmount(float amount) {


return String.format(Locale.ROOT, "$%.2f", amount);
}
6.10 Restaurar Estado Del Fragmento

En el método restoreState() restableceremos aquellas entradas del usuario cuya


obtención no es convencional.

Para nosotros estás entradas son la carga del cliente asociado a la factura y la lista de
ítems.

Por la misma razón llamaremos a los métodos encargados de cargar ambos aspectos:

@Override
public void restoreState(@NonNull String customerId) {
loadCustomer(customerId);
}

Tarea #7. Modificar Actividad

Manejar Eventos De Up Y Back Button

De acuerdo a los puntos de interacción definidos en la sección de bocetos, aquí


debemos lanzar un diálogo con botón positivo y negativo para descartar los cambios.

¿De qué forma solucionarlo?

Aquí está como:

Acción 1. Añade a addeditinvoice/presentation el diálogo


DiscardChangesDialog de tipo DialogFragment.

Importante: La forma de comunicarse hacia la actividad será con la


implementación de nuestra escucha personalizada DiscardDialogListener.
Y sobre todo recuerda usar onAttach() para tomar la instancia de la actividad
para verificar si está usando la interfaz de comunicación:

public class DiscardChangesDialog extends DialogFragment {

public interface DiscardDialogListener {


void onDialogPositiveClick(DialogFragment dialog);

void onDialogNegativeClick(DialogFragment dialog);


}

private DiscardDialogListener mListener;

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

AlertDialog.Builder builder = new


AlertDialog.Builder(getActivity());
builder.setMessage(R.string.message_discard_changes_dialog)
.setPositiveButton(android.R.string.ok, new
DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {

mListener.onDialogPositiveClick(DiscardChangesDialog.this);
}
})
.setNegativeButton(android.R.string.cancel, new
DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {

mListener.onDialogNegativeClick(DiscardChangesDialog.this);
}
});

return builder.create();
}

@Override
public void onAttach(Context activity) {
super.onAttach(activity);
try {
mListener = (DiscardDialogListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " debe implementar DiscardDialogListener");
}
}
}

Acción 2. Implementa la interfaz del diálogo en AddEditInvoiceActivity.

public class AddEditInvoiceActivity extends AppCompatActivity


implements DiscardChangesDialog.DiscardDialogListener {

...
@Override
public void onDialogPositiveClick(DialogFragment dialog) {

@Override
public void onDialogNegativeClick(DialogFragment dialog) {

}
}

Acción 3. Programamos las acciones de cada evento. En el positivo debemos


cerrar la actividad y borrar la caché de ítems.

@Override
public void onDialogPositiveClick(DialogFragment dialog) {
resetCache();
finish();
}

private void resetCache() {


// Limpiar caché de ítems de factura

DependencyProvider.provideCacheInvoiceItemsStore(this).deleteAll();
}

En el caso del controlador del evento negativo, no ejecutaremos acción


alguna ya que solo deseamos quedarnos en la actividad con el estado actual.

Con este dialogo listo, el siguiente movimiento es sobrescribir el método


onBackPressed() para su ejecución:

@Override
public void onBackPressed() {
DiscardChangesDialog dialog = new DiscardChangesDialog();
dialog.show(getSupportFragmentManager(), "DiscardDialog");
}

Y para el evento del up button sobrescribimos a onSupportNavigateUp() para


ejecutar a onBackPressed(). De esta forma tendrán el mismo comportamiento:

@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}

Generar Dominio Y Datos

Abrimos DependecyProvider y escribimos un método para proveer el caso de uso para


guardar facturas y otro para el cálculo de totales.

public static SaveInvoice provideSaveInvoice(){


return new SaveInvoice(provideInvoicesRepository());
}

Añadir Presentación

1. Vamos a onCreate() y realizamos la transacción del fragmento sobre


add_edit_invoice_container:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_edit_invoice);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mActionBar = getSupportActionBar();
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayShowHomeEnabled(true);

AddEditInvoiceFragment view = (AddEditInvoiceFragment)


getSupportFragmentManager()
.findFragmentById(R.id.add_edit_invoice_container);
if (view == null) {
view = AddEditInvoiceFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.add_edit_invoice_container, view)
.commit();
}

}
2. Luego obtenemos el ID de la factura que se fuese a editar desde la pantalla de
detalle. Este valor vendría como extra de tipo String:

String invoiceId= getIntent().getStringExtra("");

Obviamente su resultado será null ya que en esta parte #4 de App Productos no hemos
cubierto el detalle, por lo que no tenemos un identificador del extra.

3. Cambiamos el título de la toolbar entre R.string.add_invoice (“Añadir Factura”) o


R.string.edit_invoice (“Editar Factura”) dependiendo del valor de productId.

setToolbarTitle(invoiceId);

La definición del método anterior sería:

private void setToolbarTitle(String invoiceId) {


if(invoiceId==null){
setTitle(R.string.add_invoice);
}else {
setTitle(R.string.edit_invoice);
}
}

4. En seguida creamos el presentador y lo relacionamos a la vista:

AddEditInvoicePresenter presenter = new AddEditInvoicePresenter(


view,
DependencyProvider.provideGetCustomers(),
DependencyProvider.provideSaveInvoice(),
DependencyProvider.provideCacheInvoiceItemsStore(this)
);

view.setPresenter(presenter);

¡Perfecto!

¡Sección terminada!

Solo nos queda probar los eventos de UI en la Screen de la app versus el cuadro de
puntos de interacción-reacción en el boceto:
Paso #8. Añadir/Editar Ítems De Una
Factura

Diseñar Arquitectura

Crear Diagramas De Clase

En esta sección codificaremos las acciones que vimos en el boceto para la


creación/edición de ítems en una factura.

Según lo que definimos, debemos fabricar esta secuencia:


7. El vendedor selecciona un producto existente
8. Tipea la cantidad de unidades que ordena el cliente
9. (Opcional) Modificación de otros atributos como descuento, impuestos,
descripción etc.
10. Confirma/cancela la inclusión del ítem a la factura

Sabiendo esto podemos definir los comportamientos de nuestros elementos de


arquitectura.

Para la capa de presentación usaremos los siguientes componentes:

Fuentes De Datos

Ya habíamos visto que los ítems de factura los trataremos como agregados.

Por lo cual no crearemos un repositorio para esta entidad, debido a que puede ser
obtenida a través de InvoicesRepository como parte de la raíz (la factura).

Sin embargo tendremos un almacén en caché para sostener la creación momentánea.


Ya que la esperanza de vida de un ítem de factura en este proceso es corta
(dependiendo de la eficiencia del vendedor).
Habiendo aclarado la naturaleza de la persistencia de datos, podemos avanzar a
programar la resolución del formulario.
Tarea #1. Definir Microinteracciones MVP
Antes de continuar vamos a solventar la cadena de eventos desembocados desde la
vista hacia el presentador y viceversa.

De acuerdo a nuestro boceto, nuestra vista debe.

 Mostrar nombre del producto


 Mostrar stock
 Mostrar error si no se seleccionó un producto
 Mostrar error si el producto seleccionado no se encontró
 Mostrar error si la cantidad es mayor al stock
 Redirigir al usuario a la pantalla de productos para selección
 Redirigir al usuario a la pantalla de añadir/editar factura

En caso de edición solo tendremos:

 Mostrar cantidad
 Mostrar error si el ítem de factura editado no se encontró

Ahora, las reacciones por parte del presentador serían:

 Guardar ítem de factura: Recibe cada uno de los atributos para crear un
nuevo ítem de factura
 Selección de producto: Procesa el resultado de la interacción entre actividades
para setear el nombre del producto
 Manejar resultado de selección de producto: Recibe el ID del producto a
tratar
 Restaurar estado: Recarga el producto seleccionado en cambios de
configuración si es que ya había uno
 Poblar ítem de factura: Si es una edición, se debe cargar el ítem a editar sobre
la interfaz

Perfecto. Solo nos queda programar.


Tarea #2. Crear Contrato MVP
Crea el paquete appproductos/addeditinvoiceitem y agrega la interfaz
AddEditInvoiceItemMvp en el subpaquete de presentación.

El paso a seguir es escribir los métodos de ambas, considerando las acciones


nombradas anteriormente:

public interface AddEditInvoiceItemMvp {

interface View {
void showProductName(String productName);

void showStock(String stock);

void showProductNotSelectedError();

void showMissingProduct();

void showQuantityError();

void showProductsScreen();

void showAddEditInvoiceScreen();

void showQuantity(String quantity);

void showMissingInvoiceItem();

void setPresenter(Presenter presenter);


}

interface Presenter {

void saveInvoiceItem(String selectedProductId, String


productName, String quantityString);

void selectProduct();

void manageProductPickingResult(String productId);

void restoreState(String productId);

void populateInvoiceItem();
}
}
Tarea #3. Crear Fragmento
Sigamos el procedimiento común para crear el fragmento:

Click derecho en addeditinvoiceitem > New > Fragment > Fragment (Blank)

Sus atributos a configurar son:

 Fragment Name: AddEditInvoiceItemFragment


 Layout Name: fragment_add_edit_invoice_item
 Include fragment factory methods: Si
 Include interface callbacks: No

Una vez creado:

 Limpiaremos los parámetros agregados por defecto


 Aplicaremos la realización de View
 Agregaremos el campo de relación con el presentador

AddEditInvoiceItemFragment.java

public class AddEditInvoiceItemFragment extends Fragment implements


AddEditInvoiceItemMvp.View {

private AddEditInvoiceItemMvp.Presenter mPresenter;

public AddEditInvoiceItemFragment() {
// Required empty public constructor
}

public static AddEditInvoiceItemFragment newInstance() {


AddEditInvoiceItemFragment fragment = new AddEditInvoiceItemFragment();
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_add_edit_invoice_item,
container, false);
return root;
}

Vale.

Como acto seguido diseñaremos el layout.

La idea general es tomar una card como padre de dos pares etiqueta-campo.

Lo que es:

<android.support.v7.widget.CardView
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="wrap_content"
android:layout_marginLeft="@dimen/keyline_1"
android:layout_marginRight="@dimen/keyline_1"
android:layout_marginTop="@dimen/keyline_1"
android:focusable="true"
android:focusableInTouchMode="true"
app:contentPadding="@dimen/keyline_1"

tools:context="com.hermosaprogramacion.premium.appproductos.addeditinvoiceitem.presentation.
AddEditInvoiceItemFragment">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/product_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_product_name"
android:textColor="@color/colorPrimary" />

<android.support.design.widget.TextInputLayout
android:id="@+id/product_selection_field_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/product_selection_field"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_select_product"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>

<TextView
android:id="@+id/quantity_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_1"
android:text="@string/label_quantity"
android:textColor="@color/colorPrimary" />

<android.support.design.widget.TextInputLayout
android:id="@+id/quantity_field_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintEnabled="false">

<EditText
android:id="@+id/quantity_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/default_quantity_product"
android:inputType="number"
android:text="@string/default_quantity_product" />
</android.support.design.widget.TextInputLayout>

<TextView
android:id="@+id/stock_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textColor="@color/colorPrimary"
tools:text="Existencias: 20" />

</LinearLayout>

</android.support.v7.widget.CardView>

En la vista previa se vería esta imagen:


Posterior, añadiremos las referencias a los campos de texto con los que el usuario
podrá interactuar:

public static final String BUNDLE_PRODUCT_ID = "BUNDLE_PRODUCT_ID";

private AddEditInvoiceItemMvp.Presenter mPresenter;

private TextInputLayout mProductFieldWrapper;


private TextView mProductField;
private TextInputLayout mQuantityFieldWrapper;
private EditText mQuantityField;
private TextView mStockView;

private String mSelectedProductId;

La constante BUNDLE_PRODUCT_ID nos permitirá guardar el ID del producto


seleccionado que mantendremos en mSelectedProductId.
3.1 Configurar Vista De Interfaz De Usuario

En este método tomaremos las referencias UI de los views que el usuario


manipulará.

También asignaremos a mProductField una callback OnClickListener para abrir la


actividad de productos.

Y habilitaremos la contribución a la action bar por parte del fragmento:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_add_edit_invoice_item,
container, false);

mProductFieldWrapper = (TextInputLayout)
root.findViewById(R.id.product_selection_field_wrapper);

mProductField = (TextView) root.findViewById(R.id.product_selection_field);


mProductField.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mPresenter.selectProduct();
}
});

mQuantityFieldWrapper = (TextInputLayout)
root.findViewById(R.id.quantity_field_wrapper);

mQuantityField = (EditText) root.findViewById(R.id.quantity_field);

mStockView = (TextView) root.findViewById(R.id.stock_text);

setHasOptionsMenu(true);
return root;
}

3.2 Cargar Ítem De Factura A Editar

Usaremos el método onResume() para cargar el ítem de factura cada vez que la
actividad vuelva de un segundo plano.

Esto lo logramos con el método populateInvoiceItem() del presentador.


@Override
public void onResume() {
super.onResume();
mPresenter.populateInvoiceItem();
}

3.3 Manejar Eventos De La Toolbar

Para procesar la acción de guardado del ítem es necesario usar a


onOptionsItemSelected().

Llamaremos al método saveInvoiceItem() del presentador y pasaremos el ID del


producto seleccionado, el nombre del producto y la cantidad.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (R.id.action_save_invoice_item == item.getItemId()) {
mPresenter.saveInvoiceItem(
mSelectedProductId,
mProductField.getText().toString(),
mQuantityField.getText().toString()
);
}
return super.onOptionsItemSelected(item);
}

3.4 Mostrar Producto Seleccionado

Seteamos el nombre a mProductField y deshabilitamos el error del text input layout


envolvente:

@Override
public void showProductName(String productName) {
mProductFieldWrapper.setErrorEnabled(false);
mProductField.setText(productName);
}
3.5 Mostrar Cantidad

El método showQuantity() lo usaremos en el caso de que se esté en una edición.

La idea es cargar el dato de la cantidad asociada al producto en el campo.

@Override
public void showQuantity(String quantity) {
mQuantityFieldWrapper.setErrorEnabled(false);
mQuantityField.setText(quantity);
}

3.6 Mostrar Error De Producto No Seleccionado

Usa el método setError() de la envoltura para mostrar el mensaje de error guardado


en recursos.

@Override
public void showProductNotSelectedError() {
mProductFieldWrapper.setErrorEnabled(true);

mProductFieldWrapper.setError(getString(R.string.error_select_product));
requestViewFocus(mProductFieldWrapper);
}

Adicionalmente agregamos el efecto de focus hacia el campo con el método


requestViewFocus().

private void requestViewFocus(View view) {


ViewParent parent = view.getParent();
parent.requestChildFocus(view, view);
}
3.7 Mostrar Error Al No Encontrar Producto

Mostraremos en el campo del nombre un mensaje que avise al usuario que los datos
del producto seleccionado no existen:

@Override
public void showMissingProduct() {
mProductField.setText(R.string.no_data);
}

3.8 Mostrar Error De Sobrepaso De Stock

Invoca a setError() de mQuantityFieldWrapper para asignar el mensaje de error


correspondiente en los recursos.

@Override
public void showQuantityError() {
mQuantityFieldWrapper.setErrorEnabled(true);
mQuantityFieldWrapper.setError(getString(R.string.error_quantity));
requestViewFocus(mQuantityFieldWrapper);
}

3.9 Iniciar Actividad De Productos

Iniciamos la actividad de productos en un modo de selección personalizado como


acción del Intent enviado.

Veamos como lo logramos:

Modificación 1. Agregamos dos constantes en ProductsActivity. Una para la


acción de selección y otra para el extra que será devuelto (ID del producto).

public final static String ACTION_PICK_PRODUCT


= "com.hermosaprogramacion.action.ACTION_PICK_PRODUCT";
public static final String EXTRA_PRODUCT_ID =
"com.hermosaprogramacion.EXTRA_PRODUCT_ID";
Por el lado de AddEditInvoiceItemActivity necesitaremos una constante para
identificar la petición hacia la actividad de productos:

public final static int REQUEST_PICK_PRODUCT = 1;

Modificación 2. Abrimos ProductsFragment, luego comparamos que la acción


del intent entrante sea de selección y enviamos el producto seleccionado
dentro de la escucha de clicks del adaptador:

private ProductsAdapter.ProductItemListener mItemListener =


new ProductsAdapter.ProductItemListener() {
@Override
public void onProductClick(Product clickedProduct) {
FragmentActivity activity = getActivity();
if (ProductsActivity.ACTION_PICK_PRODUCT
.equals(activity.getIntent().getAction())) {
showAddEditInvoiceItemScreen(clickedProduct);
} else {

mProductsPresenter.openProductDetails(clickedProduct.getCode());
}
}
};

El método showAddEditInvoiceItemScreen() simplifica la creación de un intent


de respuesta con el extra del ID de producto:

private void showAddEditInvoiceItemScreen(Product clickedProduct) {


Intent responseIntent = new Intent();
responseIntent.putExtra(ProductsActivity.EXTRA_PRODUCT_ID,
clickedProduct.getCode());
getActivity().setResult(Activity.RESULT_OK, responseIntent);
getActivity().finish();
}

Después de esto nos queda iniciar la actividad con startActivityForResult() en


showProductsScreen():

@Override
public void showProductsScreen() {
Intent requestIntent = new Intent(getActivity(), ProductsActivity.class);
requestIntent.setAction(ProductsActivity.ACTION_PICK_PRODUCT);
startActivityForResult(requestIntent,
AddEditInvoiceItemActivity.REQUEST_PICK_PRODUCT);
}

Luego procesamos el resultado en el presentador desde onActivityResult() para que


el presentador maneje el resultado, en este caso el identificador del producto:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent
data) {
if (AddEditInvoiceItemActivity.REQUEST_PICK_PRODUCT == requestCode
&& Activity.RESULT_OK == resultCode) {

mSelectedProductId =
data.getStringExtra(ProductsActivity.EXTRA_PRODUCT_ID);
mPresenter.manageProductPickingResult(mSelectedProductId);
}
}

3.10 Retornar A La Edición De Factura

Para volver a la actividad de edición seteamos el código de resultado


Activitiy.RESULT_OK para indicar que hubo una creación exitosa del ítem de factura.
Luego terminamos la actividad:

@Override
public void showAddEditInvoiceScreen() {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}

3.11 Mostrar Error De Ítem De Factura No Encontrado

De forma similar a showMissingProduct(), showMissingInvoiceItem() setea un texto


avisando de la no existencia de datos del ítem de factura que supuestamente se está
editando:

@Override
public void showMissingInvoiceItem() {
mProductField.setText(R.string.no_data);
mQuantityField.setText("0");
}

Una solución alternativa para solventar la experiencia de usuario sería que muestres
un estado de vacío, sin dar la posibilidad de seguir editando el ítem.

3.12 Guardar Estado De La Vista

Evitaremos la pérdida del identificador del producto seleccionado sobrescribiendo a


onSaveInstanceState() de fragmento.

Nuestro propósito será poner este valor en el Bundle que viene como parámetro si es
que existe:

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mSelectedProductId != null) {
outState.putString(BUNDLE_PRODUCT_ID, mSelectedProductId);
}
}

Luego podremos recuperarlo al final de onCreateView():

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//...

// Restaurar estado
if (savedInstanceState != null
&& savedInstanceState.containsKey(BUNDLE_PRODUCT_ID)) {
mSelectedProductId = savedInstanceState.getString(BUNDLE_PRODUCT_ID);
mPresenter.restoreState(mSelectedProductId);
}

setHasOptionsMenu(true);
return root;
}
Tarea #4. Crear Presentador

4.1 Crear Implementación

Añade una nueva clase llamada AddEditInvoiceItemPresenter al paquete de


presentación y hazla implementar a Presenter:

public class AddEditInvoiceItemPresenter implements


AddEditInvoiceItemMvp.Presenter {

private final AddEditInvoiceItemMvp.View mView;

@Override
public void populateInvoiceItem() {

@Override
public void saveInvoiceItem(String selectedProductId, String productName,
String quantityString) {

@Override
public void restoreState(String productId) {
}

@Override
public void selectProduct() {
}

@Override
public void manageProductPickingResult(final String productId) {

}
}

4.2 Definir Campos

En cuanto a relaciones tenemos:

 La vista
 El caso de uso para obtener el producto
 La caché de ítems de factura
 La fuente de datos de recursos en Android. De utilidad para formatear el texto
del stock con un recurso string que tendrá un placeholder %d
 ID del ítem de factura a editar si fuera el caso
 El precio del ítem y el stock. Ambos son valores que no podemos obtener del
usuario, si no desde la selección del producto hecha en el presentador

Sabido esto podemos declararlos como campos y tomar sus referencias desde el
constructor.

public class AddEditInvoiceItemPresenter implements


AddEditInvoiceItemMvp.Presenter {

private final AddEditInvoiceItemMvp.View mView;


private final IGetProducts mGetProductByCode;
private final ICacheInvoiceItemsStore mInvoiceItemsCache;
private final Resources mResources;

private String mProductId;

private float mItemPrice;


private int mStock;

public AddEditInvoiceItemPresenter(@Nullable String productId,


@NonNull AddEditInvoiceItemMvp.View view,
@NonNull IGetProducts getProductByCode,
@NonNull ICacheInvoiceItemsStore
invoiceItemsCache,
@NonNull Resources resources) {
mProductId = productId;
mView = Preconditions.checkNotNull(view);
mGetProductByCode = Preconditions.checkNotNull(getProductByCode);
mInvoiceItemsCache = Preconditions.checkNotNull(invoiceItemsCache);
mResources = Preconditions.checkNotNull(resources);
}

...

A continuación completemos la lógica de los métodos:

4.3 Cargar Ítem De Factura A Editar

En el caso de que el usuario haya optado por una edición, cargaremos el producto
con el caso de uso y asignaremos el resultado a la vista.

@Override
public void populateInvoiceItem() {
if (isEdition()) {
mInvoiceItemsCache.getInvoiceItemUi(mProductId,
new ICacheInvoiceItemsStore.GetInvoiceItemUiCallback() {
@Override
public void onInvoiceItemUiLoaded(InvoiceItemUi
invoiceItemUi) {

mItemPrice = invoiceItemUi.getItemPrice();
mStock = invoiceItemUi.getProductStock();

showItem(invoiceItemUi.getProductName(),
mStock,
invoiceItemUi.getQuantity());
}

@Override
public void onDataNotAvailable() {
mView.showMissingInvoiceItem();
}
});

}
}

La validación de edición será con el nuevo método isEdition(), el cuál comprueba si


mProductId no es nulo:

private boolean isEdition() {


return mProductId != null;
}

La simplificación para mostrar el nombre del producto, el stock y la cantidad se


logra con showItem():

private void showItem(String productName, int productStock, int quantity)


{
String stock = mResources.getString(R.string.product_stock,
productStock);
String quantityString = String.valueOf(quantity);

mView.showProductName(productName);
mView.showStock(stock);
mView.showQuantity(quantityString);
}

Es aquí donde usamos los recursos para formatear el siguiente <string> sobre el
stock:

<string name="product_stock">Existencias: %d</string>


4.4 Guardar Ítem De Factura

Para nosotros guardar el ítem de factura significa lo siguiente:

4. Validar que la cantidad tenga no esté vacía


5. Validar que el campo del producto no esté vacío
6. Validar que la cantidad no supere al stock
7. Determinar si el ID del producto aún sigue siendo el de la edición o si viene
por una creación
8. Crear un objeto InvoiceItem con el ID del producto, la cantidad y el precio
resultante
9. Guardar el objeto en la caché
10. Volver a la actividad de creación de la factura

Traduciéndolo a código:

@Override
public void saveInvoiceItem(String selectedProductId, String productName,
String quantityString) {
String productId;
int quantity = 1;

if (!quantityString.isEmpty()) {
quantity = Integer.parseInt(quantityString);
}

if (Strings.isNullOrEmpty(productName)) {
mView.showProductNotSelectedError();
return;
}

if (quantity > mStock) {


mView.showQuantityError();
return;
}

productId = selectedProductId == null ? mProductId :


selectedProductId;

InvoiceItem invoiceItem = new InvoiceItem(


productId,
quantity,
mItemPrice);
mInvoiceItemsCache.saveInvoiceItem(invoiceItem);
mView.showAddEditInvoiceScreen();

4.5 Seleccionar Producto

Le ordenamos a la vista mostrar la pantalla de productos:

@Override
public void selectProduct() {
mView.showProductsScreen();
}

4.6 Manejar Resultado De La Selección Del Producto

Debido a que en manageProductPickingResult() recibimos el ID del producto, lo que


haremos será llamar al caso de uso IGetProducts con la especificación
ProductByCodeSpecification.

Una vez conseguido el resultado, seteamos el nombre, el stock y 1 unidad como


nuevo ítem.

@Override
public void manageProductPickingResult(final String productId) {
if (Strings.isNullOrEmpty(productId)) {
mView.showMissingProduct();
return;
}

Query query = new Query(new ProductByCodeSpecification(productId));


mGetProductByCode.getProducts(query, false, new
IGetProducts.GetProductsCallback() {
@Override
public void onSuccess(List<Product> products) {
Product selectedProduct = products.get(0);

mItemPrice = selectedProduct.getPrice();
mStock = selectedProduct.getUnitsInStock();

showItem(selectedProduct.getName(), mStock, 1);


}
@Override
public void onError(String error) {
mView.showMissingProduct();
}
});
}

Tarea #4. Modificar Actividad

Añadir Presentación

Nuestro paso a seguir es crear las instancias del fragmento y el presentador en


onCreate() para darle funcionalidad total a la característica.

Recuerda que el ID del producto para el ítem de factura a editar puedes obtenerlo
como extra.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_edit_invoice_item);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mActionBar = getSupportActionBar();
mActionBar.setDisplayShowHomeEnabled(true);
mActionBar.setDisplayHomeAsUpEnabled(true);

AddEditInvoiceItemFragment view = (AddEditInvoiceItemFragment)


getSupportFragmentManager()
.findFragmentById(R.id.add_edit_invoice_item_container);

if (view == null) {
view = AddEditInvoiceItemFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.add_edit_invoice_item_container, view)
.commit();
}

String productId = getIntent().getStringExtra(


AddEditInvoiceActivity.EXTRA_PRODUCT_ID);

// Cambiar título de la actividad


if (productId == null) {
setTitle(R.string.add_invoice_item);
} else {
setTitle(R.string.edit_invoice_item);
}

AddEditInvoiceItemPresenter presenter =
new AddEditInvoiceItemPresenter(
productId,
view,
DependencyProvider.provideGetProducts(this),

DependencyProvider.provideCacheInvoiceItemsStore(this),
getResources());
view.setPresenter(presenter);
}

El ID del producto también puede permitirnos cambiar el título de la actividad para


la edición o creación.

Adicional a eso, la dependencia de la cache la obtendremos con


provideCacheInvoicesItemStore():

public static CacheInvoiceItemsStore provideCacheInvoiceItemsStore(@NonNull


Context context) {
return
CacheInvoiceItemsStore.getInstance(provideProductsRepository(context));
}

Y obviamente los recursos con el método getResources() de la actividad.

Finalmente…

Solo nos queda probar los eventos de UI en la Screen de la app versus el cuadro de
puntos de interacción-reacción en el boceto:
Conclusión
En este tutorial aprendimos varios conocimientos de los cuales estoy seguro estarán
en tu mente por largo tiempo.

¿Y es que de qué fuimos capaces?

Fíjate:

 Creamos un menú horizontal desplegable


 Creamos una actividad de facturas
 Creamos una actividad de clientes
 Actualizamos la actividad de productos para selección
 Creamos una actividad para crear facturas
 Creamos una actividad para crear/editar los ítems de las facturas
 Y tantas otras que se me escapan…

Aunque fue un proceso largo, estamos aliviados porque conseguimos un excelente


resultado, el cual te será de utilidad en muchos proyectos más.

Y es algo por lo que te felicito: ¡Muy bien!

Déjame saber que te problemas tuviste al desarrollar el detalle del producto en los
comentarios del artículo. Como viste este tutorial y sus contenidos; Y que buenas
experiencias te quedaron al terminarlo.
¿Cuáles Son Los Pasos A Seguir?
Si este tutorial te pareció muy útil y piensas que es un recurso que tus colegas,
amigos, familiares, profesores, alumnos o socios deben conocer, entonces ayúdame
avisándoles en tus redes sociales:

Ahora, si quieres profundizar en bases de datos, descargar plantillas de nombrado o


planeación de un proyecto Android o ver mi ebook de gratuito de inicios en
Android, entonces ve mi LIBRERÍA ANDROID.
En ella pongo todos los recursos descargables que voy creando. Así que dale una
mirada de vez en cuando para ver si he liberado algún contenido nuevo.

Bien, y si lo que quieres es recibir boletines informativos con creaciones de nuevos


tutoriales, noticias, descuentos y tips los miércoles de cada semana, entonces
suscríbete a mi Newsletter para estar al día con los avances de mi blog:

Inscribirme al boletín de Hermosa Programación

Adicionalmente puedes seguirme en las redes sociales. Por estos medios también
comparto noticias que pueden serte de interés en el mundo del desarrollo Android:
Por otro lado:

Puede que en este tutorial o en algunos de mi blog, varios temas o procedimientos se


traten de forma rápida.

No es que no quiera explicarlos. Es solo que ya existen en artículos previos donde he


explicado a fondo dicho conocimiento y doy por hecho que ya lo has leído.

La forma en que puedes saber si ya está explicado es ir al INDICE DE


CONTENIDOS ANDROID del blog.
En este espacio encontrarás todo lo que he escrito sobre Android y si quieres acceder
a él rápidamente añádelo a tus Marcadores.

Finalmente…
Si has encontrado un error de redacción, o si tienes una sugerencia de didáctica, o
algún error de compilación/importación/configuración en Android Studio, házmelo
saber de inmediato a productos@hermosaprogramacion.com.

Haré lo posible por ayudarte lo más pronto posible (en ocasiones me demoro por que
recibo toneladas de mails).

Para mí es muy importante saber que mis contenidos son de excelente calidad y que
no defraudan a mis lectores, y mucho menos deseo que se sientan estafados.
Así que cualquier duda, no dudes en contármela :D

¡Saludos y buena vibra! ,


James

También podría gustarte