User Tools

Site Tools


apuntes:android

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
apuntes:android [28/07/2020 07:53] – [Práctica 1.2] Santiago Faciapuntes:android [30/11/2023 18:28] (current) – [Hacer una foto] Santiago Faci
Line 1: Line 1:
-====== Programación de móviles con Android ======+======= Programación de móviles con Android =======
  
-===== ¿Qué es Android =====+{{ android-logo.jpg?200 }} 
 + 
 +====== ¿Qué es Android ======
  
 Actualmente cuando se habla de //Android// se hace tanto para referirse al Sistema Operativo que viene instalado en nuestros móviles como para hablar del framework que se utilizar para crear las aplicaciones. Conviene por tanto distinguir claramente entre //Android// que será el Sistema Operativo y //Android SDK// que es el framework ó SDK (Software Development Kit) utilizado para desarrollar las aplicaciones que funciona sobre dicho S.O. Actualmente cuando se habla de //Android// se hace tanto para referirse al Sistema Operativo que viene instalado en nuestros móviles como para hablar del framework que se utilizar para crear las aplicaciones. Conviene por tanto distinguir claramente entre //Android// que será el Sistema Operativo y //Android SDK// que es el framework ó SDK (Software Development Kit) utilizado para desarrollar las aplicaciones que funciona sobre dicho S.O.
Line 21: Line 23:
 {{ youtube>f4_qqeHlF-4 }} {{ youtube>f4_qqeHlF-4 }}
 \\ \\
-===== Estructura de Android =====+====== Estructura de Android ======
  
-==== Estructura del Sistema Operativo ====+===== Estructura del Sistema Operativo =====
  
 {{ android-stack_2x.png?550 |Estructura Android}} {{ android-stack_2x.png?550 |Estructura Android}}
-==== Ciclo de vida de una aplicación =====+===== Ciclo de vida de una aplicación ======
  
 {{ activity_lifecycle.png |Ciclo de vida Android}} {{ activity_lifecycle.png |Ciclo de vida Android}}
Line 32: Line 34:
 El [[https://developer.android.com/guide/topics/ui/multi-window.html?hl=ES|Modo multiventana]] permite visualizar varias Activities al mismo tiempo pero no modifica este ciclo de vida. Se pueden ver más detalles sobre esto en el enlace El [[https://developer.android.com/guide/topics/ui/multi-window.html?hl=ES|Modo multiventana]] permite visualizar varias Activities al mismo tiempo pero no modifica este ciclo de vida. Se pueden ver más detalles sobre esto en el enlace
  
-==== Estructura de una aplicación ====+===== Estructura de una aplicación =====
  
 <figure> <figure>
Line 49: Line 51:
   * **build.gradle** Es el fichero de configuración de //gradle// para nuestro proyecto de aplicación //Android//   * **build.gradle** Es el fichero de configuración de //gradle// para nuestro proyecto de aplicación //Android//
  
-=== Código de la aplicación ===+==== Código de la aplicación ====
  
 El código de la aplicación debe ser empaquetado siguiendo la misma jerarquía y nombrado de paquetes que indican las [[http://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html|convenciones de código de Java]] para ello. Además, conviene seguir una serie de reglas que recomienda //Android// en su [[http://source.android.com/source/code-style.html|Guía de estilo para contribuidores]] El código de la aplicación debe ser empaquetado siguiendo la misma jerarquía y nombrado de paquetes que indican las [[http://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html|convenciones de código de Java]] para ello. Además, conviene seguir una serie de reglas que recomienda //Android// en su [[http://source.android.com/source/code-style.html|Guía de estilo para contribuidores]]
Line 55: Line 57:
 Es especialmente importante seguir las reglas para el nombrado de paquetes y que además está coincida con la que se especifica al inicio del proyecto y que quedará registrada en el fichero de manifiesto ''AndroidManifest.xml'' (que veremos más adelante). En caso contrario es muy posible que el proyecto no se pueda ensamblar y, por tanto, no funcionará. Es especialmente importante seguir las reglas para el nombrado de paquetes y que además está coincida con la que se especifica al inicio del proyecto y que quedará registrada en el fichero de manifiesto ''AndroidManifest.xml'' (que veremos más adelante). En caso contrario es muy posible que el proyecto no se pueda ensamblar y, por tanto, no funcionará.
  
-=== Recursos de imagen (drawable) ===+==== Recursos de imagen (drawable) ====
  
-=== Recursos de layout (layouts) ===+==== Recursos de layout (layouts) ====
  
 Aqui se almacenan los ficheros que contienen los layouts (diseños) de la aplicación. Lo más habitual será encontrar un layout por cada ''Activity'' que se haya creado, pero al igual que necesitaremos crear clases Java que no estén asociadas con ninguna pantalla, también podemos encontrar layouts que no estén vinculados con ninguna Activity porque sean parte de otros layouts más complejos. Por ejemplo, en una Activity podremos diseñar un layout que sea una lista de elementos, que a su vez serán otro layout. De esa forma podremos diseñar el aspecto de cada uno de los elementos y luego listar tantos elementos como haya en una lista, utilizando el mismo layout para cada uno de ellos. Aqui se almacenan los ficheros que contienen los layouts (diseños) de la aplicación. Lo más habitual será encontrar un layout por cada ''Activity'' que se haya creado, pero al igual que necesitaremos crear clases Java que no estén asociadas con ninguna pantalla, también podemos encontrar layouts que no estén vinculados con ninguna Activity porque sean parte de otros layouts más complejos. Por ejemplo, en una Activity podremos diseñar un layout que sea una lista de elementos, que a su vez serán otro layout. De esa forma podremos diseñar el aspecto de cada uno de los elementos y luego listar tantos elementos como haya en una lista, utilizando el mismo layout para cada uno de ellos.
  
-=== Recursos de idioma (values) ===+==== Recursos de idioma (values) ====
  
 Aquí se almacenan los ficheros que contienen todos los textos de la aplicación. El objetivo es separar totalmente el código de la aplicación del texto con la finalidad de simplificar la internacionalización de la misma. De esa manera, cuando se quiera traducir la aplicación a un nuevo idioma bastará con crear el fichero correspondiente con los textos en dicho idioma y no será necesario realizar ningún cambio en el código. Aquí se almacenan los ficheros que contienen todos los textos de la aplicación. El objetivo es separar totalmente el código de la aplicación del texto con la finalidad de simplificar la internacionalización de la misma. De esa manera, cuando se quiera traducir la aplicación a un nuevo idioma bastará con crear el fichero correspondiente con los textos en dicho idioma y no será necesario realizar ningún cambio en el código.
Line 181: Line 183:
 </file> </file>
  
-=== Fichero de manifiesto AndroidManifest.xml ===+==== Fichero de manifiesto AndroidManifest.xml ====
  
 A continuación se muestra como ejemplo un fichero ''AndroidManifest.xml'' de una aplicación de ejemplo. A continuación se explicarán cada una de las partes que lo forman A continuación se muestra como ejemplo un fichero ''AndroidManifest.xml'' de una aplicación de ejemplo. A continuación se explicarán cada una de las partes que lo forman
Line 227: Line 229:
   * ''<intent-filter> . . . android.intent.action.MAIN . . . </intent-filter>'' Estas 4 líneas se colocan dentro de las etiquetas de la //Activity// que queremos marcar como //Activity// principal que será la primera que se lance cuando se ejecute la aplicación   * ''<intent-filter> . . . android.intent.action.MAIN . . . </intent-filter>'' Estas 4 líneas se colocan dentro de las etiquetas de la //Activity// que queremos marcar como //Activity// principal que será la primera que se lance cuando se ejecute la aplicación
   * ''<meta-data . . . />'' En este caso utilizamos esta etiqueta para indicar información adicional como el //API KEY// de Google Maps para utilizar los mapas en la aplicación   * ''<meta-data . . . />'' En este caso utilizamos esta etiqueta para indicar información adicional como el //API KEY// de Google Maps para utilizar los mapas en la aplicación
-===== Hello World! ===== 
  
-==== Estructura de un proyecto de ejemplo ==== 
  
-===== Componentes Android ===== 
  
-==== Componentes UI ==== 
  
-=== Layouts ===+====== Componentes Android ======
  
-Los layouts permiten distribuir todos los componentes que forma la GUI de una Activity en la pantalla. Principalmente usaremos dos:+===== Layouts =====
  
-  * **LinearLayout (Horizontal|Vertical)** Permiten distribuir todos los componentes en vertical (uno debajo de otro) o en horizontal (una a la derecha del anterior). Aunque parecen muy básicos combinándolos se pueden crear estructuras complejas puesto quepor ejemplo, un layout horizontal se puede colocar como elemento dentro de uno vertical y realizar GUIs algo más complejas+Los layouts definen la forma en que se distribuyen todos los componentes en la interfaz de la aplicación. Hay que tener en cuenta que todos los componentes (//Views//como pueden ser botones, cajas de texto, . .  se distribuyen según una jerarquía y la ubicación que les vendrá dada por el //Layout// que los contenga. 
 + 
 +Todo layout puede ser considerado también como una //View//, por lo que también pueden ser añadidos como parte de otro //Layout//
 + 
 +Los principales layouts actualmente son: 
 + 
 +=== ConstraintLayout === 
 + 
 +Es el layout por defecto cuando se crea una nueva //Activity//:
  
 <figure> <figure>
-{{ linearlayout.png }} +{{ constraintlayout.png}} 
-<caption>LinearLayout</caption></figure>+<caption>ConstraintLayout</caption></figure> 
 + 
 +=== LinearLayout === 
 + 
 +Layout sencillo para alinear verticalmente/horizontalmente los componentes
  
 <figure> <figure>
-{{ linear_layout_vertical_horizontal.png }} +{{ linearLayout.png}} 
-<caption>LinearLayouts combinados (horizontal y vertical)</caption></figure>+<caption>ConstraintLayout</caption></figure> 
 + 
 +=== FrameLayout ===
  
-  * **RelativeLayout**+Permite deliminar una zona donde se pueden colocar componentes superpuestos
  
 <figure> <figure>
-{{ relative_layout.png }} +{{ FrameLayout.png}} 
-<caption>RelativeLayout</caption></figure>+<caption>ConstraintLayout</caption></figure>
  
-=== TextView ===+===== TextView =====
  
 <figure> <figure>
Line 262: Line 274:
 </figure> </figure>
  
-=== EditText ===+===== EditText =====
  
 <figure> <figure>
Line 269: Line 281:
 </figure> </figure>
  
-=== Button ===+===== Button =====
  
 <figure> <figure>
Line 275: Line 287:
 <caption>Button</caption></figure> <caption>Button</caption></figure>
  
-=== Spinner ===+=== RadioButton === 
 + 
 +<figure> 
 +{{ radiobutton.png }} 
 +<caption>RadioButton</caption></figure> 
 + 
 +=== CheckBox === 
 + 
 +<figure> 
 +{{ checkbox.png }} 
 +<caption>Checkbox</caption></figure> 
 + 
 +===== Spinner =====
  
 Un ''Spinner'' es lo que se conoce como lista desplegable. Físicamente se muestra siempre compactada ocupando una sola línea pero al pinchar en ella se despliegan en forma de lista todos los elementos que contiene. Un ''Spinner'' es lo que se conoce como lista desplegable. Físicamente se muestra siempre compactada ocupando una sola línea pero al pinchar en ella se despliegan en forma de lista todos los elementos que contiene.
Line 285: Line 309:
 En cuanto al funcionamiento interno, es muy similar a un ''ListView'' por lo que conviene estudiar el siguiente apartado si se quiere trabajar con un ''Spinner'', tanto para listar contenido simple como para crear un ''Adapter'' propio y configurar un //Layout// para cada elemento de la lista. En cuanto al funcionamiento interno, es muy similar a un ''ListView'' por lo que conviene estudiar el siguiente apartado si se quiere trabajar con un ''Spinner'', tanto para listar contenido simple como para crear un ''Adapter'' propio y configurar un //Layout// para cada elemento de la lista.
  
-=== ListView ===+===== ListView (y CustomAdapter) [deprecated] ===== 
 + 
 +Ver el apartado sobre [[https://multimedia.codeandcoke.com/apuntes:android#recyclerview|RecyclerView]]. El uso de ListView se puede considerar como desaconsejado en favor de este nuevo componente.
  
 Es un componente que permite crear una lista de elementos más o menos compleja, muy similar a ''Spinner'' pero en este caso, la lista se muestra expandida pudiendo ocupar incluso toda la pantalla del dispositivo. Es un componente que permite crear una lista de elementos más o menos compleja, muy similar a ''Spinner'' pero en este caso, la lista se muestra expandida pudiendo ocupar incluso toda la pantalla del dispositivo.
Line 484: Line 510:
   * [[https://www.youtube.com/watch?v=M52SNrSJVVg|Trabajar con ListView III]] (Videotutorial)   * [[https://www.youtube.com/watch?v=M52SNrSJVVg|Trabajar con ListView III]] (Videotutorial)
  
-=== RadioButton ===+===== RecyclerView =====
  
-<figure> +''RecyclerView'' es la ''View'' que viene a sustituir a ''ListView''. En el siguiente ejemplo organizaremos una lista de super héroes en la que cada elemento contará con un botón para realizar una acción sobre la lista (en este caso se eliminado de la misma).
-{{ radiobutton.png }} +
-<caption>RadioButton</caption></figure>+
  
-=== CheckBox ===+Primero, definimos la clase Java que modela el objeto con el que vamos a trabajar. En el caso de que fuéramos a listar simples ''String'', no sería necesario puesto que podríamos trabajar sobre una ''List<String>'' directamente.
  
-<figure+<code java
-{{ checkbox.png }} +public class Superhero 
-<caption>Checkbox</caption></figure>+    private String name; 
 +    private String surname; 
 +    private String superHeroeName;
  
-==== Otros componentes ====+    public Superhero(String name, String surname, String superHeroeName) { 
 +        this.name name; 
 +        this.surname surname; 
 +        this.superHeroeName superHeroeName; 
 +    }
  
-=== ActionBar ===+  public String getName() { 
 +    return name; 
 +  } 
 + 
 +  public String getSurname() { 
 +    return surname; 
 +  } 
 + 
 +  public String getSuperHeroeName() { 
 +    return superHeroeName; 
 +  } 
 + 
 +  public String getFullName() { 
 +     return name + " " + surname; 
 +  } 
 +
 +</code> 
 + 
 +La siguiente clase es el //Customer Adapter// que define cómo hay que renderizar cada elemento de la lista. En este caso, para cada super héroe, queremos mostrar 2 líneas con su nombre y apellidos, y una tercera con el botón para eliminar dicho elemento de la lista. 
 +Además, la lista permite marcar como seleccionado (modificando su color de fondo) el elemento seleccionado, sobre el que luego podremos actuar desde la ''Activity'' (como veremos más adelante). 
 + 
 +<code java> 
 +public class SuperheroAdapter extends RecyclerView.Adapter<SuperheroAdapter.SuperheroHolder>
 +  private List<Superhero> dataList; 
 +  private int selectedPosition; 
 + 
 +  public SuperheroAdapter(List<Superhero> dataList) { 
 +    this.dataList = dataList; 
 +    selectedPosition = -1; 
 +  } 
 + 
 +  @Override 
 +  public SuperheroHolder onCreateViewHolder(ViewGroup parent, int viewType) { 
 +    View view = LayoutInflater.from(parent.getContext()) 
 +          .inflate(R.layout.character_item, parent, false); 
 +    return new SuperheroHolder(view); 
 +  } 
 + 
 +  @Override 
 +  public void onBindViewHolder(SuperheroHolder holder, int position) { 
 +    holder.tvFullName.setText(dataList.get(position).getFullName()); 
 +    holder.tvSuperheroName.setText(dataList.get(position).getSuperHeroeName()); 
 +  } 
 + 
 +  @Override 
 +  public int getItemCount() { 
 +    return dataList.size(); 
 +  } 
 + 
 +  public class SuperheroHolder extends RecyclerView.ViewHolder { 
 +    public TextView tvFullName; 
 +    public TextView tvSuperheroName; 
 +    public Button button; 
 +    public View parentView; 
 + 
 +    public SuperheroHolder(View view) { 
 +      super(view); 
 +      parentView = view; 
 + 
 +      tvFullName = view.findViewById(R.id.tvFullName); 
 +      tvSuperheroName = view.findViewById(R.id.tvSuperheroName); 
 +      button = view.findViewById(R.id.button); 
 + 
 +      // Click on superhero (select/unselect) 
 +      view.setOnClickListener(view1 -> selectSuperhero(parentView, view1, getAdapterPosition())); 
 +      // Click on button (remove superhero from the list) 
 +      button.setOnClickListener(view12 -> deleteSuperhero(getAdapterPosition())); 
 +    } 
 +  } 
 + 
 +  private void selectSuperhero(View parentView, View view, int position) { 
 +    // Select / Unselect 
 +    if (getSelectedPosition() != position) { 
 +      // Only single selection is allowed 
 +      if (selectedPosition != -1) 
 +        return; 
 + 
 +      parentView.setBackgroundColor(view.getContext().getResources().getColor( 
 +             android.R.color.holo_blue_light)); 
 +      selectedPosition = position; 
 +      // FIXME eliminar estos Toasts 
 +      Toast.makeText(view.getContext(), "Superhero selected. Position = " + position, 
 +      Toast.LENGTH_SHORT).show(); 
 +    } else { 
 +      parentView.setBackgroundColor(view.getContext().getResources().getColor( 
 +        android.R.color.white)); 
 +      selectedPosition = -1; 
 +      // FIXME eliminar estos Toasts 
 +      Toast.makeText(view.getContext(), "Superhero deselected. Position = " + position,  
 +        Toast.LENGTH_SHORT).show(); 
 +    } 
 +  } 
 + 
 +  private void deleteSuperhero(int position) { 
 +    dataList.remove(position); 
 +    notifyItemRemoved(position); 
 +  } 
 + 
 +  public int getSelectedPosition() { 
 +    return selectedPosition; 
 +  } 
 + 
 +  public Superhero getSelectedSuperhero() { 
 +    if (getSelectedPosition() == -1) 
 +      return null; 
 + 
 +    return dataList.get(getSelectedPosition()); 
 +  } 
 +
 +</code> 
 + 
 +Por último, en la ''MainActivity'' de nuestra aplicación, definimos la ''RecyclerView'' y la poblamos con una serie de elementos directamente en el código a modo de prueba. Además hemos programado el botón de la parte de abajo de la Activity para que muestre el nombre y apellidos del elemento seleccionado en la ''RecylerView''
 + 
 +<code java> 
 +public class MainActivity extends AppCompatActivity implements View.OnClickListener { 
 + 
 +  private List<Superhero> superheroList; 
 +  private SuperheroAdapter adapter; 
 + 
 +  @Override 
 +  protected void onCreate(Bundle savedInstanceState) { 
 +    super.onCreate(savedInstanceState); 
 +    setContentView(R.layout.activity_main); 
 + 
 +    populateSuperheroList(); 
 + 
 +    RecyclerView recyclerView = findViewById(R.id.recylerView); 
 +    recyclerView.setHasFixedSize(true); 
 +    LinearLayoutManager layoutManager = new LinearLayoutManager(this); 
 +    recyclerView.setLayoutManager(layoutManager); 
 +    adapter = new SuperheroAdapter(superheroList); 
 +    recyclerView.setAdapter(adapter); 
 + 
 +    Button continueButton = findViewById(R.id.continueButton); 
 +    continueButton.setOnClickListener(this); 
 +  } 
 + 
 +  private void populateSuperheroList() { 
 +    superheroList = new ArrayList<>(); 
 +    superheroList.add(new Superhero("Clark", "Kent", "Superman")); 
 +    superheroList.add(new Superhero("Peter", "Parker", "Spiderman")); 
 +    superheroList.add(new Superhero("Bruce", "Wayne", "Batman")); 
 +  } 
 + 
 +  @Override 
 +  public void onClick(View view) { 
 +    Superhero selectedCharacter = adapter.getSelectedSuperhero(); 
 +    if (selectedCharacter == null) 
 +      Toast.makeText(this, R.string.a_superhero_must_be_selected, Toast.LENGTH_LONG).show() 
 + 
 +    Toast.makeText(this, selectedCharacter.getFullName(), Toast.LENGTH_SHORT).show(); 
 +  } 
 +
 +</code> 
 + 
 + 
 +<figure> 
 +{{ recyclerview_sample.png }} 
 +<caption>RecyclerView con layout personalizado</caption></figure> 
 +===== ActionBar =====
  
 La ''ActionBar'' es la barra superior que encontramos en todas las ''Activities'' de Android. Permite mostrar un texto indicando normalmente la ''Activity'' o aplicación que estamos mostrando y una serie de iconos con las acciones que podemos realizar en dicha ''Activity''. Estas acciones suelen cambiar dependiendo de donde nos encontramos dentro de una misma app. Este elemento es la sustitución (a partir de Android 4) del antiguo botón de opciones del que disponían los móviles Android hace un tiempo y que mostraba en la parte superior las opciones a realizar cuando el usuario estaba en una ''Activity'' determinada. Entonces se carecía de ''ActionBar''. Es por tanto importante tener en cuenta que, si ejecutamos una aplicación en un móvil anterior a Android 4, todas las opciones que incorporemos a esta ''ActionBar'' se mostrarán como menú de opciones cuando pulsemos en dicho botón. La ''ActionBar'' es la barra superior que encontramos en todas las ''Activities'' de Android. Permite mostrar un texto indicando normalmente la ''Activity'' o aplicación que estamos mostrando y una serie de iconos con las acciones que podemos realizar en dicha ''Activity''. Estas acciones suelen cambiar dependiendo de donde nos encontramos dentro de una misma app. Este elemento es la sustitución (a partir de Android 4) del antiguo botón de opciones del que disponían los móviles Android hace un tiempo y que mostraba en la parte superior las opciones a realizar cuando el usuario estaba en una ''Activity'' determinada. Entonces se carecía de ''ActionBar''. Es por tanto importante tener en cuenta que, si ejecutamos una aplicación en un móvil anterior a Android 4, todas las opciones que incorporemos a esta ''ActionBar'' se mostrarán como menú de opciones cuando pulsemos en dicho botón.
Line 605: Line 794:
 {{ youtube>3CPCI4boc8o }} {{ youtube>3CPCI4boc8o }}
  
-=== Menú contextual ===+===== Menú contextual =====
  
 En Android los menús contextuales aparecen cuando el usuario realiza una pulsación prolongada en alguna parte de la pantalla. Entonces se activa (si así se ha dispuesto) un menú asociado a dicho elemento que muestra opciones para realizar sobre dicho elemento. En Android los menús contextuales aparecen cuando el usuario realiza una pulsación prolongada en alguna parte de la pantalla. Entonces se activa (si así se ha dispuesto) un menú asociado a dicho elemento que muestra opciones para realizar sobre dicho elemento.
Line 697: Line 886:
 {{ youtube>1JOU7qi3sA0 }} {{ youtube>1JOU7qi3sA0 }}
  
-=== Diálogos ===+===== Diálogos =====
  
 Los diálogos son ventanas emergentes que aparecen cuando el usuario debe seleccionar una acción antes de seguir con la ejecución de la aplicación. Se tratan siempre de ventana modales por lo que bloquean el flujo de ejecución de la aplicación hasta que el usuario selecciona qué hacer. Los diálogos son ventanas emergentes que aparecen cuando el usuario debe seleccionar una acción antes de seguir con la ejecución de la aplicación. Se tratan siempre de ventana modales por lo que bloquean el flujo de ejecución de la aplicación hasta que el usuario selecciona qué hacer.
Line 729: Line 918:
 </code> </code>
  
-==== Mensajes emergentes ====+===== Mensajes emergentes =====
  
 Es posible mostrar pequeños mensajes emergentes de corta duración con la clase ''Toast'' ((https://developer.android.com/reference/android/widget/Toast.html)) Es posible mostrar pequeños mensajes emergentes de corta duración con la clase ''Toast'' ((https://developer.android.com/reference/android/widget/Toast.html))
Line 747: Line 936:
 </code>  </code> 
  
-==== Notificaciones ====+===== Notificaciones =====
  
 Las notificaciones son mensaje emergentes que aparecen en la pantalla de notificaciones del móvil. Sirve para mostrar información y también como forma rápida de acceso a la aplicación que emite dicha notificación, pudiendo acceder al contenido directamente. Las notificaciones son mensaje emergentes que aparecen en la pantalla de notificaciones del móvil. Sirve para mostrar información y también como forma rápida de acceso a la aplicación que emite dicha notificación, pudiendo acceder al contenido directamente.
Line 756: Line 945:
 </figure> </figure>
  
-En el siguiente ejemplo de código se puede ver como construir una Notificación con un título, un texto, un icono y una Activiy asociada a la que el usuario accederá si pulsa en la notificación+Lo primero que tendremos que hacer será definir un canal para nuestras notificaciones
  
 <code java> <code java>
 +private final String CHANNEL_ID = "mychannel_id";
 . . . . . .
-NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(MainActivity.this)+private void createNotificationChannel() { 
 +  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 
 +    CharSequence name = "Nombre del canal"; 
 +    String description = "Descripcion del canal"; 
 +    int importance = NotificationManager.IMPORTANCE_DEFAULT; 
 +    NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); 
 +    channel.setDescription(description);  
 +    channel.setImportance(NotificationManager.IMPORTANCE_HIGH); 
 +    NotificationManager notificationManager = getSystemService(NotificationManager.class); 
 +    notificationManager.createNotificationChannel(channel); 
 +    } 
 +
 +</code> 
 + 
 +Y, a continuación, definir una notificación que quedará vinculada a dicho canal: 
 + 
 +<code java> 
 +. . . 
 +NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
   .setContentTitle("Titulo de la notificación")   .setContentTitle("Titulo de la notificación")
   .setContentText("Esto es el texto de la notificación")   .setContentText("Esto es el texto de la notificación")
   .setSmallIcon(R.drawable.default_marker)   .setSmallIcon(R.drawable.default_marker)
-  .setContentIntent(PendingIntent.getActivity(MainActivity.this, (int) System.currentTimeMillis(), +</code>
-                    new Intent(MainActivity.this, MapActivity.class), 0));+
  
 +Y lanzarla cuando sea necesario:
 +
 +<code java>
 NotificationManager nManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager nManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 nManager.notify(0, nBuilder.build()); nManager.notify(0, nBuilder.build());
Line 772: Line 982:
 </code> </code>
  
-((https://developer.android.com/guide/topics/ui/notifiers/notifications.html)) +=== Asociar una Activity a una notificación === 
-===== Comunicación entre Activities =====+ 
 +Se puede asociar una Activity a una notificación, de forma que ésta sea lanzada cuando se pulse en dicha notificación: 
 + 
 +<code java> 
 +// Definimos el Intent que permitirá lanzar la Activity 
 +Intent intent = new Intent(this, OtraActivity.class); 
 +PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); 
 +. . . 
 +// Vinculamos el Intent con la notificación 
 +NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) 
 +. . . 
 +  .setContentIntent(pendingIntent) 
 +...  
 +</code> 
 + 
 +=== Añadir acciones a una Notificación === 
 + 
 +También podemos añadir diferentes acciones a una misma Notificación: 
 + 
 +<code java> 
 +// Podemos definir Intents si lo que queremos es lanzar diferentes ActivitiesEn este caso añadiremos 2 acciones diferentes a una misma notificación: 
 +. . 
 +// Y gestionar las diferentes acciones asociada a la notificación 
 +NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) 
 +. . . 
 +  .addAction(R.drawable.ic_hacer_algo, "HACER ALGO", hacerAlgoIntent); 
 +  .addAction(R.drawable.ic_otra_cosa, "OTRA COSA", otraCosaIntent); 
 +. . . 
 +</code> 
 + 
 +<figure> 
 +{{ action-notification.png }} 
 +</caption>Acciones en una Notificación</caption></figure> 
 + 
 +=== Notificar el progreso en una notificación === 
 + 
 +También podemos notificar el progreso de una tarea usando una notificación: 
 + 
 +<code java> 
 +NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) 
 +. . . 
 +  .setContentText("En progreso") 
 +  .setProgress(100, 20, false) // máximo, progreso, ¿indeterminado? 
 +. . . 
 +</code> 
 + 
 + 
 +<figure> 
 +{{ progress-notification.png }} 
 +</caption>Acciones en una Notificación</caption></figure> 
 + 
 +Si lo que queremos es ir notificando el progresivo avance de una tarea, habría que meter la actualización del mismo (''builder.setProgress()'' y la propia notificación de este mensaje (''notificationManager.notify()'') en un hilo que fuera ejecutando la tarea asociada en segundo plano. De esa manera se iría mostrando una nueva notificación con el avance actualizado de forma que parecería la notificación original actualizando la barra de progreso. 
 +====== Comunicación entre Activities ======
  
 En algunas ocasiones necesitaremos pasar información entre dos //Activities//, por ejemplo, cuando en un ''ListView'' pinchemos sobre algún elemento que queramos ver en el mapa. Este mapa estará en otra ''Activity'' a la que tendremos que pasar la localización (y quizás otra información) del elemento que queremos mostrar. Esa otra ''Activity'' tendrá que recuperar dicha información para pintarla en el mapa. En algunas ocasiones necesitaremos pasar información entre dos //Activities//, por ejemplo, cuando en un ''ListView'' pinchemos sobre algún elemento que queramos ver en el mapa. Este mapa estará en otra ''Activity'' a la que tendremos que pasar la localización (y quizás otra información) del elemento que queremos mostrar. Esa otra ''Activity'' tendrá que recuperar dicha información para pintarla en el mapa.
Line 817: Line 1079:
 {{ youtube>ASZZ_td4qKE }} {{ youtube>ASZZ_td4qKE }}
 \\ \\
-===== Gestión de permisos =====+====== Gestión de permisos ======
  
 En Android, para que las aplicaciones que instalamos en nuestro dispositivo, puedan acceder a determinadas funciones o bien a información sensible, debe estar preparada para gestionar los permisos que le permitirán realizar dicha acciones. Los permisos que puede o no requerir una aplicación son muy diversos. Se puede consultar la referencia completa en el siguiente enlace.  En Android, para que las aplicaciones que instalamos en nuestro dispositivo, puedan acceder a determinadas funciones o bien a información sensible, debe estar preparada para gestionar los permisos que le permitirán realizar dicha acciones. Los permisos que puede o no requerir una aplicación son muy diversos. Se puede consultar la referencia completa en el siguiente enlace. 
Line 871: Line 1133:
 } }
 </code> </code>
-===== Gestión de preferencias ===== 
  
-Para la gestión de preferencias se debe diseñar un //layout// específico para crear la pantalla desde donde el usuario podrá configurar los diferentes aspectos que se preparen. En el ejemplo siguiente se puede ver una pantalla de preferencias con diferentes elementos para configurar 3 aspectos de una aplicación:+====== Gestión de preferencias ======
  
-<file xml preferencias.xml>+Para empezar tendremos que añadir una dependencia al fichero ''build.gradle'': 
 + 
 +<code groovy> 
 +. . . 
 +implementation 'androidx.preference:preference:1.2.0' 
 +. . . 
 +</code> 
 + 
 +Para la gestión de preferencias se debe diseñar un //layout// específico para crear la pantalla desde donde el usuario podrá configurar los diferentes aspectos que se preparen. Esta Activity será la que contenga el fragmento que será quien realmente tenga definidas las opciones que queramos que aparezcan: 
 + 
 +<file xml activity_preferences.xml> 
 +<?xml version="1.0" encoding="utf-8"?> 
 +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
 +    xmlns:app="http://schemas.android.com/apk/res-auto" 
 +    xmlns:tools="http://schemas.android.com/tools" 
 +    android:layout_width="match_parent" 
 +    android:layout_height="match_parent" 
 +    tools:context=".MainActivity"> 
 + 
 +    <fragment 
 +        android:id="@+id/fragment" 
 +        android:layout_width="0dp" 
 +        android:layout_height="match_parent" 
 +        android:layout_weight="1" 
 +        app:layout_constraintEnd_toEndOf="parent" 
 +        app:layout_constraintStart_toStartOf="parent" 
 +        class="com.codeandcoke.preferences.PreferencesFragment" 
 +        tools:ignore="MissingConstraints"> 
 +    </fragment> 
 +</LinearLayout> 
 +</file> 
 + 
 +Este layout quedará relacionado con la Activity correspondiente donde se cargará como pantalla de Preferencias: 
 + 
 +<file java PreferencesActivity.java> 
 +public class PreferencesActivity extends AppCompatActivity { 
 +  
 +  @Override 
 +  protected void onCreate(Bundle savedInstanceState) { 
 +    super.onCreate(savedInstanceState); 
 +    setContentView(R.layout.activity_preferences); 
 +  } 
 +
 +</file> 
 + 
 +En ''xml->preference_screen.xml'' definiremos las preferencias que queremos gestionar en la Activity anterior: 
 + 
 +<file xml preference_screen.xml
 +<?xml version="1.0" encoding="utf-8"?>
 <PreferenceScreen <PreferenceScreen
-  xmlns:android="http://schemas.android.com/apk/res/android"> +    xmlns:app="http://schemas.android.com/apk/res-auto"> 
-  <PreferenceCategory android:title="Personal"> + 
-    <CheckBoxPreference +    <SwitchPreferenceCompat 
-      android:key="opcion_ver_favoritos+        app:key="notifications
-      android:title="Sólo favoritos" +        app:title="Habilitar notificaciones de la aplicación"/> 
-      android:summary="Ver sólo a los contactos favoritos" />+
     <EditTextPreference     <EditTextPreference
-      android:key="opcion_nombre+        app:key="your_name
-      android:title="Tu nombre+        app:title="Your name
-      android:summary="Cómo quieres que tus contactos te vean" +        app:summary="Type your name"/> 
-      android:dialogTitle="Introduce un valor" /> +
-  </PreferenceCategory> +
-  <PreferenceCategory android:title="Agenda"> +
-    <ListPreference +
-      android:key="opcion_datos" +
-      android:title="Mostrar Contactos" +
-      android:summary="Cómo identificar a los contactos" +
-      android:dialogTitle="Por nombre o apellidos" +
-      android:entries="@array/datos" +
-      android:entryValues="@array/datos" /> +
-  </PreferenceCategory>+
 </PreferenceScreen> </PreferenceScreen>
 </file> </file>
 +
 +En cuando al código Java, tendremos que definir el fragmento que se encargará de cargar la pantalla de preferencias según el layout y las preferencias que hemos definido anteriormente. Hay que tener en cuenta que este fragmento quedará relacionado con el layout ''preference_scree.xml'' que acabamos de definir.
 +
 +<file java PreferenceFragment>
 +public class PreferencesFragment extends PreferenceFragmentCompat {
 +
 +    @Override
 +    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
 +        setPreferencesFromResource(R.xml.preference_screen, rootKey);
 +    }
 +}
 +
 +</file>
 +
 +Hasta ahí hemos conseguido preparar la ''Activity'' de preferencias desde donde el usuario podrá configurar la aplicación. Todos los cambios que éste realice en esa ''Activity'' se almacenarán de forma automática (no hay que programar nada) como preferencias de la aplicación. Nos corresponde a nosotros programar el qué hacer en función de las preferencias que haya escogido o configurado el usuario. Así, para acceder al estado de las mismas utilizaremos la clase ''SharedPreferences'' que nos permitirá acceder a cada una de las preferencias por su nombre (''android:key'') estableciendo en cada caso un valor por defecto en caso de que el usuario no hubiera establecido ningún valor para la misma
 +
 +<code java>
 +. . .
 +SharedPreferences myPreferences = PreferenceManager.getDefaultSharedPreferences(this);
 +boolean notifications = myPreferences.getBoolean("notifications", false);
 +. . .
 +</code>
  
 <figure> <figure>
Line 905: Line 1226:
 <caption>Activity de preferencias</caption></figure> <caption>Activity de preferencias</caption></figure>
  
-En el caso de que se usen ''array'' de datos, éstos deben figurar también como ficheros //XML// dentro de la carpeta ''xml''+Hay que tener en cuenta que, en el caso de que se usen ''array'' de datos, éstos deben figurar también como ficheros //XML// dentro de la carpeta ''xml''
  
 <file xml datos.xml> <file xml datos.xml>
Line 917: Line 1238:
 </file> </file>
  
-También existe un tipo de ''Activity'' específica para la gestión de preferenciasAsíutilizando esa Activity podremos cargar el layout de preferencias que hemos preparado anteriormente.+====== Imágenes ====== 
 + 
 +En esta sección veremos una serie de artículos sobre cómo trabajar con imágenes en Android: hacer una foto, seleccionar una existente de la galería, almacenarlas en base de datos, . . 
 + 
 +===== Hacer una foto ===== 
 + 
 +El primer paso será indicar que la aplicación podrá solicitar los permisos para hacer uso de la cámara del dispositivo: 
 + 
 +<code xml> 
 +<uses-feature 
 +   android:name="android.hardware.camera" 
 +   android:required="false" /> 
 +<uses-permission android:name="android.permission.CAMERA" /> 
 +</code> 
 + 
 +Ademástendremos que solicitar expresamente los permisos al inicio de la Activity (nada nos asegura que el permiso esté ya asignado puesto que en las versiones modernas de Android el usuario puede cancelarlo en cualquier momento). Podemos hacerlo en el método ''onCreate''.
  
 <code java> <code java>
-public class Preferencias extends PreferenceActivity +public class SomeActivity extends AppCompatActivity { 
-  @Override +. . . 
-  public void onCreate(Bundle savedInstanceState) { +  private final int PICK_PICTURE = 1; 
-    super.onCreate(savedInstanceState);+  private Uri pictureUri; 
 +. . . 
 +  protected void onCreate(Bundle savedInstanceState) { 
 +    . . . 
 +    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == 
 +        PackageManager.PERMISSION_DENIED) { 
 +      ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1); 
 +    } 
 +    . . . 
 +  } 
 +  . . . 
 +
 +. . . 
 +</code>
  
-    addPreferencesFromResource(R.layout.preferencias);+Justo en el momento en que el usuario pincha en un botón o un ''ImageView'' para que salte la cámara y hacer la foto, tendremos que comprobar si los permisos, efectivamente, han siado asignados. Es entonces cuando lanzaremos el método ''launchCamera'' que lanzará la cámara. 
 + 
 +<code java> 
 +. . . 
 +if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==       
 +    PackageManager.PERMISSION_GRANTED) { 
 +  launchCamera(); 
 +
 +. . . 
 +</code> 
 + 
 +El proceso tiene 2 pasos: 
 + - ''launchCamera'': que lanza la cámara y permite que realicemos una foto 
 + - El objeto ''startCamera'' que es una ''ActivityResultLauncher'' y que será invocado cuando la cámara haya realizado la foto para seleccionarla y que podamos visualizarla, por ejemplo, en un ''ImageView'' para verla a modo de vista previa. 
 + 
 +A partir de aqui, si lo que queremos hacer es almacenarla en base de datos, podemos hacerlo siguiendo los pasos que se indican en [[https://multimedia.codeandcoke.com/apuntes:android#almacenar_una_imagen_en_base_de_datos|Almacenar una imagen en base de datos]] 
 + 
 +<code java> 
 +ActivityResultLauncher<Intent> startCamera = registerForActivityResult( 
 +  new ActivityResultContracts.StartActivityForResult(), 
 +  new ActivityResultCallback<ActivityResult>() { 
 +    @Override 
 +    public void onActivityResult(ActivityResult result) { 
 +      if (result.getResultCode() == RESULT_OK) { 
 +        // Podemos mostrar la miniatura en un ImageView 
 +        imageView.setImageURI(pictureUri); 
 +        // Podemos tener la imagen como Bitmap si queremos guardarla en base de datos, por ejemplo 
 +        Bitmap bitmapImage = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); 
 +      } 
 +    } 
 +  }); 
 + 
 +private void launchCamera() { 
 +  ContentValues values = new ContentValues(); 
 +  values.put(MediaStore.Images.Media.TITLE, "Nueva imagen"); 
 +  values.put(MediaStore.Images.Media.DESCRIPTION, "Cámara"); 
 +  pictureUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 
 +  Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 
 +  cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); 
 + 
 +  startCamera.launch(cameraIntent); 
 +
 +</code> 
 +===== Seleccionar una imagen de la galería del móvil ===== 
 + 
 +En el siguiente ejemplo suponemos que tenemos un ''ImageView'' con el que, haciendlo click sobre él, queremos seleccionar una imagen de nuestra galería para terminar visualizándola sobre si mismo. Podríamos utilizarlo, por ejemplo, para el caso en que queramos registrar información en un formulario y uno de los datos sea una imagen. 
 + 
 +<code java> 
 +public class RegisterActivity extends AppCompatActivity { 
 +  . . . 
 +  private ImageView imageView; 
 +   
 +  public void onCreate(.....) { 
 +    . . . 
 +    imageView = findViewById(R.id.myImageView); 
 +    . . .  
   }   }
 +  
 +  ActivityResultLauncher<Intent> galleryActivityResultLauncher = registerForActivityResult(
 +    new ActivityResultContracts.StartActivityForResult(),
 +    result -> {
 +      if (result.getResultCode() == Activity.RESULT_OK) {
 +        Uri image_uri = result.getData().getData();
 +        imageView.setImageURI(image_uri);
 +      }
 +    }
 +  );
 +
 +  public void selectImage(View view) {
 +    Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
 +    galleryActivityResultLauncher.launch(galleryIntent);
 +  }
 +  . . .
 } }
 </code> </code>
  
-Hasta ahí hemos conseguido preparar la ''Activity'' de preferencias desde donde el usuario podrá configurar la aplicación. Todos los cambios que éste realice en esa ''Activity'' se almacenarán de forma automática (no hay que programar nadacomo preferencias de la aplicaciónNos corresponde a nosotros programar el qué hacer en función de las preferencias que haya escogido o configurado el usuario. Así, para acceder al estado de las mismas utilizaremos la clase ''SharedPreferences'' que nos permitirá acceder a cada una de las preferencias por su nombre (''android:key''estableciendo en cada caso un valor por defecto en caso de que el usuario no hubiera establecido ningún valor para la misma+Más adelante, cuando queramos guardar la información en la base de datos, podemos obtener el objeto imagen como ''Bitmap'' (es el tipo de dato que podemos usar en Android para almacenar una imagen) de la siguiente forma
 + 
 +<code java> 
 +Bitmap bitmapImage = ((BitmapDrawableimageView.getDrawable()).getBitmap(); 
 +</code> 
 + 
 +Y ese objeto //bitmapImage// ya puede ser almacenado tal cual en una base de datos utilizando la [[https://multimedia.codeandcoke.com/apuntes:android#acceso_a_bases_de_datos_con_room|librería Room]]. 
 + 
 +===== Almacenar una imagen en base de datos ===== 
 + 
 +Una forma sencilla de "almacenar" una imagen en la base de datos del móvil (usando la librería //Room//consiste, precisamente, en evitar almacenarla y guardar simplemente la ruta a la misma, puesto que estará almacenada en el móvil de alguna forma.
  
 +El campo que almacenará la imagen será, por tanto, un ''String''
 <code java> <code java>
 +public class Book {
 . . . . . .
-SharedPreferences preferencias = PreferenceManager.getDefaultSharedPreferences(this); +  @ColumnInfo 
-boolean verFavoritos = preferencias.getBoolean("opcion_ver_favoritos", false);+  private String image;
 . . . . . .
 +}
 </code> </code>
  
 +En el momento en que cogemos la Uri de la foto para luego ver su vista previa en un ImageView, podemos también convertir ese valor a un String y asignarlo al campo que hemos definido en el momento de dar de alta, en este caso, un libro:
  
 +<code java>
 +. . .
 +if (result.getResultCode() == Activity.RESULT_OK) {
 +  Uri uri = result.getData().getData();
 +  bookImage.setImageURI(uri);
 +  imageUri = uri.toString();
 +}
 +. . .
 +</code>
  
-===== Acceso a Bases de Datos =====+Asi, al crear el objeto justo antes de pasarlo al Dao de //Room//, podemos asignarle el valor: 
 + 
 +<code java> 
 +book.setImage(imageUri); 
 +</code> 
 + 
 +Y a la hora de cargarlo (por ejemplo en el //Adapter// del ''RecyclerView'' donde visualicemos todos los datos) solamente tendremos que asignarle al ''ImageView'' la Uri que crearemos a partir del ''String'' que hemos almacenado: 
 + 
 +<code java> 
 +. . . 
 +if (book.getImage() != null) { 
 +  holder.ivImage.setImageURI(Uri.parse(book.getImage())); 
 +
 +. . . 
 +</code> 
 +====== Acceso a Bases de Datos con SQLite [deprecated] ======
  
 El acceso a Bases de Datos está integrado con el API de //Android// por lo que resulta muy sencillo. Además, como ya se ha visto en la estructura de este framework, se incluye con el mismo el motor de almacenamiento //SQLite//, que será el que se emplee para almacenar información dentro del dispositivo móvil. Si se quiere almacenar información fuera del teléfono (que tendrá que ser accedida vía Servicio Web, por ejemplo) se podrán utilizar otros motores puesto que son otros equipos quienes tendrán que gestionar el acceso a los datos. El acceso a Bases de Datos está integrado con el API de //Android// por lo que resulta muy sencillo. Además, como ya se ha visto en la estructura de este framework, se incluye con el mismo el motor de almacenamiento //SQLite//, que será el que se emplee para almacenar información dentro del dispositivo móvil. Si se quiere almacenar información fuera del teléfono (que tendrá que ser accedida vía Servicio Web, por ejemplo) se podrán utilizar otros motores puesto que son otros equipos quienes tendrán que gestionar el acceso a los datos.
Line 953: Line 1411:
   * Se puede utilizar ''SQL'' para comunicarse con él o bien la API si no vamos a hacer algo muy complicado   * Se puede utilizar ''SQL'' para comunicarse con él o bien la API si no vamos a hacer algo muy complicado
  
-=== Acceso a Bases de Datos ===+=== Acceso a Bases de Datos con SQLiteHelper (deprecated) ===
  
 En //Android//, para acceder a una Base de Datos, debemos crearnos una clase que herede de la clase auxiliar ''SQLiteOpenHelper'' (incluida en el framework) de forma que tendremos que implementar allí todos los métodos que permitan acceder a los datos para insertar, consultar, eliminar o cualquier otra operación. Además, en esta clase de deben incluir las sentencias que permitan crear o actualizar la Base de Datos de forma que el framework pueda realizar estas operaciones cuando sea necesario. En //Android//, para acceder a una Base de Datos, debemos crearnos una clase que herede de la clase auxiliar ''SQLiteOpenHelper'' (incluida en el framework) de forma que tendremos que implementar allí todos los métodos que permitan acceder a los datos para insertar, consultar, eliminar o cualquier otra operación. Además, en esta clase de deben incluir las sentencias que permitan crear o actualizar la Base de Datos de forma que el framework pueda realizar estas operaciones cuando sea necesario.
Line 1147: Line 1605:
 </code> </code>
  
-==== Almacenar imágenes en Base de Datos ====+=== Almacenar imágenes en Base de Datos (con SQLite) [deprecated] ===
  
 Merece especial atención este caso, puesto que almacenar imágenes en una Base de Datos SQLite no es una tarea trivial. Merece especial atención este caso, puesto que almacenar imágenes en una Base de Datos SQLite no es una tarea trivial.
Line 1155: Line 1613:
  
 En este caso, podríamos utilizar una vista ''ImageView'' ((https://developer.android.com/reference/android/widget/ImageView.html)) para mostrar la imagen seleccionada por el usuario. Además, sobre esa vista podemos asignar un ''ClickListener'' para lanzar la galería/cámara cuando el usuario pulse sobre la foto para cambiarla. En este caso, podríamos utilizar una vista ''ImageView'' ((https://developer.android.com/reference/android/widget/ImageView.html)) para mostrar la imagen seleccionada por el usuario. Además, sobre esa vista podemos asignar un ''ClickListener'' para lanzar la galería/cámara cuando el usuario pulse sobre la foto para cambiarla.
 +
 +Usaremos la librería [[https://square.github.io/picasso/|Picasso]] que permite cargar imágenes directamente sobre cualquier ImageView de nuestra Activity. Hay que tener en cuenta que tendremos que añadir la siguiente línea al ''build.gradle'' para importarla en nuestro proyecto: ''implementation 'com.squareup.picasso:picasso:2.8'''
  
 <figure> <figure>
Line 1164: Line 1624:
 @Override @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- +    super(requestCode, resultCode, data); 
-if ((requestCode == RESULTADO_CARGA_IMAGEN) && (resultCode == RESULT_OK)  +    if ((requestCode == RESULTADO_CARGA_IMAGEN) && (resultCode == RESULT_OK)  
-   && (data != null)) { +        && (data != null)) { 
-  // Obtiene el Uri de la imagen seleccionada por el usuario +        Picasso.get().load(data.getData()).noPlaceholder().centerCrop().fit() 
-  Uri imagenSeleccionada = data.getData()+                    .into((ImageView) findViewById(R.id.imageView)); 
-  String[] ruta = {MediaStore.Images.Media.DATA }; +    }
- +
-  // Realiza una consulta a la galería de imágenes solicitando la imagen seleccionada +
-  Cursor cursor = getContentResolver().query(imagenSeleccionada, ruta, null, null, null)+
-  cursor.moveToFirst()+
- +
-  // Obtiene la ruta a la imagen +
-  int indice = cursor.getColumnIndex(ruta[0])+
-  String picturePath = cursor.getString(indice); +
-  cursor.close(); +
- +
-  // Carga la imagen en una vista ImageView que se encuentra en  +
-  // en layout de la Activity actual +
-  ImageView imageView = (ImageView) findViewById(R.id.ivImagen); +
-  imageView.setImageBitmap(BitmapFactory.decodeFile(picturePath)); +
-  }+
 } }
 . . . . . .
Line 1309: Line 1754:
 } }
 </code> </code>
-===== Acceder a la red ===== 
  
-Uno de los mayores atractivos de los dispositivos móviles es el acceso a Internet para recuperar y tratar información de cualquier tipo. Prácticamente todas las aplicaciones permiten comunicar los dispositivos entre sí o con Internet. Para ello, siempre tendremos que contar con una aplicación servidor que permita la comunicación entre los diferentes dispositivos. Dicho servidor, normalmente, hará disponible la información mediante diferentes mecanismos como //APIs// o bien a través del formato //JSON//, que es un formato de intercambio de información que en los últimos años ha ganado mucha popularidad para la comunicación asíncrona de información, desplazando en cierta manera al formato //XML// que se venía usando ampliamente hasta entonces.+ 
 +====== Acceso a Bases de Datos con Room ====== 
 + 
 +=== Room persistence library === 
 + 
 +  * Librería incluida con Android que abstrae todavía más al usuario de conocer los detalles de la base de datos 
 +  * Según se define la clase Java es posible añadir anotaciones para indicar cómo debe almacenar dicho objeto cuando se quiera llevar a base de datos 
 + 
 +<figure> 
 +{{ room.png }} 
 +<caption>Ejemplo anotaciones Room</caption></figure> 
 + 
 +En primer lugar, necesitamos añadir las dependencias necesarias al fichero ''build.gradle'': 
 + 
 +<code groovy> 
 +. . . 
 +implementation "androidx.room:room-runtime:2.4.3" 
 +annotationProcessor "androidx.room:room-compiler:2.4.3" 
 +. . . 
 +</code> 
 + 
 +Asi, para una clase ''Product'', quedaría como sigue: 
 + 
 +<code java> 
 +@Entity 
 +public class Product { 
 + 
 +  @PrimaryKey(autoGenerate = true) 
 +  private int id; 
 +  @ColumnInfo 
 +  private String name; 
 +  @ColumnInfo 
 +  private String category; 
 +  @ColumnInfo 
 +  private int quantity; 
 +  @ColumnInfo 
 +  private float price; 
 +  @ColumnInfo 
 +  private boolean important; 
 +  @ColumnInfo(typeAffinity = ColumnInfo.BLOB) 
 +  private byte[] image; 
 +  . . . 
 +  . . . 
 +</code> 
 + 
 +A continuación definiremos el //DAO// con todas las operaciones que queramos que estén disponibles (Habría que hacer un //DAO// para cada clase del modelo de datos: 
 + 
 +<code java> 
 +@Dao 
 +public interface ProductDao { 
 +  @Query("SELECT * FROM product"
 +  List<Product> getAll(); 
 + 
 +  @Query("SELECT * FROM product WHERE name = :name"
 +  List<Product> findByName(String name); 
 + 
 +  @Insert 
 +  void insert(Product product); 
 + 
 +  @Update 
 +  void update(Product product); 
 + 
 +  @Delete 
 +  void delete(Product product); 
 +   
 +  @Query("DELETE FROM product WHERE name = :name"
 +  void deleteByName(String name); 
 +
 +</code> 
 + 
 +Y, por último, la clase que nos dará acceso a la Base de datos allá donde la queramos utilizar para realizar alguna operación sobre ella: 
 + 
 +<code java> 
 +@Database(entities = {Product.class}, version = 2) 
 +public abstract class AppDatabase extends RoomDatabase { 
 +  public abstract ProductDao productDao(); 
 +
 +</code> 
 + 
 +=== Registrar/Modificar información === 
 + 
 +<code java> 
 +// Se recogen los datos para construir el nuevo Producto 
 +Product product = . . . 
 +. . . 
 +final AppDatabase db = Room.databaseBuilder(this, AppDatabase.class, DATABASE_NAME) 
 +                .allowMainThreadQueries().build(); 
 +try { 
 +  db.productDao().insert(product); 
 +  . . . 
 +} catch (SQLiteConstraintException sce) { 
 +  Snackbar.make(aView, "Error al registrar el producto", BaseTransientBottomBar.LENGTH_LONG).show(); 
 +
 +</code> 
 + 
 +=== Consultas === 
 + 
 +Podríamos necesitar listar todos los productos de la base de datos: 
 + 
 +<code java> 
 +AppDatabase db = AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, "products"
 +             .allowMainThreadQueries() 
 +             .fallbackToDestructiveMigration().build(); 
 +List<Product> myProducts = db.productDao().getAll(); 
 +</code> 
 + 
 +O bien hacer alguna de las consultas que hemos definido en el DAO: 
 + 
 +<code java> 
 +// Recogemos el nombre del producto por el que queremos filtrar 
 +String productName = . . . 
 +AppDatabase db = AppDatabase db = Room.databaseBuilder(context, AppDatabase.class, "products"
 +             .allowMainThreadQueries() 
 +             .fallbackToDestructiveMigration().build(); 
 +Product product = db.productDao().findByName(productName); 
 +</code> 
 + 
 +=== Eliminar información === 
 + 
 +<code java> 
 +// Nos hacemos con el producto que queremos eliminar 
 +Book selectedBook = . . . 
 +. . . 
 +final AppDatabase db = Room.databaseBuilder(this, AppDatabase.class, DATABASE_NAME) 
 +                .allowMainThreadQueries().build(); 
 +try { 
 +  db.productDao().delete(product); 
 +  . . . 
 +} catch (SQLiteConstraintException sce) { 
 +  Snackbar.make(aView, "Error al eliminar el producto", BaseTransientBottomBar.LENGTH_LONG).show(); 
 +
 +</code> 
 + 
 + 
 + 
 +====== Consumo de APIs ====== 
 + 
 +Uno de los mayores atractivos de los dispositivos móviles es el acceso a Internet para recuperar y tratar información de cualquier tipo. Prácticamente todas las aplicaciones permiten comunicar los dispositivos entre sí o con Internet. Para ello, siempre tendremos que contar con una aplicación servidor que permita la comunicación entre los diferentes dispositivos. Dicho servidor, normalmente, hará disponible la información mediante diferentes mecanismos como //APIs// con mensajes que normalmente estarán escritos en formato //JSON//, que es un formato de intercambio de información que en los últimos años ha ganado mucha popularidad para la comunicación asíncrona de información, desplazando en cierta manera al formato //XML// que se venía usando ampliamente hasta entonces.
  
 <figure> <figure>
Line 1317: Line 1898:
 <caption>Petición/Respuesta JSON-REST</caption></figure> <caption>Petición/Respuesta JSON-REST</caption></figure>
  
- +===== Formato JSON =====
-==== Formato JSON ====+
  
 <code java> <code java>
Line 1350: Line 1930:
 } }
 </code> </code>
-==== Tareas asíncronas: AsyncTask ====+ 
 +===== Consumo de APIs con Retrofit ===== 
 + 
 +Para hacer este ejemplo, de consumo de APIs usando Retrofit, vamos a suponer: 
 +  * Tenemos una API de productos funcionando en local con las siguientes operaciones: 
 +    * POST /products 
 +    * GET /product/{productId} 
 +    * DELETE /product/{productId} 
 +    * GET /products 
 +  * Tenemos la clase Java definida con sus atributos, getters y setters 
 +  * Estamos usando una arquitectura MVP (Model-View-Presenter) 
 +  * Solamente vamos a montar el ejemplo que corresponde con el registro de un nuevo Producto 
 + 
 +Para empezar, añadimos las dependencias que necesitamos al fichero ''build.gradle'': 
 + 
 +<code groovy> 
 +implementation 'com.squareup.retrofit2:retrofit:2.9.0' 
 +implementation 'com.squareup.retrofit2:converter-gson:2.4.0' 
 +</code> 
 + 
 +Y también tendremos que crear las clases e interfaces que nos permiten "configurar" Retrofit: 
 + 
 +Primero la clase que nos permitirá crear una instancia de nuestra API en este proyecto, "en el lado Android" para comunicarnos con ella (''BASE_URL'' será una constante que tendrá como valor la dirección base de nuestra API, normalmente ''http://10.0.2.2:8080'' si es una prueba depurando por USB): 
 + 
 +<code java> 
 +public class ProductApi { 
 + 
 +  public static ProductApiInterface buildInstance() { 
 +    Retrofit retrofit = new Retrofit.Builder() 
 +                .baseUrl(BASE_URL) 
 +                .addConverterFactory(GsonConverterFactory.create()) 
 +                .build(); 
 +    return retrofit.create(ProductApiInterface.class); 
 +  } 
 +
 +</code> 
 + 
 +A continuación la interface con las operaciones de la API que queremos que Retrofit implemente para nosotros: 
 + 
 +<code java> 
 +public interface ProductApiInterface { 
 + 
 +  Call<Product> addProduct(@Body Product product); 
 +  . . . 
 +  . . . 
 +
 +</code> 
 + 
 +A continuación la interface que define el contrato para nuestra Arquitectura MVP. Hay que tener en cuenta que para este ejemplo solamente hemos dejado el código que corresponde a la operación de registrar un producto: 
 + 
 +<file java NewProductContract.java> 
 +public interface NewProductContract { 
 +  interface Model { 
 +    interface OnAddProductListener { 
 +      void onAddProductSuccess(Product newProduct); 
 +      void onAddProductError(String message); 
 +    } 
 +    void addProduct(Product product, OnAddProductListener listener); 
 +    . . . 
 + } 
 + 
 +  interface View { 
 +    void addProduct(android.view.View view); 
 +    . . . 
 +  } 
 + 
 +  interface Presenter { 
 +    void addProduct(String name, String category, String quantity, String price, boolean important, byte[] productImage); 
 +    . . . 
 +  } 
 +
 +</file> 
 + 
 +El modelo es la capa que contiene las operaciones donde nuestra aplicación Android se comunica con la API del lado servidor. En función de como se ejecuten esas operaciones, invocaremos a un método u otro del listener, que acabará ejecutando esos métodos en el Presenter: 
 + 
 +<file java NewProductModel.java> 
 +public class NewProductModel implements NewProductContract.Model { 
 + 
 +  private Context context; 
 + 
 +  public NewProductModel(Context context) { 
 +    this.context = context; 
 +  } 
 + 
 +  @Override 
 +  public void addProduct(Product product, OnAddProductListener listener) { 
 +    ProductApiInterface api = ProductApi.buildInstance(); 
 +    Call<Product> callProducts = api.addProduct(product); 
 +    callProducts.enqueue(new Callback<Product>() { 
 +      @Override 
 +      public void onResponse(Call<Product> call, Response<Product> response) { 
 +        Product product = response.body(); 
 +        listener.onAddProductSuccess(product); 
 +      } 
 +      @Override 
 +      public void onFailure(Call<Product> call, Throwable t) { 
 +        listener.onAddProductError("Se ha producido un error al conectar con el servidor"); 
 +        t.printStackTrace(); 
 +      } 
 +    }); 
 +  } 
 +  . . . 
 +  . . . 
 +</file> 
 + 
 +El Presenter, como capa que hacer de "mensajero" entre el Model y la View, valida la información antes de invocar al Model para que se comunique con la API en el lado servidor. Dependiendo de como vaya esa comunicación, el Model acabará invocando a sus métodos ''onAddProductSuccess'' si todo van bien o ''onAddProductError'' si se produce algún fallo durante la comunicación. Será entonces cuando el Presenter se comunique con la View para comunicar al usuario qué ha pasado: 
 + 
 +<file java NewProductPresenter.java> 
 +public class NewProductPresenter implements NewProductContract.Presenter, 
 +      NewProductContract.Model.OnAddProductListener, NewProductContract.Model.OnModifyProductListener { 
 + 
 +  private NewProductModel model; 
 +  private NewProductView view; 
 + 
 +  public NewProductPresenter(NewProductView view) { 
 +    this.view = view; 
 +    model = new NewProductModel(view.getApplicationContext()); 
 +  } 
 + 
 +  @Override 
 +  public void addProduct(String name, String category, String quantity, String price, boolean important, byte[] productImage) { 
 +    if (!validData(name, category, quantity, price, important, productImage)) 
 +      view.showMessage("Error al validar la información"); 
 + 
 +    Product product = new Product(name, category, Integer.parseInt(quantity), 
 +                      Float.parseFloat(price), important, productImage); 
 +    model.addProduct(product, this); 
 +  } 
 + 
 +  @Override 
 +  public void onAddProductSuccess(Product product) { 
 +    view.showMessage("ok"); 
 +  } 
 + 
 +  @Override 
 +  public void onAddProductError(String message) { 
 +    view.showMessage("error"); 
 +  } 
 +  . . . 
 +  . . . 
 +</file> 
 + 
 +Para este caso, en esta capa, el usuario ve el formulario con el que podrá registrar un nuevo producto. También se implementan aqui los métodos a los que el Presenter tiene que invocar cuando todo vaya bien o mal al intentar registrar un producto: 
 + 
 +<file java NewProductView.java> 
 +public class NewProductView extends AppCompatActivity implements NewProductContract.View { 
 + 
 +  private int SELECT_PICTURE_RESULT = 1; 
 +  private NewProductPresenter presenter; 
 +  private Action action; 
 +  private Product product; 
 + 
 +  @Override 
 +  protected void onCreate(Bundle savedInstanceState) { 
 +    super.onCreate(savedInstanceState); 
 +    setContentView(R.layout.activity_new_product); 
 + 
 +    // Tenemos una enumeración definida para saber si tenemos que Registrar o Modificar un producto 
 +    // Eso nos permite reutilizar el formulario que usamos para la recogida de datos 
 +    action = Action.valueOf(getIntent().getStringExtra("ACTION")); 
 +    if (action == PUT) { 
 +      product = getIntent().getParcelableExtra("product"); 
 +      fillProductDetails();    // TODO No está hecho para este ejemplo 
 +    } 
 + 
 +    presenter = new NewProductPresenter(this); 
 +  } 
 + 
 +  public void addProduct(View view) { 
 +    EditText etName = findViewById(R.id.product_name); 
 +    EditText etCategory = findViewById(R.id.product_category); 
 +    EditText etQuantity = findViewById(R.id.product_quantity); 
 +    EditText etPrice = findViewById(R.id.product_price); 
 +    CheckBox checkImportant = findViewById(R.id.important_product); 
 +    ImageView productImageView = findViewById(R.id.product_image); 
 + 
 +    String name = etName.getText().toString(); 
 +    String category = etCategory.getText().toString(); 
 +    String quantity = etQuantity.getText().toString(); 
 +    String price = etPrice.getText().toString(); 
 +    boolean important = checkImportant.isChecked(); 
 +    byte[] productImage = ImageUtils.fromImageViewToByteArray(productImageView); 
 + 
 +    if (action == POST) 
 +      presenter.addProduct(name, category, quantity, price, important, productImage); 
 +    else 
 +      presenter.modifyProduct(product.getId(), name, category, quantity, price, important, productImage); 
 + 
 +    etName.setText(""); 
 +    etCategory.setText(""); 
 +    etQuantity.setText(""); 
 +    etPrice.setText(""); 
 +    checkImportant.setChecked(false); 
 +    etName.requestFocus(); 
 +  } 
 + 
 +  @Override 
 +  public void showMessage(String message) { 
 +    Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 
 +  } 
 + 
 +  public void selectPicture(View view) { 
 +    Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 
 +    startActivityForResult(intent, SELECT_PICTURE_RESULT); 
 +  } 
 + 
 +  @Override 
 +  protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
 +    super.onActivityResult(requestCode, resultCode, data); 
 +    if ((requestCode == SELECT_PICTURE_RESULT) && (resultCode == RESULT_OK) 
 +                                               && (data != null)) { 
 +       Picasso.get().load(data.getData()) 
 +                    .noPlaceholder() 
 +                    .centerCrop() 
 +                    .fit() 
 +                    .into((ImageView) findViewById(R.id.product_image)); 
 +    } 
 +  } 
 +  . . . 
 +  . . . 
 +
 +</file> 
 + 
 +===== Tareas asíncronas: AsyncTask [deprecated] =====
  
 Las tareas asíncronas (''AsynTask'') permiten ejecutar código en segundo plano de forma que mientras se ejecute dicha tarea no se bloquea la GUI para el usuario y éste puede seguir interactuando con ella o simplemente visualizar el avance o progreso de la misma. Las tareas asíncronas (''AsynTask'') permiten ejecutar código en segundo plano de forma que mientras se ejecute dicha tarea no se bloquea la GUI para el usuario y éste puede seguir interactuando con ella o simplemente visualizar el avance o progreso de la misma.
Line 1404: Line 2207:
 </code> </code>
  
-==== Acceder a contenido en la red ====+ 
 +===== Acceder a contenido en la red [deprecated] =====
  
 En el caso concreto de que queramos acceder a contenido en la red es muy probable que éste nos venga preparado en formato //JSON// como hemos visto anteriormente. Además, hay que tener en cuenta que //Android// nos obliga a implementar en un //AsyncTask// cualquier fragmento de código que establezca alguna conexión con Internet (de esa manera se realiza en segundo plano y no bloquea el interfaz de usuario). Así, una vez visto la estructura de un fichero //JSON// y la de una tarea asíncrona, nos queda ver como unir ambos conceptos para preparar una //Activity// capaz de conectarse a Internet, acceder a un fichero //JSON// y //parsearlo// para extraer la información que nos interesa y poderla mostrar al usuario de la forma más cómoda posible. En el caso concreto de que queramos acceder a contenido en la red es muy probable que éste nos venga preparado en formato //JSON// como hemos visto anteriormente. Además, hay que tener en cuenta que //Android// nos obliga a implementar en un //AsyncTask// cualquier fragmento de código que establezca alguna conexión con Internet (de esa manera se realiza en segundo plano y no bloquea el interfaz de usuario). Así, una vez visto la estructura de un fichero //JSON// y la de una tarea asíncrona, nos queda ver como unir ambos conceptos para preparar una //Activity// capaz de conectarse a Internet, acceder a un fichero //JSON// y //parsearlo// para extraer la información que nos interesa y poderla mostrar al usuario de la forma más cómoda posible.
Line 1585: Line 2389:
 . . . . . .
 </code> </code>
-===== Mapas y ubicaciones ===== 
  
-==== Utilizar el mapa de Google Maps ====+====== Google Maps ====== 
 + 
 +Antes de poder trabajar con la API de Google Maps en cualquier proyecto de Android, tendremos que configurar nuestra cuenta en ''Google Cloud Console'', registrar nuestro proyecto alli y conseguir una API Key que nos permita hacer uso de la API. Para ello tendremos que seguir los pasos que se describen en la [[https://developers.google.com/maps/documentation/android-sdk/cloud-setup|guia que Android ha preparado para empezar con el uso de Google Maps]]. 
 + 
 +Una vez tengamos el proyecto registrado en la Google Cloud Console y hayamos solicitado la API Key asociada a la API de Google Maps, guardaremos ésta en nuestro proyecto en res->values->google_maps_api.xml de la siguiente forma: 
 + 
 +<code xml> 
 +<resources> 
 +    <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">API_KEY_HERE</string> 
 +</resources> 
 +</code> 
 + 
 +Y tenemos que asegurarnos de que indicamos la ubicación de esta API Key en el manifest de nuestro proyecto en Android Studio (podemos colocarlo justo donde termina la definición de todas las Activities): 
 + 
 +<code xml> 
 +. . . 
 +<meta-data 
 +    android:name="com.google.android.geo.API_KEY" 
 +    android:value="@string/google_maps_key" /> 
 +. . . 
 +</code> 
 + 
 +===== Utilizar el mapa de Google Maps =====
  
 <figure> <figure>
Line 1596: Line 2421:
  
 <file xml activity_mapa.xml> <file xml activity_mapa.xml>
-<fragment xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 
-    android:id="@+id/map"+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"     android:layout_width="match_parent"
     android:layout_height="match_parent"     android:layout_height="match_parent"
-    class="com.google.android.gms.maps.MapFragment"/>+    android:orientation="vertical"> 
 +     
 +    <fragment xmlns:android="http://schemas.android.com/apk/res/android" 
 +        xmlns:tools="http://schemas.android.com/tools" 
 +        android:id="@+id/map_fragment" 
 +        android:name="com.google.android.gms.maps.SupportMapFragment" 
 +        android:layout_width="match_parent" 
 +        android:layout_height="match_parent" 
 +        tools:context=".MapsActivity" /
 + 
 +</androidx.constraintlayout.widget.ConstraintLayout>
 </file> </file>
  
-Más adelante, en la ''Activity'' que esté asociada con el layout que acabamos de crear, tendremos que inicializar el sistema de mapas. También podemos obtener la referencia al componente del mapa para más adelante trabajar con él, aunque no es necesario hacerlo si sólo se quiere mostrar el mapa (aunque no es habitual que sólo queramos hacer eso)+Más adelante, en la ''Activity'' que esté asociada con el layout que acabamos de crear, tendremos que cargar el ''fragment'' del mapa. También podemos obtener la referencia al componente del mapa para más adelante trabajar con él, aunque no es necesario hacerlo si sólo se quiere mostrar el mapa (aunque no es habitual que sólo queramos hacer eso)
 + 
 +Asi, en el método ''onMapReady'' disponemos ya del objeto googleMap listo para trabajar con él, tras haberse cargado completamente tras llamar al método ''getMapAsync'' al que hemos invocado en el método ''onCreate''.
  
 <code java> <code java>
-public class Mapa extends Activity {+public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {
  
-  private GoogleMap mapa; +  private GoogleMap map;
- +
-  private double latitud; +
-  private double longitud; +
-  private String nombre;+
  
   @Override   @Override
Line 1619: Line 2452:
     setContentView(R.layout.activity_mapa);     setContentView(R.layout.activity_mapa);
  
-    // Inicializa el sistema de mapas de Google +    SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() 
-    try { +                .findFragmentById(R.id.map_fragment); 
-      MapsInitializer.initialize(this); +    mapFragment.getMapAsync(this); 
-    catch (Exception e) { +  } 
-      e.printStackTrace(); +   
-    }+  @Override 
 +  public void onMapReady(GoogleMap googleMap) { 
 +      map = googleMap; 
 +      map.getUiSettings().setZoomControlsEnabled(true); 
 +      map.getUiSettings().setMapToolbarEnabled(false); 
 +  } 
 +
 +</code>
  
-    // Obtiene una referencia al objeto que permite "manejar" el mapa +===== Seleccionar ubicaciones utilizando un mapa Mapbox ===== 
-    mapa ((MapFragmentgetFragmentManager().findFragmentById(R.id.map)).getMap();+ 
 +Una vez que ya tenemos lo necesario para visualizar el mapa siguiendo los pasos del punto anterior, vamos a ver cómo empezar a interactuar con él. 
 + 
 +Como primer ejemplo veremos cómo seleccionar una ubicación en el mapa haciendo click en ella y colocando un "marker" en la posición para representar el lugar escogido. A partir de ahi, podremos obtener las coordenadas de dicha ubicación para almacenarlas o hacer con ella lo que queramos (que serán los valores de latitud y longitud del punto seleccionado). 
 + 
 +Para ello, necesitaremos los componentes ''PointAnnotationManager'' para gestionar los markers que coloquemos en el mapa y ''GesturesPlugin'' que nos permite interactuar con el mapa a través de eventos como hacer click sobre él. Además de esto, necesitaremos lo mínimo que ya sabemos hacer para representar el mapa. 
 + 
 +  * El método ''OnStyleLoaded'' se encarga de todo lo que queramos hacer en cuanto el mapa esté activo. Nos viene de implementar ''Style.OnStyleLoaded''. Hay que tener en cuenta que el mapa se carga en segundo plano y es la forma que tenemos de "esperara que lo haga para poder empezar a dibujar o hacer algo sobre él (podríamos, por ejemplo, pintar unas ubicaciones que previamente he traído o cargado en la Activity). 
 +  * El método ''initializeMapView'' inicializa el Mapa para que empiece a cargarse en pantalla (cuando acabe de hacerlo es cuando llamará al método que hemos comentado anteriormente ''OnStyleLoaded''
 +  * El método ''initializeGesturesPlugin'' inicializa el componente que se encarga de actuar frente a las interacciones del usuario sobre el mapa. En nuestro caso añadiremos un listener para estar atentos a los clicks que el usuario haga sobre el mapa 
 +  * El método ''initializePointAnnotationManager'' nos permite initializar el componente que se encarga de añadir o eliminar markers en el mapa 
 +  * El método ''addMarker'' es quién se encarga de colocar un marker en la ubicación que se pasa como parámetro junto con el texto que se indique 
 +  * El método ''onMapClick'' atiende al evento //click// sobre el mapa (a través de la interface ''OnMapClickListener'' que hemos implementado al definir la clase) para poder programar alli qué queremos hacer cuando el usuario haga click en el mapa. En nuestro caso, borramos todos los markers anterior (por si habíamos pinchado ya), almacenamos la posición para luego poder hacer lo que queramos con ella y visualizar un marker justo en esa misma localización. 
 + 
 +<code java> 
 +public class MapActivity extends AppCompatActivity implements Style.OnStyleLoaded, OnMapClickListener { 
 + 
 +  private MapView mapView; 
 +  private PointAnnotationManager pointAnnotationManager; 
 +  private GesturesPlugin gesturesPlugin; 
 +  private Point currentPoint; 
 +   
 +  @Override 
 +  protected void onCreate(Bundle savedInstanceState
 +    . . 
 +    initializeMapView(); 
 +    initializePointAnnotationManager(); 
 +    initializeGesturesPlugin(); 
 +    . . . 
 +  } 
 +   
 +  @Override 
 +  public void onStyleLoaded(@NonNull Style style) { 
 + 
 +  } 
 + 
 +  private void initializeMapView() { 
 +    mapView = findViewById(R.id.mapView)
 +    mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, this)
 +  } 
 + 
 +  private void initializePointAnnotationManager() { 
 +    AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); 
 +    AnnotationConfig annotationConfig = new AnnotationConfig(); 
 +    pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, annotationConfig); 
 +  } 
 +   
 +  private void initializeGesturesPlugin() { 
 +    gesturesPlugin = GesturesUtils.getGestures(mapView); 
 +    gesturesPlugin.addOnMapClickListener(this); 
 +  } 
 + 
 +  private void addMarker(double latitude, double longitude, String title) { 
 +    PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions() 
 +           .withPoint(Point.fromLngLat(longitude, latitude)) 
 +           .withIconImage(BitmapFactory.decodeResource(getResources(), R.mipmap.red_marker)) 
 +           .withTextField(title); 
 +    pointAnnotationManager.create(pointAnnotationOptions); 
 +  } 
 + 
 +  @Override 
 +  public boolean onMapClick(@NonNull Point point) { 
 +    pointAnnotationManager.deleteAll(); 
 +    currentPoint = point; 
 +    addMarker(point.latitude(), point.longitude(), getString(R.string.here)); 
 +    return false;
   }   }
 } }
 </code> </code>
  
-==== Marcar ubicaciones en el mapa de Google Maps ====+===== Marcar ubicaciones en el mapa de Google Maps =====
  
 Para los ejemplos de estos apuntes se han utilizado datos geolocalizados del [[http://www.zaragoza.es/ciudad/risp/buscar_Risp|catálogo de datos abiertos del Ayuntamiento de Zaragoza]]. En este catálogo las coordenadas vienen en formato [[https://es.wikipedia.org/wiki/Sistema_de_coordenadas_universal_transversal_de_Mercator|UTM]] y éstas tienen que transformarse al sistema que se usa en Google Maps. Por eso, a través de la librería [[http://www.jstott.com/jcoord/|jcoord]] se transforman de UTM al sistema utilizado en Google Maps con el siguiente método que podemos implementar en nuestra clase ''Utils'' junto al resto de métodos de utilidad que tengamos en nuestro proyecto. Para los ejemplos de estos apuntes se han utilizado datos geolocalizados del [[http://www.zaragoza.es/ciudad/risp/buscar_Risp|catálogo de datos abiertos del Ayuntamiento de Zaragoza]]. En este catálogo las coordenadas vienen en formato [[https://es.wikipedia.org/wiki/Sistema_de_coordenadas_universal_transversal_de_Mercator|UTM]] y éstas tienen que transformarse al sistema que se usa en Google Maps. Por eso, a través de la librería [[http://www.jstott.com/jcoord/|jcoord]] se transforman de UTM al sistema utilizado en Google Maps con el siguiente método que podemos implementar en nuestra clase ''Utils'' junto al resto de métodos de utilidad que tengamos en nuestro proyecto.
Line 1718: Line 2623:
 {{ youtube>xHYRgHyWWUw }} {{ youtube>xHYRgHyWWUw }}
  
-==== Utilizar los mapas con la librería Mapbox ====+====== Mapbox ======
  
 <figure> <figure>
Line 1725: Line 2630:
  
 Mapbox es [[http://wiki.openstreetmap.org/wiki/Android|uno de tantos SDKs]] que permite trabajar con los mapas de [[http://www.openstreetmap.org|OpenStreetMap]]. Se puede utilizar como alternativa al servicio de mapas de Google Maps. Mapbox es [[http://wiki.openstreetmap.org/wiki/Android|uno de tantos SDKs]] que permite trabajar con los mapas de [[http://www.openstreetmap.org|OpenStreetMap]]. Se puede utilizar como alternativa al servicio de mapas de Google Maps.
-Lo primero que tenemos que hacer, para poder utilizar la librería //Mapbox// es añadir, al fichero ''build.gradle (Module:app)'' las líneas que se indican a continuación. Así, tras la sincronización de //Gradle// podremos utilizar las clases de dicha librería.  
  
-<code java+El primer paso es seguir la [[https://docs.mapbox.com/android/maps/guides/install/|Guía de Instalación]] de su web, que nos permitirá configurar gradle y preparar un pequeño proyecto de ejemplo para visualizar el primer mapa. 
-repositories + 
-  mavenCentral() +En la gúia, una vez realizados los pasos para registrarnos, crear los tokens y configurar gradle, se nos explica cómo crear un layout y su correspondiente Activity para visualizar un mapa por primera vez. 
-} + 
-dependencies +También hay bastantes [[https://docs.mapbox.com/android/maps/examples/|ejemplos disponibles en su web]]. 
-  . . . + 
-  compile('com.mapbox.mapboxsdk:mapbox-android-sdk:4.1.1@aar') { +===== Configurar el proyecto para comenzar a usar Mapbox ===== 
-    transitive = true+ 
 +  * Una vez que nos hemos registrado en https://www.mapbox.com, tendremos que acceder a nuestra cuenta para crear un token. Será suficiente con dejar seleccionar los //Public scopes// y añadir el //Secret scope// de //DOWNLOADS:READ//. Ese será el token que usaremos en nuestro proyecto 
 + 
 +<figure
 +{{ create-token-mapbox.png }} 
 +<caption>Crear un nuevo token Mapbox</caption></figure> 
 + 
 +<figure> 
 +{{ token-mapbox.png }} 
 +<caption>Configurar el token Mapbox</caption></figure> 
 + 
 +  * El siguiente paso será crear un fichero llamado ''gradle.properties'' en la carpeta ''.gradle'' que habrá en nuestra carpeta de usuario. Añadiremos la siguiente línea, cuyo valor es el token obtenido en el paso anterior: 
 + 
 +<code javascript> 
 +MAPBOX_DOWNLOADS_TOKEN=sk.eyJ1Ijoic2ZhY2kiLCkjahsdjkhJHJHMGFzZTJpazFqZGd6ZGVxYiJ9.jBfUMuKJHjhjhjh3nMg 
 +</code> 
 + 
 +  * En el fichero ''settings.gradle.tks'' o ''settings.gradle'' de nuestro proyecto tendremos que asegurarnos de tener la sección ''dependencyResolutionManagement'' con esta configuración (de nuevo usando nuestro token donde corresponda): 
 + 
 +<code kotlin> 
 +. . . 
 +dependencyResolutionManagement 
 +  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 
 +  repositories { 
 +    google(
 +    mavenCentral() 
 + 
 +    maven { 
 +      url = uri("https://api.mapbox.com/downloads/v2/releases/maven"
 +      authentication { 
 +        create<BasicAuthentication>("basic"
 +      } 
 +      credentials { 
 +        // Do not change the username below. 
 +        // This should always be `mapbox` (not your username). 
 +        username = "mapbox
 +        // Use the secret token you stored in gradle.properties as the password 
 +        password = "sk.eyJ1Ijoic2ZhY2kiLCkjahsdjkhJHJHMGFzZTJpazFqZGd6ZGVxYiJ9.jBfUMuKJHjhjhjh3nMg" 
 +      } 
 +    }
   }   }
 } }
 +. . .
 </code> </code>
  
-El siguiente paso, en el layout de la Activity donde queramos que aparezca el mapa, tendremos que añadir el código XML que permite insertar el componente donde se dibujará el mapa. En este caso, hemos dejado predefinidas las coordenadas de la ciudad de Zaragoza y un nivel de zoom de 12. En cualquier caso son parámetros que luego desde el código pueden ser modificados e incluso el usuario haciendo uso de la pantalla podrá modificar a su gusto.+  * En el fichero ''AndroidManifest.xml'' añadiremos los permisos para que nuestra aplicación pueda disponer de los servicios de localización del dispositivo:
  
-<file xml activity_main.xml>+<code xml
 +. . 
 +<manifest . . . . . > 
 +  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> 
 +  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 
 +   
 +  <application . . . 
 +  . . . 
 +. . . 
 +</code> 
 + 
 +  * También tendremos que crear un fichero ''developer-config.xml'' en la carpeta ''res/values'' del proyecto donde definiremos el valor del token público (disponible en nuestro perfil de Mapbox): 
 + 
 +<code xml>
 <?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<resources> 
-  xmlns:tools="http://schemas.android.com/tools" +    <string name="mapbox_access_token" translatable="false">sk.eyJ1Ijoic2ZhY2kiLjUHjhjJiklcmdHMGFzZTJpazFqZGd6ZGVxYiJ9.jBfUMuKJHjhjhjh3nMg</string> 
-  android:layout_width="match_parent" +</resources> 
-  android:layout_height="match_parent+</code> 
-  xmlns:mapbox="http://schemas.android.com/apk/res-auto" + 
-  android:paddingBottom="@dimen/activity_vertical_margin+Y ahora es el momento de comenzar ya a trabajar con nuestra aplicación, preparando el layout y el código para que nuestro mapa funcione. 
-  android:paddingLeft="@dimen/activity_horizontal_margin+ 
-  android:paddingRight="@dimen/activity_horizontal_margin+En el layout de la Activity donde queramos que aparezca el mapa, tendremos que añadir el código XML que permite insertar el componente donde se dibujará el mapa. En este caso, hemos dejado predefinidas las coordenadas de la ciudad de Zaragoza y un nivel de zoom de 12. En cualquier caso son parámetros que luego desde el código pueden ser modificados e incluso el usuario haciendo uso de la pantalla podrá modificar a su gusto
-  android:paddingTop="@dimen/activity_vertical_margin" + 
-  tools:context="sfaci.com.ejemplomapbox.MainActivity">+<file xml activity_maps.xml> 
 +<?xml version="1.0encoding="utf-8"?> 
 +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 
 +    android:layout_width="match_parent
 +    android:layout_height="match_parent
 +    android:orientation="vertical">
  
-  <com.mapbox.mapboxsdk.maps.MapView +    <com.mapbox.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android" 
-    android:id="@+id/mapaView+        xmlns:tools="http://schemas.android.com/tools" 
-    android:layout_width="fill_parent+        xmlns:mapbox="http://schemas.android.com/apk/res-auto" 
-    android:layout_height="fill_parent+        android:id="@+id/mapView
-    mapbox:center_latitude="41.656287+        android:layout_width="match_parent
-    mapbox:center_longitude="-0.876538+        android:layout_height="match_parent
-    mapbox:zoom="12+        mapbox:mapbox_cameraTargetLat="40.7128
-    mapbox:style_url="@string/style_mapbox_streets"+        mapbox:mapbox_cameraTargetLng="-74.0060
-  </com.mapbox.mapboxsdk.maps.MapView> +        mapbox:mapbox_cameraZoom="9.0
-</RelativeLayout>+        /> 
 +</androidx.constraintlayout.widget.ConstraintLayout>
 </file> </file>
  
-En cuanto al código Javanecesitaremos inicializar la librería con la llamada ''MapboxAccountManager.start()'' pasando como parámetro el //token// que nos hayan asignado previa creación de una cuenta de usuario en [[http://www.mapbox.com|la página web de Mapbox]], y más adelante cargar el layout donde se dibujará el mapa.+En cuanto al ćodigoserá necesario acceder al objeto ''MapView'' del layout para poder ya cargar el mapa y que éste aparezca en pantalla.
  
 <file java MainActivity.java> <file java MainActivity.java>
-public class MainActivity extends Activity {+public class MapsActivity extends AppCompatActivity {
  
-  MapView mapaView;+    private MapView mapView;
  
-  @Override +    @Override 
-  protected void onCreate(Bundle savedInstanceState) { +    protected void onCreate(Bundle savedInstanceState) { 
-    super.onCreate(savedInstanceState);+        super.onCreate(savedInstanceState); 
 +        setContentView(R.layout.activity_maps);
  
-    MapboxAccountManager.start(this, "Pon aqui tu token"); +        mapView = findViewById(R.id.mapView); 
-   +        mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS); 
-    setContentView(R.layout.activity_main); +    }
-    mapaView = (MapViewfindViewById(R.id.mapaView); +
-    mapaView.onCreate(savedInstanceState); +
-  }+
 } }
 </file> </file>
  
-Y, por último, antes de poder lanzar la aplicación, tendremos que habilitar una serie de permisos necesarios para poder trabajar con el mapa. Hay que tener en cuenta que los mapas se obtiene en línea desde Internet y que será habitual acceder a la ubicación del dispositivo también+<figure> 
-Además, tenemos que habilitar el servicio de telemetría de la librería //Mapbox//. +{{ map-view.png }} 
-Todos estos cambios se deben realizar en el fichero ''AndroidManifest.xml'' de nuestra aplicación.+<caption>Mapa Mapbox cargando en aplicación Android</caption></figure> 
 +===== Seleccionar ubicaciones utilizando un mapa Mapbox =====
  
-<code xml> +Una vez que ya tenemos lo necesario para visualizar el mapa siguiendo los pasos del punto anterior, vamos a ver cómo empezar a interactuar con él
-. . + 
-<manifest . . . +Como primer ejemplo veremos cómo seleccionar una ubicación en el mapa haciendo click en ella y colocando un "marker" en la posición para representar el lugar escogidoA partir de ahi, podremos obtener las coordenadas de dicha ubicación para almacenarlas o hacer con ella lo que queramos (que serán los valores de latitud y longitud del punto seleccionado)
-+ 
-  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE/> +Para ello, necesitaremos los componentes ''PointAnnotationManager'' para gestionar los markers que coloquemos en el mapa y ''GesturesPlugin'' que nos permite interactuar con el mapa a través de eventos como hacer click sobre élAdemás de esto, necesitaremos lo mínimo que ya sabemos hacer para representar el mapa. 
-  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + 
-  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> +  * El método ''OnStyleLoaded'' se encarga de todo lo que queramos hacer en cuanto el mapa esté activoNos viene de implementar ''Style.OnStyleLoaded''. Hay que tener en cuenta que el mapa se carga en segundo plano y es la forma que tenemos de "esperara que lo haga para poder empezar a dibujar o hacer algo sobre él (podríamos, por ejemplo, pintar unas ubicaciones que previamente he traído o cargado en la Activity)
-  <uses-permission android:name="android.permission.INTERNET" /> +  * El método ''initializeMapView'' inicializa el Mapa para que empiece a cargarse en pantalla (cuando acabe de hacerlo es cuando llamará al método que hemos comentado anteriormente ''OnStyleLoaded''
-  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /+  * El método ''initializeGesturesPlugin'' inicializa el componente que se encarga de actuar frente a las interacciones del usuario sobre el mapaEn nuestro caso añadiremos un listener para estar atentos a los clicks que el usuario haga sobre el mapa 
-  . . +  * El método ''initializePointAnnotationManager'' nos permite initializar el componente que se encarga de añadir o eliminar markers en el mapa 
-  <application . . .+  * El método ''addMarker'' es quién se encarga de colocar un marker en la ubicación que se pasa como parámetro junto con el texto que se indique 
 +  * El método ''onMapClick'' atiende al evento //click// sobre el mapa (a través de la interface ''OnMapClickListener'' que hemos implementado al definir la clase) para poder programar alli qué queremos hacer cuando el usuario haga click en el mapaEn nuestro caso, borramos todos los markers anterior (por si habíamos pinchado ya), almacenamos la posición para luego poder hacer lo que queramos con ella y visualizar un marker justo en esa misma localización
 + 
 +<code java
 +public class MapActivity extends AppCompatActivity implements Style.OnStyleLoaded, OnMapClickListener { 
 + 
 +  private MapView mapView; 
 +  private PointAnnotationManager pointAnnotationManager; 
 +  private GesturesPlugin gesturesPlugin; 
 +  private Point currentPoint; 
 +   
 +  @Override 
 +  protected void onCreate(Bundle savedInstanceState) {
     . . .     . . .
-    <service android:name="com.mapbox.mapboxsdk.telemetry.TelemetryService" />+    initializeMapView(); 
 +    initializePointAnnotationManager(); 
 +    initializeGesturesPlugin();
     . . .     . . .
-  </application> +  } 
-. . . +   
-</manifest> +  @Override 
-. . .+  public void onStyleLoaded(@NonNull Style style) { 
 + 
 +  } 
 + 
 +  private void initializeMapView() { 
 +    mapView = findViewById(R.id.mapView); 
 +    mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, this); 
 +  } 
 + 
 +  private void initializePointAnnotationManager() { 
 +    AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); 
 +    AnnotationConfig annotationConfig = new AnnotationConfig(); 
 +    pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, annotationConfig); 
 +  } 
 +   
 +  private void initializeGesturesPlugin() { 
 +    gesturesPlugin = GesturesUtils.getGestures(mapView); 
 +    gesturesPlugin.addOnMapClickListener(this); 
 +  } 
 + 
 +  private void addMarker(double latitude, double longitude, String title) { 
 +    PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions() 
 +           .withPoint(Point.fromLngLat(longitude, latitude)) 
 +           .withIconImage(BitmapFactory.decodeResource(getResources(), R.mipmap.red_marker)) 
 +           .withTextField(title); 
 +    pointAnnotationManager.create(pointAnnotationOptions); 
 +  } 
 + 
 +  @Override 
 +  public boolean onMapClick(@NonNull Point point) { 
 +    pointAnnotationManager.deleteAll(); 
 +    currentPoint = point; 
 +    addMarker(point.latitude(), point.longitude(), getString(R.string.here)); 
 +    return false; 
 +  } 
 +}
 </code> </code>
  
-{{ youtube>BkdAksPHiF8 }}+<figure> 
 +{{ map-view-marker.png }} 
 +<caption>Marker añadido al mapa tras hacer click</caption></figure>
  
-==== Marcar ubicaciones en un mapa Mapbox ====+===== Visualizar ubicaciones en un mapa Mapbox =====
  
-Antes de marcar las ubicaciones en el mapa utilizando esta librería tendremos que tener en cuenta en que formato se encuentran éstas. Ya he comentado cómo convertirlas en el caso de que se traten de coordenadas extraídas del catálogo de datos abiertos del ayuntamiento de Zaragoza en el apartado de [[http://multimedia.codeandcoke.com/apuntes:android#marcar_ubicaciones_en_el_mapa_de_google_maps|Cómo marcar ubicaciones con Google Maps]]. Las consideraciones sobre la conversión de coordenadas son las mismas aunque el código para marcar la ubicación en el mapa difiere ligeramente y lo pasaré a explicar aqui.+Para visualizar una ubicación en el mapa necesitamos: 
 +  * latitud y longitud del punto que queremos visualizar 
 +  * Un texto o title que queramos asignar (opcional) 
 +  * La imagen que queremos colocar como "marker" (en nuestro caso se llama red_marker y la hemos obtenido siguiendo el tutorial de Mapbox)
  
-Así, suponiendo que tenemos las coordenadas de un punto en las variables ''latitud''''longitud'', para añadir un //marker// al mapa tendremos que añadir el siguiente fragmento de código. El //marker// se añadirá tan pronto como el mapa esté listo (''onMapReady'')+En Mapbox lo que se solía conocer como ''marker'' ahora se llama Annotation y para colocar uno en un mapa de Mapbox hay que seguir el código que se ejecuta en el método ''addMarker(latitude, longitude, title)''Hay que tener en cuenta que el código tendrá que se ejecuta tras cargarse el mapa. Es por eso que lo hacemos invocándolo desde el método ''onStyleLoaded'', que a su vez será ejecutado una vez termine de cargarse el mapa. 
 + 
 +Hay que tener en cuenta que, antes de poder añadir markers, tendremos que inicializar el ''PointAnnotationManager'' ejecutando el método ''initializePointAnnotationManager()''
  
 <code java> <code java>
 . . . . . .
-mapaView.getMapAsync(new OnMapReadyCallback() {+public class MapsActivity extends AppCompatActivity implements Style.OnStyleLoaded { 
 +  . . . 
 +  private PointAnnotationManager pointAnnotationManager; 
 +  . . . 
 +  protected void onCreate(Bundle savedInstanceState) { 
 +    . . . 
 +    mapView = findViewById(R.id.mapView); 
 +    mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, this); 
 +    initializePointAnnotationManager(); 
 +  } 
 +   
 +  private void initializePointAnnotationManager() { 
 +    AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); 
 +    AnnotationConfig annotationConfig = new AnnotationConfig(); 
 +    pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, annotationConfig); 
 +    } 
 +  
   @Override   @Override
-  public void onMapReady(MapboxMap mapboxMap) { +  public void onStyleLoaded(@NonNull Style style) { 
-    mapboxMap.addMarker(new MarkerOptions() +    addMarker(40.1-0.8, "Zaragoza");
-             .position(new LatLng(latitudlongitud)) +
-             .title(nombre) +
-             .snippet(descripcion));+
   }   }
 +  
 +  private void addMarker(double latitude, double longitude, String title) {
 +    PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions()
 +      .withPoint(Point.fromLngLat(longitude, latitude))
 +      .withIconImage(BitmapFactory.decodeResource(getResources(), R.mipmap.red_marker))
 +      .withTextField(title);
 +    pointAnnotationManager.create(pointAnnotationOptions);
 +  }
 +
 +  . . .
 }); });
-. . . 
 </code> </code>
  
Line 1836: Line 2876:
 <caption>Marker en un mapa Mapbox</caption></figure> <caption>Marker en un mapa Mapbox</caption></figure>
  
-Y si además queremos posicionarnos directamente en esa posición y, opcionalmente, acercar la cámara, podemos hacerlo como sigue, añadiendo el código dentro del método ''onMapReady'', tal y como hemos hecho para añadir el //marker//+Y si además queremos posicionar directamente la cámara del mapa en esa posición y, opcionalmente, acercarla y modificar otros parámetros, podemos hacerlo como sigue, añadiendo el código dentro del método ''onStyleLoaded'' justo después de invocar a ''addMarker'', tal y como hemos hecho para añadir el //marker// 
 + 
 +En este caso metemos el código en un método para que podamos usarlo siempre que queramos posicionar sobre un punto determinado con una latitud y longitud dadas. También se podrían parametrizar otros valores como el nivel de zoom para poder personalizarlo en cada llamada.
  
 <code java> <code java>
 . . . . . .
-CameraPosition position = new CameraPosition.Builder() +  private void setCameraPosition(double latitude, double longitude) { 
-  .target(new LatLng(latitudlongitud)) // Fija la posición +    CameraOptions cameraPosition = new CameraOptions.Builder() 
-  .zoom(17// Fija el nivel de zoom +      .center(Point.fromLngLat(longitudelatitude)) 
-  .tilt(30// Fija la inclinación de la cámara +      .pitch(45.0) 
-  .build(); +      .zoom(15.5) 
- +      .bearing(-17.6
-mapboxMap.animateCamera(CameraUpdateFactory +      .build(); 
-  .newCameraPosition(position), 7000);+    mapView.getMapboxMap().setCamera(cameraPosition); 
 +  }
 . . .                   . . .                  
 </code> </code>
  
-==== Ubicaciones y GPS con Mapbox ====+ 
 +===== Ubicaciones y GPS con Mapbox =====
  
 En el siguiente ejemplo se muestra como, utilizando el GPS, podemos acceder a la ubicación actual del usuario. En este caso se calcula su ubicación y se moviliza la cámara del mapa para posicionarnos en ella. Hay que tener en cuenta que puesto en los ejemplos anteriores ya hemos añadido los permisos necesarios para trabajar con el GPS no lo tendremos que hacer ahora. En caso de que no se haya hecho habrá que añadir los permisos en el ''AndroidManifest.xml'' En el siguiente ejemplo se muestra como, utilizando el GPS, podemos acceder a la ubicación actual del usuario. En este caso se calcula su ubicación y se moviliza la cámara del mapa para posicionarnos en ella. Hay que tener en cuenta que puesto en los ejemplos anteriores ya hemos añadido los permisos necesarios para trabajar con el GPS no lo tendremos que hacer ahora. En caso de que no se haya hecho habrá que añadir los permisos en el ''AndroidManifest.xml''
Line 1880: Line 2924:
  
 <code java> <code java>
-public class MapActivity extends AppCompatActivity {+public class MapActivity extends AppCompatActivity implements LocationEngineCallback<LocationEngineResult> {
   private MapView mapaView;   private MapView mapaView;
-  private MapboxMap mapa; +  . . .
-  private FloatingActionButton btUbicacion; +
-  private LocationServices servicioUbicacion;+
        
   @Override   @Override
   protected void onCreate(Bundle savedInstanceState) {   protected void onCreate(Bundle savedInstanceState) {
-    . . . +    . . .     
-    mapaView.getMapAsync(new OnMapReadyCallback() +    LocationEngine locationEngine = LocationEngineProvider.getBestLocationEngine(this); 
-      @Override +    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { 
-      public void onMapReady(MapboxMap mapboxMap) { +        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1); 
-        mapa = mapboxMap; +    } 
-        . . . +    locationEngine.getLastLocation(this);
-      } +
-    }); +
-     +
-    . . .   +
-    ubicarUsuario();+
   }   }
        
-  // Obtiene y enfoca a la ubicación del usuario +  @Override 
-  private void ubicarUsuario() {+  public void onSuccess(LocationEngineResult result) { 
 +    Location currentLocation = result.getLastLocation(); 
 +    Point point = Point.fromLngLat(currentLocation.getLongitude(), currentLocation.getLatitude()); 
 +    setCameraPosition(point); 
 +    addMarker(point, "Estoy aqui"); 
 +  }
  
-    servicioUbicacion = LocationServices.getLocationServices(this); +  @Override 
- +  public void onFailure(@NonNull Exception exception{ 
-    btUbicacion = (FloatingActionButtonfindViewById(R.id.btUbicacion); +  } 
-    btUbicacion.setOnClickListener(new View.OnClickListener() { +   
-      @Override +  private setCameraPosition(Point point{ 
-      public void onClick(View view{ +    CameraOptions cameraPosition = new CameraOptions.Builder() 
-        if (mapa != null{ +            .center(point
-          Location lastLocation = servicioUbicacion.getLastLocation(); +            .pitch(0.0
-          if (lastLocation != null+            .zoom(13.5
-            mapa.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lastLocation), 16)); +            .bearing(-17.6
- +            .build(); 
-          // Resalta la posición del usuario en el mapa +    mapView.getMapboxMap().setCamera(cameraPosition);
-          mapa.setMyLocationEnabled(true)+
-        } +
-      } +
-    });+
   }   }
 } }
Line 1927: Line 2965:
 {{ gps.png?400 }} {{ gps.png?400 }}
 <caption>Ubicación del usuario en un mapa Mapbox</caption></figure> <caption>Ubicación del usuario en un mapa Mapbox</caption></figure>
- +===== Cálculo de rutas con Mapbox =====
-También existe la posibilidad de implementar un ''LocationListener'' de forma que podemos actuar ante cualquier cambio de ubicación. Por ejemplo, podríamos ir almacenando las ubicaciones por las que el usuario pasa de forma que luego pudieramos almacenar la ruta para mostrarla más adelante. +
- +
-<code java> +
-. . . +
-servicioUbicacion.addLocationListener(new LocationListener() { +
-  @Override +
-  public void onLocationChanged(Location location) { +
-    // Qué hacer cuando la ubicación del usuario cambie +
-    // El parámetro location siempre contiene la nueva ubicación del usuario                  +
-  } +
-}); +
-. . . +
-</code> +
- +
-==== Cálculo de rutas con Mapbox ====+
  
 <figure> <figure>
Line 1950: Line 2973:
 Utilizando los servicios de Android de //Mapbox// se pueden calcular las rutas y distancias entre dos puntos dados sobre el mapa, para posteriormente pintarla. Utilizando los servicios de Android de //Mapbox// se pueden calcular las rutas y distancias entre dos puntos dados sobre el mapa, para posteriormente pintarla.
  
-Para ello, lo primero que tenemos que hacer es añadir la correspondiente librería en el ''build.gradle'' de nuestra //app//, tal y como ya hicimos con la API de Android antes de empezar a trabajar con la librería //Mapbox//. En este caso se trata de acceder a la API específica para el cálculo de rutas.+Para ello, lo primero que tenemos que hacer es añadir la correspondiente librería en el ''build.gradle'' de nuestra //app//, tal y como ya hicimos con la API de Android antes de empezar a trabajar con la librería //Mapbox//. En este caso se trata de acceder a la API específica para el cálculo de rutas. En total, tendríamos que añadir las siguientes librerías //Mapbox//:
  
 <code java> <code java>
Line 1956: Line 2979:
 dependencies { dependencies {
   . . .   . . .
-  compile ('com.mapbox.mapboxsdk:mapbox-android-services:1.3.1@aar'){ +    implementation 'com.mapbox.maps:android:10.9.1' 
-    transitive=true+    implementation 'com.mapbox.navigation:core:2.11.0' 
 +    implementation 'com.mapbox.navigation:android:2.11.0'
   }   }
 } }
Line 1963: Line 2987:
 </code> </code>
  
-Y a continuación, dos métodos, ''obtenerRuta(Location location1Location location2)'' para hacer el cálculo de la ruta entre dos puntos dados, utilizando como punto de partida dos objetos ''Location'' que identifican la longitud y la latitud de los mismos. Con el primero de los métodos obtendremos el objeto ''ruta'' (de la clase ''DirectionsRoute''que más adelante utilizaremos como punto de partida para pintar la línea que defina la ruta calculadacon el método ''pintarRuta(DirectionsRoute ruta)'' a partir de los puntos que la forman.+Y a continuación, dos métodos, ''calculateRoute(Point originPoint destination)'' para hacer el cálculo de la ruta entre dos puntos dados, utilizando como punto de partida dos objetos ''Point'' que identifican la longitud y la latitud de los mismos. Con este método prepararemos la ruta que queremos calcular e invocaremos a ''directions.enqueeCall(this)'' para calcular, en segundo plano, la ruta entre los puntos datos. 
 + 
 +Hemos implementado la interface ''Callback<DirectionsResponse'' en nuestra Activity por lo que, cuando la ruta esté listase invocará al método ''onResponse'' automáticamente para dibujarla en el mapa.
  
 <code java> <code java>
 +public class MapsActivity extends AppCompatActivity implements Callback<DirectionsResponse> {
 . . . . . .
-// Calcula la ruta entre el marker y la posición del usuario +  // Calcula la ruta entre dos puntos dados 
-private void obtenerRuta(Location markerLocationLocation userLocationthrows ServicesException {+  private void calculateRoute(Point originPoint destination) { 
 +    RouteOptions routeOptions = RouteOptions.builder() 
 +          .baseUrl(Constants.BASE_API_URL) 
 +          .user(Constants.MAPBOX_USER) 
 +          .profile(DirectionsCriteria.PROFILE_WALKING) 
 +          .steps(true) 
 +          .coordinatesList(List.of(origin, destination)) 
 +          .build(); 
 +    MapboxDirections directions = MapboxDirections.builder() 
 +          .routeOptions(routeOptions) 
 +          .accessToken(getString(R.string.mapbox_access_token)) 
 +          .build(); 
 +    directions.enqueueCall(this); 
 +  }
  
-  Position posicionMarker = Position.fromCoordinates(markerLocation.getLongitude()markerLocation.getLatitude()); +  // Este método será invocado cuando la ruta esté listaEn élpintamos la ruta en el mapa 
-  Position posicionUsuario = Position.fromCoordinates(userLocation.getLongitude()userLocation.getLatitude()); +  @Override 
- +  public void onResponse(Call<DirectionsResponse> callResponse<DirectionsResponse> response{ 
-  // Obtiene la dirección entre los dos puntos +    DirectionsRoute mainRoute response.body().routes().get(0); 
-  MapboxDirections direccion new MapboxDirections.Builder() +    Log.d("ROUTELEGS", String.valueOf(mainRoute.legs().size())); 
-    .setOrigin(posicionMarker) +    for (RouteLeg routeLeg: mainRoute.legs()) { 
-    .setDestination(posicionUsuario+      Log.d("LEGS", String.valueOf(routeLeg.steps().size())); 
-    .setProfile(DirectionsCriteria.PROFILE_CYCLING) +      for (LegStep legStep : routeLeg.steps()) { 
-    .setAccessToken(MapboxAccountManager.getInstance().getAccessToken()) +        Log.d("STEP"legStep.name() + " " + legStep.speedLimitSign() + " " + legStep); 
-    .build(); +      }
- +
-  direccion.enqueueCall(new Callback<DirectionsResponse>() +
-    @Override +
-    public void onResponse(Call<DirectionsResponse> call, Response<DirectionsResponse> response) { +
- +
-      ruta = response.body().getRoutes().get(0); +
-      Toast.makeText(MapActivity.this, "Distancia: " + ruta.getDistance() + " metros", Toast.LENGTH_SHORT).show(); +
- +
-      pintarRuta(ruta);+
     }     }
 +    mapView.getMapboxMap().getStyle(style -> {
 +      LineString routeLine = LineString.fromPolyline(mainRoute.geometry(), PRECISION_6);
  
-    @Override +      GeoJsonSource routeSource = new GeoJsonSource.Builder("trace-source") 
-    public void onFailure(Call<DirectionsResponse> callThrowable throwable{ +              .geometry(routeLine) 
-      // Qué hacer en caso de que falle el cálculo de la ruta +              .build(); 
-    +      LineLayer routeLayer = new LineLayer("trace-layer""trace-source") 
-  }); +              .lineWidth(7.f) 
-}+              .lineColor(Color.BLUE) 
 +              .lineOpacity(1f); 
 +      SourceUtils.addSource(style, routeSource); 
 +      LayerUtils.addLayer(style, routeLayer); 
 +    }); 
 +  }
  
-// Pinta la ruta sobre el mapa +  @Override 
-private void pintarRuta(DirectionsRoute ruta) { +  public void onFailure(Call<DirectionsResponsecall, Throwable t) { 
- +    Log.e("CalculateRoute""Fallo al invocar a la API de Mapbox", t);
-  // Recoge los puntos de la ruta +
-  LineString lineString = LineString.fromPolyline(ruta.getGeometry(), Constants.OSRM_PRECISION_V5); +
-  List<Positioncoordenadas = lineString.getCoordinates(); +
-  LatLng[] puntos = new LatLng[coordenadas.size()]; +
-  for (int i = 0; i < coordenadas.size(); i++) { +
-    puntos[i] = new LatLng(coordenadas.get(i).getLatitude()coordenadas.get(i).getLongitude());+
   }   }
- 
-  // Pinta los puntos en el mapa 
-  mapa.addPolyline(new PolylineOptions() 
-    .add(puntos) 
-    .color(Color.parseColor("#009688")) 
-    .width(5)); 
- 
-  // Resalta la posición del usuario si no lo estaba ya 
-  if (!mapa.isMyLocationEnabled()) 
-    mapa.setMyLocationEnabled(true); 
-} 
 . . . . . .
 </code> </code>
-===== Firebase ===== 
  
-==== Registro y configuración de la base de datos ====+====== Firebase ======
  
-==== Registrar información ====+  * Plataforma de desarrollo en la nube 
 +  * Nosotros nos centraremos en ver a Firebase como una plataforma de almacenamiento en la nube 
 +    * Base de Datos en tiempo real 
 +    * Autenticación
  
-==== Consultar información ====+=== Configuración de Firebase con Android ===
  
-==== Modificar información ====+  * ¿Dónde empezar? -> https://firebase.google.com/docs/database/android/start 
 +  * Acceder a la consola de Firebase y crear una base de datos (Database): https://console.firebase.google.com/ 
 +  * Añadir la App a Firebase 
 +  * Configuración gradle 
 +  * Crear una instancia de Firebase y almacenar información (código Java)
  
-==== Eliminar información ====+=== Introducción a Firestore ===
  
- +  * Base de datos NoSQL 
-===== Creación de Servicios Web =====+  * Los datos se guardan en colecciones (collections) 
 +  * Cada fila/dato/objeto se corresponde con un documento (document) 
 +  * Cada atributo/campo es un campo (field) del documento
  
 <figure> <figure>
-{{ spring-logo.png }} +{{ firestore.png }} 
-<caption>Framework Spring</caption></figure>+<caption>Firestore</caption></figure>
  
-[[http://www.spring.io|Spring]] es un framework de Java para el desarrollo de aplicaciones web. En nuestro caso, lo que queremos construir es una pequeña aplicación web que nos permita disponer de servicios web para comunicar nuestra aplicación móvil hecha en Android con una Base de Datos que podamos, si asi lo queremos, proporcionar algo de lógica en el lado servidor cuando sea necesario. Para eso utilizaremos //Spring Boot// que es una parte de este framework que facilita bastante el trabajo para casos como el que a nosotros nos interesa.+===== Registro configuración de la base de datos =====
  
-Para eso, lo primero que haremos será utilizar el [[https://start.spring.io/|Spring Initializr]] para preparar el proyecto inicial sobre el que luego diseñaremos nuestra pequeña aplicación web. Para ello podemos seguir el videotutorial que aparece al final de este apartado. +<code groovy>
- +
-Una vez tengamos creado el proyecto inicial, podemos empezar a trabajar en él para tener nuestro servidor. En este caso se trata de crear un servidor que tendrá los servicios web necesarios para que los usuarios de una aplicación Android puedan registrar sus opiniones en nuestra Base de Datos. Así, otros usuarios podrán visualizarlas en sus terminales. +
- +
-==== Configuración del servidor ==== +
- +
-Lo primero de todo será editar el fichero de configuración del proyecto para personalizarlo a nuestro caso: +
- +
-<file java application.properties> +
-# Configuración para el acceso a la Base de Datos +
-spring.jpa.hibernate.ddl-auto=none +
-spring.jpa.properties.hibernate.globally_quoted_identifiers=true +
-# Puerto donde escucha el servidor una vez se inicie +
-server.port=${port:8082} +
- +
-# Datos de conexion con la base de datos MySQL +
-spring.datasource.url=jdbc:mysql://localhost:3306/opiniones +
-spring.datasource.username=root +
-spring.datasource.password= +
-spring.datasource.driverClassName=com.mysql.jdbc.Driver +
-</file> +
- +
-Sobre el fichero ''build.gradle'' tendremos que realizar algunos cambios: +
- +
-<file java build.gradle>+
 . . . . . .
-apply plugin'java+implementation 'com.google.firebase:firebase-database:20.2.2
-apply plugin: 'idea' +implementation 'com.google.firebase:firebase-firestore:24.6.1
-apply plugin'spring-boot+. . . 
-apply plugin: 'war'+</code>
  
-jar { +===== Registrar/Modificar información =====
-  baseName 'eventoserver' +
-  version '0.0.1' +
-+
- +
-repositories { +
-  mavenCentral() +
-+
- +
-dependencies { +
-  compile('org.springframework.boot:spring-boot-starter-web'+
-  compile('org.springframework.boot:spring-boot-starter-data-jpa'+
-  compile('mysql:mysql-connector-java:5.1.16'+
-  providedRuntime("org.springframework.boot:spring-boot-starter-tomcat"+
-+
- +
-configurations { +
-  providedRuntime +
-+
-</file> +
- +
-Ahora, modificaremos la clase principal ''Application''. De esta forma podremos iniciar nuestro servidor directamente desde la consola de IntelliJ o bien contenida dentro de un servidor de aplicaciones como //Tomcat//, aunque no lo haremos así en este caso. +
- +
-Conviene prestar atención a los comentarios que he dejado en esta clase, donde se explica cómo lanzar la aplicación servidor una vez que este lista. +
- +
-<file java Application.java> +
-/** +
- * Clase que lanza la aplicación +
- * +
- * Cómo compilar/ejecutar la aplicación: +
-  - Si se hacen cambios en el build.gradle conviene ejecutar (desde la terminal): +
-      - ./gradlew idea +
-      - ./gradlew build +
-  - Una vez compilado se pueden ejecutar por dos vias +
-      - ./gradlew bootRun +
-      - También se puede ejecutar el jar (con java -jar) que se genera en la carpeta 'build/libs' según el fichero 'build.gradle' +
- * +
-  El proyecto parte de un proyecto base creado con la herramienta Spring Initializr, +
-  disponible en https://start.spring.io/. Conviene seleccionar ya de inicio las dependencias de Web, JPA y MySQL +
-  De todas formas se pueden añadir luego a gradle y sincronizar el proyecto como se indica más arriba +
- * +
- * @author Santiago Faci +
- * @version curso 2015-2016 +
- */ +
-@SpringBootApplication +
-public class Application extends SpringBootServletInitializer { +
- +
-  public static void main(String[] args) { +
-    SpringApplication.run(Application.class, args); +
-  } +
- +
-  @Override +
-  protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { +
-    return application.sources(applicationClass); +
-  } +
- +
-  private static Class<Application> applicationClass Application.class; +
-+
-</file> +
- +
-==== Definir la Base de Datos ===+
- +
-Hay que tener en cuenta que //Spring// utiliza por debajo el frame de //Hibernate// para trabajar con la Base de Datos. Eso nos va a permitir trabajar con nuestras clases Java directamente sobre la Base de Datos, ya que será //Hibernate// quién realizará el mapeo entre el objeto Java (y sus atributos) y la tabla de MySQL (y sus columnas) a la hora de realizar consultas, inserciones, modificaciones o borrados. +
- +
-A continuación se muestra el script ''SQL'' que creará la tabla de la base de datos que usaremos para este ejemplo. Y más adelante la clase Java que se utilizará para hacer el mapeo con dicha tabla. +
- +
-<file sql opiniones.sql> +
-CREATE DATABASE IF NOT EXISTS opiniones; +
-USE opiniones; +
- +
-CREATE TABLE IF NOT EXISTS opiniones ( +
-  id INT UNSIGNED PRIMARY KEY, +
-  titulo VARCHAR(50) NOT NULL, +
-  texto VARCHAR(50), +
-  fecha DATETIME, +
-  puntuacion INT UNSIGNED +
-); +
-</file> +
- +
-Así, simplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a //Hibernate// para saber a qué tabla corresponden los objetos de la clase y a qué columnas sus atributos.+
  
 <code java> <code java>
-/** +Book book = new Book();  
- * Opinion que los usuarios tienen sobre un monumento +book.setId(UUID.randomUUID().toString());  
- * Se deben definir las anotaciones que indican la tabla y columnas a las que +book.setTitle("El Quijote") 
- * representa esta clase y sus atributos +book.setPageCount(400);
- * +
- * @author Santiago Faci +
- * @version curso 2015-2016 +
- */ +
-@Entity +
-@Table(name = "opiniones") +
-public class Opinion {+
  
-  @Id +FirebaseFirestore db = FirebaseFirestore.getInstance()db.collection("books").document(book.getId()).set(book);
-  @GeneratedValue +
-  private int id; +
-  @Column +
-  private String titulo; +
-  @Column +
-  private String texto; +
-  @Column +
-  private Date fecha; +
-  @Column +
-  private int puntuacion; +
- +
-  // Constructor +
-  // Getters y Setters +
-  . . . +
-}+
 </code> </code>
  
-==== El Acceso a la Base de Datos ====+<figure> 
 +{{ books.png }} 
 +<caption>base de datos Books</caption></figure>
  
-Ahora creamos la ''interface'' donde se definirán los métodos que permitirán acceder a la Base de Datos. En este caso nos basta con definir las cabeceras de los mismos, puesto que se trata de una ''interface''. Será el framework el que se encargue de su implementación. En este caso hemos definido métodos para obtener todas las puntuaciones y otro para obtener las que tengan una puntuación determinada. Además, podremos contar con que tenemos las operaciones que nos permiten registrar/modificar (''save'') y eliminar (''delete''información de la Base de Datos.+===== Consultar información =====
  
 <code java> <code java>
-/** +FirebaseFirestore db = FirebaseFirestore.getInstance();  
- * Clase que hace de interfaz con la Base de Datos +db.collection("books").get() 
- * Al heredar de CrudRepository se asumen una serie de operaciones +  .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>()  
- * para registrar o eliminar contenido (save/delete+    @Override 
- * Se pueden añadir operaciones ya preparadas como las que hay de ejemplo ya hechas +    public void onComplete(@NonNull Task<QuerySnapshottask) {  
- * +      if (task.isSuccessful()) { 
- @author Santiago Faci +        for (QueryDocumentSnapshot document : task.getResult()) {  
- * @version curso 2015-2016 +          Book book = document.toObject(Book.class); 
- */ +          // TODO Añadir el objeto a una lista, por ejemplo  
-public interface OpinionRepository extends CrudRepository<Opinion, Integer> { +        } 
- +      } 
-  List<Opinion> findAll(); +    });
-  List<Opinion> findByPuntuacion(int puntuacion); +
-}+
 </code> </code>
  
-==== Implementación del Controller ==== +Se pueden añadir condicionesantes de invocar al método get(). Por ejemplo:
- +
-Por últimocrearemos la clase que hará de ''Controller'' de la aplicación. En ella introduciremos los métodos con las operaciones que queremos que nuestros usuarios puedan realizar, programaremos la lógica que necesitemos y accederemos a los datos a través del ''OpinionRepository'' que hemos creado en el paso anterior. +
- +
-En este caso hemos creado tres operaciones: +
- +
-  * getOpiniones(): Devuelve todas las opiniones de la base de datos +
-  * getOpiniones(int puntuacion): Devuelve las opiniones que tienen una determinada puntuacion +
-  * addOpinion(String titulo, String texto, int puntuacion): Registra una nueva opinión en la base de datos +
- +
-Cada una de las operaciones tienen una URL de mapeo que nos permite acceder a las mismas desde cualquier cliente (navegador, aplicación Java, aplicación Android). Por ejemplo, si quisieramos obtener todas las opiniones que tienen una determinada puntuación utilizaríamos la siguiente URL: http://localhost:8082/opiniones_puntuacion?puntuacion=4 (cambiando ''localhost'' por la IP o nombre del servidor que corresponda en cada caso). Más adelante se verá cómo hacerlo desde una aplicación Android pero es posible probar nuestro servidor accediendo a estas URLs directamente desde el navegador, de forma que podamos comprobar que todo funciona correctamente antes de seguir. +
- +
-  * http://localhost:8082/opiniones_puntuacion +
-  * http://localhost:8082/opiniones_puntuacion?puntuacion=4 +
-  * http://localhost:8082/add_opinion?titulo=eltitulo&texto=eltexto&puntuacion=10 +
 <code java> <code java>
-/** +db.collection("books")  
- * Controlador para las opiniones +  .whereGreaterThan("pageCount", 100
- * Contendrá todos los métodos que realicen operaciones sobre opiniones de los usuarios +  .get()
- * +
- * @author Santiago Faci +
- * @version curso 2015-2016 +
- */ +
-@RestController +
-public class OpinionController { +
- +
-  @Autowired +
-  private OpinionRepository repository; +
- +
-  /** +
-   * Obtiene todas las opiniones de los usuarios +
-   * @return +
-   */ +
-  @RequestMapping("/opiniones"+
-  public List<Opinion> getOpiniones() { +
- +
-    List<Opinion> listaOpiniones = repository.findAll(); +
-    return listaOpiniones; +
-  } +
- +
-  /** +
-   * Obtiene todas las opiniones con una puntuacion determinada +
-   * @param puntuacion +
-   * @return +
-   */ +
-  @RequestMapping("/opiniones_puntuacion") +
-  public List<Opinion> getOpiniones(int puntuacion) { +
- +
-    List<Opinion> listaOpiniones = repository.findByPuntuacion(puntuacion); +
-    return listaOpiniones; +
-  } +
- +
-  /** +
-   * Registra una nueva opinión en la Base de Datos +
-   * @param titulo +
-   * @param texto +
-   * @param puntuacion +
-   */ +
-  @RequestMapping("/add_opinion") +
-  public void addOpinion(@RequestParam(value = "titulo", defaultValue = "nada") String titulo, +
-                         @RequestParam(value = "texto" , defaultValue = "nada mas") String texto, +
-                         @RequestParam(value = "puntuacion", defaultValue = "-1") int puntuacion) { +
- +
-    Opinion opinion = new Opinion(); +
-    opinion.setTitulo(titulo)+
-    opinion.setTexto(texto); +
-    opinion.setFecha(new Date(System.currentTimeMillis())); +
-    opinion.setPuntuacion(puntuacion); +
- +
-    repository.save(opinion); +
-  } +
-}+
 </code> </code>
  
-==== Ejecución del servidor ====+Y ordenar o limitar el número de documentos con ''.limit()'' y ''.orderBy()''
  
-Una vez terminado todo, para lanzar el servidor tenemos dos opciones: +===== Eliminar información =====
-  * Desde el propio IDE, ejecutando ''./gradlew bootRun'' (o bien ''gradlew bootRun'' si estamos en Windows) +
-  * Utilizando el jar que podemos generar con el comando ''./gradlew jar build'' (''gradlew jar build'' en Windows) y ejecutarlo con el comando ''java -jar''. El ''.jar'' generado lo podremos encontrar en la carpeta ''build/libs'' +
- +
-==== Lado cliente (Lado Android) ==== +
- +
-Desde el lado cliente (aplicación Android en nuestro caso), podremos acceder a los servicios web mediante la URL que hayamos definido de la siguiente forma, y siempre dentro de una ''AsyncTask'': +
- +
-  * Para obtener todas las opiniones de la Base de Datos en el servidor:+
  
 <code java> <code java>
-. . +FirebaseFirestore db = FirebaseFirestore.getInstance();  
-private List<Opinion> listaOpiniones = new ArrayList<>(); +db.collection("books").document("id"
-private final String URL_SERVIDOR = "http://192.168.0.5:8082"+  .delete() 
-. . +  .addOnSuccessListener(new OnSuccessListener<Void>() 
-RestTemplate restTemplate = new RestTemplate(); +    @Override 
-restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter()); +    public void onSuccess(Void aVoid{ 
-Opinion[] opinionesArray = restTemplate.getForObject(URL_SERVIDOR + "/opiniones", Opinion[].class); +      /TODO Documento eliminado correctamente 
-listaOpiniones.addAll(Arrays.asList(opinionesArray)); +    } 
-. . .+  }
 +  .addOnFailureListener(new OnFailureListener() 
 +    @Override 
 +    public void onFailure(@NonNull Exception e
 +      // TODO Error eliminando documento 
 +    } 
 +  });
 </code> </code>
  
-  * O bien para registrar una nueva opinión en el servidor desde nuestro dispositivo móvil:+======= Arquitectura MVP =======
  
-<code java+<figure
-. . . +{{ mvp.png?400 }} 
-RestTemplate restTemplate = new RestTemplate(); +<caption>Arquitectura Model-View-Presenter</caption></figure>
-restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter()); +
-restTemplate.getForObject(URL_SERVIDOR + "/add_opinion?titulo=" + titulo + "&texto=" + texto +  +
-  "&puntuacion=" + puntuacion, Void.class); +
-. . . +
-</code> +
- +
-Aunque antes de poder utilizar las llamadas a los métodos que permiten invocar los servicios web de nuestro servidor, tendremos que añadir un par de dependencias a nuestro ''build.gradle (Module: app)'' +
- +
-<code java> +
-. . . +
-android { +
-  . . . +
-  packagingOptions { +
-    exclude "META-INF/notice.txt" +
-    exclude "META-INF/license.txt" +
-    exclude "META-INF/LICENSE" +
-    exclude "META-INF/NOTICE" +
-  } +
-  . . . +
-+
-. . . +
-dependencies { +
-  . . . +
-  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3' +
-  compile 'com.fasterxml.jackson.core:jackson-databind:2.3.2' +
-  . . . +
-+
-. . . +
-</code> +
- +
-{{ youtube>TBIzVT5dHC4 }} +
-\\+
  
 ---- ----
-===== Ejercicios =====+====== Ejercicios ======
  
 {{ ejercicio.png?75}} {{ ejercicio.png?75}}
Line 2359: Line 3161:
 ---- ----
  
-===== Proyectos de ejemplo ===== +====== Proyectos de ejemplo ======
- +
-Todos los proyectos de ejemplo se pueden encontrar en el [[https://github.com/codeandcoke/android|repositorio android de GitHub]]. +
- +
-Todos los ejercicios que vayamos haciendo en clase se pueden encontrar en el [[https://github.com/codeandcoke/android-ejercicios|repositorio android-ejercicios de GitHub]] +
- +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Controles/?at=master|Android_Controles]] Ejemplo de diferentes controles gráficos +
-  * [[https://bitbucket.org/sfaci/android|Listado completo de proyectos]] +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Listas3/?at=master|Android_Listas3]] Ejemplo con Listas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Listas4/?at=master|Android_Listas4]] Ejemplo con Listas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Listas6/?at=master|Android_Listas6]] Ejemplo con Listas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Listas7/?at=master|Android_Listas7]] Ejemplo con Listas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_ComunicarActivities/?at=master|Android_ComunicarActivities]] Cómo comunicar dos Activities +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Tabs/?at=master|Android_Tabs]] Ejemplo con Tabs +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Agendroid_v3/?at=master|Agendroid]] Ejemplo de Agenda de contactos +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_BBBDD/?at=master|Android_BBDD]] Ejemplo sencillo con Bases de Datos +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Mapas/?at=master|Android_Mapas]] Ejemplo de uso de mapas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_Mapas2/?at=master|Android_Mapas2]] Ejemplo de uso de mapas +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/Android_WS_Tarea/?at=master|Android_WS]] Ejemplo de uso de datos en formato JSON +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/GuiaGasolineras2016/?at=master|GuiaGasolineras2016]] Aplicación guía de gasolineras de Zaragoza +
-  * [[https://bitbucket.org/sfaci/android/src/1725b260596557393304e23d74994dc93a9f7388/GuiaRestaurantes2016/?at=master|GuiaRestaurantes2016]] Aplicación guía de restaurantes de Zaragoza +
-  * [[https://bitbucket.org/sfaci/eventosapp|EventosApp]] Aplicación móvil para la gestión de eventos +
-  * [[https://bitbucket.org/sfaci/eventoserver|EventoServer]] Aplicación servidor para la gestión de eventos +
- +
-===== Práctica 1.1 ===== +
- +
-=== Objetivos === +
- +
-Desarrollar una aplicación para un dispositivo móvil Android. +
- +
-=== Herramientas Necesarias === +
- +
-  * Android Studio +
-  * IntelliJ IDEA (para la implementación del servidor) +
-  * Pencil (para el prototipado de las pantallas) +
- +
-=== Enunciado === +
- +
-Debes diseñar e implementar una aplicación para dispositivo móvil Android que cumpla, al menos, con los requisitos que se indican como mínimos. Hay que tener en cuenta que la aplicación se deberá poder lanzar sobre móviles cuyo SDK sea cualquier de la rama 5.X.X (o en adelante) de Android. +
- +
-El tema de la aplicación debe ser escogido por el alumno, que lo tendrá que notificar al menos con dos semanas de antelación a la entrega de la práctica. En ese momento se debe presentar un documento donde se describa la aplicación a desarrollar, las funcionalidades  de las que dispondrá (cómo máximo) y un prototipo que permita mostrar la apariencia de la misma. +
- +
-=== Requisitos Mínimos (1 pto cada uno) === +
- +
-  * La aplicación contará con, al menos, 4 Activities o Fragments, utilizando controles ''ImageView'', ''TextView'', ''Button'', ''CheckBox'' y ''ListView'' para presentar la información en pantalla y se hará, como mínimo, en dos idiomas. +
-  * Se deberán usar Bases de Datos para almacenar información. El usuario deberá ser capaz de registrar, modificar y visualizar en un ''ListView'' esa información con un adaptador personalizado y un menú contextual desde donde será posible ejecutar algunas de estas operaciones necesarias (modificar, por ejemplo). +
-  * La aplicación contará con un menú de opciones o ''ActionBar'' desde donde se podrá acceder a las acciones que el usuario pueda realizar en cada ''Activity''. También dispondrá de un diálogo con la información de ''Acerca de''+
-  * Añadir alguna función que interactúe con otras aplicaciones del dispositivo +
-  * Diseñar para alguna ''Activity'' varios layouts según el tamaño o la posición de la pantalla +
- +
-=== Otras funcionalidades (1 pto cada una) === +
- +
-  * Utilizar en diferentes ''Activity'', Listviews con diferentes adaptadores y Layouts para visualizar otra información +
-  * Gestionar el proyecto utilizando las herramientas proporcionadas por //GitHub// o //BitBucket//, registrando al menos 10 //issues// reales y su resolución. Añadir también una Wiki con las instrucciones de la aplicación y la puesta en marcha del servidor +
-  * Usar imágenes como atributos de algún objeto +
-  * Añadir la opción de eliminar objetos de la lista +
-  * Utilizar diálogos siempre que sea necesario +
- +
-=== Observaciones === +
- +
-Para la entrega se creará un repositorio con el código del proyecto y una descarga que permita acceder directamente al APK de una versión instalable de la aplicación. +
- +
-===== Práctica 1.2 ===== +
- +
-=== Objetivos === +
- +
-Desarrollar una aplicación para un dispositivo móvil Android. +
- +
-=== Herramientas Necesarias === +
- +
-  * Android Studio +
-  * IntelliJ IDEA (para la implementación del servidor) +
-  * Spring Boot +
-  * Pencil (para el prototipado de las pantallas) +
- +
-=== Enunciado === +
- +
-Sobre la aplicación desarrollada para la práctica 1.1, se deberán añadir nuevas funcionalidades. +
- +
-=== Requisitos Mínimos (1 pto cada uno) === +
- +
-  * Añadir otras 4 Activities a la aplicación +
-  * Se mostrará información útil para la aplicación en un mapa (con Google Maps o MapBox), de forma que pueda interactuarse con él para llevar alguna acción de utilidad para la aplicación. +
-  * Utilizar y generar datos en la aplicación disponibles a través de un servidor (implementado con ''Spring'') que ofrecerá un Servicio Web (JSON, XML, . . .). +
-  * Añadir un menú de preferencias con al menos 3 opciones que modifiquen el comportamiento de la aplicación. Este menú de preferencias estará accesible en todo momento desde el ''ActionBar''+
-  * Utilizar el GPS del dispositivo para realizar alguna función sobre el mapa de la aplicación. +
- +
-=== Otras funcionalidades (1 pto cada una) === +
- +
-  * Diseñar algún servicio que interactúe con el usuario mediante notificaciones del sistema. +
-  * Añadir alguna función que interactúe con otras aplicaciones del dispositivo +
-  * Presentar la aplicación utilizando pestañas +
-  * Permitir que en la aplicación puedan interactuar, de alguna manera, diferentes usuarios desde sus propios dispositivos +
-  * Utilizar en diferentes ''Activity'', Listviews con diferentes adaptadores y Layouts para visualizar otra información +
-  * Gestionar el proyecto utilizando las herramientas proporcionadas por //GitHub// o //BitBucket//, registrando al menos 10 //issues// reales y su resolución. Añadir también una Wiki con las instrucciones de la aplicación y la puesta en marcha del servidor +
- +
-=== Observaciones ===+
  
-Para la entrega se creará un repositorio con el código del proyecto y una descarga que permita acceder directamente al APK de una versión instalable de la aplicación.+[[https://github.com/codeandcoke/android-samples|Android samples]] Repositorio con varios ejemplos de código actualizados: 
 +   * **BiziStations**: Full project with SwipeRefresh, Maps, Directions API, RecyclerView, API consumption and more 
 +   * **BottomNavigation**: How to create an Activity with some Fragments and a Bootom Navigation bar (loading some data to a ListView) 
 +   * **Drawer**: Drawer sample 
 +   * **FileSystem**: How to access to filesystem 
 +   * **Fragments**: How to design an Activity using two Fragments 
 +   * **Fragments2**: Fragments sample project 
 +   * **GPS**: GPS sample project 
 +   * **Mapbox**: Mapbox library sample project (load map, add markers and set camera position) 
 +   * **Maps**: Maps sample project: Google Maps, Markers and Directions API 
 +   * **MasterDetail** (Fragments): Master-Detail sample project using Fragments 
 +   * **Notifications**: How to show notifications 
 +   * **Permissions**: How to ask for Permissions sample project 
 +   * **Preferences**: How to create a Preference Activity 
 +   * **RecyclerView**: How to use a RecyclerView 
 +   * **Room database**: CRUD sample using Room library 
 +   * **themealdb**: Consume an API using Retrofit
  
 ---- ----
  
-(c) 2016-2020 Santiago Faci+(c) 2016-2023 Santiago Faci
apuntes/android.1595922833.txt.gz · Last modified: 28/07/2020 07:53 by Santiago Faci