Crear Un WebService para Android Con Mysql, PHP y Json
Crear Un WebService para Android Con Mysql, PHP y Json
¿Has intentado crear un web service con Php para la comunicación de datos de tu aplicativo web con tu aplicativo móvil android, pero aún no
comprendes bien cómo hacerlo?
Pues bien, en este artículo te mostraré algunas ideas sobre la creación de una aplicación android que consuma los datos de un servidor
externo a través de Php, Mysql y Json.
Para ello he creado una aplicación llamada “I Wish”, la cual permite a nuestros usuarios guardar una lista de deseos y metas que tienen en su
vida. Con este ejemplo podrás ver cómo implementar la inserción, edición, eliminación y consulta de datos a través de un Web Service.
Descargar Código
Por ejemplo…
Facebook es un aplicativo web construido con una determinada arquitectura y lenguajes de programación basados en el protocolo HTTP. Sin
embargo podemos usar esta red social en nuestro dispositivo Android.
¿Cómo es posible esto, si la aplicación Android está construida con lenguaje Java?
A través de un Web Service construido para gestionar todas aquellas operaciones sobre una base de datos alojada en los servidores de
Facebook. Quiere decir que ambos aplicativos usan como puente la web para acceder a un solo repositorio de datos.
Como ves, un Web Service se crea con funcionalidades que permitan obtener datos actualizados en tiempo real. El hecho de que sea dinámico
incorpora el uso de un lenguaje web para la gestión HTTP que en este caso será Php.
2. Requerimientos De La Aplicación
Como leíste al inicio, la aplicación I Wish gestiona las metas y sueños de los usuarios permitiéndoles tener un registro completo. Básicamente el
alcance del proyecto se resumen en:
Como usuario de I Wish, deseo mantener los datos de todas mis metas y sueños (se refiere al CRUD).
Como usuario de I Wish, deseo ver el detalle de cada meta.
Como usuario de I Wish, deseo que cada ítem tenga un título, una descripción, una fecha límite de cumplimiento, prioridad y categoría.
Las categorías posibles son: Salud, Finanzas, Profesional y Espiritual.
Estos requerimientos no son nada del otro mundo. Básicamente estas ante una situación de listas y detalles. Algo que ya has visto en artículos
pasados con gran frecuencia.
El meollo del asunto se encuentra en el Web Service que debes crear con Php y Mysql para el mantenimiento de los datos. Esta vez no
usaremos caching para el soporte de los datos locales como lo hicimos al crear el lector Rss. Nos enfocaremos en como usar Volley para
realizar las peticiones en el localhost.
3. Wireframing De La Aplicación
A primera vista I Wish es una aplicación que se basa en la funcionalidad básica de un crud. Tendremos una lista de los elementos que existen,
podremos ver el detalle de cada uno, modificar su contenido e incluso borrarlos.
Teniendo en cuenta este razonamiento, puedes imaginar la aplicación en primera instancia de la siguiente forma:
Basado en el boceto que acabas de crear ya puedes identificar que la cantidad de actividades, fragmentos, diálogos y formularios que
necesitas. Así que veamos la siguiente lista de materiales a crear:
En este tutorial usaremos actividades basadas en fragmentos, ya que muchos lectores han preguntado cómo hacer para comunicar fragmentos
con actividades y viceversa.
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" />
El fragmento pudiese heredar de ListFragment pero debido a que vamos a usar un RecyclerView , el diseño es diferente. La idea es añadir el
recycler para recubrir toda la actividad y además añadir un Floating Action Button en la parte inferior derecha con el fin de que el usuario
añada nuevas metas.
Para añadir el FAB (Floating Action Button) podemos hacer uso de una de las siguientes librerías que existen en la web:
Incluso podrías basarte en el ejemplo del sitio de android devepers llamado FloatingActionButtonBasic. Todo depende de ti. Cada librería trae
la explicación de su implementación, así que no hay excusas.
Por mi parte, en este ejemplo usaré la librería de makovkastar, ya que necesitamos fabs muy simples. Para ello incluimos la siguiente
dependencia de Gradle:
compile 'com.melnykov:floatingactionbutton:1.3.0'
fragment_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res‐auto"
android:id="@+id/fragment_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/reciclador"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="3dp"
android:scrollbars="vertical" />
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="@mipmap/ic_add"
fab:fab_colorNormal="@color/accent"
fab:fab_colorPressed="@color/primary"
fab:fab_colorRipple="@color/ripple" />
</RelativeLayout>
Se usa una etiqueta <com.melnykov.fab.FloatingActionButton> para implementar el FAB. Simplemente se ubica en la parte inferior derecha y le
añadimos los colores correspondientes a su interacción.
Donde colorNormal es el color que tiene en estado natural; colorPressed es aquel que se proyecta cuando lo presionamos rapidamente y
colorRipple se evidencia cuando mantienes un click largo sobre él.
Otro aspecto a tener en cuenta es que los mipmaps o drawables que uses para el icono de un FAB debe tener dimensiones de 24dp, para una
buena experiencia de usuario:
El patrón anterior muestra un FAB grande para representar la inserción con unas dimensiones reglamentarias de 56dpx56dp. El icono que lleva
debe mantenerse en 24dpx24dp.
También podemos tener un FAB mini con dimensiones de 40dpx40dp, donde el icono se mantiene sobre 24dpx24dp.
fragment_detail.xml
<ImageView
android:id="@+id/cabecera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="30"
android:layout_marginBottom="28dp" />
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_gravity="bottom|right"
android:src="@mipmap/ic_edit"
fab:fab_colorNormal="@color/colorNormalMini"
fab:fab_colorPressed="@color/colorPressedMini"
fab:fab_colorRipple="@color/colorRippleMini"
android:layout_marginLeft="16dp"
fab:fab_type="mini"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/titulo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Titulo"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_marginBottom="48dp"
android:layout_toRightOf="@+id/fab"
android:layout_alignParentBottom="true"
android:layout_marginLeft="16dp"
android:textColor="@android:color/white" />
</RelativeLayout>
<TextView
android:id="@+id/categoria"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/categoria_label"
android:text="Categoría"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/fecha"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/fecha_label"
android:text="Fecha"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/prioridad"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/prioridad_label"
android:text="Prioridad"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/descripcion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/descripcion_label"
android:text="Descripción"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/descripcion_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Descripción"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/fecha_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/descripcion"
android:text="Fecha Límite"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/categoria_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/fecha"
android:text="Categoría"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/prioridad_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/categoria"
android:text="Prioridad"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black" />
</RelativeLayout>
</LinearLayout>
En este caso se usó como nodo un LinearLayout con dos RelativeLayout dentro. Esto nos permite dividir por pesos (weight) la ocupación de
espacio entre ambos layouts y así mantener una proporción.
4.3 Diseñar Actividad Con Formulario
La inserción y edición requiere de la proyección de un formulario que contenga los controles necesarios para que el usuario especifique la
información personalizada que desea almacenar en la base de datos. Para ello debes crear un layout con los datos que viste en los
requerimientos de la aplicación con las respectivos views para obtener la información.
Por ejemplo…
El titulo de cada meta recibe texto escrito desde el input del dispositivo, por lo que sabemos que el EditText es la solución para este caso. La
descripción es igual, necesita un campo de texto. La fecha limite puede ser obtenida a través de un DatePicker y para la categoría que tiene un
dominio de varias opciones, puedes usar un Spinner .
Veamos:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="com.herprogramacion.iwish.ui.fragmentos.UpdateFragment">
<!‐‐ Titulo‐‐>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/titulo_input"
android:layout_alignParentTop="false"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:hint="Título"
android:minLines="1"
android:maxLines="1"
android:maxLength="55"
android:phoneNumber="false"
android:singleLine="true"
android:paddingTop="16dp"
android:paddingBottom="16dp" />
</RelativeLayout>
4.4 Diseñar Layout Personalizado De Los Items
La organización de los atributos de cada meta dentro de los ítems de la lista debe ser un resumen de sus características principales. Puedes
dejar la descripción solo para la actividad del detalle y eliminarlo de la presentación en la lista.
item_list.xml
Si deseas aprender a crear Web Services con diseño REST, entonces te recomiendo este excelente curso online con Laravel.
Para desarrollar este aplicativo usaré el entorno de desarrollo XAMPP, el cual provee automáticamente una configuración de un servidor
Apache local, el intérprete de Php y el gestor Mysql.
Sin embargo tu puedes usar las herramientas que desees para gestionar pruebas locales. Lo importante es que puedas correr Mysql e
interpretar scripts de Php.
Meta debe tener los atributos que hemos venido viendo más una llave primaria que mantenga la integridad de los datos. Observa el siguiente
minidiagrama entidad-relación:
Crear base de datos: Para implementar la base de datos lo primero que debes hacer es crear una nueva base de datos en la aplicación
phpMyAdmin que te otorga tu distribución XAMPP. Donde le asignaremos el nombre de “i_wish”.
Ahora crea la tabla meta para que contenga seis columnas en su estructura y además usa el formato UTF‐8 para soportar acentos. Puedes
hacerlo a través del editor de phpMyAdmin o con el siguiente comando CREATE :
Luego añade 5 registros de ejemplo en la tabla que permitan probar el funcionamiento en la aplicación android más adelante.
Luego de eso crea una clase que mapee la estructura de la tabla meta . El objetivo de ello es proveerla de comportamientos de inserción,
actualización, eliminación y consulta a través de la conexión a la base de datos.
Finalmente implementaré scripts Php para gestionar las peticiones que lanzan los clientes. La idea es parsear los datos en formato Json para
que nuestra aplicación Android interprete los resultados de forma legible.
Para este caso te compartiré un patrón singleton de PDO para limitar el número de aperturas a la base de datos en una sola. Con ello podremos
disponer de un solo objeto a través de todo el proyecto.
No obstante hay patrones de diseño muy interesantes que puedes consultar en la web. Por ejemplo el repositorio del usuario indieteq en
github. Él se enfoca en la implementación del CRUD de una forma muy sencilla y orientada a objetos.
Database.php
<?php
/**
* Clase que envuelve una instancia de la clase PDO
* para el manejo de la base de datos
*/
require_once 'mysql_login.php';
class Database
{
/**
* Única instancia de la clase
*/
private static $db = null;
/**
* Instancia de PDO
*/
private static $pdo;
/**
* Retorna en la única instancia de la clase
* @return Database|null
*/
public static function getInstance()
{
if (self::$db === null) {
self::$db = new self();
}
return self::$db;
}
/**
* Crear una nueva conexión PDO basada
* en los datos de conexión
* @return PDO Objeto PDO
*/
public function getDb()
{
if (self::$pdo == null) {
self::$pdo = new PDO(
'mysql:dbname=' . DATABASE .
';host=' . HOSTNAME .
';port:63343;',
USERNAME,
PASSWORD,
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")
);
// Habilitar excepciones
self::$pdo‐>setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return self::$pdo;
}
/**
* Evita la clonación del objeto
*/
final protected function __clone()
{
}
function _destructor()
{
self::$pdo = null;
}
}
?>
Ten en cuenta que la conexión se abre con 4 cadenas descriptivas del entorno que estás usando declaradas en el archivo mysql_login.php. Con
ello me refiero al nombre del host, el nombre de la base de datos, el usuario con que deseas ingresar y su respectiva contraseña.
Por el momento usaremos el localhost debido a las pruebas que estamos haciendo. El usuario ya depende de ti, en mi caso uso el usuario por
defecto "root" y sin contraseña alguna.
mysql_login.php
<?php
/**
* Provee las constantes para conectarse a la base de datos
* Mysql.
*/
define("HOSTNAME", "localhost");// Nombre del host
define("DATABASE", "i_wish"); // Nombre de la base de datos
define("USERNAME", "root"); // Nombre del usuario
define("PASSWORD", ""); // Nombre de la constraseña
?>
Adicionalmente debes añadir al cuarto parámetro del constructor de PDO la indicación SET NAMES UTF‐8 para el servidor. Esto permite que los
datos de la base de datos vengan codificados en este formato para evitar problemas de compatibilidad.
Meta.php
<?php
/**
* Representa el la estructura de las metas
* almacenadas en la base de datos
*/
require 'Database.php';
class Meta
{
function __construct()
{
}
/**
* Retorna en la fila especificada de la tabla 'meta'
*
* @param $idMeta Identificador del registro
* @return array Datos del registro
*/
public static function getAll()
{
$consulta = "SELECT * FROM meta";
try {
// Preparar sentencia
$comando = Database::getInstance()‐>getDb()‐>prepare($consulta);
// Ejecutar sentencia preparada
$comando‐>execute();
return $comando‐>fetchAll(PDO::FETCH_ASSOC);
/**
* Obtiene los campos de una meta con un identificador
* determinado
*
* @param $idMeta Identificador de la meta
* @return mixed
*/
public static function getById($idMeta)
{
// Consulta de la meta
$consulta = "SELECT idMeta,
titulo,
descripcion,
prioridad,
fechaLim,
categoria
FROM meta
WHERE idMeta = ?";
try {
// Preparar sentencia
$comando = Database::getInstance()‐>getDb()‐>prepare($consulta);
// Ejecutar sentencia preparada
$comando‐>execute(array($idMeta));
// Capturar primera fila del resultado
$row = $comando‐>fetch(PDO::FETCH_ASSOC);
return $row;
/**
* Actualiza un registro de la bases de datos basado
* en los nuevos valores relacionados con un identificador
*
* @param $idMeta identificador
* @param $titulo nuevo titulo
* @param $descripcion nueva descripcion
* @param $fechaLim nueva fecha limite de cumplimiento
* @param $categoria nueva categoria
* @param $prioridad nueva prioridad
*/
public static function update(
$idMeta,
$titulo,
$descripcion,
$fechaLim,
$categoria,
$prioridad
)
{
// Creando consulta UPDATE
$consulta = "UPDATE meta" .
" SET titulo=?, descripcion=?, fechaLim=?, categoria=?, prioridad=? " .
"WHERE idMeta=?";
// Preparar la sentencia
$cmd = Database::getInstance()‐>getDb()‐>prepare($consulta);
return $cmd;
}
/**
* Insertar una nueva meta
*
* @param $titulo titulo del nuevo registro
* @param $descripcion descripción del nuevo registro
* @param $fechaLim fecha limite del nuevo registro
* @param $categoria categoria del nuevo registro
* @param $prioridad prioridad del nuevo registro
* @return PDOStatement
*/
public static function insert(
$titulo,
$descripcion,
$fechaLim,
$categoria,
$prioridad
)
{
// Sentencia INSERT
$comando = "INSERT INTO meta ( " .
"titulo," .
" descripcion," .
" fechaLim," .
" categoria," .
" prioridad)" .
" VALUES( ?,?,?,?,?)";
// Preparar la sentencia
$sentencia = Database::getInstance()‐>getDb()‐>prepare($comando);
return $sentencia‐>execute(
array(
$titulo,
$descripcion,
$fechaLim,
$categoria,
$prioridad
)
);
/**
* Eliminar el registro con el identificador especificado
*
* @param $idMeta identificador de la meta
* @return bool Respuesta de la eliminación
*/
public static function delete($idMeta)
{
// Sentencia DELETE
$comando = "DELETE FROM meta WHERE idMeta=?";
// Preparar la sentencia
$sentencia = Database::getInstance()‐>getDb()‐>prepare($comando);
return $sentencia‐>execute(array($idMeta));
}
}
?>
Recuerda que el método prepare() permite reemplazar los placeholders ( '?' ) a través de execute() . Esto protege la operación de
inyecciones que puedan atentar contra la seguridad de los datos.
Tenemos un flujo que se asegura de satisfacer el debido resultado y aquellos resultados adversos. La trata de errores debe comprender todos
aquellos posibles caminos que puedan generarse como una petición fallida, la falla de autenticación, la no existencia del recurso, la no
disponibilidad del servidor, etc. En resumen, contempla todas las fallas tanto del lado del servidor (códigos 5xx) como las del cliente (códigos
4xx).
No obstante este ejemplo se basa en el comportamiento ideal de nuestro servidor local. Donde solo reportaremos aquellas anomalías que
sucedan en la base de datos, asumiendo que la respuesta siempre tendrá un código de estado 200. Esto permitirá trackear si nuestro web
service está operando bien la base de datos.
Además de ello PDO puede retornar en excepciones por distintas causas que puedes estandarizar para el envío de mensajes. Pero este trabajo
te queda a tí
Es justo donde entra Json para actuar como formato de comunicación. En cada respuesta enviaremos una seria de elementos Json que puedan
ser interpretados del lado del cliente. Esto te será posible usando las funciones json_encode() y json_decode() . La primera parsea un tipo de
dato a un string en formato json y la segunda es el procedimiento contrario.
obtener_metas.php
<?php
/**
* Obtiene todas las metas de la base de datos
*/
require 'Meta.php';
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if ($metas) {
$datos["estado"] = 1;
$datos["metas"] = $metas;
print json_encode($datos);
} else {
print json_decode(array(
"estado" => 2,
"mensaje" => "Ha ocurrido un error"
));
}
}
El objeto Json que retornaremos tiene un atributo llamado "estado" el cual representa un código para indicar la calidad del resultado. Si es
entonces añadiremos otro atributo llamado "metas" el cual es un array de objetos con los datos de las metas. Si es 2 , entonces usaremos un
atributo "mensaje" para avisar a la aplicación cliente que ocurrió un error en la operación a la base de datos.
{
"estado":1,
"metas":[
{
"idMeta":"2",
"titulo":"Obtener mi t\u00edtulo de ingenier\u00eda de sistemas",
"descripcion":"Ya solo faltan 2 semestres para terminar mi carrera de ingenier\u00eda. Debo prepararme al m\u00e1ximo para desarrollar mi t
"prioridad":"Media",
"fechaLim":"2015‐05‐29",
"categoria":"Profesional"
},
{
"idMeta":"3",
"titulo":"Conquistar a Natasha",
"descripcion":"Natasha es la mujer de vida. Tengo que dec\u00edrselo antes de que acabe el semestre",
"prioridad":"Alta",
"fechaLim":"2015‐05‐25",
"categoria":"Espiritual"
}
]
}
{
"estado":"2",
"mensaje":"Ha ocurrido un error"
}
Por ejemplo…
Puede que requieras en orden ascendente o descendente de los registros con respecto a un campo. O simplemente obtener las metas que van
de una fecha a otra.
Para tener en cuenta estos casos, puedes consultar los datos de acuerdo a una serie de parámetros establecidos en la API. Esto quiere decir
que podrías incluir en el cuerpo de la petición variables que actúen como filtros en la selección. Sin embargo dicho tema está fuera del alcance
de nuestro artículo.
El diseño RESTful para Web Services provee reglas supremamente estilizadas para filtrar y consultar datos de forma más sencilla que
estableciendo filtros manuales.
Paso #6: Crear un script php para consultar el detalle de una meta
El segundo caso requiere que la petición traiga consigo el identificador de la meta que se desea ver en detalle. Con este dato es posible usar el
método getById() de Meta para conseguir el array necesario.
Veamos:
<?php
/**
* Obtiene el detalle de una meta especificada por
* su identificador "idMeta"
*/
require 'Meta.php';
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if (isset($_GET['idMeta'])) {
// Tratar retorno
$retorno = Meta::getById($parametro);
if ($retorno) {
$meta["estado"] = "1";
$meta["meta"] = $retorno;
// Enviar objeto json de la meta
print json_encode($meta);
} else {
// Enviar respuesta de error general
print json_encode(
array(
'estado' => '2',
'mensaje' => 'No se obtuvo el registro'
)
);
}
} else {
// Enviar respuesta de error
print json_encode(
array(
'estado' => '3',
'mensaje' => 'Se necesita un identificador'
)
);
}
}
Para retornar el detalle obviamente primero debes comprobar que el parámetro vino con la petición GET y si vino bien definido. Recuerda que
la función isset() es quién realiza este trabajo.
Esta vez tenemos tres casos generales posibles. Que la consulta sea un éxito y el registro con el identificador enviado existe. Lo que retorna en
un objeto Json con un objeto interno que tiene los datos de la meta.
{
"estado":"1",
"meta":{
"idMeta":"2",
"titulo":"Obtener mi t\u00edtulo de ingenier\u00eda de sistemas",
"descripcion":"Ya solo faltan 2 semestres para terminar mi carrera de ingenier\u00eda. Debo prepararme al m\u00e1ximo para desarrollar mi tesi
"prioridad":"Media",
"fechaLim":"2015‐05‐29",
"categoria":"Profesional"
}
}
O también puede que PDO haya arrojado una excepción por algún motivo. Por ejemplo un error de sintaxis, la inexistencia del registro, etc. Con
ello envías tu objeto representativo del estado 2 .
{
"estado":"2",
"mensaje":"No se obtuvo el registro"
}
Ahora bien, puede que por alguna razón el parámetro no haya venido en la petición, o que pueda que haya venido pero con otro nombre. Para
este caso envías tu código 3 indicando este mensaje.
{
"estado":"3",
"mensaje":"Se necesita un identificador"
}
<?php
/**
* Insertar una nueva meta en la base de datos
*/
require 'Meta.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Insertar meta
$retorno = Meta::insert(
$body['titulo'],
$body['descripcion'],
$body['fechaLim'],
$body['categoria'],
$body['prioridad']);
if ($retorno) {
// Código de éxito
print json_encode(
array(
'estado' => '1',
'mensaje' => 'Creación exitosa')
);
} else {
// Código de falla
print json_encode(
array(
'estado' => '2',
'mensaje' => 'Creación fallida')
);
}
}
La primera instrucción es comprobar la petición POST obtenida. Luego de ello conviertes el cuerpo de la petición a un arreglo de strings. Esto es
posible consultando el flujo con file_get_contents() , que convierte un archivo a string. Obviamente es necesario que uses la convención
“php://input” para acceder al cuerpo de la petición POST.
Ahora, el resultado que obtengas con file_get_contents() debe estar en formato Json, por lo que convertiremos esos datos a un arreglo
asociativo que nos permita acceder a la información. Para ello usa la función json_decode() y pasa como segundo parámetro el valor de true
Luego usa el método insert() de Meta y comprueba el resultado. Esta vez no retornas en filas de la base de datos, así que el estado 1
contiene un mensaje de éxito.
{
"estado":"1",
"mensaje":"Creación éxitosa"
}
{
"estado":"2",
"mensaje":"Creación fallida"
}
<?php
/**
* Actualiza una meta especificada por su identificador
*/
require 'Meta.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Actualizar meta
$retorno = Meta::update(
$body['idMeta'],
$body['titulo'],
$body['descripcion'],
$body['fechaLim'],
$body['categoria'],
$body['prioridad']);
if ($retorno) {
// Código de éxito
print json_encode(
array(
'estado' => '1',
'mensaje' => 'Actualización exitosa')
);
} else {
// Código de falla
print json_encode(
array(
'estado' => '2',
'mensaje' => 'Actualización fallida')
);
}
}
{
"estado":"1",
"mensaje":"Actualización éxitosa"
}
{
"estado":"2",
"mensaje":"Actualización fallida"
}
<?php
/**
* Elimina una meta de la base de datos
* distinguida por su identificador
*/
require 'Meta.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$retorno = Meta::delete($body['idMeta']);
if ($retorno) {
print json_encode(
array(
'estado' => '1',
'mensaje' => 'Eliminación exitosa')
);
} else {
print json_encode(
array(
'estado' => '2',
'mensaje' => 'Eliminación fallida')
);
}
}
Como ves este servicio no es nada complicado. Su respuesta de éxito ser vería de la siguiente forma:
{
"estado":"1",
"mensaje":"Eliminación éxitosa"
}
Y los errores se mostrarían así:
{
"estado":"2",
"mensaje":"Eliminación fallida"
}
La idea es enfocarnos en el uso del servicio web y aprovechar al máximo las peticiones Json que nos provee Volley.
VolleySingleton.java
import android.content.Context;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
/**
* Creado por Hermosa Programación.
*
* Clase que representa un cliente HTTP Volley
*/
// Atributos
private static VolleySingleton singleton;
private RequestQueue requestQueue;
private static Context context;
/**
* Retorna la instancia unica del singleton
* @param context contexto donde se ejecutarán las peticiones
* @return Instancia
*/
public static synchronized VolleySingleton getInstance(Context context) {
if (singleton == null) {
singleton = new VolleySingleton(context.getApplicationContext());
}
return singleton;
}
/**
* Obtiene la instancia de la cola de peticiones
* @return cola de peticiones
*/
public RequestQueue getRequestQueue() {
if (requestQueue == null) {
requestQueue = Volley.newRequestQueue(context.getApplicationContext());
}
return requestQueue;
}
/**
* Añade la petición a la cola
* @param req petición
* @param <T> Resultado final de tipo T
*/
public <T> void addToRequestQueue(Request<T> req) {
getRequestQueue().add(req);
}
Para acceder a las URLs del web service con aislamiento, crea una clase para referenciar constantes de la aplicación. Allí añadirás todas las
direcciones para evitar múltiples declaraciones:
/**
* Clase que contiene los códigos usados en "I Wish" para
* mantener la integridad en las interacciones entre actividades
* y fragmentos
*/
public class Constantes {
/**
* Transición Home ‐> Detalle
*/
public static final int CODIGO_DETALLE = 100;
/**
* Transición Detalle ‐> Actualización
*/
public static final int CODIGO_ACTUALIZACION = 101;
/**
* URLs del Web Service
*/
public static final String GET = "http://10.0.3.2:63343/I%20Wish/obtener_metas.php";
public static final String GET_BY_ID = "http://10.0.3.2:63343/I%20Wish/obtener_meta_por_id.php";
public static final String UPDATE = "http://10.0.3.2:63343/I%20Wish/actualizar_meta.php";
public static final String DELETE = "http://10.0.3.2:63343/I%20Wish/borrar_meta.php";
public static final String INSERT = "http://10.0.3.2:63343/I%20Wish/insertar_meta.php";
/**
* Clave para el valor extra que representa al identificador de una meta
*/
public static final String EXTRA_ID = "IDEXTRA";
Como ves, yo uso para el localhost la dirección 10.0.3.2 debido a que Genymotion (emulador alternativo) estableció este valor. Si vas a usar el
emulador de android usa la dirección 10.0.2.2 . Aquí el sitio oficial te habla un poco mas sobre estás convenciones de direcciones para
operaciones en la web.
Crea una nueva clase en Android Studio y llámala Meta . Pon todos aquellos atributos puestos en la base de datos:
/**
* Reflejo de la tabla 'meta' en la base de datos
*/
public class Meta {
/*
Atributos
*/
private String idMeta;
private String titulo;
private String descripcion;
private String prioridad;
private String fechaLim;
private String categoria;
public Meta(String idMeta, String titulo, String descripcion, String prioridad, String fechaLim, String categoria) {
this.idMeta = idMeta;
this.titulo = titulo;
this.descripcion = descripcion;
this.prioridad = prioridad;
this.fechaLim = fechaLim;
this.categoria = categoria;
}
/**
* Compara los atributos de dos metas
* @param meta Meta externa
* @return true si son iguales, false si hay cambios
*/
public boolean compararCon(Meta meta) {
return this.titulo.compareTo(meta.titulo) == 0 &&
this.descripcion.compareTo(meta.descripcion) == 0 &&
this.fechaLim.compareTo(meta.fechaLim) == 0 &&
this.categoria.compareTo(meta.categoria) == 0 &&
this.prioridad.compareTo(meta.prioridad) == 0;
}
}
Si te fijas, tenemos un método para comparar una meta con otra para determinar si son iguales o no. Este método será de gran ayuda al
momento de validar si hay cambios en los datos de los formularios cuando el usuario interactúa con ellos. Lo que permitirá determinar si hay
que lanzar diálogos de confirmación antes de aplicar acciones.
No olvides usar le patrón ViewHolder para reducir la cantidad de llamadas del método findViewById().
Además de ello tenemos que implementar sobre cada view holder una escucha OnClickListener para recibir los eventos del usuario en la lista.
Para ello se creará una interfaz intermediaria entre el ViewHolder y el adaptador, de tal forma que cuando se active el evento onClick() este
inicie la actividad de detalle.
import android.app.Activity;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.modelo.Meta;
import com.herprogramacion.iwish.ui.actividades.DetailActivity;
import java.util.List;
/**
* Adaptador del recycler view
*/
public class MetaAdapter extends RecyclerView.Adapter<MetaAdapter.MetaViewHolder>
implements ItemClickListener {
/**
* Lista de objetos {@link Meta} que representan la fuente de datos
* de inflado
*/
private List<Meta> items;
/*
Contexto donde actua el recycler view
*/
private Context context;
@Override
public int getItemCount() {
return items.size();
}
@Override
public MetaViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_list, viewGroup, false);
return new MetaViewHolder(v, this);
}
@Override
public void onBindViewHolder(MetaViewHolder viewHolder, int i) {
viewHolder.titulo.setText(items.get(i).getTitulo());
viewHolder.prioridad.setText(items.get(i).getPrioridad());
viewHolder.fechaLim.setText(items.get(i).getFechaLim());
viewHolder.categoria.setText(items.get(i).getCategoria());
}
/**
* Sobrescritura del método de la interfaz {@link ItemClickListener}
*
* @param view item actual
* @param position posición del item actual
*/
@Override
public void onItemClick(View view, int position) {
DetailActivity.launch(
(Activity) context, items.get(position).getIdMeta());
}
@Override
public void onClick(View v) {
listener.onItemClick(v, getAdapterPosition());
}
}
}
interface ItemClickListener {
void onItemClick(View view, int position);
}
ItemClickListener es la interfaz de comunicación que nos ayudará a relacionar lo posición del view con el evento onClick() . Como ves se
implementa en la clase MetaAdapter para iniciar la actividad detalle a través de su método de fabricación launch() .
Es necesario que enviemos el identificador de la meta para tener una referencia de la meta que debemos detallar.
Esto significa que se debe realizar otra petición para obtener los datos de la meta seleccionada. Lo que podría evitarse a través de caching con
SQLite o enviando todos los datos de la meta. Sin embargo el fin de este tutorial es el uso al máximo de nuestro Web Service para que puedas
interiorizar el conocimiento y practicar esta metodología. Por ahora no te preocupes en la arquitectura u optimizaciones.
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.ui.fragmentos.MainFragment;
/**
* Actividad principal que contiene un fragmento con una lista.
* Recuerda que la nueva librería de soporte reemplazó la clase
* {@link android.support.v7.app.ActionBarActivity} por
* {@link AppCompatActivity} para el uso de la action bar
* en versiones antiguas.
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
La comunicación inicial con el servidor es la lectura de todas las metas que se han guardado hasta el momento. Con ellas poblaremos la lista a
penas inicie la aplicación. Por lo que debemos dirigirnos al fragmento principal y generar una petición GET hacia el servidor en onCreateView()
Veamos:
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.google.gson.Gson;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.modelo.Meta;
import com.herprogramacion.iwish.tools.Constantes;
import com.herprogramacion.iwish.ui.MetaAdapter;
import com.herprogramacion.iwish.ui.actividades.InsertActivity;
import com.herprogramacion.iwish.web.VolleySingleton;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
/**
* Fragmento principal que contiene la lista de las metas
*/
public class MainFragment extends Fragment {
/*
Etiqueta de depuracion
*/
private static final String TAG = MainFragment.class.getSimpleName();
/*
Adaptador del recycler view
*/
private MetaAdapter adapter;
/*
Instancia global del recycler view
*/
private RecyclerView lista;
/*
instancia global del administrador
*/
private RecyclerView.LayoutManager lManager;
/*
Instancia global del FAB
*/
com.melnykov.fab.FloatingActionButton fab;
private Gson gson = new Gson();
public MainFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return v;
}
/**
* Carga el adaptador con las metas obtenidas
* en la respuesta
*/
public void cargarAdaptador() {
// Petición GET
VolleySingleton.
getInstance(getActivity()).
addToRequestQueue(
new JsonObjectRequest(
Request.Method.GET,
Constantes.GET,
null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
// Procesar la respuesta Json
procesarRespuesta(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error Volley: " + error.getMessage());
}
}
)
);
}
/**
* Interpreta los resultados de la respuesta y así
* realizar las operaciones correspondientes
*
* @param response Objeto Json con la respuesta
*/
private void procesarRespuesta(JSONObject response) {
try {
// Obtener atributo "estado"
String estado = response.getString("estado");
switch (estado) {
case "1": // EXITO
// Obtener array "metas" Json
JSONArray mensaje = response.getJSONArray("metas");
// Parsear con Gson
Meta[] metas = gson.fromJson(mensaje.toString(), Meta[].class);
// Inicializar adaptador
adapter = new MetaAdapter(Arrays.asList(metas), getActivity());
// Setear adaptador a la lista
lista.setAdapter(adapter);
break;
case "2": // FALLIDO
String mensaje2 = response.getString("mensaje");
Toast.makeText(
getActivity(),
mensaje2,
Toast.LENGTH_LONG).show();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
El código anterior muestra el uso de una constante llamada Constantes.GET, la cual contiene la dirección del servicio de obtención
obtener_metas.php.
Con esa URL ya es posible realizar la petición JsonObjectRequest con su respectivo método GET a través del método cargarAdaptador() .
Para aislar un poco los procesos, he creado el método procesarRespuesta() , el cual recibe un objeto JSONObject en bruto para comenzar el
parsing. Donde he divido los caminos a través de una estructura switch basado en el valor del atributo "estado" .
Si el estado es exitoso inmediatamente obtendremos el array de metas que viene en el atributo "metas" . Este arreglo de objetos Json se parsea
directamente a un arreglo de objetos Meta a través de la librería Gson.
Recuerda que el adaptador recibe una serie de metas en formato List<Meta> , por lo que usaremos la clase Arrays para convertir el arreglo de
metas a lista. Con eso listo ya es posible instanciar el adaptador y asignarlo al recycler.
DetailActivity.java
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.tools.Constantes;
import com.herprogramacion.iwish.ui.fragmentos.DetailFragment;
/**
* Esta actividad contiene un fragmento que muestra el detalle
* de las metas.
*/
public class DetailActivity extends AppCompatActivity {
/*
Valor extra que identifica a la meta a detallar
*/
private static final String EXTRA_ID = "IDMETA";
/**
* Instancia global de la meta a detallar
*/
private String idMeta;
/**
* Inicia una nueva instancia de la actividad
*
* @param activity Contexto desde donde se lanzará
* @param idMeta Identificador de la meta a detallar
*/
public static void launch(Activity activity, String idMeta) {
Intent intent = getLaunchIntent(activity, idMeta);
activity.startActivityForResult(intent, Constantes.CODIGO_DETALLE);
}
/**
* Construye un Intent a partir del contexto y la actividad
* de detalle.
*
* @param context Contexto donde se inicia
* @param idMeta Identificador de la meta
* @return Intent listo para usar
*/
public static Intent getLaunchIntent(Context context, String idMeta) {
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra(EXTRA_ID, idMeta);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getSupportActionBar() != null) {
// Dehabilitar titulo de la actividad
getSupportActionBar().setDisplayShowTitleEnabled(false);
// Setear ícono "X" como Up button
getSupportActionBar().setHomeAsUpIndicator(R.mipmap.ic_close);
}
// Retener instancia
if (getIntent().getStringExtra(EXTRA_ID) != null)
idMeta = getIntent().getStringExtra(EXTRA_ID);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, DetailFragment.createInstance(idMeta), "DetailFragment")
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_detail, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
Este código tiene varias cosas interesantes. En primera instancia el uso de un método estático llamado launch() , el cual construye una
instancia de la actividad de detalle y la inicia a través de un Intent construido a partir del contexto que el adaptador proveerá.
La actividad detalle se basa en el identificador de la meta, por lo que idMeta es un atributo que permitirá retener esa instancia, cuando sea
pedida con getIntent() .
A los fragmentos que hemos iniciado dinámicamente se les está asignando una etiqueta que los diferencie de los otros. Esto es de suprema
importancia, ya que necesitamos obtener sus instancias cuando la actividad se comunique con ellos.
Dependiendo del enfoque de experiencia de usuario que tengas, puede que sean muchas cosas. Sin embargo para este ejemplo el usuario
tiene dos caminos evidentes:
La primera interacción ya la tenemos cubierta en DetailActivity , ya que hemos sobrescrito el comportamiento del Up Button por el cierre de
la actividad.
En el segundo caso de edición es necesario consultar la base de datos para setear los datos en los views. Además de ello asignar una escucha al
FAB para que inicie la actividad de actualización.
Realizar petición HTTP: La realización de la petición HTTP requiere consultar el detalle con el identificador que el adaptador envío a través del
Intent.
Recuerda que la convención createInstance() inicializa un nuevo fragmento con los extras necesarios para su funcionamiento. Por lo que en
onCreateView() es posible acceder al identificador y enviar una petición GET hacia el Web Service:
DetailFragment.java
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.google.gson.Gson;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.modelo.Meta;
import com.herprogramacion.iwish.tools.Constantes;
import com.herprogramacion.iwish.ui.actividades.UpdateActivity;
import com.herprogramacion.iwish.web.VolleySingleton;
import org.json.JSONException;
import org.json.JSONObject;
/**
* A placeholder fragment containing a simple view.
*/
public class DetailFragment extends Fragment {
/*
Etiqueta de valor extra
*/
private static final String EXTRA_ID = "IDMETA";
/**
* Etiqueta de depuración
*/
private static final String TAG = DetailFragment.class.getSimpleName();
/*
Instancias de Views
*/
private ImageView cabecera;
private TextView titulo;
private TextView descripcion;
private TextView prioridad;
private TextView fechaLim;
private TextView categoria;
private ImageButton editButton;
private String extra;
private Gson gson = new Gson();
public DetailFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_detail, container, false);
// Obtención de views
cabecera = (ImageView) v.findViewById(R.id.cabecera);
titulo = (TextView) v.findViewById(R.id.titulo);
descripcion = (TextView) v.findViewById(R.id.descripcion);
prioridad = (TextView) v.findViewById(R.id.prioridad);
fechaLim = (TextView) v.findViewById(R.id.fecha);
categoria = (TextView) v.findViewById(R.id.categoria);
editButton = (ImageButton) v.findViewById(R.id.fab);
return v;
}
/**
* Obtiene los datos desde el servidor
*/
public void cargarDatos() {
@Override
public void onResponse(JSONObject response) {
// Procesar respuesta Json
procesarRespuesta(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error Volley: " + error.getMessage());
}
}
)
);
}
/**
* Procesa cada uno de los estados posibles de la
* respuesta enviada desde el servidor
*
* @param response Objeto Json
*/
private void procesarRespuesta(JSONObject response) {
try {
// Obtener atributo "mensaje"
String mensaje = response.getString("estado");
switch (mensaje) {
case "1":
// Obtener objeto "meta"
JSONObject object = response.getJSONObject("meta");
//Parsear objeto
Meta meta = gson.fromJson(object.toString(), Meta.class);
break;
case "2":
String mensaje2 = response.getString("mensaje");
Toast.makeText(
getActivity(),
mensaje2,
Toast.LENGTH_LONG).show();
break;
case "3":
String mensaje3 = response.getString("mensaje");
Toast.makeText(
getActivity(),
mensaje3,
Toast.LENGTH_LONG).show();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
Para la inclusión de parámetros en la petición GET, adjunta a la URL el valor de idMeta con la convención de formularios '?clave=valor' .
Al igual que en MainFragment , se creó un método para procesar la respuesta dependiendo del estado que se obtuvo. Si hubo éxito, entonces
seteamos los valores correspondientes de cada view.
La lógica funciona así: Una vez el usuario haya modificado los datos, entonces puede confirmar sus datos presionando el action button de check
que usaremos para la confirmación. Si no está de acuerdo o se arrepiente de ello, entonces puede descartar los cambios con el segundo action
button. Incluso puedes incluir la eliminación entre los action buttos.
Así que lo primero es crear un archivo de menú para poblar la action bar:
menu_form.xml
<menu 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"
tools:context="com.herprogramacion.iwish.ui.actividades.UpdateActivity">
Teniendo en cuenta esa apreciación las tareas que tienes por implementar son:
Veamos la solución:
UpdateFragment.java
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.google.gson.Gson;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.modelo.Meta;
import com.herprogramacion.iwish.tools.Constantes;
import com.herprogramacion.iwish.web.VolleySingleton;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* Fragmento con formulario para actualizar la meta
*/
public class UpdateFragment extends Fragment {
/*
Etiqueta de depuración
*/
private static final String TAG = UpdateFragment.class.getSimpleName();
/*
Etiqueta de valor extra para modo edición
*/
private static final String EXTRA_ID = "IDMETA";
/*
Controles
*/
private EditText titulo_input;
private EditText descripcion_input;
private Spinner prioridad_spinner;
private TextView fecha_text;
private Spinner categoria_spinner;
/*
Valor del argumento extra
*/
private String idMeta;
/**
* Es la meta obtenida como respuesta de la petición HTTP
*/
private Meta metaOriginal;
/**
* Instancia Gson para el parsing Json
*/
private Gson gson = new Gson();
public UpdateFragment() {
}
/**
* Crea un nuevo fragmento basado en un argumento
*
* @param extra Argumento de entrada
* @return Nuevo fragmento
*/
public static Fragment createInstance(String extra) {
UpdateFragment detailFragment = new UpdateFragment();
Bundle bundle = new Bundle();
bundle.putString(EXTRA_ID, extra);
detailFragment.setArguments(bundle);
return detailFragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
fecha_text.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
DialogFragment picker = new DatePickerFragment();
picker.show(getFragmentManager(), "datePicker");
}
}
);
if (idMeta != null) {
cargarDatos();
}
return v;
}
/**
* Obtiene los datos desde el servidor
*/
private void cargarDatos() {
// Añadiendo idMeta como parámetro a la URL
String newURL = Constantes.GET_BY_ID + "?idMeta=" + idMeta;
@Override
public void onResponse(JSONObject response) {
// Procesa la respuesta GET_BY_ID
procesarRespuestaGet(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error Volley: " + error.getMessage());
}
}
)
);
}
/**
* Procesa la respuesta de obtención obtenida desde el sevidor *
*/
private void procesarRespuestaGet(JSONObject response) {
try {
String estado = response.getString("estado");
switch (estado) {
case "1":
JSONObject meta = response.getJSONObject("meta");
// Guardar instancia
metaOriginal = gson.fromJson(meta.toString(), Meta.class);
// Setear valores de la meta
cargarViews(metaOriginal);
break;
case "2":
String mensaje = response.getString("mensaje");
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de falla
getActivity().setResult(Activity.RESULT_CANCELED);
// Terminar actividad
getActivity().finish();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* Carga los datos iniciales del formulario con los
* atributos de un objeto {@link Meta}
*
* @param meta Instancia
*/
private void cargarViews(Meta meta) {
// Seteando valores de la respuesta
titulo_input.setText(meta.getTitulo());
descripcion_input.setText(meta.getDescripcion());
fecha_text.setText(meta.getFechaLim());
break;
}
}
/**
* Compara los datos actuales con aquellos que se obtuvieron
* por primera vez en la respuesta HTTP
*
* @return true si los datos no han cambiado, de lo contrario false
*/
public boolean validarCambios() {
return metaOriginal.compararCon(obtenederDatos());
}
/**
* Retorna en una nueva meta creada a partir
* de los datos del formulario actual
*
* @return Instancia {@link Meta}
*/
private Meta obtenederDatos() {
String titulo = titulo_input.getText().toString();
String descripcion = descripcion_input.getText().toString();
String fecha = fecha_text.getText().toString();
String categoria = (String) categoria_spinner.getSelectedItem();
String prioridad = (String) prioridad_spinner.getSelectedItem();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true); // Contribución a la AB
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:// CONFIRMAR
if (!validarCambios())
guardarMeta();
else
// Terminar actividad, ya que no hay cambios
getActivity().finish();
return true;
}
;
return super.onOptionsItemSelected(item);
}
/**
* Guarda los cambios de una meta editada.
* <p>
* Si está en modo inserción, entonces crea una nueva
* meta en la base de datos
*/
private void guardarMeta() {
map.put("idMeta", idMeta);
map.put("titulo", titulo);
map.put("descripcion", descripcion);
map.put("fechaLim", fecha);
map.put("categoria", categoria);
map.put("prioridad", prioridad);
) {
@Override
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content‐Type", "application/json; charset=utf‐8");
headers.put("Accept", "application/json");
return headers;
}
@Override
public String getBodyContentType() {
return "application/json; charset=utf‐8" + getParamsEncoding();
}
}
);
/**
* Procesa todos las tareas para eliminar
* una meta en la aplicación. Este método solo se usa
* en la edición
*/
public void eliminarMeta() {
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error Volley: " + error.getMessage());
}
}
) {
@Override
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content‐Type", "application/json; charset=utf‐8");
headers.put("Accept", "application/json");
return headers;
}
@Override
public String getBodyContentType() {
return "application/json; charset=utf‐8" + getParamsEncoding();
}
}
);
}
/**
* Procesa la respuesta de eliminación obtenida desde el sevidor
*/
private void procesarRespuestaEliminar(JSONObject response) {
try {
// Obtener estado
String estado = response.getString("estado");
// Obtener mensaje
String mensaje = response.getString("mensaje");
switch (estado) {
case "1":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de éxito
getActivity().setResult(203);
// Terminar actividad
getActivity().finish();
break;
case "2":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de falla
getActivity().setResult(Activity.RESULT_CANCELED);
// Terminar actividad
getActivity().finish();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
/**
* Procesa la respuesta de actualización obtenida desde el sevidor
*/
private void procesarRespuestaActualizar(JSONObject response) {
try {
// Obtener estado
String estado = response.getString("estado");
// Obtener mensaje
String mensaje = response.getString("mensaje");
switch (estado) {
case "1":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de éxito
getActivity().setResult(Activity.RESULT_OK);
// Terminar actividad
getActivity().finish();
break;
case "2":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de falla
getActivity().setResult(Activity.RESULT_CANCELED);
// Terminar actividad
getActivity().finish();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
/**
* Actualiza la fecha del campo {@link fecha_text}
*
* @param ano Año
* @param mes Mes
* @param dia Día
*/
public void actualizarFecha(int ano, int mes, int dia) {
// Setear en el textview la fecha
fecha_text.setText(ano + "‐" + (mes + 1) + "‐" + dia);
}
/**
* Muestra un diálogo de confirmación, cuyo mensaje esta
* basado en el parámetro identificador de Strings
*
* @param id Parámetro
*/
private void mostrarDialogo(int id) {
DialogFragment dialogo = ConfirmDialogFragment.
createInstance(
getResources().
getString(id));
dialogo.show(getFragmentManager(), "ConfirmDialog");
}
Este código es un poco largo debido a que tenemos la implementación de diálogos y comunicaciones de datos. Por lo que a continuación te
explico la esencia de las peticiones de información.
Cargar los datos de la meta en los componentes del formulario: En el método onCreateView() obtenemos el valor extra con que fue creado el
fragmento. Si existe un valor extra, lanzamos la misma petición que hemos usado para conseguir el detalle de la meta con el método
cargarDatos() .
Inmediatamente los datos conseguidos en la petición, los seteamos en cada view del formulario.
Manejar los eventos en cada action button: Para lograr esta tarea se implementó el método onOptionsItemSelected() , donde se creó una
estructura switch que permitiera la ejecución del método correspondiente a la acción. Recuerda usar onHasOptionMenu() en onCreate() para
que el fragmento pueda escuchar los eventos de la action bar.
Implementar la inserción, eliminación y borrado de las metas: Cada operación en la base de datos tiene un método asignado para su
realización. Estos son: guardarMeta() y borrarMeta() . El primer método realiza una petición POST con la respectiva URL del servicio de
actualización, usando los valores actuales del formulario.
Similarmente borrarMeta() envía el id de la meta que se desea eliminar hacia la dirección correspondiente.
En cuanto a los diálogos, simplemente usamos el formato clásico de ACEPTAR|CANCELAR para permitir o no el efecto de los métodos en la base
de datos. Puedes encontrar la implementación completa descargando el código en la parte superior del artículo.
Iniciaremos el fragmento y estaremos a la espera de que el usuario guarde los datos o los descarte. Veamos:
InsertActivity.java
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.ui.fragmentos.ConfirmDialogFragment;
import com.herprogramacion.iwish.ui.fragmentos.DatePickerFragment;
import com.herprogramacion.iwish.ui.fragmentos.InsertFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getSupportActionBar() != null)
getSupportActionBar().setHomeAsUpIndicator(R.mipmap.ic_done);
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_form, menu);
return true;
}
@Override
public void onDateSelected(int year, int month, int day) {
InsertFragment insertFragment = (InsertFragment)
getSupportFragmentManager().findFragmentByTag("InsertFragment");
if (insertFragment != null) {
insertFragment.actualizarFecha(year, month, day);
}
}
@Override
public void onDialogPositiveClick(DialogFragment dialog) {
InsertFragment insertFragment = (InsertFragment)
getSupportFragmentManager().findFragmentByTag("InsertFragment");
if (insertFragment != null) {
finish(); // Finalizar actividad descartando cambios
}
}
@Override
public void onDialogNegativeClick(DialogFragment dialog) {
InsertFragment insertFragment = (InsertFragment)
getSupportFragmentManager().findFragmentByTag("InsertFragment");
if (insertFragment != null) {
// Nada por el momento
}
}
}
Ahora el fragmento de inserción tiene las siguientes características:
InsertFragment.java
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.herprogramacion.iwish.R;
import com.herprogramacion.iwish.tools.Constantes;
import com.herprogramacion.iwish.web.VolleySingleton;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* Fragmento que permite al usuario insertar un nueva meta
*/
public class InsertFragment extends Fragment {
/**
* Etiqueta para depuración
*/
private static final String TAG = InsertFragment.class.getSimpleName();
/*
Controles
*/
EditText titulo_input;
EditText descripcion_input;
Spinner prioridad_spinner;
TextView fecha_text;
Spinner categoria_spinner;
public InsertFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Habilitar al fragmento para contribuir en la action bar
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflando layout del fragmento
View v = inflater.inflate(R.layout.fragment_form, container, false);
}
}
);
return v;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// Remover el action button de borrar
menu.removeItem(R.id.action_delete);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:// CONFIRMAR
if (!camposVacios())
guardarMeta();
else
Toast.makeText(
getActivity(),
"Completa los campos",
Toast.LENGTH_LONG).show();
return true;
return super.onOptionsItemSelected(item);
}
/**
* Guarda los cambios de una meta editada.
* <p>
* Si está en modo inserción, entonces crea una nueva
* meta en la base de datos
*/
public void guardarMeta() {
map.put("titulo", titulo);
map.put("descripcion", descripcion);
map.put("fechaLim", fecha);
map.put("categoria", categoria);
map.put("prioridad", prioridad);
) {
@Override
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content‐Type", "application/json; charset=utf‐8");
headers.put("Accept", "application/json");
return headers;
}
@Override
public String getBodyContentType() {
return "application/json; charset=utf‐8" + getParamsEncoding();
}
}
);
/**
* Procesa la respuesta obtenida desde el sevidor
*
* @param response Objeto Json
*/
private void procesarRespuesta(JSONObject response) {
try {
// Obtener estado
String estado = response.getString("estado");
// Obtener mensaje
String mensaje = response.getString("mensaje");
switch (estado) {
case "1":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de éxito
getActivity().setResult(Activity.RESULT_OK);
// Terminar actividad
getActivity().finish();
break;
case "2":
// Mostrar mensaje
Toast.makeText(
getActivity(),
mensaje,
Toast.LENGTH_LONG).show();
// Enviar código de falla
getActivity().setResult(Activity.RESULT_CANCELED);
// Terminar actividad
getActivity().finish();
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
/**
* Valida si los campos {@link titulo_input} y {@link descripcion_input}
* se han rellenado
*
* @return true si alguno o dos de los campos están vacios, false si ambos
* están completos
*/
public boolean camposVacios() {
String titulo = titulo_input.getText().toString();
String descripcion = descripcion_input.getText().toString();
/**
* Actualiza la fecha del campo {@link fecha_text}
*
* @param ano Año
* @param mes Mes
* @param dia Día
*/
public void actualizarFecha(int ano, int mes, int dia) {
// Setear en el textview la fecha
fecha_text.setText(ano + "‐" + (mes + 1) + "‐" + dia);
}
/**
* Muestra un diálogo de confirmación
*/
public void mostrarDialogo() {
DialogFragment dialogo = ConfirmDialogFragment.
createInstance(
getResources().
getString(R.string.dialog_discard_msg));
dialogo.show(getFragmentManager(), "ConfirmDialog");
}
Esta vez hemos creado un método llamado guardarMeta() basado en la URL del servicio de inserción y los datos que el usuario haya
completado. Si te fijas en el procesamiento de los eventos sobre la action bar, puedes ver que existe la posibilidad de guardar y descartar los
datos.
Ambos se basan en la validación de los campos del formulario que requieren texto escrito por parte del usuario. Para ello se creó el método
camposVacios() . Dependiendo de su retorno así mismo procederemos.
Esto quiere decir que el usuario no puede guardar una meta sin completar alguno de los campos. Ni tampoco puede intentar descartar
cambios sin ver un diálogo si ya ha escrito algún dato.
Json es un formato muy flexible y cómodo a la vista. Esto lo hace un excelente complemento para implementar una API entre Android, Mysql y
Php.
Añade caching de información a través de SQLite para evitar realizar gran cantidad de operaciones de red.
Usa la clase ContentProvider para completar tu patrón MVC de Red y añadir restricciones RESTful en tu aplicación. Esto te permitirá
independizar tus clases y evitar problemas en tu hilo principal de forma sencilla.