3. Caso práctico - Gestión de citas
3.1. El proyecto
En el documento [AngularJS / Tutorial Primavera 4], se desarrolló una aplicación cliente/servidor para gestionar las citas con el médico. Nos referiremos a este documento como [rdvmedecins-angular] en adelante. La aplicación tenía dos tipos de clientes:
- un cliente HTML/CSS/JS;
- un cliente Android;
El cliente de Android se generó automáticamente a partir de la versión HTML del cliente utilizando la función [Córdoba]. El objetivo de este proyecto es recrear este cliente Android manualmente utilizando los conocimientos adquiridos en los capítulos anteriores.
Nótese una diferencia importante entre las dos soluciones:
- el que vamos a crear sólo funcionará en tabletas Android;
- en el [rdvmedecins-angular], el cliente web móvil (HTML/CSS/JS) funciona en cualquier plataforma (Android, iOS, Windows);
3.2. Vistas del cliente Android
Hay cuatro vistas.
Vista de configuración

Vista de selección de médico y fecha de cita

Vista de selección de franjas horarias de citas

Vista de selección de clientes para citas

3.3. Arquitectura de proyectos
Utilizaremos una arquitectura cliente/servidor similar a la del ejemplo [Ejemplo-15] (véase la sección 1.16) de este documento:

La comunicación asíncrona entre el cliente y el servidor se gestionará mediante la biblioteca RxAndroid.
3.4. La base de datos
No desempeña un papel fundamental en este documento. Lo proporcionamos a título informativo. Lo llamaremos [ dbrdvmedecins]. Se trata de una base de datos MySQL5 con cuatro tablas:
![]() |
3.4.1. La tabla [MEDECINS]
Contiene información sobre los médicos gestionados por la aplicación [RdvMedecins].
![]() | ![]() |
- ID: el número del médico ID: la clave principal de la tabla
- VERSION: un número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
- LAST_NAME: apellido del médico
- FIRST_NAME: nombre del médico
- TITLE: su título (Sra., Sra., Sr.)
3.4.2. La tabla [CLIENTS]
Los clientes de los distintos médicos se almacenan en la tabla [CLIENTS]:
![]() | ![]() |
- ID: el número del cliente ID: la clave primaria de la tabla
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
- LAST NAME: apellido del cliente
- FIRST NAME: nombre del cliente
- TITLE: su título (Sra., Sra., Sr.)
3.4.3. La tabla [SLOTS]
Enumera las franjas horarias en las que hay citas disponibles:
![]() |
![]() | ![]() | ![]() |
- ID: ID número de la franja horaria - clave primaria de la tabla (fila 8)
- VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
- DOCTOR_ID: ID número que identifica al médico al que pertenece esta franja horaria - clave externa en la columna DOCTORS(ID).
- START_TIME: hora de inicio de la franja horaria
- MSTART: Minuto de inicio de la franja horaria
- HFIN: hora de fin de ranura
- MFIN: Minutos finales de la ranura
La segunda fila de la tabla [SLOTS] (véase [1] más arriba) indica, por ejemplo, que la franja horaria nº 2 comienza a las 8:20 a.m. y termina a las 8:40 a.m. y pertenece al médico nº 1 (Sra. Marie PELISSIER).
3.4.4. La tabla [RV]
Enumera las citas concertadas para cada médico:
![]() | ![]() |
- ID: identificador único de la cita - clave primaria
- DAY: día de la cita
- SLOT_ID: franja horaria de la cita - clave externa sobre el campo [ID] de la tabla [SLOTS] - determina tanto la franja horaria como el médico implicado.
- CUSTOMER_ID: el cliente ID para el que se realiza la reserva - una clave externa en el campo [ID] de la tabla [CUSTOMERS]
Esta tabla tiene una restricción de unicidad en los valores de las columnas unidas (DAY, SLOT_ID):
If a row in the [RV] table has the value (DAY1, SLOT_ID1) for the columns (DAY, SLOT_ID), this value cannot appear anywhere else. Otherwise, this would mean that two appointments were booked at the same time for the same doctor. From a Java programming perspective, the database’s JDBC driver throws an SQLException when this occurs.
La fila con ID igual a 3 (véase [1] más arriba) significa que se reservó una cita para la ranura nº 20 y el cliente nº 4 el 23/08/2006. La tabla [SLOTS] nos indica que la franja horaria nº 20 corresponde a la franja horaria 4:20 PM - 4:40 PM y pertenece al médico nº. 1 (Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que la cliente nº 4 es la Sra. Brigitte BISTROU.
3.4.5. Generación de la base de datos
Para crear las tablas y rellenarlas, puede utilizar el script [dbrdvmedecins.sql], que se encuentra en el archivo de ejemplos |HERE|.
![]() |
Con [WampServer] (véase la sección 6.15), proceda del siguiente modo:
![]() | ![]() |
- En [1], haga clic en el icono [WampServer] y seleccione la opción [PhpMyAdmin] [2],
- en [3], en la ventana que se abre, seleccione el enlace [Bases de datos],
![]() |
- en [4-6], importar un archivo SQL,
![]() | ![]() | ![]() |
- en [7], selecciona el script SQL y en [8] ejecútalo,
- en [9], se han creado las tablas de la base de datos. Siga uno de los enlaces,
![]() |
- en [10], el contenido de la tabla.
No volveremos a esta base de datos, pero invitamos al lector a seguir su evolución a lo largo de las pruebas, sobre todo cuando la aplicación no funciona.
3.5. El servidor web / JSON

Aquí nos centramos en el servidor [1]. No lo desarrollaremos más. Se ha detallado en el documento [Primavera MVC y Thymeleaf por ejemplo]. Los lectores interesados pueden consultarlo. Fue desarrollado como el servidor del Ejemplo 15. Su código fuente se incluye en los ejemplos. Su código fuente está incluido en los ejemplos. Aquí usaremos su binario:
![]() |
- [rdvmedecins-server-all-1.0.jar] es el binario del servidor;
3.5.1. Aplicación
En una ventana de comandos, vaya a la carpeta que contiene el binario del servidor:
...\rdvmedecins>dir
The volume in drive D is named Data
The volume’s serial number is 7A34-AE5F
Directory: D:\data\istia-1516\projects\dvp-android-studio\rdvmedecins
06/09/2016 10:50 <DIR> .
06/09/2016 10:50 <DIR> ..
07/06/2014 16:36 7,631 dbrdvmedecins.sql
06/08/2016 4:31 PM <DIR> rdvmedecins-client
06/08/2016 4:22 PM <DIR> doctorappointments-server
06/08/2016 4:23 PM 29,618,709 rdvmedecins-server-all-1.0.jar
A continuación, para iniciar el servidor, introduzca el siguiente comando (el MySQL DBMS debe estar ya en ejecución):
...\rdvmedecins>java -jar rdvmedecins-server-all-1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.0)
10:55:48.617 [main] INFO rdvmedecins.boot.Boot - Starting Boot v1.0 on st-PC (D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins\rdvmedecins-server-all-1.0.jar started by st in D:\data\istia-1516\projets\dvp-android-studio\rdvmedecins)
10:55:48.621 [main] INFO rdvmedecins.boot.Boot - No active profile set, falling back to default profiles: default
10:55:48.662 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Thu Jun 09 10:55:48 CEST 2016]; root of context hierarchy
10:55:49.948 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
Jun 09, 2016 10:55:50 AM org.apache.catalina.core.StandardService startInternal
INFOS: Starting Tomcat service
June 09, 2016 10:55:50 AM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/8.0.33
June 9, 2016 10:55:50 AM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
10:55:50.255 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root
WebApplicationContext: initialization completed in 1596 ms
...
10:55:55.765 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain
- Creating filter chain: ...]
10:55:55.785 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
10:55:55.791 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
...
10:55:56.249 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String)
throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.252 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.255 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws
com.fasterxml.jackson.core.JsonProcessingException
10:55:56.257 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.259 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET],produces=[application/json;charset=UTF-8]}" onto
public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.261 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}"
onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getClientById(long, javax.servlet.http.HttpServletResponse, java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.264 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.266 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.268 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.270 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.273 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
10:55:56.276 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String rdvmedecins.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
10:55:56.681 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
10:55:56.686 [main] INFO rdvmedecins.boot.Boot - Started Boot in 8.231 seconds
El servidor muestra numerosos registros. Hemos incluido sólo los relevantes para entender el proceso anterior:
- líneas 14-18: Se lanza un servidor Tomcat embebido en el puerto 8080 de la máquina. Este servidor ejecuta la aplicación web de gestión de citas. Esta aplicación es en realidad un servicio web/JSON: se consulta a través de URLs y responde enviando una cadena JSON;
- línea 24: el servicio web está protegido mediante el framework [Spring Security]. Se accede al URLs del servicio web mediante autenticación;
- Líneas 29-44: el URLs expuesto por el servicio web;
Vamos a entrar en más detalles al respecto.
3.5.2. Protección del servicio web
Los URLs expuestos por el servicio web son seguros. El servidor espera la siguiente cabecera en la solicitud HTTP del cliente:
El código esperado es la codificación Base64 [http://fr.wikipedia.org/wiki/Base64] de la cadena "nombre de usuario:contraseña'. En su estado inicial, el servicio web sólo acepta un usuario llamado 'admin' con la contraseña 'admin'. Para este usuario en particular, el encabezado anterior se convierte en la siguiente línea:
Para enviar esta cabecera HTTP, utilizamos el cliente HTTP [Cliente de descanso avanzado], que es un complemento del navegador Chrome (véase la sección 6.13). Probaremos manualmente los distintos URLs expuestos por el servicio web para comprender:
- los parámetros esperados por el URL;
- la naturaleza exacta de su respuesta;
3.5.3. Lista de médicos
El URL [/getAllMedecins] recupera la lista de médicos:
![]() |
- en [1], el URL que se consulta;
- en [2], el método HTTP utilizado para esta solicitud;
- en [3], la cabecera de seguridad HTTP del usuario (admin, admin);
- en [4], se envía la petición HTTP;
La respuesta del servidor es la siguiente:
![]() |
- en [5], la respuesta JSON formateada del servidor;
![]() |
- en [6], la misma respuesta en formato bruto;
El formulario [5] facilita la visualización de la estructura de la respuesta. Todas las respuestas del servicio web son instancias de la siguiente clase [Response]:
package rdvmedecins.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// response body
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- línea 9: el estado de la respuesta. Un valor de 0 significa que no se ha producido ningún error; en caso contrario, se ha producido un error;
- línea 11: una lista de mensajes de error si se ha producido un error;
- línea 13: la respuesta realmente esperada por el cliente;
La respuesta al URL [/getAllMedecins] es una cadena JSON de un objeto de tipo [Response<List<Medecin>>]. La clase [Medecin] es la siguiente:
package rdvmedecins.android.dao.entities;
public class Doctor extends Person {
// default constructor
public Doctor() {
}
// constructor with parameters
public Doctor(String title, String lastName, String firstName) {
super(title, lastName, firstName);
}
public String toString() {
return String.format("Doctor[%s]", super.toString());
}
}
Línea 3: La clase [Doctor] extiende la siguiente clase [Persona]:
package rdvmedecins.android.dao.entities;
public class Person extends AbstractEntity {
// attributes of a person
private String title;
private String lastName;
private String lastName;
// default constructor
public Person() {
}
// constructor with parameters
public Person(String title, String lastName, String firstName) {
this.title = title;
this.lastName = lastName;
this.firstName = firstName;
}
// toString
public String toString() {
return String.format("Person[%s, %s, %s, %s, %s]", id, version, title, lastName, firstName);
}
// getters and setters
...
}
Línea 3: La clase [Persona] extiende la siguiente clase [AbstractEntity]:
package rdvmedecins.android.dao.entities;
import java.io.Serializable;
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
protected Long id;
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters and setters
...
}
En definitiva, la estructura de un objeto [Doctor] es la siguiente:
y la de [Response<List<Doctor>>] es la siguiente:
En adelante, utilizaremos estas definiciones abreviadas para describir la respuesta del servidor. Además, por el momento, ya no incluiremos capturas de pantalla. Basta con repasar lo que acabamos de cubrir. Volveremos a las capturas de pantalla cuando llegue el momento de realizar una petición POST. También presentaremos un ejemplo de ejecución en el siguiente formato:
3.5.4. Lista de clientes
|
Ejemplo:
3.5.5. Lista de citas con el médico
|
- [idMedecin]: ID del médico para el que desea las franjas horarias;
- [startTime] : hora de inicio de la cita;
- [start_time]: hora de inicio de la consulta;
- [hfin]: hora de finalización de la consulta;
- [endmin] : acta final de la consulta;
For a time slot between 10:20 and 10:40, we have [starts, starts, ends, ends] = [10, 20, 10, 40].
Ejemplo:
3.5.6. Lista de citas con el médico
|
- [idMedic] : identificador del médico cuyas citas se solicitan;
- URL [día]: día de las citas en el formato 'aaaa-mm-dd';
- Respuesta [día]: igual que arriba, pero en forma de fecha Java;
- [cliente]: el cliente de la cita. Su estructura se ha descrito anteriormente;
- [idClient]: el identificador del cliente;
- [ranura]: la ranura de la cita. Su estructura se ha descrito anteriormente;
- [slotId]: el identificador de la ranura;
Ejemplo:
3.5.7. La agenda de un médico
|
- [doctorId]: identificador del médico cuyas citas se desean;
- URL [día] : día de las citas en el formato 'aaaa-mm-dd' ;
- [calendario] : calendario del médico;
- [médico] : el médico en cuestión. Su estructura se definió anteriormente;
- Respuesta [día]: el día del calendario en forma de fecha Java;
- [doctorDaySlots]: una matriz de elementos de tipo [DoctorDaySlot];
- [ranura]: una ranura. Su estructura se ha descrito anteriormente;
- [cita]: una cita. Su estructura se ha descrito anteriormente;
Ejemplo:
|
Hemos destacado el caso en que hay una cita en la franja horaria y el caso en que no la hay.
3.5.8. Obtener un médico por su ID
|
- [doctorId]: el ID del médico;
Ejemplo 1:
Ejemplo 2:
3.5.9. Obtener un cliente por ID
|
- [idClient]: el cliente ID;
Ejemplo 1:
Ejemplo 2:
3.5.10. Reserve una franja horaria con su ID
|
- [slotId]: la ranura ID;
Ejemplo 1:
Tenga en cuenta que la respuesta no incluye el médico propietario de la ranura, sólo su ID.
Ejemplo 2:
3.5.11. Obtener una cita por su ID
|
- [idRv]: la cita ID;
Ejemplo 1:
Tenga en cuenta que la respuesta no incluye el cliente ni la franja horaria de la cita, sino sólo sus identificadores.
Ejemplo 2:
3.5.12. Añadir una cita
El URL [/addAppointment] permite añadir una cita. La información necesaria para esta adición (el día, la franja horaria y el cliente) se envía a través de una solicitud HTTP POST. Mostramos cómo realizar esta solicitud utilizando la herramienta [Cliente Resto Avanzado].

- en [1], el URL que se consulta;
- en [2], se consulta mediante una petición POST;
- en [3-4], especificamos al servidor que los valores que se publican están en formato JSON;
- en [4], la cabecera de autenticación HTTP;
- en [5], la información enviada a través de la petición POST. Se trata de una cadena JSON que contiene:
- [día]: el día de la cita en el formato "aaaa-mm-dd",
- [idClient]: el ID del cliente para el que se concierta la cita,
- [idCreneau]: identificador de la franja horaria de la cita. Dado que una franja horaria pertenece a un médico concreto, también se refiere al médico;
- en [6], se envía la solicitud;
La cadena JSON que se contabiliza es la del siguiente [PostAjouterRv] objeto:
public class PostAjouterRv {
// post data
private String day;
private long clientId;
private long slotId;
// constructors
public PostAddAppointment() {
}
public PostAddAppointment(String day, long slotId, long clientId) {
this.day = day;
this.clientId = clientId;
this.slotId = slotId;
}
// getters and setters
...
}
La respuesta del servidor es del tipo [Response<Rv>] [int status; List<String> messages; Rv rv], donde [rv] es la cita añadida.
La respuesta del servidor a la solicitud anterior es la siguiente:
![]() |
Tenga en cuenta que algunos datos no se incluyen [idClient, idCreneau], pero se pueden encontrar en los campos [cliente] y [creneau]. La información importante es el ID de la cita añadida (209). El servicio web podría haberse limitado a devolver este único dato.
3.5.13. Borrar una cita
Esta operación también se realiza mediante una petición POST:
|
El valor contabilizado es la cadena JSON de un objeto de tipo [PostSupprimerRv] como sigue:
public class PostDeleteRv {
// post data
private long idRv;
// constructors
public PostSupprimerRv() {
}
public PostDeleteRv(long idRv) {
this.idRv = idRv;
}
// getters and setters
...
}
- Línea 4: [idRv] es el ID de la cita a eliminar.
Ejemplo 1:
La cita nº 209 se ha eliminado correctamente porque [status=0].
Ejemplo 2:
3.6. El cliente Android

Ahora que el servidor [1] se ha descrito en detalle y está en funcionamiento, examinaremos el cliente Android [2].
3.6.1. Arquitectura de proyectos de Android Studio
El proyecto utiliza la arquitectura del proyecto [client-android-skel] (véase la sección 1.17). En la arquitectura de cliente Android mostrada anteriormente, hay tres capas distintas:
- la capa [DAO] responsable de la comunicación con el servicio web;
- las [vistas] responsables de la comunicación con el usuario;
- la [Actividad] que actúa de enlace entre los dos bloques anteriores. Las vistas no conocen la capa [DAO]. Sólo se comunican con la Actividad.
Esta arquitectura se refleja en el proyecto de Android Studio para el cliente Android:
![]() |
- el paquete [activity] implementa la actividad;
- el paquete [arquitectura] incluye los elementos arquitectónicos que hemos desarrollado anteriormente;
- el paquete [dao] implementa la capa [DAO];
- el paquete [fragments] implementa las [views];
3.6.2. Personalización de proyectos
![]() |
La carpeta [architecture/custom] contiene los elementos personalizables de la arquitectura.
La interfaz [IMainActivity] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// Change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for server response
int TIMEOUT = 1000;
// wait time before executing the client request
int DELAY = 000;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = true;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// loading icon
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 4;
// number of views
int VIEW_CONFIG = 0;
int HOME_VIEW = 1;
int CALENDAR_VIEW = 2;
int VIEW_ADD_APPT = 3;
}
- líneas 25, 28: personalización de la capa [DAO];
- línea 31: esta aplicación realiza peticiones autenticadas al servidor;
- línea 40: se requiere una imagen de carga;
- línea 43: la aplicación tiene cuatro fragmentos;
- líneas 46-49: los números de los cuatro fragmentos;
- línea 37: no hay pestañas;
La clase base [CoreState] para los estados de fragmento será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.HomeFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AddRvFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = HomeFragmentState.class),
@JsonSubTypes.Type(value = AgendaFragmentState.class),
@JsonSubTypes.Type(value = AjoutRvFragmentState.class),
@JsonSubTypes.Type(value = ConfigFragmentState.class)
}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- líneas 15-18: los cuatro fragmentos tienen un estado:
![]() |
Por último, la sesión contiene los datos compartidos entre fragmentos:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
import client.android.dao.entities.DoctorSchedule;
import client.android.dao.entities.Client;
import client.android.dao.entities.Doctor;
import client.android.fragments.state.HomeFragmentState;
import client.android.fragments.state.AgendaFragmentState;
import client.android.fragments.state.AddAppointmentFragmentState;
import client.android.fragments.state.ConfigFragmentState;
import java.util.List;
public class Session extends AbstractSession {
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
// list of doctors
private List<Doctor> doctors;
// list of clients
private List<Client> clients;
// a doctor's schedule for a given day
private DoctorScheduleAgenda schedule;
// position of the clicked item in the schedule
private int position;
// Appointment date in "yyyy-MM-dd" format
private String appointmentDay;
// Appointment day in French format "dd-MM-yyyy"
private String appointmentDay;
// getters and setters
...
}
- Líneas 17-28: La sesión almacena seis datos. Explicaremos sus funciones cuando sea necesario.
3.6.3. La capa [DAO]
![]() |
![]() | ![]() |
- en [1], las entidades encapsuladas en las respuestas del servidor. Éstas se presentaron en la sección 3.5;
- en [2], los componentes cliente que se encargan de la comunicación con el servidor;
No volveremos sobre los componentes de [1]. Ya se han presentado. Invitamos al lector a consultar la sección 3.5 si es necesario. Examinaremos la implementación del paquete [service]. Esto también nos llevará a discutir la implementación de la comunicación segura entre el cliente y el servidor.
3.6.3.1. Aplicación de la comunicación cliente/servidor
![]() |
La clase [WebClient] es un componente AA que describe:
- el URLs expuesto por el servicio web;
- sus parámetros;
- sus respuestas;
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import org.androidannotations.rest.spring.annotations.*;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
public void setRestTemplate(RestTemplate restTemplate);
// list of doctors
@Get("/getAllDoctors")
public Response<List<Doctor>> getAllDoctors();
// list of clients
@Get("/getAllClients")
public Response<List<Client>> getAllClients();
// list of a doctor's available slots
@Get("/getAllSlots/{doctorId}")
public Response<List<Slot>> getAllSlots(@Path long doctorId);
// list of a doctor's appointments
@Get("/getDoctorAppointmentsDay/{doctorId}/{day}")
public Response<List<Appointment>> getDoctorAppointmentsByDay(@Path long doctorId, @Path String day);
// Client
@Get("/getClientById/{id}")
public Response<Client> getClientById(@Path long id);
// Doctor
@Get("/getDoctorById/{id}")
public Response<Doctor> getDoctorById(@Path long id);
// Rv
@Get("/getRvById/{id}")
public Response<Rv> getRvById(@Path long id);
// Slot
@Get("/getCreneauById/{id}")
public Response<TimeSlot> getTimeSlotById(@Path long id);
// Add an appointment
@Post("/addAppointment")
public Response<Rv> addAppointment(@Body PostAddAppointment post);
// delete an appointment
@Post("/deleteAppointment")
public Response<Rv> deleteAppointment(@Body PostDeleteAppointment post);
// Get a doctor's schedule
@Get(value = "/getDoctorScheduleDay/{doctorId}/{day}")
public Response<DoctorScheduleDay> getDoctorScheduleDay(@Path long doctorId, @Path String day);
}
- líneas 19-60: todos los URLs tratados en la sección 3.5 están presentes;
- línea 16: el componente [RestTemplate] de [Spring Android] en el que se basa la comunicación cliente/servidor;
3.6.3.2. La interfaz [IDao]
![]() |
La interfaz [IDao] de la capa [DAO] es la siguiente:
package rdvmedecins.android.dao.service;
import rdvmedecins.android.dao.entities.*;
import rx.Observable;
import java.util.List;
public interface IDao {
// Web service URL
public void setWebServiceJsonUrl(String url);
// User
public void setUser(String user, String password);
// Client timeout
public void setTimeout(int timeout);
// list of clients
public Observable<List<Client>> getAllClients();
// list of doctors
public Observable<List<Doctor>> getAllDoctors();
// list of a doctor's time slots
public Observable<List<TimeSlot>> getAllTimeSlots(long doctorId);
// List of a doctor's appointments on a given day
public Observable<List<Appointment>> getDoctorAppointmentsByDay(long doctorId, String day);
// find a client identified by their ID
public Observable<Client> getClientById(long id);
// Find a doctor by their ID
public Observable<Doctor> getDoctorById(long id);
// Find an appointment identified by its ID
public Observable<Appointment> getAppointmentById(long id);
// find a time slot identified by its ID
public Observable<TimeSlot> getTimeSlotById(long id);
// add an appointment
public Observable<Rv> addAppointment(String day, long slotId, long clientId);
// delete an appointment
public Observable<Appointment> deleteAppointment(long appointmentId);
// business logic
public Observable<DoctorDailySchedule> getDoctorDailySchedule(long doctorId, String day);
// debug mode
void setDebugMode(boolean isDebugEnabled);
}
- línea 10: establecer el URL del servicio web / JSON;
- línea 13: establecer el usuario para la comunicación cliente/servidor. [user] es el usuario ID, [password] es la contraseña;
- línea 16: para establecer un tiempo máximo de espera para la respuesta del servidor;
- líneas 18-49: cada URL expuesto por el servicio web corresponde a un método. Utilizan las mismas firmas de método que el componente AA [WebClient];
- línea 52: para controlar el depurar de la capa [DAO];
3.6.3.3. La clase [Dao]
![]() |
La implementación [DAO] de la interfaz anterior [IDao] es la siguiente:
package client.android.dao.service;
import android.util.Log;
import client.android.dao.entities.*;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// Web service client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
...
}
@Override
public void setUrlServiceWebJson(String url) {
...
}
@Override
public void setUser(String user, String password) {
...
}
@Override
public void setTimeout(int timeout) {
...
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// Implementation of the IDao interface --------------------------------------------------------------------
@Override
public Observable<Response<List<Client>>> getAllClients() {
// log
log("getAllClients");
// result
return getResponse(new IRequest<Response<List<Client>>>() {
@Override
public Response<List<Client>> getResponse() {
return webClient.getAllClients();
}
});
}
@Override
public Observable<Response<List<Doctor>>> getAllDoctors() {
// log
log("getAllDoctors");
// result
return getResponse(new IRequest<Response<List<Doctor>>>() {
@Override
public Response<List<Doctor>> getResponse() {
return webClient.getAllDoctors();
}
});
}
@Override
public Observable<Response<List<TimeSlot>>> getAllTimeSlots(final long doctorId) {
// log
log("getAllSlots");
// result
return getResponse(new IRequest<Response<List<Creneau>>>() {
@Override
public Response<List<Creneau>> getResponse() {
return webClient.getAllSlots(doctorId);
}
});
}
@Override
public Observable<Response<List<Rv>>> getRvMedecinJour(final long idMedecin, final String jour) {
// log
log("getRvMedecinJour");
// result
return getResponse(new IRequest<Response<List<Rv>>>() {
@Override
public Response<List<Rv>> getResponse() {
return webClient.getRvMedecinJour(idMedecin, jour);
}
});
}
@Override
public Observable<Response<Client>> getClientById(final long id) {
// log
log("getClientById");
// result
return getResponse(new IRequest<Response<Client>>() {
@Override
public Response<Client> getResponse() {
return webClient.getClientById(id);
}
});
}
@Override
public Observable<Response<Doctor>> getDoctorById(final long id) {
// log
log("getMedecinById");
// result
return getResponse(new IRequest<Response<Doctor>>() {
@Override
public Response<Doctor> getResponse() {
return webClient.getDoctorById(id);
}
});
}
@Override
public Observable<Response<Rv>> getRvById(final long id) {
// log
log("getRvById");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.getRvById(id);
}
});
}
@Override
public Observable<Response<Creneau>> getCreneauById(final long id) {
// log
log("getCreneauById");
// result
return getResponse(new IRequest<Response<Creneau>>() {
@Override
public Response<Creneau> getResponse() {
return webClient.getSlotById(id);
}
});
}
@Override
public Observable<Response<Rv>> addRv(final String day, final long slotId, final long clientId) {
// log
log("addRv");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.addRv(new PostAddRv(day, slotId, clientId));
}
});
}
@Override
public Observable<Response<Rv>> deleteRv(final long idRv) {
// log
log("deleteRv");
// result
return getResponse(new IRequest<Response<Rv>>() {
@Override
public Response<Rv> getResponse() {
return webClient.deleteRv(new PostDeleteRv(idRv));
}
});
}
@Override
public Observable<Response<DoctorScheduleDay>> getDoctorScheduleDay(final long doctorId, final String day) {
// log
log("getAgendaMedecinJour");
// result
return getResponse(new IRequest<Response<DoctorScheduleDay>>() {
@Override
public Response<DailyDoctorSchedule> getResponse() {
return webClient.getAgendaMedecinJour(idMedecin, jour);
}
});
}
}
- líneas 18-72: estas son las líneas por defecto en la clase [Dao] del proyecto [client-android-skel];
- líneas 74-216: implementación de la interfaz [IDao]. Los métodos que consultan el URLs expuesto por el servicio web delegan esta consulta en el componente AA [WebClient] (líneas 22-23);
- líneas 58-63: si los intercambios cliente/servidor se autentican mediante autenticación básica, se añade un interceptor al componente [RestTemplate]. Esto hará que cualquier petición HTTP enviada por el componente [RestTemplate] sea interceptada por la clase [MyAuthInterceptor] (líneas 25-26);
La clase [MyAuthInterceptor] es la siguiente:
package rdvmedecins.android.dao.security;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// user
private String user;
private String password;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
HttpAuthentication auth = new HttpBasicAuthentication(user, password);
headers.setAuthorization(auth);
return execution.execute(request, body);
}
public void setUser(String user, String password) {
this.user = user;
this.password = password;
}
}
- línea 15: la clase [MyAuthInterceptor] es un componente AA de tipo [singleton];
- línea 16: la clase [MyAuthInterceptor] extiende la interfaz Spring [ClientHttpRequestInterceptor]. Esta interfaz tiene un método, el método [intercept] de la línea 22. Extendemos esta interfaz para interceptar cualquier petición HTTP del cliente. El método [intercept] toma tres parámetros;
- [Solicitud HttpRequest]: la solicitud HTTP interceptada,
- [byte[] body]: su cuerpo, si lo tiene (valores publicados, por ejemplo),
- [Ejecución ClientHttpRequestExecution]: el componente de Spring que ejecuta la solicitud;
Interceptamos todas las peticiones HTTP del cliente Android para añadir la cabecera de autenticación HTTP presentada en la sección 3.5.
- línea 23: recuperamos las cabeceras HTTP de la petición interceptada;
- línea 24: creamos la cabecera de autenticación HTTP. El método de autenticación utilizado (codificación Base64 de la cadena 'user:mdp') lo proporciona la clase Spring [HttpBasicAuthentication];
- línea 25: la cabecera de autenticación que acabamos de crear se añade a las cabeceras actuales de la petición interceptada;
- línea 26: continuamos ejecutando la petición interceptada. En resumen, la petición interceptada se ha enriquecido con la cabecera de autenticación;
Todas las implementaciones de los métodos de la interfaz [IDao] siguen el mismo patrón. Tomemos el ejemplo del método [getAgendaMedecinJour]:
@Override
public Observable<Response<AgendaMedecinJour>> getAgendaMedecinJour(final long idMedecin, final String jour) {
// log
log("getAgendaMedecinJour");
// result
return getResponse(new IRequest<Response<DoctorScheduleDay>>() {
@Override
public Response<AgendaMedecinJour> getResponse() {
return webClient.getDoctorScheduleForDay(doctorId, day);
}
});
}
- Línea 2: El método espera dos parámetros:
- [idMedecin]: el ID del médico cuyo horario se busca;
- [día]: el día para el que queremos el horario;
- línea 6: llamamos al método [getResponse] de la clase padre [AbstractDao]. Este método espera un parámetro de tipo [IRequest<T>], donde T es el tipo devuelto por el método [getAgendaMedecinJour] de la línea 2, en este caso [Response<AgendaMedecinJour>]. La interfaz [IRequest] sólo tiene un método: [getResponse] (línea 8);
- líneas 8-10: implementación del método [IRequest.getResponse]. Este método debe devolver el resultado esperado por el método [getAgendaMedecinJour] de la línea 2, de tipo [Response<AgendaMedecinJour>];
- línea 9: la respuesta es devuelta por el método [webClient.getAgendaMedecinJour]:
// retrieve a doctor's schedule
@Get(value = "/getAgendaMedecinJour/{idMedecin}/{jour}")
Response<AgendaMedecinJour> getAgendaMedecinJour(@Path long idMedecin, @Path String jour);
Los parámetros utilizados en la línea 9 son los pasados al método [getAgendaMedecinJour] en la línea 2. Por esta razón, estos parámetros deben tener el valor final atributo;
3.6.4. El [MainActivity]
Servidor ![]() |
![]() |
La clase [MainActivity] es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.HomeFragment_;
import client.android.fragments.behavior.AgendaFragment_;
import client.android.fragments.behavior.AddAppointmentFragment_;
import client.android.fragments.behavior.ConfigFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import rx.Observable;
import java.util.List;
@EActivity
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// parent class ---------------------------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
AbstractFragment[] fragments = new AbstractFragment[] {new ConfigFragment(), new HomeFragment(), new CalendarFragment(), new AddAppointmentFragment()};
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return IMainActivity.VUE_CONFIG;
}
// IDao interface -----------------------------------------------------
...
@Override
public Observable<Response<List<Client>>> getAllClients() {
return dao.getAllClients();
}
@Override
public Observable<Response<List<Doctor>>> getAllDoctors() {
return dao.getAllDoctors();
}
@Override
public Observable<Response<List<TimeSlot>>> getAllTimeSlots(long doctorId) {
return dao.getAllSlots(doctorId);
}
@Override
public Observable<Response<List<Appointment>>> getDoctorAppointmentsForDay(long doctorId, String day) {
return dao.getRvMedecinJour(idMedecin, day);
}
@Override
public Observable<Response<Client>> getClientById(long id) {
return dao.getClientById(id);
}
@Override
public Observable<Response<Doctor>> getDoctorById(long id) {
return dao.getDoctorById(id);
}
@Override
public Observable<Response<Rv>> getRvById(long id) {
return dao.getRvById(id);
}
@Override
public Observable<Response<Creneau>> getCreneauById(long id) {
return dao.getCreneauById(id);
}
@Override
public Observable<Response<Rv>> addRv(String day, long slotId, long clientId) {
return dao.addRv(day, slotId, clientId);
}
@Override
public Observable<Response<Rv>> deleteRv(long idRv) {
return dao.deleteRv(idRv);
}
@Override
public Observable<Response<DoctorScheduleDay>> getDoctorScheduleDay(long doctorId, String day) {
return dao.getDoctorSchedule(doctorId, day);
}
}
- líneas 21-66: estas líneas se proporcionan por defecto en la plantilla [client-android-skel];
- líneas 66-119: implementación de la interfaz [IDao]. Todos los métodos delegan el trabajo a la capa [DAO] de la línea 26;
- líneas 42-46: el método [getFragments] devuelve el array de los cuatro fragmentos de la aplicación;
- líneas 58-61: la vista de configuración es la primera vista que se muestra cuando se inicia la aplicación;
3.6.5. La Sesión
![]() |
La clase [Session] se utiliza para almacenar información que debe pasarse entre fragmentos. Es la siguiente:
package rdvmedecins.android.architecture;
import rdvmedecins.android.dao.entities.AgendaMedecinJour;
import rdvmedecins.android.dao.entities.Client;
import rdvmedecins.android.dao.entities.Doctor;
import org.androidannotations.annotations.EBean;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Session {
// list of doctors
private List<Doctor> doctors;
// list of clients
private List<Client> clients;
// calendar
private DoctorDailyCalendar calendar;
// position of the clicked item in the calendar
private int position;
// Appointment date in the "yyyy-MM-dd" format
private String dayRv;
// Appointment day in French format "dd-MM-yyyy"
private String appointmentDay;
// getters and setters
...
}
- línea 10: la clase [Session] es un componente AA instanciado como una única instancia;
- líneas 12-15: En este caso de estudio, supondremos que las listas de médicos y clientes no cambian. Las recuperaremos cuando se inicie la aplicación y las almacenaremos en la sesión para que los fragmentos puedan utilizarlas;
- líneas 20-23: la fecha deseada para una cita. Se maneja en dos formatos: en notación francesa (línea 23) dentro del cliente Android, y en notación inglesa (línea 21) para la comunicación con el servidor;
- línea 19: la posición del elemento pulsado (enlace añadir/borrar) en el calendario;
3.6.6. Gestión de la vista de configuración
3.6.6.1. La vista
La vista de configuración es la vista que se muestra cuando se inicia la aplicación:

Los elementos de la interfaz visual son los siguientes:
3.6.6.2. El fragmento
La vista de configuración está gestionada por el siguiente fragmento [ConfigFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.Client;
import client.android.dao.entities.Doctor;
import client.android.dao.service.Response;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.net.URI;
import java.util.List;
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_config)
public class ConfigFragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.edt_urlServiceRest)
protected EditText edtUrlServiceRest;
@ViewById(R.id.txt_errorUrlServiceRest)
protected TextView txtErrorUrlServiceRest;
@ViewById(R.id.txt_user_error)
protected TextView txtUserError;
@ViewById(R.id.edt_user)
protected EditText userEdit;
@ViewById(R.id.edt_password)
protected EditText edtPassword;
// user input
private String urlServiceRest;
private String user;
private String password;
// page validation
@OptionsItem(R.id.actionValider)
protected void doValidate() {
...
}
..
// implementation of parent class methods -------------------------------------------
...
}
- línea 25: el fragmento está asociado al siguiente menú [menu_config]:
![]() |
<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=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionCancel"
android:title="@string/actionCancel"/>
</menu>
</item>
</menu>
- líneas 28-38: los elementos de la interfaz visual;
- líneas 41-43: los tres campos del formulario;
El método [doValidate] se encarga de hacer clic en la opción de menú [Validar]:
// page validation
@OptionsItem(R.id.actionValider)
protected void doValider() {
// hide any previous error messages
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
// Check the validity of the entries
if (!isPageValid()) {
return;
}
// Set the web service URL
mainActivity.setUrlServiceWebJson(urlServiceRest);
// Enter the user
mainActivity.setUser(username, password);
// Start waiting—we will launch 2 asynchronous tasks
beginWaiting(2);
// doctors
executeInBackground(mainActivity.getAllDoctors(), new Action1<Response<List<Doctor>>>() {
@Override
public void call(Response<List<Doctor>> responseDoctors) {
// process the response
consumeDoctors(responseDoctors);
}
});
// clients
executeInBackground(mainActivity.getAllClients(), new Action1<Response<List<Client>>>() {
@Override
public void call(Response<List<Client>> responseClients) {
// process the response
consumeClients(responseClients);
}
});
}
private void consumeDoctors(Response<List<Doctor>> responseDoctors) {
// log
if (isDebugEnabled) {
Log.d(className, "consume doctors");
}
// error?
if (responseDoctors.getStatus() != 0) {
// message
showAlert(responseDoctors.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// store the doctors in the session
session.setDoctors(responseDoctors.getBody());
}
private void consumeClients(Response<List<Client>> responseClients) {
// log
if (isDebugEnabled) {
log.d(className, "customer acquisition");
}
// error?
if (responseClients.getStatus() != 0) {
// message
showAlert(responseClients.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// store clients in the session
session.setClients(responseClients.getBody());
}
- líneas 8-10: se comprueba la validez de las tres entradas del formulario. Si el formulario no es válido, el proceso se detiene ahí;
- líneas 11-14: las entradas requeridas por la capa [DAO] se pasan a la actividad;
- línea 16: se notifica a la clase padre que se van a lanzar dos tareas asíncronas y se prepara la espera;
- líneas 17-24: se solicita la lista de médicos;
- línea 18: el método [executeInBackground] espera dos parámetros:
- línea 18: el proceso a ejecutar y observar es proporcionado por el [mainActivity.getAllMedecins()método
- líneas 18-24: el segundo parámetro es una instancia de tipo [Acción1<T>], donde T es el tipo devuelto por el proceso observado, aquí [Respuesta<Lista<Medecina>>]
- línea 22: cuando se recibe la respuesta, se pasa al método [consumeMedecins] en la línea 36;
- líneas 25-33: tras lanzar una primera tarea asíncrona, lanzamos una segunda para solicitar la lista de clientes. Tendremos por tanto dos tareas ejecutándose en paralelo;
- líneas 36-52: hemos recibido la respuesta de la tarea de los médicos. La procesamos;
- líneas 42-49: En primer lugar, comprobamos si el servidor ha informado de un error en el campo [status] de la respuesta;
- línea 44: si hay un error, mostramos los mensajes que el servidor colocó en el campo [messages] de la respuesta;
- línea 46: cancelamos todas las tareas;
- línea 48: volvemos al UI;
- línea 51: si no se ha producido ningún error, la lista de médicos se carga en la sesión;
La validez de la entrada (línea 8) se comprueba mediante el siguiente método:
private boolean isPageValid() {
// Check the validity of the entered data
boolean error;
URI service;
// validity of the REST service URL
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
try {
service = new URI(urlServiceRest);
error = service.getHost() == null || service.getPort() == -1;
} catch (Exception ex) {
// log the error
error = true;
}
if (error) {
// display error
txtErrorUrlServiceRest.setVisibility(View.VISIBLE);
}
// user
user = edtUser.getText().toString().trim();
if (user.length() == 0) {
// display the error
txtErrorUser.setVisibility(View.VISIBLE);
// mark the error
error = true;
}
// password
password = edtPassword.getText().toString().trim();
// return
return !error;
}
El método [beginWaiting] (línea 16) es el siguiente:
// Start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to start the tasks
beginRunningTasks(numberOfRunningTasks);
// button and menu states
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionAnnuler, true)});
}
- línea 4: le decimos a la tarea padre que vamos a lanzar tareas [numberOfRunningTasks];
- línea 6: todas las opciones del menú están ocultas;
- línea 7: hace visible la opción [Acciones/Cancelar];
El método [doCancel] se encarga de hacer clic en la opción [Cancelar] del menú:
@OptionsItem(R.id.actionAnnuler)
protected void doCancel() {
if (isDebugEnabled) {
Log.d(className, "Undo requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
- línea 8: pedimos a la clase padre que cancele las tareas asíncronas;
3.6.6.3. Gestión del ciclo de vida de los fragmentos
El fragmento tiene el siguiente estado [ConfigFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// visibility of the two error messages
private boolean txtErrorUrlServiceRestVisible;
private boolean txtErrorUserVisible;
// getters and setters
...
}
- Cuando la clase padre lo solicite, el fragmento guardará la visibilidad de sus dos mensajes de error;
El ciclo de vida del fragmento se implementa de la siguiente manera:
// implementation of parent class methods -------------------------------------------
@Override
public CoreState saveFragment() {
// save fragment state
ConfigFragmentState state = new ConfigFragmentState();
state.setTxtErrorUrlServiceRestVisible(txtErrorUrlServiceRest.getVisibility() == View.VISIBLE);
state.setTxtErrorUserVisible(txtErrorUser.getVisibility() == View.VISIBLE);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_CONFIG;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
if (previousState == null) {
// First visit
// hide error messages
txtErrorUtilisateur.setVisibility(View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(View.INVISIBLE);
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// Restore visibility of error messages
ConfigFragmentState state = (ConfigFragmentState) previousState;
// not the first visit - restore error messages
txtUserError.setVisibility(state.isTxtUserErrorVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorUrlServiceRest.setVisibility(state.isTxtErrorUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.HOME_VIEW, ISession.Action.SUBMIT);
}
}
// private methods ------------------------------------------------
private void initMenu(){
// menu state
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- líneas 2-9: cuando lo solicita su clase padre, el fragmento guarda el estado de sus dos mensajes de error;
- líneas 11-14: el fragmento ID es [IMainActivity.VUE_CONFIG];
- líneas 16-19: se ejecuta cuando el fragmento se genera por primera vez (previousState == null) o se regenera en ocasiones posteriores (previousState != null). Aquí no hay nada que hacer;
- líneas 21-31: se ejecuta cuando la vista asociada al fragmento se construye por primera vez (previousState == null) o se reconstruye en ocasiones posteriores (previousState != null);
- líneas 24-29: en la primera visita, los mensajes de error se ocultan y el menú se muestra sin la acción [Cancelar] (líneas 62-66);
- líneas 33-35: se ejecuta cuando se alcanza el fragmento mediante una operación [SUBMIT]. Esto nunca ocurre aquí;
- líneas 37-44: se ejecuta cuando se alcanza el fragmento mediante una operación [NAVIGATION] o [RESTORE]. El estado de los mensajes de error se restablece desde el estado anterior;
- líneas 47-49: se ejecuta cuando se han realizado todas las actualizaciones anteriores. No hay nada más que hacer;
- líneas 51-59: se ejecuta cuando se completan todas las tareas asíncronas;
- líneas 53-54: restablecer el menú a su estado por defecto;
- líneas 56-58: si las tareas se completan con éxito, se pasa a la siguiente vista; en caso contrario, se permanece en la misma vista;
3.6.7. Gestión de la vista doméstica
3.6.7.1. La vista
La vista inicial es la siguiente:

Los elementos de la interfaz visual son los siguientes:
3.6.7.2. El fragmento
La pantalla de inicio está gestionada por el siguiente fragmento [HomeFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.Spinner;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.DoctorDailySchedule;
import client.android.dao.entities.Doctor;
import client.android.dao.service.Response;
import client.android.fragments.state.HomeFragmentState;
import org.androidannotations.annotations.*;
import rx.functions.Action1;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.home)
@OptionsMenu(R.menu.menu_home)
public class HomeFragment extends AbstractFragment {
// UI elements
@ViewById(R.id.spinnerDoctors)
protected Spinner spinnerDoctors;
@ViewById(R.id.edt_AppointmentDay)
protected DatePicker edtAppDate;
// local data
private List<Doctor> doctors;
private Calendar calendar;
private String[] spinnerDocsDataSource;
// page validation
@OptionsItem(R.id.actionValider)
protected void doValidate() {
...
}
...
// implementation of parent class methods -------------------------------------
...
}
- línea 26: el fragmento está asociado al siguiente menú [menu_accueil]:
![]() |
<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=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionCancel"
android:title="@string/actionCancel"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
</menu>
</item>
</menu>
- líneas 31-34: los elementos de la interfaz visual;
- Línea 37: la lista de médicos;
- línea 38: un calendario;
- línea 39: la fuente de datos para el spinner de médicos;
Al hacer clic en el enlace [Validar] se utiliza el siguiente método [doValidate]:
// page validation
@OptionsItem(R.id.actionValider)
protected void doValidate() {
// note the ID of the selected doctor
Long doctorId = doctors.get(spinnerDoctors.getSelectedItemPosition()).getId();
// Store the date in the session
String appointmentDate = String.format(new Locale("Fr-fr"), "%02d-%02d-%04d", edtAppointmentDate.getDayOfMonth(), edtAppointmentDate.getMonth() + 1, edtAppointmentDate.getYear());
session.setAppDate(appDate);
// convert to yyyy-MM-dd date format
String dayRv = String.format(new Locale("Fr-fr"), "%04d-%02d-%02d", edtJourRv.getYear(), edtJourRv.getMonth() + 1, edtJourRv.getDayOfMonth());
session.setDayRv(dayRv);
// Start waiting - we're going to launch 1 asynchronous task
beginWaiting(1);
// request the doctor's schedule
executeInBackground(mainActivity.getDoctorSchedule(doctorId, dayRv), new Action1<Response<DoctorSchedule>>() {
@Override
public void call(Response<DoctorDailySchedule> responseDoctorDailySchedule) {
// process the response
consumeAgenda(responseAgendaMedecinJour);
}
});
}
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// error?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// add the appointment to the session
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- línea 5: recuperar el ID del médico seleccionado;
- líneas 7-8: almacenamos la fecha seleccionada en la sesión en formato francés;
- líneas 10-11: fijamos la fecha seleccionada en la sesión, en formato inglés;
- línea 13: notificamos a la clase padre que estamos a punto de lanzar una tarea asíncrona y nos preparamos para la espera;
- líneas 15-22: se recupera la agenda del médico;
- línea 15: el método [executeInBackground] espera dos parámetros:
- línea 15: el proceso a ejecutar y observar es proporcionado por el [mainActivity.getAgendaMedecinJour(idMedecin, dayRv)método
- líneas 15-22: el segundo parámetro es una instancia de tipo [Acción1<T>], donde T es el tipo devuelto por el proceso observado, aquí [Respuesta<AgendaMedecinJour>]
- línea 20: cuando se recibe la respuesta, se pasa al método [consumeAgenda] en la línea 25;
- línea 15: el método [executeInBackground] espera dos parámetros:
- líneas 25-37: hemos recibido la agenda del médico. Lo procesamos;
- líneas 27-34: En primer lugar, comprobamos si el servidor ha informado de un error en el campo [status] de la respuesta;
- línea 29: si hay un error, mostramos los mensajes que el servidor colocó en el campo [messages] de la respuesta;
- línea 31: cancelar todas las tareas;
- línea 33: volvemos al UI;
- línea 36: si no ha habido errores, se enfoca el calendario;
El método [beginWaiting] (línea 13) es el siguiente:
// start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// Prepare to start the tasks
beginRunningTasks(numberOfRunningTasks);
// button and menu states
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionAnnuler, true)});
}
- línea 4: le decimos a la tarea padre que vamos a lanzar tareas [numberOfRunningTasks];
- línea 6: todas las opciones del menú están ocultas;
- línea 7: hace visible la opción [Acciones/Cancelar];
El método [doCancel] se encarga de hacer clic en la opción [Cancelar] del menú:
@OptionsItem(R.id.actionAnnuler)
protected void doCancel() {
if (isDebugEnabled) {
Log.d(className, "Undo requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
- línea 8: pedimos a la clase padre que cancele las tareas asíncronas;
Al hacer clic en la opción de menú [Volver a configuración], se procede de la siguiente manera:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- Línea 4: Navegamos a la vista de configuración usando la acción [NAVIGATION]. Esto significa que queremos restaurar la vista de configuración al estado en que la dejamos;
3.6.7.3. Gestión del ciclo de vida de los fragmentos
El fragmento tiene el siguiente [HomeFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.DoctorSlotDay;
public class HomeFragmentState extends CoreState {
// [Home] fragment state
// position of the selected doctor
private int selectedDoctorPosition;
// selected date
private int year;
private int month;
private int dayOfMonth;
// data source for the doctor spinner
private String[] doctorsSpinnerDataSource;
// constructors
public AccueilFragmentState() {
}
// getters and setters
...
}
- línea 11: devuelve el elemento seleccionado de la lista de médicos;
- líneas 13-15: devuelve la fecha seleccionada del calendario;
- línea 17: recupera la fuente de datos para la lista de médicos;
El ciclo de vida del fragmento se implementa de la siguiente manera:
// implementation of parent class methods -------------------------------------
@Override
public CoreState saveFragment() {
// save the view
HomeFragmentState state = new HomeFragmentState();
state.setSelectedDoctorPosition(doctorSpinner.getSelectedItemPosition());
state.setDayOfMonth(edtJourRv.getDayOfMonth());
state.setMonth(edtJourRv.getMonth());
state.setYear(edtJourRv.getYear());
state.setSpinnerMedecinsDataSource(spinnerMedecinsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.HOME_VIEW;
}
@Override
protected void initFragment(CoreState previousState) {
// retrieve the doctors from the session
doctors = session.getDoctors();
// First visit?
if (previousState == null) {
// build the array displayed by the spinner
spinnerDocsDataSource = new String[docs.size()];
int i = 0;
for (Doctor doctor : doctors) {
spinnerDocsDataSource[i] = String.format("%s %s %s", doc.getTitle(), doc.getFirstName(), doc.getLastName());
i++;
}
} else {
// not first visit
HomeFragmentState state = (HomeFragmentState) previousState;
spinnerMedecinsDataSource = state.getSpinnerMedecinsDataSource();
}
// the calendar
calendar = Calendar.getInstance();
}
@Override
protected void initView(CoreState previousState) {
// bind the doctors spinner to its data source
ArrayAdapter<String> doctorsDataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, doctorsSpinnerDataSource);
dataAdapterDoctors.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerDoctors.setAdapter(doctorsDataAdapter);
// Minimum date in the calendar to today
edtJourRv.setMinDate(calendar.getTimeInMillis());
// First visit?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// Restore the current session state
HomeFragmentState state = (HomeFragmentState) previousState;
// selection in the doctors spinner
spinnerMedecins.setSelection(state.getSelectedMedecinPosition());
// calendar
edtJourRv.updateDate(state.getYear(), state.getMonth(), state.getDayOfMonth());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called after all tasks have been completed or canceled
// menu state
initMenu();
// Next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
}
}
// private methods ------------------------------------------------
private void initMenu() {
// menu state
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- líneas 2-9: cuando lo solicita su clase padre, el fragmento guarda el estado de los siguientes elementos:
- línea 6: la posición seleccionada en la lista de médicos;
- líneas 7-9: el día del mes, el mes y el año de la fecha seleccionada en el calendario;
- línea 10: la fuente de datos para el spinner de médicos;
- líneas 14-17: el fragmento ID es [IMainActivity.VUE_ACCUEIL];
- líneas 19-39: se ejecuta cuando el fragmento se genera por primera vez (previousState == null) o se regenera en ocasiones posteriores (previousState != null);
- líneas 25-31: para una primera visita, se construye la fuente de datos para el spinner de médicos;
- líneas 33-35: para visitas posteriores, la fuente de datos del spinner se recupera del estado anterior del fragmento;
- líneas 41-54: se ejecuta cuando la vista asociada al fragmento se construye por primera vez (previousState==null) o se reconstruye en visitas posteriores (previousState !=null);
- líneas 50-53: para la primera visita, el menú aparece sin la acción [Cancelar] (líneas 88-92);
- líneas 43-48: para todas las visitas, ya sean las primeras o no, se asocia el spinner de los médicos a su origen (líneas 44-46) y se fija la fecha mínima del calendario en la fecha de hoy (línea 48);
- líneas 56-60: se ejecuta cuando se llega al fragmento a través de una operación [SUBMIT]. El usuario procede de la vista [CONFIG]. El menú vuelve a su estado inicial;
- líneas 62-70: se ejecuta cuando se alcanza el fragmento a través de una operación [NAVIGATION] o [RESTORE];
- línea 67: la ruleta de médicos se restablece al último médico seleccionado;
- línea 69: el calendario se fija en la última fecha seleccionada;
- líneas 72-74: se ejecuta una vez que se han completado todas las actualizaciones anteriores. No hay nada más que hacer;
- líneas 76-85: se ejecuta cuando se completan todas las tareas asíncronas;
- línea 80: restablecer el menú a su estado por defecto;
- líneas 82-84: si las tareas se completan con normalidad, se pasa a la vista siguiente; en caso contrario, se permanece en la misma vista;
3.6.8. Gestión de la vista del calendario
3.6.8.1. La vista
La pantalla de inicio tiene este aspecto:

Los elementos de la interfaz visual son los siguientes:
3.6.8.2. El fragmento
La vista Calendario está gestionada por el siguiente fragmento [AgendaFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.DoctorSchedule;
import client.android.dao.entities.DoctorDailySlot;
import client.android.dao.entities.Doctor;
import client.android.dao.entities.Rv;
import client.android.dao.service.Response;
import client.android.fragments.state.AgendaFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
@EFragment(R.layout.agenda)
@OptionsMenu(R.menu.menu_agenda)
public class AgendaFragment extends AbstractFragment {
// UI elements
@ViewById(R.id.txt_title2_agenda)
protected TextView txtTitle2;
@ViewById(R.id.listViewAgenda)
protected ListView lstSlots;
// calendar displayed by the fragment
private DailyDoctorSchedule agenda;
// ListView information for time slots
private int firstPosition;
private int top;
// appointment deleted or not
private boolean appointmentDeleted;
// number of the slot added or deleted
private int slotNumber;
// Update the calendar after an addition or deletion
private void updateCalendar() {
...
}
...
// Implementation of parent class methods ------------------------------------------------------
...
}
- línea 27: el fragmento está asociado al siguiente menú [menu_agenda]:
![]() |
<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=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/cancelAction"
android:title="@string/cancelAction"/>
<item
android:id="@+id/actionCalendar"
android:title="@string/actionAgenda"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToHome"
android:title="@string/navigationToHome"/>
</menu>
</item>
</menu>
- líneas 32-35: elementos de la interfaz visual;
- líneas 37-45: datos globales de los métodos;
3.6.8.2.1. Método [updateAgenda]
La (re)generación de la lista de franjas horarias del calendario es necesaria en varios lugares del código. Se ha factorizado en el siguiente método privado [updateAgenda]:
// update the calendar after an addition/deletion
private void updateAgenda() {
// (re)generation of the calendar slots
// the agenda is retrieved from the session and stored in a field of the fragment
agenda = session.getAgenda();
// Regenerate the ListView of time slots
ArrayAdapter<DaytimeDoctorSlot> adapter = new SlotListAdapter(activity, R.layout.doctor_slot,
agenda.getDoctorSlotsDay(), this);
slotList.setAdapter(adapter);
// reposition to the correct location in the ListView
slotList.setSelectionFromTop(firstPosition, top);
}
- línea 5: el calendario se recupera de la sesión y se almacena en el campo [calendar] del fragmento;
- líneas 7-9: Definimos el adaptador para el componente [ListView]. Este adaptador define tanto la fuente de datos para [ListView] como el modelo de visualización para cada uno de sus elementos. En breve presentaremos este adaptador;
- línea 11: volvemos a la posición anterior en el calendario. Esto se debe a que sólo vemos una parte de las franjas horarias del día. Si añadimos o eliminamos una cita en la última franja horaria, el código anterior actualizará la página para mostrar el nuevo calendario. Esta actualización hace que la vista vuelva a la posición primero lo cual no es deseable. La línea 5 resuelve este problema. Una descripción de esta solución se puede encontrar en el URL [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview];
La clase [ListCreneauxAdapter] se utiliza para definir una fila en el [ListView]:

Como se muestra arriba, la visualización difiere en función de si la franja horaria tiene cita o no. El código de la clase [ListCreneauxAdapter] es el siguiente:
...
public class ListCreneauxAdapter extends ArrayAdapter<CreneauMedecinJour> {
// the array of time slots
private CreneauMedecinJour[] creneauxMedecinJour;
// the execution context
private Context context;
// the ID of the layout for displaying a row in the list of time slots
private int layoutResourceId;
// click listener
private AgendaFragment view;
// constructor
public ListCreneauxAdapter(Context context, int layoutResourceId, CreneauMedecinJour[] creneauxMedecinJour,
AgendaFragment view) {
super(context, layoutResourceId, dailyDoctorSlots);
// store the information
this.daytimeSlots = daytimeSlots;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.view = view;
// Sort the array of doctor's slots by time
Arrays.sort(daytimeDoctorSlots, new MyComparator());
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
// Sorting the slot array
class MyComparator implements Comparator<DoctorSlotDay> {
...
}
}
- Línea 3: La clase [ListCreneauxAdapter] debe extender un adaptador predefinido para [ListView]s, en este caso la clase [ArrayAdapter], que, como su nombre indica, rellena el [ListView] con un array de objetos, en este caso de tipo [CreneauMedecinJour]. Revisemos el código de esta entidad:
public class DayDoctorSlot implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
...
}
- La clase [CreneauMedecinJour] contiene una franja horaria (línea 5) y una cita potencial (línea 6) o null si no hay cita previa;
Volvemos al código de la clase [ListCreneauxAdapter]:
- línea 15: el constructor toma cuatro parámetros:
- la actividad actual de Android,
- el archivo XML que define el contenido de cada elemento [ListView],
- la matriz de las franjas horarias del médico,
- la propia vista;
- Línea 24: La matriz de franjas horarias se ordena en orden ascendente por tiempo;
El método [getView] se encarga de generar la vista correspondiente a una fila del [ListView]. Esta vista consta de tres elementos:
![]() |
El código del método [getView] es el siguiente:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// move to the correct slot
DoctorSlotDay doctorSlot = doctorSlotsDay[position];
// create the row
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// the time slot
TextView txtSlot = (TextView) row.findViewById(R.id.txt_Slot);
txtSlot.setText(String.format("%02d:%02d-%02d:%02d", doctorSlot.getSlot().getStartTime(), doctorSlot
.getTimeSlot().getStartMinute(), doctorTimeSlot.getTimeSlot().getStartHour(), doctorTimeSlot.getTimeSlot().getEndMinute(), doctorTimeSlot.getTimeSlot().getEndHour()));
// the client
TextView txtClient = (TextView) row.findViewById(R.id.txt_Client);
String text;
if (doctorAppointment.getRv() != null) {
Client client = doctorAppointment.getRv().getClient();
text = String.format("%s %s %s", client.getTitle(), client.getFirstName(), client.getLastName());
} else {
text = "";
}
txtClient.setText(text);
// the link
final TextView btnValider = (TextView) row.findViewById(R.id.btn_Valider);
if (doctorSlot.getRv() == null) {
// add
btnValider.setText(R.string.btn_ajouter);
btnValider.setTextColor(context.getResources().getColor(R.color.blue));
} else {
// delete
btnValider.setText(R.string.btn_delete);
btnValider.setTextColor(context.getResources().getColor(R.color.red));
}
// link listener
btnValider.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Pass the information to the calendar view
view.doValidate(position, btnValidate.getText().toString());
}
});
// return the row
return row;
}
- línea 2: posición es el número de fila que se generará en el [ListView]. También es el número de ranura en la matriz [creneauxMedecinJour]. Ignoramos los otros dos parámetros;
- línea 4: recuperamos la franja horaria a mostrar en la fila [ListView];
- línea 6: la fila se construye a partir de su definición XML
![]() |
El código de [creneau_medecin.xml] es el siguiente:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/wheat" >
<TextView
android:id="@+id/txt_Creneau"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/txt_Client"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Creneau"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Creneau"
android:text="@string/txt_dummy" />
<TextView
android:id="@+id/btn_Valider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/txt_Client"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/txt_Client"
android:text="@string/btn_validate"
android:textColor="@color/blue" />
</RelativeLayout>
![]() |
- líneas 8-10: se construye la franja horaria [1];
- líneas 12-20: se construye el cliente ID [2];
- línea 23: si la franja horaria no tiene cita;
- líneas 25-26: se crea el enlace azul [Añadir];
- líneas 29-30: en caso contrario, se crea el enlace rojo [Suprimir];
- líneas 33-40: independientemente del tipo de enlace [Añadir / Borrar], el método [doValider] de la vista gestionará el clic sobre el enlace. El método recibirá dos argumentos:
- el número de la ranura en la que se ha hecho clic,
- la etiqueta del enlace en el que se ha hecho clic;
- línea 42: devolvemos la línea que acabamos de crear.
Nótese que es el método [doValider] del fragmento [AgendaFragment] el que se encarga de los enlaces. Es el siguiente:
// Click on an [Add / Delete] link
public void doValider(int slotNumber, String text) {
// operation in progress?
if (numberOfRunningTasks != 0) {
Toast.makeText(activity, "An operation is in progress. Please wait or Cancel...", Toast.LENGTH_SHORT).show();
return;
}
// note the scroll position to return to it
// read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of the first element, whether fully visible or not
firstPosition = lstCreneaux.getFirstVisiblePosition();
// Y offset of this element relative to the top of the ListView
// Measure the height of any hidden portion
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// We also note the number of the slot clicked
this.slotNumber = slotNumber;
// depending on the link text, we do different things
if (text.equals(getResources().getString(R.string.lnk_ajouter))) {
doAdd();
} else {
doDelete();
}
}
- El método [doValider] recibe dos informaciones:
- el número de la ranura en la que se ha hecho clic;
- el texto (Añadir / Suprimir) del enlace que se ha pulsado;
- líneas 4-7: hacer clic en los enlaces [Eliminar / Añadir] se desactiva si hay tareas asíncronas en curso. Se trata de una elección de diseño que simplifica la escritura de código. Está abierto a debate;
- líneas 11-15: almacenamos la información (firstPosition, arriba) de la ranura ListView en campos dentro del fragmento para que el método privado [updateAgenda] pueda regenerarlo con la misma posición de desplazamiento;
- línea 17: almacenamos el número de la ranura pulsada;
- líneas 19-23: en función del texto del enlace pulsado, añadimos o eliminamos un elemento;
3.6.8.2.2. Método [doDelete]
El método [doSupprimer] garantiza la eliminación de la cita de la ranura seleccionada:
// Delete an appointment
private void doDelete() {
// wait for two tasks to finish
beginWaiting(2);
// delete the appointment in the background
appointmentDeleted = false;
// ID of the appointment to be deleted
long idRv = agenda.getDoctorSlotsDay()[slotNumber].getAppointment().getId();
// Delete using an asynchronous task
executeInBackground(mainActivity.deleteAppointment(idAppointment), new Action1<Response<Appointment>>() {
@Override
public void call(Response<Rv> responseRv) {
// consume the result
consumeRv(responseRv);
}
});
}
// consuming a response
private void consumeRv(Response<Rv> responseRv) {
// error?
if (responseRv.getStatus() != 0) {
// message
showAlert(responseRv.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// Note that the appointment has been deleted
appointmentDeleted = true;
// request the most recent calendar
executeInBackground(
mainActivity.getToday'sDoctorSchedule(agenda.getDoctor().getId(), session.getDayAppointment()),
new Action1<Response<DailyDoctorSchedule>>() {
@Override
public void call(Response<DailyDoctorSchedule> responseDailyDoctorSchedule) {
// Process the response
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// consuming an agenda
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// error?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// add the appointment to the session
session.setAgenda(responseAgendaMedecinJour.getBody());
// update the view's calendar
updateAgenda();
}
- línea 4: notificamos a la clase padre que vamos a lanzar dos tareas asíncronas y comenzamos a esperar a que estas dos tareas se completen;
- línea 8: recuperar el ID de la cita a borrar. El servidor necesita esta información;
- líneas 9-18: solicitamos la eliminación de la cita a través de una tarea asíncrona;
- línea 10: el método [executeInBackground] espera dos parámetros:
- línea 10: el proceso a ejecutar y observar es proporcionado por el [mainActivity.deleteRv(idRv)método
- líneas 10-17: el segundo parámetro es una instancia de tipo [Acción1<T>], donde T es el tipo devuelto por el proceso observado, aquí [Respuesta<Rv>]
- línea 15: cuando se recibe la respuesta, se pasa al método [consumeRv] en la línea 21;
- línea 10: el método [executeInBackground] espera dos parámetros:
- líneas 21-44: hemos recibido la respuesta de la tarea asíncrona. La procesamos;
- líneas 23-30: En primer lugar, comprobamos si el servidor ha informado de un error en el campo [status] de la respuesta;
- línea 25: si hay un error, mostramos los mensajes que el servidor colocó en el campo [messages] de la respuesta;
- línea 27: cancelamos todas las tareas;
- línea 29: volver al UI;
- línea 32: si no ha habido error, observamos que se ha suprimido el nombramiento;
- líneas 34-43: en lugar de simplemente borrar la cita del calendario que muestra actualmente el fragmento, solicitamos el nuevo calendario del médico. Dado que la aplicación es multiusuario, es posible que otros usuarios también hayan modificado el calendario del médico. Por lo tanto, es mejor utilizar la versión más reciente;
- líneas 34-43, 47-61: repetimos lo hecho en el fragmento [AccueilFragment], esta vez utilizando información recuperada de la sesión;
El método [beginWaiting] (línea 4) es el siguiente:
// start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch the tasks
beginRunningTasks(numberOfRunningTasks);
// button and menu states
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionAnnuler, true)});
}
- línea 4: le decimos a la tarea padre que vamos a lanzar tareas [numberOfRunningTasks];
- línea 6: todas las opciones del menú están ocultas;
- línea 7: a continuación, haga visible la opción [Acciones/Cancelar];
3.6.8.2.3. Método [doCancel]
El método [doAnnuler] se encarga de hacer clic en la opción [Cancelar] del menú:
@OptionsItem(R.id.actionAnnuler)
protected void doCancel() {
if (isDebugEnabled) {
Log.d(className, "Cancel requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
- línea 7: pedimos a la clase padre que cancele las tareas asíncronas;
3.6.8.2.4. Opción de menú [Volver a configuración]
Al hacer clic en la opción de menú [Volver a la configuración], el proceso es el siguiente:
@OptionsItem(R.id.navigationToConfig)
protected void navigationToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
- Línea 4: Navegamos a la vista de configuración usando la acción [NAVIGATION]. Esto significa que queremos restaurar la vista de configuración al estado en que la dejamos;
3.6.8.2.5. Opción de menú [Volver a Inicio]
Hacer clic en la opción de menú [Volver a Inicio] se gestiona de forma similar:
@OptionsItem(R.id.navigationToAccueil)
protected void navigationToHome() {
// navigate to the home view
mainActivity.navigateToView(IMainActivity.HOME_VIEW, ISession.Action.NAVIGATION);
}
3.6.8.3. Gestión del ciclo de vida de los fragmentos
El fragmento tiene el siguiente estado [AgendaFragmentState]:
package client.android.fragments.state;
import android.widget.ArrayAdapter;
import client.android.architecture.custom.CoreState;
import client.android.dao.entities.DoctorAppointmentDay;
public class AgendaFragmentState extends CoreState {
// view title
private String title;
// ListView
private int firstPosition;
private int top;
// constructors
public AgendaFragmentState() {
}
public AgendaFragmentState(String title) {
this.title = title;
}
// getters and setters
...
}
- línea 10: el título que aparece en la parte superior de la vista;
- líneas 12-13: permite desplazamiento de el ListView mostrando las plazas disponibles del médico;
El ciclo de vida del fragmento se implementa de la siguiente manera:
// implementation of parent class methods ------------------------------------------------------
@Override
public CoreState saveFragment() {
// save state
AgendaFragmentState state = new AgendaFragmentState();
state.setTitle(txtTitle2.getText().toString());
// note the scroll position to return to it
// read [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
// position of the first element, whether fully visible or not
firstPosition = lstCreneaux.getFirstVisiblePosition();
// Y offset of this element relative to the top of the ListView
// measures the height of the potentially hidden portion
View v = lstCreneaux.getChildAt(0);
top = (v == null) ? 0 : v.getTop();
// store all of this
state.setTop(top);
state.setFirstPosition(firstPosition);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AGENDA;
}
@Override
protected void initFragment(CoreState previousState) {
// First visit?
if (previousState != null) {
// Not the first visit
AgendaFragmentState state = (AgendaFragmentState) previousState;
// and the ListView data
firstPosition = state.getFirstPosition();
top = state.getTop();
}
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// retrieve the agenda
agenda = session.getAgenda();
// generate the page title
Doctor doctor = agenda.getDoctor();
txtTitle2.setText(String.format("Appointment for %s %s %s on %s", doctor.getTitle(), doctor.getFirstName(),
doctor.getLastName(), session.getAppointmentDay()));
// menu state
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// regenerate the page title
AgendaFragmentState state = (AgendaFragmentState) previousState;
txtTitle2.setText(state.getTitle());
}
@Override
protected void notifyEndOfUpdates() {
// regenerate the list of time slots
updateAgenda();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu state
initMenu();
// If the task was canceled but the appointment has been deleted, update the local calendar
if (runningTasksHaveBeenCanceled && appointmentDeleted) {
// delete the appointment from the local calendar (could not access the global calendar)
agenda.getDoctorSlotsToday()[slotNumber].setAppointment(null);
// update the user interface
updateAgenda();
}
}
// private methods ------------------------------------------------
private void initMenu() {
// menu state
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- líneas 2-19: cuando lo solicita su clase padre, el fragmento guarda el estado de los siguientes elementos:
- línea 6: el título que aparece en la parte superior de la vista;
- líneas 7-17: la información (top, firstPosition) que permitirá al ListView's scrolling a restaurar;
- líneas 21-24: el fragmento ID es [IMainActivity.VUE_AGENDA];
- líneas 26-35: se ejecuta cuando el fragmento se genera por primera vez (previousState == null) o se regenera en visitas posteriores (previousState != null);
- líneas 30-34: si no se trata de la primera visita al fragmento, recuperamos la información (top, firstPosition) necesaria para restaurar el estado de desplazamiento de ListView;
- líneas 38-40: se ejecuta cuando la vista asociada al fragmento se construye por primera vez (previousState == null) o se reconstruye en visitas posteriores (previousState != null). No hay nada que hacer aquí porque el ListView de las ranuras será generado por el método privado [updateAgenda] (líneas 61-65);
- líneas 42-52: se ejecuta cuando se llega al fragmento a través de una operación [SUBMIT]. Venimos de la vista [HOME];
- línea 45: recuperamos la agenda establecida por [AccueilFragment];
- líneas 47-49: se genera el título de la vista;
- el ListView de franjas horarias será generado por el método privado [updateAgenda] (líneas 61-65);
- líneas 54-59: se ejecuta cuando se alcanza el fragmento a través de una operación [NAVIGATION] o [RESTORE];
- líneas 57-58: se regenera el título de la vista;
- el ListView de franjas horarias será generado por el método privado [updateAgenda] (líneas 61-65);
- líneas 72-74: se ejecuta cuando se han completado todas las actualizaciones anteriores. La dirección ListView de franjas horarias se actualiza porque esta actualización es necesaria independientemente de cómo se acceda al fragmento;
- líneas 67-77: se ejecuta cuando se completan todas las tareas asíncronas;
- línea 70: el menú se restablece a su estado por defecto (líneas 82-86);
- línea 72: había dos tareas asíncronas. Comprobamos si la primera (eliminar la cita) tuvo éxito, a pesar de una cancelación;
- línea 74: en caso afirmativo, la cita se elimina de la agenda local
- línea 75: y actualizar la visualización del calendario;
3.6.9. Manejo de la vista de añadir cita
3.6.9.1. La vista
La vista para añadir una cita es la siguiente:

Los elementos de la interfaz visual son los siguientes:
3.6.9.2. El fragmento
La vista para añadir una cita se gestiona mediante el siguiente fragmento [AjoutRvFragment]:
![]() |
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.dao.entities.*;
import client.android.dao.service.Response;
import client.android.fragments.state.AddRvFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.functions.Action1;
import java.util.List;
import java.util.Locale;
@EFragment(R.layout.ajout_rv)
@OptionsMenu(R.menu.menu_ajout_rv)
public class AddRvFragment extends AbstractFragment {
// Visual interface elements
@ViewById(R.id.spinnerClients)
protected Spinner spinnerClients;
@ViewById(R.id.txt_title2_addRv)
protected TextView txtTitle2;
// clients
private List<Client> clients;
// local data
private Slot slot;
private Doctor doctor;
private boolean appointmentAdded;
private Appointment appointment;
private String[] clientDataSource;
// page validation
@OptionsItem(R.id.actionValider)
protected void doValidate() {
...
}
...
// implementation of parent class methods ----------------------------------
...
}
- línea 26: el fragmento está asociado al siguiente menú [menu_ajout_rv]:
![]() |
<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=".activity.MainActivity1">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionValider"
android:title="@string/actionValider"/>
<item
android:id="@+id/actionCancel"
android:title="@string/actionCancel"/>
</menu>
</item>
<item
android:id="@+id/menuNavigation"
app:showAsAction="ifRoom"
android:title="@string/menuNavigation">
<menu>
<item
android:id="@+id/navigationToConfig"
android:title="@string/navigationToConfig"/>
<item
android:id="@+id/navigationToHome"
android:title="@string/navigationToHome"/>
<item
android:id="@+id/navigationToCalendar"
android:title="@string/navigationToAgenda"/>
</menu>
</item>
</menu>
- líneas 30-33: los elementos de la interfaz visual;
- línea 36: la lista de clientes;
- línea 43: la fuente de datos para el spinner cliente;
Al hacer clic en el enlace [Validar] se utiliza el siguiente método [doValidate]:
// clients
private List<Client> clients;
// local data
private Slot slot;
private Doctor doctor;
private boolean appointmentAdded;
private Appointment appointment;
private String[] clientDataSource;
...
// page validation
@OptionsItem(R.id.actionValider)
protected void doValidate() {
// retrieve the selected client
Client client = clients.get(spinnerClients.getSelectedItemPosition());
// Start waiting for 2 asynchronous tasks
beginWaiting(2);
// add the appointment
appointmentAdded = false;
executeInBackground(
mainActivity.addAppointment(session.getDayAppointment(), slot.getId(), client.getId()),
new Action1<Response<Rv>>() {
@Override
public void call(Response<Rv> responseRv) {
// consume the response
consumeRv(responseRv);
}
});
}
// Consume a Response<Rv> object
void consumeRv(Response<Rv> responseRv) {
// error?
if (responseRv.getStatus() != 0) {
// message
showAlert(responseRv.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// note that the appointment has been added
appointmentAdded = true;
// save the appointment
this.rv = responseRv.getBody();
// request the new calendar
executeInBackground(mainActivity.getAgendaMedecinJour(session.getAgenda().getMedecin().getId(), session.getDayRv()), new Action1<Response<AgendaMedecinJour>>() {
@Override
public void call(Response<DailyDoctorSchedule> responseDailyDoctorSchedule) {
// process the response
consumeAgenda(responseAgendaMedecinJour);
}
});
}
// Consume a Response<AgendaMedecinJour> object
private void consumeAgenda(Response<AgendaMedecinJour> responseAgendaMedecinJour) {
// Error?
if (responseAgendaMedecinJour.getStatus() != 0) {
// message
showAlert(responseAgendaMedecinJour.getMessages());
// cancel
doCancel();
// return to UI
return;
}
// add the appointment to the session
session.setAgenda(responseAgendaMedecinJour.getBody());
}
- línea 13: cuando comienza el método [doValider], los campos 2, 5, 6 y 9 han sido inicializados durante el ciclo de vida del fragmento. Veremos cómo;
- línea 15: recuperamos la entidad [Cliente] correspondiente al elemento seleccionado en el spinner cliente;
- línea 17: notificamos a la clase padre que vamos a lanzar dos tareas asíncronas y nos preparamos para la espera;
- línea 19: inicialmente, la cita aún no se ha añadido a la agenda del médico;
- líneas 20-30: solicitamos que el servidor añada una cita;
- línea 20: el método [executeInBackground] espera dos parámetros:
- línea 20: el proceso a ejecutar y observar es proporcionado por el método [mainActivity.addRv(session.getDayRv(), slot.getId(), client.getId())];
- líneas 22-29: el segundo parámetro es una instancia de tipo [Acción1<T>], donde T es el tipo devuelto por el proceso observado, aquí [Respuesta<Rv>]
- línea 27: cuando se recibe la respuesta, se pasa al método [consumeRV] en la línea 33;
- línea 20: el método [executeInBackground] espera dos parámetros:
- líneas 33-56: hemos recibido la respuesta del servidor. La procesamos;
- líneas 35-42: En primer lugar, comprobamos si el servidor ha informado de un error en el campo [status] de la respuesta;
- línea 37: si hay un error, mostramos los mensajes que el servidor colocó en el campo [messages] de la respuesta;
- línea 39: cancelamos todas las tareas;
- línea 41 : volvemos al UI;
- línea 44: si no ha habido error, constatamos que se ha añadido la cita;
- línea 46: la cita añadida se almacena en un campo del fragmento;
- líneas 47-55: al igual que se hizo al eliminar una cita, después de añadir la cita, solicitar al servidor la agenda más reciente del médico;
- líneas 47-56, 59-71: este código ya se ha encontrado varias veces;
El método [beginWaiting] (línea 17) es el siguiente:
// start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to launch the tasks
beginRunningTasks(numberOfRunningTasks);
// button and menu states
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionAnnuler, true)});
}
- línea 4: le decimos a la tarea padre que vamos a lanzar tareas [numberOfRunningTasks];
- línea 6: todas las opciones del menú están ocultas;
- línea 7: hace visible la opción [Acciones/Cancelar];
El método [doCancel] se encarga de hacer clic en la opción [Cancelar] del menú:
@OptionsItem(R.id.actionAnnuler)
protected void doCancel() {
if (isDebugEnabled) {
Log.d(className, "Undo requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
- línea 7: pedimos a la clase padre que cancele las tareas asíncronas;
La navegación hacia atrás se realiza mediante los tres métodos siguientes:
@OptionsItem(R.id.navigationToConfig)
protected void navigateToConfig() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_CONFIG, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToHome)
protected void navigateToHome() {
// navigate to the configuration view
mainActivity.navigateToView(IMainActivity.VUE_ACCUEIL, ISession.Action.NAVIGATION);
}
@OptionsItem(R.id.navigationToAgenda)
protected void navigateToCalendar() {
// navigate to the calendar view
mainActivity.navigateToView(IMainActivity.AGENDA_VIEW, ISession.Action.NAVIGATION);
}
3.6.9.3. Gestión del ciclo de vida de los fragmentos
El fragmento tiene el siguiente estado [AjoutRvFragmentState]:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
// fragment state AjoutRvFragment
public class AjoutRvFragmentState extends CoreState {
// selected client position
private int selectedClientPosition;
// view title
private String title;
// data source for the client spinner
private String[] clientSpinnerDataSource;
// getters and setters
...
}
El ciclo de vida del fragmento se implementa de la siguiente manera:
// implementation of parent class methods ----------------------------------
@Override
public CoreState saveFragment() {
// save view
AjoutRvFragmentState state = new AjoutRvFragmentState();
state.setTitle(txtTitle2.getText().toString());
state.setSelectedClientPosition(spinnerClients.getSelectedItemPosition());
state.setSpinnerClientsDataSource(spinnerClientsDataSource);
return state;
}
@Override
protected int getNumView() {
return IMainActivity.VUE_AJOUT_RV;
}
@Override
protected void initFragment(CoreState previousState) {
// retrieve clients in session
clients = session.getClients();
// First visit?
if (previousState == null) {
// We create the array displayed by the spinner
spinnerClientsDataSource = new String[clients.size()];
int i = 0;
for (Client client : clients) {
spinnerClientsDataSource[i] = String.format("%s %s %s", client.getTitle(), client.getFirstName(), client.getLastName());
i++;
}
} else {
// not first visit
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
spinnerClientsDataSource = state.getSpinnerClientsDataSource();
}
}
@Override
protected void initView(CoreState previousState) {
// bind the spinner to its data source
ArrayAdapter<String> dataAdapterClients = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item,
spinnerClientsDataSource);
dataAdapterClients.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerClients.setAdapter(dataAdapterClients);
// First visit?
if (previousState == null) {
// menu
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// retrieve the slot number to be reserved in the session
int position = session.getPosition();
// retrieve the doctor's calendar from the session
Doctor'sDailySchedule agenda = session.getAgenda();
// retrieve the doctor and the time slot for the appointment
doctor = calendar.getDoctor();
slot = agenda.getDoctorSlotList()[position].getSlot();
// build the second header of the page
String day = session.getDayApp();
txtTitle2.setText(String.format(Locale.FRANCE,
"Appointment booking for %s %s %s on %s for slot %02d:%02d-%02d:%02d", doctor.getTitle(),
doctor.getFirstName(), doctor.getLastName(), day, slot.getStartHour(), slot.getStartMinute(), slot.getEndHour(),
slot.getEndTime()));
// client selection
spinnerClients.setSelection(0);
// menu
initMenu();
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore previous state
AjoutRvFragmentState state = (AjoutRvFragmentState) previousState;
// title
txtTitle2.setText(state.getTitle());
// spinner
spinnerClients.setSelection(state.getSelectedClientPosition());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu state
initMenu();
// Next view?
if (!runningTasksHaveBeenCanceled) {
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
// Cancellation occurred - has the appointment already been added?
if (appointmentAdded) {
// update the local calendar (we didn't get the global calendar)
DailyDoctorSchedule schedule = session.getSchedule();
calendar.getDailyDoctorSlots()[session.getPosition()].setAppointment(appointment);
// display the calendar
mainActivity.navigateToView(IMainActivity.VUE_AGENDA, ISession.Action.SUBMIT);
return;
}
}
// private methods -------------------
private void initMenu() {
// menu state
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
- líneas 2-10: cuando lo solicita su clase padre, el fragmento guarda el estado de los siguientes elementos:
- línea 6: el título en la parte superior de la vista;
- línea 7: posición del elemento seleccionado en la ruleta de clientes;
- línea 8: la fuente de datos del spinner cliente;
- líneas 12-15: el fragmento ID es [IMainActivity.VUE_AJOUT_RV];
- líneas 17-35: se ejecuta cuando el fragmento se genera por primera vez (previousState == null) o se regenera en ocasiones posteriores (previousState != null);
- línea 20: la lista de clientes se recupera de la sesión y se coloca en un campo fragmentado;
- líneas 22-30: para una primera visita, se construye la fuente de datos para el spinner de clientes;
- líneas 32-33: para visitas posteriores, la fuente de datos para el spinner de cliente se recupera del estado anterior del fragmento;
- líneas 37-49: se ejecuta cuando la vista asociada al fragmento se construye por primera vez (previousState == null) o se reconstruye en ocasiones posteriores (previousState != null);
- líneas 40-43: en todos los casos, el spinner cliente está asociado a su fuente de datos;
- líneas 45-48: para la primera visita, el menú se muestra sin la acción [Cancelar] (líneas 107-111);
- líneas 51-70: se ejecuta cuando se llega al fragmento a través de una operación [SUBMIT]. Venimos de la vista [CALENDAR];
- línea 54: recuperamos el número de franja horaria donde programaremos una cita;
- líneas 56-59: recuperamos las entidades [Doctor] y [Franja horaria] necesarias para añadir esta cita y las colocamos en campos dentro del fragmento;
- líneas 61-65: utilizando esta información, podemos construir el título de la vista;
- línea 67: el spinner cliente se fija en su primer elemento;
- línea 69: el menú vuelve a su estado inicial (sin la opción [Cancelar]);
- líneas 72-80: se ejecuta cuando se alcanza el fragmento a través de una operación [NAVIGATION] o [RESTORE];
- línea 77: se regenera el título de la vista;
- línea 79: el spinner de cliente se restablece al último cliente seleccionado;
- líneas 82-84: se ejecuta cuando se han completado todas las actualizaciones anteriores. No hay nada más que hacer aquí;
- líneas 86-104: se ejecutan cuando se completan todas las tareas asíncronas;
- línea 89: el menú se restablece a su estado por defecto;
- líneas 91-94: si las tareas se completaron normalmente, volver a la vista [CALENDAR] mediante una acción [SUBMIT] (aquí, esto también podría haber sido una acción NAVIGATION);
- líneas 96-103: si las tareas terminaron con una cancelación, seguimos comprobando si se añadió la cita (esto significaría que la recuperación del nuevo calendario falló);
- líneas 98-99: si se ha añadido la cita;
- líneas 98-99: la cita devuelta por el servidor se añade a la agenda actual, la que está activa;
- línea 101: volvemos a la vista [AGENDA] a través de una [SUBMIT] (aquí, esto también podría haber sido una acción de tipo NAVEGACIÓN);
3.7. Ejecución
Realice las siguientes pruebas:
- utilice la aplicación en condiciones normales y compruebe que funciona;
- Gire el dispositivo para cada vista y compruebe que cada una se restaura correctamente;
- Añade una espera de unos segundos en [IMainActivity];
- Next, cancel the tasks and verify that the result matches the expected outcome;
- gira el dispositivo durante los periodos de espera y comprueba que las tareas se cancelan correctamente y que no se producen caídas;
- Cambie el orden de los fragmentos en [IMainActivity] y compruebe que la aplicación sigue funcionando;













































