Documentos de Académico
Documentos de Profesional
Documentos de Cultura
App Productos 4
App Productos 4
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.
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:
Subcaso: Este podrá buscar por nombre del cliente y filtrar los
resultados por fecha, monto y tipo de pago.
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:
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.
Ahora bien, ¿Qué atributos nos indica la farmacia que debe tener persistencia?
Fíjate:
Listar Clientes
De forma similar a la necesidad de revisar pedidos, los clientes también son
requeridos:
Los siguientes son los datos que la farmacia desea almacenar por cada cliente:
Crear Pedidos
Descripción:
Reglas de negocio:
No lo olvides:
Has una correcta investigación para profundizar y descubrir sobre las reglas de
negocio del tu problema.
Modelo De Datos
En resumidas cuentas, las definiciones anteriores de las entidades pueden
relacionarse y establecerse en el siguiente diagrama entidad-relación:
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.
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.
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)
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)
Te recomiendo sigas con el diseño de cards para separar el formato por áreas de
contenidos visibles.
Tap en Up/Back Button Vuelve a la screen de nueva factura sin efecto alguno
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.
Una vez lo hayas hecho puedes seguir con facilidad los siguientes pasos.
Esto quiere decir que deseamos que cada actividad tenga el mismo comportamiento
en su NavigationView.
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.
Ahora, nos queda crear la actividad padre BaseActivity para que asuma las
responsabilidades del navigation drawer.
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.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nav_drawer);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
Tarea #2 Modificar Layouts De Las Actividades
<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
...
<!--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>
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).
<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>
Además del vector ic_account_circle.xml que representa la imagen por defecto del
perfil, el cual puedes obtener desde el siguiente link.
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.
</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.
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.
Crearemos un campo enumerado con el fin de tomar las referencias de los IDs del
menú para los ítems activables en el drawer.
NavDrawerItemEnum(int navItemId) {
id = navItemId;
}
Inicializar Actividad
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.
@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();
}
}
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():
@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
getToolbar();
}
Hemos tenido un avance inicial que actuará como base en la preparación del menú
horizontal.
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);
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
}
};
mDrawerLayout.addDrawerListener(mDrawerToggle);
mDrawerToggle.syncState();
}
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.
Para intervenir en las interacciones del usuario al tocar las secciones del menú
usamos la escucha OnNavigationItemSelectedListener.
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.
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;
}
});
// Cerrarmos el drawer
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(GravityCompat.START);
}
}
Para las opciones que requieran el inicio de una sesión he creado un método con el
nombre launchSectionActivity():
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.
// Poner email
TextView userNameView = (TextView)
mNavHeader.findViewById(R.id.salesman_name);
String userName = UserPrefs.getInstance(this).getUserName();
userNameView.setText(userName);
}
...
String getUserName();
}
...
@Override
public String getUserName() {
return mSharedPreferences.getString(PREF_USERNAME, null);
}
}
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.
@Override
public void setContentView(int layoutResID) {
setNavDrawerCheckedItem();
setupUserBox();
}
Cerrar Drawer Si El Back Button Es Presionado
Ya sabemos que para procesar el evento de presión del Back Button se usa el
método onBackPressed().
@Override
public void onBackPressed() {
if(mDrawerLayout.isDrawerOpen(GravityCompat.START)){
mDrawerLayout.closeDrawer(GravityCompat.START);
}else {
super.onBackPressed();
}
}
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.
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
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.
//...
@Override
protected NavDrawerItemEnum getNavDrawerItem() {
return NavDrawerItemEnum.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.
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.
activity_invoices.xml
<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>
Así:
InvoicesActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_invoices);
@Override
protected NavDrawerItemEnum getNavDrawerItem(){
return NavDrawerItemEnum.INVOICES;
}
}
Crear Action Buttons
Invoices_menu.xml
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.invoices_menu, menu);
return true;
}
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:
<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>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_customers);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
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;
}
Crea un nuevo recurso de menú y añade este elemento (aprovecha para incorporar
otros que tengas por cuenta propia):
customers_menu.xml
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.customers_menu, menu);
return true;
}
4. Crear Actividad Para Añadir/Editar Factura
Modificar Layout
activity_add_edit_invoice.xml
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>
Para ello declararemos un campo tipo ActionBar y luego usaremos a los métodos
setDisplayHomeAsUpEnabled() y setDisplayShowHomeEnabled() en onCreate().
AddEditInvoiceActivity.java
@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
¿Sus características?
Veamos:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.add_edit_invoice_menu, menu);
return true;
}
Crea la clase Java de la actividad a través del asistente y su secuencia New >
Activity > Basic Activity.
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
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
AddEditInvoiceItemActivity.java
@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);
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.add_edit_invoice_item_menu, menu);
return true;
}
@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.
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.
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.
No está de más decirte que las propiedades get/set pueden autogenerarse con Click
derecho dentro de la clase > Generate… > Getter and Setter:
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.
Aquí defines las operaciones estándar que serán realizadas sobre los objetos del
modelo.
¿Cuáles serán?
void deleteCustomers();
boolean isCacheReady();
}
/**
* Este campo representa la caché fundamental de clientes
*/
HashMap<String, Customer> mCachedCustomers = null;
/**
* 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;
}
}
getCustomers(Query query)
Lo bueno fue que hicimos unas clases de alcance global que comprendían cualquier
objeto de dominio.
@Override
public List<Customer> selectListRows(List<Customer> items) {
return null;
}
}
@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;
}
@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);
}
});
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:
@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()
@Override
public void deleteCustomers() {
if (mCachedCustomers == null) {
mCachedCustomers = new LinkedHashMap<>();
}
mCachedCustomers.clear();
}
isCacheReady()
@Override
public boolean isCacheReady() {
return mCachedCustomers!=null;
}
Con el objetivo de tener información rápida para poblar nuestra UI, añadiremos 10
clientes de prueba para la farmacia.
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);
}
En este caso necesitamos centralizar la administración sobre los clientes sin importar
desde que fuente de datos provengan.
Mejor dicho:
(No olvides agregar una callback para considerar el desconocimiento del tiempo que
tarda una lectura)
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.
@Override
public void getCustomers(@NonNull Query query, GetCustomersCallback
callback) {
}
}
getCustomers()
@Override
public void getCustomers(@NonNull Query query, GetCustomersCallback
callback) {
callback.onCustomersLoaded(mCacheCustomersStore.getCustomers(query));
}
}
interface ExecuteCallback {
void onSuccess(List<Customer> customers);
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.
@Override
public void execute(@NonNull Query query, boolean refresh,
ExecuteCallback callback) {
}
}
execute()
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);
}
});
}
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);
}
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
@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.
@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
}
@Override
public void onError(String error) {
// TODO: Ocultar indicador de carga
// TODO: Ocultar indicador de página siguiente
// TODO: Mostrar error
}
});
}
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.
void showEmptyState();
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
public CustomersFragment() {
@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);
}
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>
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
<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>
...
}
Como lo expresa el comentario TODO, aún nos falta el adaptador de la lista, así que
efectuemos su creación.
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*()
CustomersAdapter.java
@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);
}
@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();
@Override
public int getItemCount() {
return mItems.size() + (mIsLoading ? 1 : 0);
}
@Override
public boolean isLoadingData() {
return mIsLoading;
}
@Override
public boolean isThereMoreData() {
return mEndless;
}
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int position = getAdapterPosition();
Customer customer = getItem(position);
mItemListener.onCustomerClick(customer);
}
}
onCreate()
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onCreateView()
@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;
}
mSwipeToRefreshView.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
mCustomersPresenter.loadCustomers(true);
}
});
}
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()
@Override
public void showCustomers(List<Customer> customers) {
mCustomersAdapter.replaceItems(customers);
showList(true);
}
@Override
public void showLoadingState(final boolean show) {
if (getView() == null) {
return;
}
mSwipeToRefreshView.post(new Runnable() {
@Override
public void run() {
mSwipeToRefreshView.setRefreshing(show);
}
});
}
showEmptyState()
@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()
@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()
@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");
}
@Override
public void loadCustomers(final boolean manualRefresh) {
@Override
public void onError(String error) {
mView.showLoadingState(false);
mView.showLoadMoreIndicator(false);
mView.showCustomersError(error);
}
});
}
Por esta razón crearemos la actividad que será el punto de entrada del flujo principal
para que estos componentes cobren vida.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_customers);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportFragmentManager().findFragmentById(R.id.customers_container);
// Instancear presentador
CustomersPresenter presenter = new CustomersPresenter(
fragment,
DependencyProvider.provideGetCustomers());
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.
Diseñar Arquitectura
En el dominio tendremos la entidad Factura.
¿Y qué pasa con los ítems que se agregan a una factura, como se representan?
Interfaz De Usuario
A la hora de mostrar una factura en la lista no requeriremos leer todos sus atributos.
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.
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.
InvoiceUi
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:
this(
id,
customerId,
number,
date, 0, state,
new ArrayList<InvoiceItem>(0)
);
}
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.
interface LoadInvoicesUiCallback{
void onInvoicesUiLoaded(List<InvoiceUi> invoiceUis);
void onDataNotAvailable();
}
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.
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
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;
}
}
@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);
}
});
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");
// Selección de facturas
InvoicesSelector selector = new InvoicesSelector(query);
return selector.selectListRows(invoices);
}
@Override
public void getInvoicesUis(final Query query, final LoadInvoicesUiCallback
callback) {
mCustomersRepo.getCustomers(
customerQuery,
new ICustomersRepository.GetCustomersCallback() {
@Override
public void onCustomersLoaded(List<Customer> customers) {
List<InvoiceUi> invoiceUis = new ArrayList<>();
invoiceUis.add(invoiceInfo);
}
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());
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.
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);
}
@Override
public void deleteInvoices() {
if(mCachedInvoices ==null){
mCachedInvoices= new LinkedHashMap<>();
}
mCachedInvoices.clear();
}
@Override
public boolean isCacheReady() {
return mCachedInvoices != null;
}
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"};
// 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.
interface GetInvoicesUiCallback {
void onInvoicesInfoLoaded(List<InvoiceUi> invoicesInfos);
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
@Override
public void getInvoices(@NonNull Query query, @NonNull
GetInvoicesCallback callback) {
if (mCacheInvoicesStore.isCacheReady()) {
callback.onInvoicesLoaded(mCacheInvoicesStore.getInvoices(query));
}
}
@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
@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.
Crear interfaz
Crear Implementación
@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.
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.
Cargar facturas
Crear una nueva factura
Manejar resultado de la creación de factura
void showEmptyState();
void showSuccessfullySavedMessage();
interface Presenter {
void loadInvoices(boolean refresh, boolean resume);
void addNewInvoice();
@Override
public void loadInvoices(final boolean manualRefresh, final boolean resume) {
@Override
public void addNewInvoice() {
}
@Override
public void manageSavingResult(int requestCode, int resultCode) {
}
}
Cargar Facturas
@Override
public void loadInvoices(final boolean manualRefresh, final boolean
resume) {
if (normalLoad) {
mView.showLoadingState(true);
mCurrentPage = 1; // Reset del páginado
} else {
mView.showLoadMoreIndicator(true);
mCurrentPage++; // Preparar página siguiente
}
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);
}
@Override
public void onError(String error) {
mView.showLoadingState(false);
mView.showLoadMoreIndicator(false);
mView.showInvoicesError(error);
}
});
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();
}
@Override
public void manageSavingResult(int requestCode, int resultCode) {
if (InvoicesActivity.REQUEST_ADD_INVOICE == requestCode
&& Activity.RESULT_OK == resultCode) {
mView.showSuccessfullySavedMessage();
}
}
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);
}
El layout de los ítems para las facturas posee 2 grupos de 3 textos como lo refleja el
boceto.
<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>
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>
Una vez completo el layout, vuelve al fragmento y agrega los campos relacionados.
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
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == TYPE_NEXT_PAGE_INDICATOR) {
view = inflater.inflate(R.layout.item_loading_footer, parent, false);
return new InvoicesAdapter.NextPageIndicatorHolder(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();
@Override
public int getItemCount() {
return mItems.size() + (mIsLoading ? 1 : 0);
}
holder.customerName.setText(invoice.getCustomerName());
holder.number.setText(invoice.getNumber());
holder.date.setText(date);
holder.totalAmount.setText(totalAmount);
mIsLoading = true;
mIsLoading = false;
@Override
public boolean isLoadingData() {
return mIsLoading;
}
@Override
public boolean isThereMoreData() {
return mEndless;
}
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);
}
NextPageIndicatorHolder(View view) {
super(view);
progress = (ProgressBar) view.findViewById(R.id.progressBar);
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@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();
if (savedInstanceState != null) {
showList(true);
}
return rootView;
}
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.
Cargar Facturas
@Override
public void onResume() {
super.onResume();
mInvoicesPresenter.loadInvoices(false, true);
}
Mostrar Facturas
@Override
public void showInvoices(List<InvoiceUi> invoices) {
mInvoicesAdapter.replaceItems(invoices);
showList(true);
}
@Override
public void showAddInvoice() {
Intent intent = new Intent(getContext(),
AddEditInvoiceActivity.class);
startActivityForResult(intent, InvoicesActivity.REQUEST_ADD_INVOICE);
}
@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
@Override
public void showEmptyState() {
showList(false);
}
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();
}
@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();
}
}
@Override
public void showSuccessfullySavedMessage() {
Snackbar.make(getActivity().findViewById(android.R.id.content),
R.string.message_successfully_saved_invoice,
Snackbar.LENGTH_LONG).show();
}
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.
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().
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();
}
Ahora es el turno de ejecutar la aplicación para verificar el caso de uso versus las
interacciones del boceto.
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.
En esencia para App Productos ese sería la regla base de este caso de uso y sobre la
cual trabajaremos el desarrollo.
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:
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.
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.
Y recuerda que en el alcance de App Productos parte 4 solo tendremos una fuente de
facturas en memoria.
ProductUi.java
InvoiceItemUi.java
¿Entradas?
¿Procesos?
¿Salidas?
interface ExecuteCallback {
void onSuccess(Invoice invoice);
@Override
public void execute(@NonNull Invoice invoice,
ExecuteCallback callback) {
}
}
Insertar Factura
@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:
En cuanto al presentador:
interface View {
void showCustomer(String name);
void showCustomerError();
void showItemsError();
void showCustomersScreen();
void showAddInvoiceItemScreen();
interface Presenter {
void saveInvoice(String customerId, Date date);
void selectCustomer();
void addNewInvoiceItem();
void manageAdditionResult();
void manageEditionResult();
void loadInvoiceItems();
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.
void onDataNotAvailable();
interface GetInvoiceItemUiCallback {
void onDataNotAvailable();
List<InvoiceItem> getInvoiceItems();
void deleteAll();
float getTotal();
float getSubtotal();
float getTax();
Es decir:
// Totales
private float mTotal;
private float mTax;
private float mSubtotal;
Lo cual te quede de tarea cuando hayas definido las reglas y valores para el negocio
que estudias.
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);
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():
Obtener Totales
@Override
public float getTotal() {
return mTotal;
}
@Override
public float getSubtotal() {
return mSubtotal;
}
@Override
public float getTax() {
return mTax;
}
@Override
public List<InvoiceItem> getInvoiceItems() {
return getValues();
}
@NonNull
private ArrayList<InvoiceItem> getValues() {
return Lists.newArrayList(mCachedInvoiceItems.values());
}
Veamos:
@Override
public void getInvoiceItemsUi(@NonNull final LoadInvoiceItemsUiCallback callback)
{
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();
}
});
@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());
}
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);
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();
}
});
}
@Override
public void deleteInvoiceItem(@NonNull String productId) {
InvoiceItem removedItem = mCachedInvoiceItems.remove(productId);
calculateAmounts(-removedItem.getTotal());
generateItemNumbers();
}
Limpiar La Caché
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
Haz click derecho en el paquete addinvoice y crea el fragmento a través de New >
Fragment > Fragment (Blank).
AddInvoiceFragment.java
public AddEditInvoiceFragment() {
// Required empty public constructor
}
@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;
}
}
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:
</android.support.constraint.ConstraintLayout>
<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.
dependencies {
compile
"de.hdodenhof:circleimageview:$rootProject.circleImageViewVersion"
<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" />
<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.
<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" />
<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" />
<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" />
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" />
<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:
<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>
// Keys de estados
private static final String BUNDLE_CUSTOMER_ID =
"BUNDLE_CUSTOMER_ID";
El bloque siguiente son todos los views del layout que usaremos de algún modo.
Por esta razón la interfaz interna que le añadiremos (ItemListener) debe poseer dos
métodos controladores.
Por ende creemos una nueva clase con el nombre InvoiceItemAdapter dentro de
addeditinvoice.
@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);
}
}
}
}
@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());
@Override
public int getItemCount() {
return invoiceItems.size();
}
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().
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();
}
});
Campo de fecha
Pero antes actualizaremos getDateTime() para que reciba el patrón necesario para
mostrar en la UI propuesta:
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 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();
}
});
// 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
// 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");
@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.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_save_invoice) {
mPresenter.saveInvoice(
mCustomerId,
mDate
);
}
return super.onOptionsItemSelected(item);
}
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
@Override
public void showCustomerError() {
mCustomerFieldWrapper.setErrorEnabled(true);
mCustomerFieldWrapper.setError("El cliente es requerido");
requestViewFocus(mCustomerFieldWrapper);
}
@Override
public void showItemsError() {
mAddItemButtonWrapper.setErrorEnabled(true);
mAddItemButtonWrapper.setError("Al menos un item es requerido");
requestViewFocus(mAddItemButtonWrapper);
}
@Override
public void showInvoicesScreen(String invoiceId) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}
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();
}
Veamos:
if (CustomersActivity.ACTION_PICK_CUSTOMER.equals(
getActivity().getIntent().getAction())) {
showAddEditInvoiceScreen(clickedCustomer.getId());
}
}
});
intent.putExtra(CustomersActivity.EXTRA_CUSTOMER_ID,customerId);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
}
@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;
}
}
@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().
@Override
public void showCustomersUi() {
Intent intent = new Intent(getActivity(), CustomersActivity.class);
intent.setAction(CustomersActivity.ACTION_PICK_CUSTOMER);
startActivityForResult(intent,
CustomersActivity.REQUEST_PICK_CUSTOMER);
}
@Override
public void showAddInvoiceItemScreen() {
Intent requestIntent = new Intent(getActivity()
, AddEditInvoiceItemActivity.class);
startActivityForResult(requestIntent
, AddEditInvoiceActivity.REQUEST_ADD_INVOICE_ITEM);
}
@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);
}
@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;
}
}
@Override
public void showInvoiceItems(List<InvoiceItemUi> invoiceItemUis) {
mInvoiceItemAdapter.replaceData(invoiceItemUis);
mAddItemButtonWrapper.setErrorEnabled(false);
}
5.18 Mostrar Importes Totales De La Factura
@Override
public void showInvoiceAmounts(String subtotal, String tax, String total)
{
mSubtotalText.setText(subtotal);
mTaxText.setText(tax);
mTotalText.setText(total);
}
La selección del cliente trae consigo el identificador del mismo para retenerlo en el
campo mCustomerId.
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);
}
@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;
}
@Override
public void setPresenter(AddInvoiceMvp.Presenter presenter) {
mPresenter = Preconditions.checkNotNull(presenter);
}
Tarea #6. Crear Presentador
La vista
El caso de uso para obtener el cliente
El caso de uso para guardar la factura
Caché de ítems de factura
Observemos:
// Relaciones
private AddEditInvoiceMvp.View mView;
private IGetCustomers mGetCustomer;
private ISaveInvoice mSaveInvoice;
private ICacheInvoiceItemsStore mCache;
@Override
public boolean isSatisfiedBy(Customer item) {
return mCustomerId.equals(item.getId());
}
}
@Override
public void manageCustomerPickingResult(final String customerId) {
loadCustomer(customerId);
}
@Override
public void onError(String error) {
mView.showCustomerError();
}
});
}
@Override
public void manageAdditionResult() {
@Override
public void manageEditionResult() {
}
6.5 Guardar Factura
@Override
public void saveInvoice(String customerId, Date date) {
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);
}
});
}
}
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();
}
Exactamente igual que el anterior. Ordenamos a la vista abrir la screen para añadir
ítem de factura.
@Override
public void addNewInvoiceItem() {
mView.showAddInvoiceItemScreen();
}
@Override
public void editInvoiceItem(String productId) {
mView.showEditInvoiceItemScreen(productId);
}
6.9 Eliminar Ítem De Factura
@Override
public void deleteInvoiceItem(final String productId) {
// Eliminar ítem
mCache.deleteInvoiceItem(productId);
// Actualizar totales
mView.showInvoiceAmounts(
formatAmount(mCache.getSubtotal()),
formatAmount(mCache.getTax()),
formatAmount(mCache.getTotal()));
}
@Override
public void onDataNotAvailable() {
}
});
}
Luego formatea los totales albergados en caché con formatAmount() para al final
mostrarlos:
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);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
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");
}
}
}
...
@Override
public void onDialogPositiveClick(DialogFragment dialog) {
@Override
public void onDialogNegativeClick(DialogFragment dialog) {
}
}
@Override
public void onDialogPositiveClick(DialogFragment dialog) {
resetCache();
finish();
}
DependencyProvider.provideCacheInvoiceItemsStore(this).deleteAll();
}
@Override
public void onBackPressed() {
DiscardChangesDialog dialog = new DiscardChangesDialog();
dialog.show(getSupportFragmentManager(), "DiscardDialog");
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
Añadir Presentación
@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);
}
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:
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.
setToolbarTitle(invoiceId);
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
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).
Mostrar cantidad
Mostrar error si el ítem de factura editado no se encontró
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
interface View {
void showProductName(String productName);
void showProductNotSelectedError();
void showMissingProduct();
void showQuantityError();
void showProductsScreen();
void showAddEditInvoiceScreen();
void showMissingInvoiceItem();
interface Presenter {
void selectProduct();
void populateInvoiceItem();
}
}
Tarea #3. Crear Fragmento
Sigamos el procedimiento común para crear el fragmento:
Click derecho en addeditinvoiceitem > New > Fragment > Fragment (Blank)
AddEditInvoiceItemFragment.java
public AddEditInvoiceItemFragment() {
// Required empty public constructor
}
@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.
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>
@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);
mQuantityFieldWrapper = (TextInputLayout)
root.findViewById(R.id.quantity_field_wrapper);
setHasOptionsMenu(true);
return root;
}
Usaremos el método onResume() para cargar el ítem de factura cada vez que la
actividad vuelva de un segundo plano.
@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);
}
@Override
public void showProductName(String productName) {
mProductFieldWrapper.setErrorEnabled(false);
mProductField.setText(productName);
}
3.5 Mostrar Cantidad
@Override
public void showQuantity(String quantity) {
mQuantityFieldWrapper.setErrorEnabled(false);
mQuantityField.setText(quantity);
}
@Override
public void showProductNotSelectedError() {
mProductFieldWrapper.setErrorEnabled(true);
mProductFieldWrapper.setError(getString(R.string.error_select_product));
requestViewFocus(mProductFieldWrapper);
}
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);
}
@Override
public void showQuantityError() {
mQuantityFieldWrapper.setErrorEnabled(true);
mQuantityFieldWrapper.setError(getString(R.string.error_quantity));
requestViewFocus(mQuantityFieldWrapper);
}
mProductsPresenter.openProductDetails(clickedProduct.getCode());
}
}
};
@Override
public void showProductsScreen() {
Intent requestIntent = new Intent(getActivity(), ProductsActivity.class);
requestIntent.setAction(ProductsActivity.ACTION_PICK_PRODUCT);
startActivityForResult(requestIntent,
AddEditInvoiceItemActivity.REQUEST_PICK_PRODUCT);
}
@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);
}
}
@Override
public void showAddEditInvoiceScreen() {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}
@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.
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);
}
}
@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
@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) {
}
}
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.
...
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();
}
});
}
}
mView.showProductName(productName);
mView.showStock(stock);
mView.showQuantity(quantityString);
}
Es aquí donde usamos los recursos para formatear el siguiente <string> sobre el
stock:
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;
}
@Override
public void selectProduct() {
mView.showProductsScreen();
}
@Override
public void manageProductPickingResult(final String productId) {
if (Strings.isNullOrEmpty(productId)) {
mView.showMissingProduct();
return;
}
mItemPrice = selectedProduct.getPrice();
mStock = selectedProduct.getUnitsInStock();
Añadir Presentación
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);
if (view == null) {
view = AddEditInvoiceItemFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.add_edit_invoice_item_container, view)
.commit();
}
AddEditInvoiceItemPresenter presenter =
new AddEditInvoiceItemPresenter(
productId,
view,
DependencyProvider.provideGetProducts(this),
DependencyProvider.provideCacheInvoiceItemsStore(this),
getResources());
view.setPresenter(presenter);
}
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.
Fíjate:
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:
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:
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