Ebook Android
Ebook Android
El ttulo es claro.
En esta serie de tutoriales te mostrar como crear una aplicacin que administre
productos, clientes y pedidos basados en una base de datos en un servidor.
Vamos a ver crear una REST API para leer los productos que tienes en tu servidor
y ponerlos en una lista (RecyclerView).
App Productos
Al igual que en todos los tutoriales que he creado sobre Desarrollo Android, este
tambin trae una app de ejemplo llamada App Productos.
App Products es una aplicacin administrativa para una farmacia de gran tamao,
cuyo fin es movilizar su proceso de toma de pedidos y consulta de productos por
parte de los vendedores.
Qu te parece?
Puede que su contenido o contexto pueda diferir un poco a tus necesidades, ya que
no todos los problemas son iguales.
Pero he aqu la parte buena:
Te he creado una plantilla con la ruta ptima para que superes las adversidades
particulares de tu proyecto.
Quieres verla?
Bien!
2. Recolectar requerimientos
Por ejemplo:
Ves el formato?
Bsicamente aqu debes crear una lista con las funcionalidades que tendr el
sistema basado en los objetivos del usuario.
Aadir pedido
Modificar pedido
Eliminar pedido
Para ello puedes usar varios artefactos. Dos de los ms populares son: diagramas
entidad-relacin y un diccionario de datos.
El uso de ambos lo defino muy bien en mi ebook 8 Pasos para disear tus bases
de datos. En l vers estrategias para capturar dichas necesidades.
Ahora bien:
Qu propiedades posee?
Cdigo
Nombre
Descripcin
Marca
Precio
Unidades en stock
Imagen(es) de producto
Por ejemplo, podra que tambin debas desarrollar un mdulo para los
supervisores de los vendedores.
Ninjamock
Pothoshop
Mockplus
balsamiq
Pidoco
Lucidchart
Proto.io
Retomando
El estado actual de este tutorial solo tiene una pantalla: la lista de productos.
Y basada en ella, estos son los puntos de interaccin del usuario:
R/ Por el momento (nivel 1), en App Products tendremos una base de datos remota
(servidor) para persistencia. Tambin usaremos la memoria del dispositivo como
cach al mostrar elementos.
R/ S.
Adems:
R/
Al llegar a este punto, debes tener definido al menos un 70% de lo que har tu app.
Te olvidars por un momento de las fuentes de datos y te centrar en crear una app
que solo utilice informacin falsa.
Hars que tu usuario pruebe de forma temprana todas las interacciones y flujos, de
modo que obtengas el restante de opiniones que solo se dan en la accin real.
Sus pros?
Dominio: Aqu van las reglas del negocio definidas por casos de uso
(interactores) y las entidades bsicas (objetos planos java).
Ya la conocas?
Generalmente se habla de usar MVP como arquitectura. Sin embargo este es solo
un patrn para simplificar la capa de presentacin.
http://www.genbetadev.com/paradigmas-de-programacion/usando-mvp-e-
inversion-de-dependencias-para-abstraernos-del-framework-en-android
Lo importante es que elijas una forma de escribir tu cdigo que sea entendible para
ti y futuros lectores de tu proyecto.
Tareas de programacin
A continuacin, debes recopilar en orden las tareas que tienes que llevar a cabo de
forma incremental, hasta que hayas terminado tu app.
Por ejemplo, tal vez solo requieras primero crear todas las UI de las actividades y
fragmentos.
O crear los componentes de las tres capas para una sola caracterstica (como
haremos aqu).
Dependiendo de que hayas decidido, como mnimo usa una lista para determinar
su posicin de realizacin.
3. Preparar fragmento
Fcil cierto?
SDK mnimo: 11
Paquetes Java
Antes que nada, organicemos la estructura de paquetes Java basados en la
arquitectura propuesta.
activity_products.xml
<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>
</android.support.design.widget.CoordinatorLayout>
content_products.xml
Este archivo representa el contenido principal de la actividad.
content_products.xml
Lgica de la actividad
Esta parte va ya es conocida por ti.
ProductsActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_products);
// Referencias UI
mToolbar = (Toolbar) findViewById(R.id.toolbar);
mProductsFragment =
getSupportFragmentManager().findFragmentById(R.id.products_container);
// Setup
setUpToolbar();
setUpProductsFragment();
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_products, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
Luego selecciona New > Fragment > Fragment (Blank) y crea un fragmento
llamado ProductsFragment.
Desmarca Include interface callbacks, ya que por el momento no tendremos
interfaz entre el fragmento y la actividad.
fragment_products.xml
Lleg la hora de representar los bocetos que realizaste de la pantalla de productos.
Recuerda:
Qu views usar?
Veamos
Puedes usar cualquiera, pero como la mayora de mis lectores me piden tutoriales
sobre el segundo, entonces ese ser nuestro objetivo.
Observa:
</RelativeLayout>
dependencies {
compile 'com.android.support:recyclerview-v7:24.2.0'
}
Haz que se ajuste al padre y su linear manager sea del tipo LinearLayoutManager.
<android.support.v7.widget.RecyclerView
android:id="@+id/products_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F0F0F0"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"/>
Estado de carga: Ya sabemos que para refrescar los productos, usaremos el patrn
Swipe to refresh.
<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/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".products.ProductsFragment">
<RelativeLayout>
<android.support.v7.widget.RecyclerView>
...
Cmo hacerlo?
<android.support.v7.widget.RecyclerView
... />
<LinearLayout
android:id="@+id/noProducts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:id="@+id/noProductsIcon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:tint="@android:color/darker_gray"
app:srcCompat="@drawable/ic_package_variant_closed" />
<TextView
android:id="@+id/noProductsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/list_item_padding"
android:text="@string/no_products_message" />
</LinearLayout>
</RelativeLayout>
Ahora solo queda crear un objeto Java tradicional que represente la entidad del
negocio.
Por eso, crea una nueva clase Java (File > New > Java Class) y llmala
ProductsAdapter.
La has pillado?
Bien!
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int
viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view;
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int
position) {
productsHolder.unitsInStock.setText(product.getFormattedUnitsInStock());
Glide.with(viewHolder.itemView.getContext())
.load(product.getImageUrl())
.diskCacheStrategy(DiskCacheStrategy.ALL)
.centerCrop()
.into(productsHolder.featuredImage);
}
@Override
public int getItemCount() {
return getDataItemCount();
}
@Override
public void onClick(View v) {
int position = getAdapterPosition();
Product product = getItem(position);
mItemListener.onProductClick(product);
}
}
}
Aspectos a resaltar:
Usamos la librera Glide para cargar la imagen del producto desde la url. Recuerda
incluir esta dependencia para llevarla a cabo:
compile 'com.github.bumptech.glide:glide:3.7.0'
replaceData() renueva todos los datos del adaptador, pero addData() agrega sobre
los ya existentes.
Por qu?
Flexibilidad.
Las posiciones que tienen los elementos exigen orientaciones con respecto al
padre y a los hermanos.
VITAL:
Mira:
compile 'com.android.support:cardview-v7:24.2.0'
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="8dp">
<ImageView
android:id="@+id/product_featured_image"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentLeft="true"
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_toRightOf="@+id/product_featured_image"
android:maxLength="65"
android:maxLines="1"
android:text="New Text"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textSize="16sp"
tools:text="Piperacina Compuesta" />
<TextView
android:id="@+id/product_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Small Text"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?colorPrimary"
tools:text="$44" />
<TextView
android:id="@+id/units_in_stock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@+id/product_name"
android:layout_toRightOf="@+id/product_featured_image"
android:text="New Text"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="20 u." />
</RelativeLayout>
</android.support.v7.widget.CardView>
Efecto Ripple: Si quieres que el efecto Ripple funcione sobre el tem, usa la
caracterstica android:foreground del CardView para definirle el ripple por defecto
del framework ?attr/selectableItemBackgroundBorderless.
Algo ms?
Claro!
Obtn referencias UI
Usa findViewById() y consigue todos los elementos de la interfaz:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_products, container, false);
// Referencias UI
mProductsList = (RecyclerView) root.findViewById(R.id.products_list);
mEmptyView = root.findViewById(R.id.noProducts);
mSwipeRefreshLayout = (SwipeRefreshLayout)
root.findViewById(R.id.refresh_layout);
// Setup
setUpProductsList();
setUptRefreshLayout();
return root;
}
}
Este mtodo queda abierto para ms configuraciones relacionadas a la lista.
mSwipeRefreshLayout.setOnRefreshListener(new
SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
}
});
}
Entonces.
Crea una interfaz nueva yendo a New > Java Class : Interface. Denomnala como
ProductsMvp.
Como te vena diciendo, incluimos dos interfaces para la vista y el presentador. El
modelo lo supeditamos a la capa de datos.
public interface ProductsMvp {
interface View {
interface Presenter {
}
}
Ahora,
Y es correcto!
El estado de carga.
En este caso debes crear un mtodo que inicie/termine la carga en la vista.
Por ejemplo
Por esta razn necesitamos un mtodo para poner los productos de la pgina:
void showProductsPage(List<Product> products);
Por otro lado, como debemos mostrar un indicador circular de progreso al final de
la lista, entonces aadimos ese comportamiento:
void showLoadMoreIndicator(boolean show);
Al mismo tiempo, debemos evitar que se genere el indicador si ya no hay ms
datos. Por lo que un mtodo que nos ayude a encender/apagar dicha condicin
vendra bien:
void allowMoreData(boolean show);
El presentador solo debe recuperar los datos del repositorio y los pone en la vista.
Pero cabe resaltar que la carga puede ser para refrescar el contenido. As que usa
el parmetro booleano reload para especificarlo.
El fragmento de productos.
El ser el que manipule los views existentes para darle vida al flujo de interfaz.
@Override
public void showLoadingState(final boolean show) {
@Override
public void showEmptyState() {
@Override
public void showProductsError(String msg) {
@Override
public void showProductsPage(List<Product> products) {
@Override
public void showLoadMoreIndicator(boolean show) {
@Override
public void allowMoreData(boolean allow) {
Sobrescribe ShowProducts()
Lo primero es reemplazar los datos del adaptador (replaceData()) y luego mostrar
la lista (setVisibility(View.VISIBLE)) y ocultar el estado de vaco
(setVisibility(View.GONE).
@Override
public void showProducts(List<Product> products) {
mProductsAdapter.replaceData(products);
mProductsList.setVisibility(View.VISIBLE);
mEmptyView.setVisibility(View.GONE);
}
Sobrescribe showLoadingState()
Ahora iniciaremos la animacin del SwipeRefreshLayout.
Para asegurarte que no se sobreponen las llamadas, pon en cola el mensaje con
post().
@Override
public void showLoadingState(final boolean show) {
if (getView() == null) {
return;
}
mSwipeRefreshLayout.post(new Runnable() {
@Override
public void run() {
mSwipeRefreshLayout.setRefreshing(show);
}
});
}
Sobrescribe showEmptyState()
Este mtodo es intuitivo.
Sobrescribe showProductsError()
Cuando vengan los errores desde el presentador es buena opcin mostrarlos con
un Toast.
El cdigo es fcil:
@Override
public void showProductsError(String msg) {
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG)
.show();
}
Sobrescribe showProductsPage()
Apila los productos de la pgina en el adaptador con addData().
@Override
public void showProductsPage(List<Product> products) {
mProductsAdapter.addData(products);
}
Pero claro
Cmo obtenerlos?
De la capa de presentacin debemos usar una interfaz que nos permita solicitar la
informacin a la capa de dominio.
Dicha interfaz puede ser un Interactor (caso de uso). Un elemento que sigue el
patrn Command.
Entonces, Qu haremos?
Crear presentador
Aade una nueva clase al paquete products llamada ProductsPresenter.
@Override
public void loadProducts(final boolean reload) {
}
}
Genera un constructor
Lo primero que pondrs ser un constructor.
Sobrescribe loadProducts()
Cmo funciona la carga de productos?
Al iniciar, no habr datos. Por lo que tendremos una bandera booleana que lleve
este seguimiento.
if (reallyReload) {
mProductsView.showLoadingState(true);
mProductsRepository.refreshProducts();
mCurrentPage = 1;
} else {
mProductsView.showLoadMoreIndicator(true);
mCurrentPage++;
}
mProductsRepository.getProducts(..);
As que para saber como resulta (xito, fallo o proceso), es buena idea crear
interfaces para capturar estos eventos.
Veamos el cdigo
void refreshProducts();
}
Tambin el contexto donde lo usaremos y una bandera para saber si hay que
refrescar los datos.
private final IMemoryProductsDataSource mMemoryProductsDataSource;
private final ICloudProductsDataSource mCloudProductsDataSource;
private final Context mContext;
Para este ejemplo dijimos que los productos tienen un rol esclavo, es decir, en la
app solo sern consultados.
Implementar polticas
Hay varias formas de hacer esto.
Pero por otro lado Fernando Cejas nos muestra cmo usar un patrn Factory para
elegir la fuente de datos aisladamente:
if (mReload) {
getProductsFromServer(callback, criteria);
} else {
List<Product> products = mMemoryProductsDataSource.find();
if (products.size() > 0) {
callback.onProductsLoaded(products);
} else {
getProductsFromServer(callback);
}
}
Qu operaciones tendremos?
Obtencin (find())
Guardado (save())
void deleteAll();
boolean mapIsNull();
}
Sus operaciones son sencillas. Usa los mtodos put(), remove(), clear() y values()
para satisfacer los mtodos propuestos:
@Override
public List<Product> find() {
ArrayList<Product> products =
Lists.newArrayList(mCachedProducts.values());
return products;
}
@Override
public void save(Product product) {
if (mCachedProducts == null) {
mCachedProducts = new LinkedHashMap<>();
}
mCachedProducts.put(product.getCode(), product);
}
@Override
public void deleteAll() {
if (mCachedProducts == null) {
mCachedProducts = new LinkedHashMap<>();
}
mCachedProducts.clear();
}
@Override
public boolean mapIsNull() {
return mCachedProducts == null;
}
Continuando:
Al igual que hicimos para la fuente en memoria, crea una interfaz llamada
ICloudProductsDataSource dentro de data/products/datasource/cloud.
interface ProductServiceCallback {
}
Lo siguiente es crear una clase llamada CloudProductsDataSource con la interfaz de
fuentes.
static {
API_DATA = new LinkedHashMap<>();
for (int i = 0; i < 100; i++) {
addProduct(43, "Producto " + (i + 1),
"file:///android_asset/mock-product.png");
}
}
Completemos el repositorio
Ahora s!
callback.onProductsLoaded(mMemoryProductsDataSource.find());
}
if (!isOnline()) {
callback.onDataNotAvailable("No hay conexin de red.");
return;
}
mCloudProductsDataSource.getProducts(
new ICloudProductsDataSource.ProductServiceCallback() {
@Override
public void onLoaded(List<Product> products) {
refreshMemoryDataSource(products);
getProductsFromMemory(callback);
}
@Override
public void onError(String error) {
callback.onDataNotAvailable(error);
}
});
}
@Override
public void refreshProducts() {
mReload = true;
}
Fijate que cuando se termina la carga de productos desde el servidor, la fuente de
datos en memoria se refresca con resfreshMemoryDataSource().
TRUE
Carga exitosa? FALSE
TRUE FALSE
Hay productos?
Mostrar error
FALSE FALSE
TRUE TRUE
Refresco? Refresco?
Ocular indicador
Mostrar estado vaco
"cargar ms"
Mostrar productos Mostrar pgina de productos
FIN
Copiado?
Excelente!
Observa:
mProductsRepository.getProducts(
new ProductsRepository.GetProductsCallback() {
@Override
public void onProductsLoaded(List<Product> products) {
mProductsView.showLoadingState(false);
processProducts(products, reallyReload);
@Override
public void onDataNotAvailable(String error) {
mProductsView.showLoadingState(false);
mProductsView.showLoadMoreIndicator(false);
mProductsView.showProductsError(error);
}
});
if (reload) {
mProductsView.showProducts(products);
} else {
mProductsView.showLoadMoreIndicator(false);
mProductsView.showProductsPage(products);
}
mProductsView.allowMoreData(true);
}
}
Dado que se relaciona con la vista, el fragmento es un buen lugar para mantenerlo.
En consecuencia, ve al fragmento y crea un miembro presentador.
private ProductsPresenter mProductsPresenter;
setRetainInstance(true);
}
La respuesta concreta:
As que dentro del paquete di agrega una clase con ese nombre aade los
siguientes mtodos:
public final class DependencyProvider {
private DependencyProvider() {
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
}
#2. Para que la escucha detecte cuando se est cargando datos o cuando se
terminaron los datos, crea una interfaz llamada DataLoading dentro de products.
interface DataLoading {
boolean isLoadingData();
boolean isThereMoreData();
}
El primero es la cantidad requerida de tems visibles antes del final, para comenzar
a cargar una nueva pgina.
mLayoutManager nos ser de utilidad al obtener informacin sobre los tems actuales.
Para determinar las condiciones donde se inicia otra carga, usaremos estas
variables:
Bien.
@Override
public boolean isLoadingData() {
return false;
}
@Override
public boolean isThereMoreData() {
return false;
}
@Override
public boolean isThereMoreData() {
return mMoreData;
}
#1. Dentro de products crea un paquete llamado domain. Dentro de este crea otro
llamado criteria.
#4. Ahora sobrescribe match() y obtn los {mLimit} tems que pertenezcan a la
pgina {mPage}.
public class PagingProductCriteria implements ProductCriteria {
@Override
public List<Product> match(List<Product> products) {
List<Product> criteriaProducts = new ArrayList<>();
// Sanidad
if(mLimit <= 0 || mPage <=0){
return criteriaProducts;
}
a = (mPage - 1) * mLimit;
if (a == size) {
return criteriaProducts;
}
b = a + mLimit;
return criteriaProducts;
}
}
Fjate:
mProductsRepository.getProducts(
new ProductsRepository.GetProductsCallback() {
@Override
public void onProductsLoaded(List<Product> products) {
mProductsView.showLoadingState(false);
processProducts(products, reload);
}
@Override
public void onDataNotAvailable() {
mProductsView.showLoadingState(false);
mProductsView.showProductsError("");
}
},
criteria);
En find() debes llamar a match() para retornar solo los elementos necesarios:
@Override
public List<Product> find(ProductCriteria criteria) {
ArrayList<Product> products =
Lists.newArrayList(mCachedProducts.values());
return criteria.match(products);
}
Concretemos la idea:
#1. Agrega dos constantes de tipo. Una para los productos y otra para el tem de
carga:
private final static int PRODUCT_ITEM_TYPE = 1;
private final static int FOOTER_ITEM_TYPE = 2;
return TYPE_LOADING_MORE;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int
viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view;
if (viewType == TYPE_LOADING_MORE) {
view = inflater.inflate(R.layout.item_loading_footer, parent, false);
return new LoadingMoreHolder(view);
}
<ProgressBar
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/progressBar"
android:indeterminate="true" />
</LinearLayout>
Cmo?
Con esto en mente, ya sabemos cmo ligar los mtodos del adaptador:
@Override
public void showLoadMoreIndicator(boolean show) {
if (!show) {
mProductsAdapter.dataFinishedLoading();
} else {
mProductsAdapter.dataStartedLoading();
}
}
@Override
public void allowMoreData(boolean allow) {
mProductsAdapter.setMoreData(allow);
}
Genial, cierto?
El cascarn tiene vida!
listo?
Excelente!
1. La app Android realiza una peticin HTTP con el mtodo GET para pedir
todos los productos
No es el caso?
Listo!
Ya que tenemos solo una entidad estudiada para los productos, no es necesario
entrar en ms detalles.
Pero ojo:
Diagrama ER
En mi caso, este ejemplo solo requiere el uso de un diagrama entidad-relacin para
comprender el modelo de datos:
Esto requiere que uses el comando CREATE TABLE en la pestaa SQL relacionada a
app_products.
Est ms que claro que poner los comandos INSERT en este tutorial es una locura.
Sin embargo, puedes usar el archivo product.sql que viene en la carpeta del
tutorial.
Por ltimo presiona Continuar y espera hasta que se carguen los elementos.
(Si lo deseas, usa productos por si tus reglas de nombrado usan el espaol)
http://api.appproductos.com/v1/products
Pensemos:
Si deseo que vengan todos los productos con todas sus columnas, qu debera
hacer?
Esquema de modelo:
[
{
"code": "50436-1263",
"name": "Gabapentin",
"description": "Maecenas leo odio, condimentum id, luctus nec, molestie.",
"brand": "Gabapentin",
"price": "16.42",
"unitsInStock": 58,
"imageUrl": "file:///android_asset/mock-product.png"
},
{
"code": "61727-307",
"name": "Insomnia Relief",
"description": "Donec ut mauris eget massa tempor convallis.",
"brand": "Insomnia Relief",
"price": "50.36",
"unitsInStock": 55,
"imageUrl": "file:///android_asset/mock-product.png"
}
]
Qu tanto estructurarlos?
Necesitamos cdigos de error?
Evaluando la situacin de nuestra app, donde suponemos que es una app privada
para los vendedores de una farmacia. Un solo mensaje bastar para orientarnos.
Esquema de modelo:
{
"message": "No tienes acceso a la API"
}
.htaccess
Para ello debes crear un archivo .htaccess en la carpeta v1.
Fjate:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?PATH_INFO=$1 [L,QSA]
De qu manera?
Buenopensemos.
Cundo el cliente Android nos enva una peticin, que debe pasar?
1. Llega una peticin compuesta de: mtodo HTTP, segmentos de url, cuerpo
(POST, PUT), tipo de contenido, autorizacin, etc.
Segn lo que detallamos, esta tiene varios atributos. Pero por el momento
pondremos solo los que usaremos.
class Request
{
public $url_elements;
public $verb;
return true;
}
El asunto es:
Debemos cargar sus clases en tiempo de ejecucin con el nombre del recurso
extrado.
Por ejemplo
La pillas?
Programticamente lo que tenemos que hacer, es poner en mayscula la primera
letra del recurso y luego concatenarlo a las plantillas.
Observa:
$controller_name = $plural_uc_resource_name . 'Controller';
$repository_name = $plural_uc_resource_name . 'Repository';
$sql_data_source_name = 'Sql' . $plural_uc_resource_name . 'DataSource';
Sabiendo los nombres de las clases, entonces creamos instancias de ellas (si es
que existen).
if (class_exists($controller_name)
&& class_exists($repository_name)
&& class_exists($sql_data_source_name)
) {
De qu manera hacerlo?
Es decir,
Ves?
Qu acciones tendr?
Renderizar la respuesta.
As que crea una nueva interfaz llamada View dentro de la carpeta views:
interface View
{
public function render($response);
}
Se trata de JsonView.
Veamos:
class JsonView implements View
{
Respuestas
Al igual que las peticiones, tambin conceptualizaremos las respuestas en cdigo.
Como vimos hace poco, la vista usar estos elementos para renderizar el resultado
al cliente.
Para manifestar esta declaracin, creemos una interfaz llamada Controller dentro
de controllers con los cuatro mtodos principales.
interface Controller
{
function getAction($request);
function postAction($request);
function putAction($request);
function deleteAction($request);
Constructor
Luego genera un constructor que reciba el repositorio como parmetro y lo asigne
a una variable miembro predefinida.
private $productsRepository;
getAction()
Enseguida escribe las 4 acciones.
public function getAction($request) {
La pregunta es:
if (isset($request->url_elements[1])) {
} else {
$results = $this->productsRepository->getAllProducts();
if (is_array($results)) {
$response->setBody($results);
$response->setStatus(200);
} else if (is_string($results)) {
$response->setBody(['message' => $results]);
$response->setStatus(200);
}
}
return $response;
}
Qu tal si lo creamos?
Cules tenemos?
interface IProductsRepository
{
public function getAllProducts();
}
En otras palabras:
Y el problema es claro.
Usaremos MySQL como fuente de datos y el nico recurso son los productos
(temporalmente).
Necesitamos obtener todos los productos, as que el nico mtodo ser retrieve().
interface ProductsDataSource {
function retrieve();
Siguiendo la misma rutina, pon una variable miembro para la conexin PDO
llamada $dbh (database handler).
function retrieve() {
$sql = 'SELECT * FROM ' . $this->table_name;
$stmt = $this->dbh->prepare($sql);
if ($stmt->execute()) {
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
return $stmt->errorInfo()[2];
}
}
Lo comn. Cre un comando $sql con la sentencia SELECT para la tabla productos.
Y esta es la solucin:
Usa las excepciones de PHP para crear respuestas para estos comportamientos.
#1. Crea una clase que extienda de Exception dentro de exceptions. Llmala
ApiException.
Este se encargar de recibir la excepcin, crear una nueva vista JSON e imprimir la
respuesta:
set_exception_handler(function (ApiException $exception) {
$json_view = new JsonView();
$json_view->render($exception->response);
}
);
Ahora
Respuesta concreta:
Autoloaders.
El paso a seguir?
spl_autoload_register('apiAutoload');
function apiAutoload($classname) {
if (preg_match('/[a-zA-Z]+Controller$/', $classname)) {
@include __DIR__ . '/controllers/' . $classname . '.php';
return true;
} elseif (preg_match('/[a-zA-Z]+Repository$/', $classname)) {
@include __DIR__ . '/data/' . $classname . '.php';
return true;
} elseif (preg_match('/[a-zA-Z]+DataSource$/', $classname)) {
@include __DIR__ . '/data/datasource/' . $classname . '.php';
return true;
}
return false;
}
Dependiendo del string que entra como $classname (nombre de la clase), as mismo
se incluye (include) el archivo.
Este tip y varios de los que te he mostrado han sido influencia de Lorna Jane. Una excelente
desarrolladora de IBM. Tiene lecturas obligadas en su blog y publicaciones.
Contenedor de dependencias
Para crear la conexin PDO, usaremos una clase llamada InjectionContainer.
Bsicamente el objeto PDO ser inicializado con los datos de conexin. Y luego lo
proveeremos a travs de un mtodo.
chale un vistazo:
class InjectionContainer {
private static $pdo = null;
// Habilitar excepciones
self::$pdo->setAttribute(PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION);
} catch (PDOException $exception) {
throw new ApiException(500, STATUS_CODE_500);
}
}
return self::$pdo;
}
}
data/datasource/mysql_login.php
<?php
/**
* Provee las constantes para conectarse a la base de data
* Mysql.
*/
define('HOST', 'localhost');// Nombre del host
define('DATABASE', 'app_products'); // Nombre de la base de controllers
define('MYSQL_USER', 'root'); // Nombre del usuario
define('MYSQL_PASSWORD', ''); // Constrasea
if (class_exists($controller_name)
&& class_exists($repository_name)
&& class_exists($sql_data_source_name)
) {
Uff!
Qu tal funcionar?
Vamos a checar
http://localhost/api.appproducts.com/v1/products
Tambin, puedes probar los errores para ver que tal anda.
Por ejemplo, ver qu sucede si usas POST en el recurso de los productos. Escribir la
URL de forma incorrecta, etc.
Ahora vamos a ver cmo usar la librera Retrofit para construir el cliente REST en
Android.
Nuestro servicio REST con PHP est funcionando y nuestra app Android lista para
consumir.
Para qu sirve?
Y nos permite usar Gson para convertir las respuestas directamente en objetos de
negocio.
Realmente poderosa!
Convencido a usarla?
Iniciemos
En este momento mientras escribo este tutorial, Retrofit est en su versin 2.1, as
que tendrs la siguiente lnea:
dependencies {
compile 'com.squareup.retrofit2:retrofit:2.1.0'
Ahora, para aadir el mdulo de agregacin para Gson, usa las siguientes
dependencias:
dependencies {
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.google.code.gson:gson:2.7'
}
La forma?
<uses-permission android:name="android.permission.INTERNET"/>
<application>
...
</application>
</manifest>
Verifica que tu POJO tenga los mismo atributos (nombre, tipo y cantidad) para
recibir los objetos JSON.
Si recuerdas la entidad para los productos, yo antepuse una m para los nombres de
los campos.
Cmo arreglarlo?
Esta indica al parser, que en el campo con la anotacin debe ir el valor del atributo
JSON.
Por ejemplo, nosotros tendramos que cambiar todos los atributos de la siguiente
manera:
public class Product {
@SerializedName("code")
private String mCode;
@SerializedName("name")
private String mName;
@SerializedName("description")
private String mDescription;
@SerializedName("brand")
private String mBrand;
@SerializedName("price")
private float mPrice;
@SerializedName("unitsInStock")
private int mUnitsInStock;
@SerializedName("imageUrl")
private String mImageUrl;
Cmo funcionan?
La respuesta sencilla: definen el tipo de respuesta y los componentes de la url sin
ms.
Por ejemplo
y as sucesivamente.
IMPORTANTE:
mRestService = mRetrofit.create(RestService.class);
}
De paso, crea la instancia de RestService con la ayuda del cliente, a travs del
mtodo create().
Cul elegir?
enqueue()
Es as, porque evitar que el hilo de UI se entorpezca y Android nos arroje dilogos
ANR.
call.enqueue(new Callback<List<Product>>() {
@Override
public void onResponse(Call<List<Product>> call,
Response<List<Product>> response) {
// Procesamos los posibles casos
processGetProductsResponse(response, callback);
@Override
public void onFailure(Call<List<Product>> call, Throwable t) {
callback.onError(t.getMessage());
}
});
}
// Hubo un error?
if (errorBody != null) {
// Fu de la API?
if (errorBody.contentType().subtype().equals("json")) {
try {
// Parseamos el objeto
ErrorResponse er = new Gson()
.fromJson(errorBody.string(), ErrorResponse.class);
error = er.getMessage();
} catch (IOException e) {
e.printStackTrace();
}
}
callback.onError(error);
}
Respuesta de error: Crea una clase llamada ErrorResponse con un atributo string
para capturar las respuestas de error que diseos en la API.
Recuerda que el mtodo Gson.from() parsea un flujo JSON en un objeto cuya clase
pones como parmetro.
Qu ves?
Swipe to refresh
Endless scroll
Empty state
Errores
Qu Tal Te Fue?
Te pareci ms ordenado el uso del patrn MVP en Android?
Chvere, cierto?
Empezar a incluir libreras como Retrofit para ahorrar tiempo y paz mental
Uso de Dagger
Pantalla de login
Filtros
Bsqueda
Finalmente
estos son los pasos a seguir:
Comenta que te pareci este tutorial AQU. Djame saber a m y a los dems
lectores de la comunidad:
Saludos,
James